mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-09 18:28:15 +01:00
Let Trunk watch the Windows checks, quality gate and eval jobs on pull requests, learning only (GST-1847) (#4148)
* Let Trunk watch the Windows checks, quality gate and eval jobs on pull requests, learning only (GST-1847) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Say in the testing docs that the Trunk observer is the only secret-holding job in Free Tests only, and list its new test Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * Say in the testing docs which skipped jobs to leave out when reviewing Trunk's verdicts, workflow by workflow Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
aa46032f2b
commit
892c3b13cd
11 files changed
+261
-24
No files matched your search
@@ -69,6 +69,37 @@ env:
|
||||
EVALS_FRESH: ${{ ((github.event_name == 'workflow_dispatch' && inputs.pr_receipts == '') || contains(github.event.pull_request.labels.*.name, 'evals-fresh')) && '1' || '' }}
|
||||
|
||||
jobs:
|
||||
# Trunk Dynamic CI, LEARNING ONLY (GST-1847; free-tests.yml has the full
|
||||
# rationale). Trunk records whether it would have run or skipped build-image and eval-slices
|
||||
# on same-repository pull requests; every job still runs. Trunk keys verdicts
|
||||
# by workflow file, so each observed workflow carries its own observer. No job
|
||||
# needs it, no condition reads it and it exports no outputs, so a mode change
|
||||
# in Trunk's dashboard cannot skip anything. It checks out nothing, runs no
|
||||
# pull-request code, holds no GITHUB_TOKEN scopes, never fails the run, and
|
||||
# skips fork pull requests, pushes and dispatches. docs/TESTING_INTERNALS.md.
|
||||
trunk-dynamic-ci:
|
||||
name: Trunk Dynamic CI (learning only, evals)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
permissions: {}
|
||||
env:
|
||||
TRUNK_API_TOKEN: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
steps:
|
||||
- name: Record Trunk's verdicts for build-image and eval-slices (nothing reads them)
|
||||
if: env.TRUNK_API_TOKEN != ''
|
||||
timeout-minutes: 3
|
||||
continue-on-error: true
|
||||
uses: trunk-io/dynamic-ci@5bcddbf3615c2b032c21eed1b514c91164bda4a8 # v0.1.7
|
||||
env:
|
||||
TRUNK_DISABLE_TELEMETRY: 'true'
|
||||
with:
|
||||
token: ${{ env.TRUNK_API_TOKEN }}
|
||||
job-keys: build-image,eval-slices
|
||||
github-token: ''
|
||||
enable-annotation: 'true'
|
||||
|
||||
# Build Docker image with pre-baked toolchain (cached — only rebuilds on Dockerfile/lockfile change)
|
||||
build-image:
|
||||
# Dependabot-triggered pull_request runs get a read-only GITHUB_TOKEN, so
|
||||
|
||||
@@ -31,6 +31,37 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Trunk Dynamic CI, LEARNING ONLY (GST-1847; free-tests.yml has the full
|
||||
# rationale). Trunk records whether it would have run or skipped quality
|
||||
# on same-repository pull requests; every job still runs. Trunk keys verdicts
|
||||
# by workflow file, so each observed workflow carries its own observer. No job
|
||||
# needs it, no condition reads it and it exports no outputs, so a mode change
|
||||
# in Trunk's dashboard cannot skip anything. It checks out nothing, runs no
|
||||
# pull-request code, holds no GITHUB_TOKEN scopes, never fails the run, and
|
||||
# skips fork pull requests, pushes and dispatches. docs/TESTING_INTERNALS.md.
|
||||
trunk-dynamic-ci:
|
||||
name: Trunk Dynamic CI (learning only, quality-gate)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
permissions: {}
|
||||
env:
|
||||
TRUNK_API_TOKEN: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
steps:
|
||||
- name: Record Trunk's verdicts for quality (nothing reads them)
|
||||
if: env.TRUNK_API_TOKEN != ''
|
||||
timeout-minutes: 3
|
||||
continue-on-error: true
|
||||
uses: trunk-io/dynamic-ci@5bcddbf3615c2b032c21eed1b514c91164bda4a8 # v0.1.7
|
||||
env:
|
||||
TRUNK_DISABLE_TELEMETRY: 'true'
|
||||
with:
|
||||
token: ${{ env.TRUNK_API_TOKEN }}
|
||||
job-keys: quality
|
||||
github-token: ''
|
||||
enable-annotation: 'true'
|
||||
|
||||
quality:
|
||||
# A ~1-minute job: GitHub's free hosted runner (public repo), not a
|
||||
# billed 8-vCPU Ubicloud machine.
|
||||
|
||||
@@ -57,6 +57,37 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Trunk Dynamic CI, LEARNING ONLY (GST-1847; free-tests.yml has the full
|
||||
# rationale). Trunk records whether it would have run or skipped windows-scoped-shard
|
||||
# on same-repository pull requests; every job still runs. Trunk keys verdicts
|
||||
# by workflow file, so each observed workflow carries its own observer. No job
|
||||
# needs it, no condition reads it and it exports no outputs, so a mode change
|
||||
# in Trunk's dashboard cannot skip anything. It checks out nothing, runs no
|
||||
# pull-request code, holds no GITHUB_TOKEN scopes, never fails the run, and
|
||||
# skips fork pull requests, pushes and dispatches. docs/TESTING_INTERNALS.md.
|
||||
trunk-dynamic-ci:
|
||||
name: Trunk Dynamic CI (learning only, windows-scoped-tests)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
permissions: {}
|
||||
env:
|
||||
TRUNK_API_TOKEN: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
steps:
|
||||
- name: Record Trunk's verdicts for windows-scoped-shard (nothing reads them)
|
||||
if: env.TRUNK_API_TOKEN != ''
|
||||
timeout-minutes: 3
|
||||
continue-on-error: true
|
||||
uses: trunk-io/dynamic-ci@5bcddbf3615c2b032c21eed1b514c91164bda4a8 # v0.1.7
|
||||
env:
|
||||
TRUNK_DISABLE_TELEMETRY: 'true'
|
||||
with:
|
||||
token: ${{ env.TRUNK_API_TOKEN }}
|
||||
job-keys: windows-scoped-shard
|
||||
github-token: ''
|
||||
enable-annotation: 'true'
|
||||
|
||||
windows-scope:
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
|
||||
@@ -101,6 +101,37 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Trunk Dynamic CI, LEARNING ONLY (GST-1847; free-tests.yml has the full
|
||||
# rationale). Trunk records whether it would have run or skipped windows-setup and held-install-journey
|
||||
# on same-repository pull requests; every job still runs. Trunk keys verdicts
|
||||
# by workflow file, so each observed workflow carries its own observer. No job
|
||||
# needs it, no condition reads it and it exports no outputs, so a mode change
|
||||
# in Trunk's dashboard cannot skip anything. It checks out nothing, runs no
|
||||
# pull-request code, holds no GITHUB_TOKEN scopes, never fails the run, and
|
||||
# skips fork pull requests, pushes and dispatches. docs/TESTING_INTERNALS.md.
|
||||
trunk-dynamic-ci:
|
||||
name: Trunk Dynamic CI (learning only, windows-setup-e2e)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
permissions: {}
|
||||
env:
|
||||
TRUNK_API_TOKEN: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
steps:
|
||||
- name: Record Trunk's verdicts for windows-setup and held-install-journey (nothing reads them)
|
||||
if: env.TRUNK_API_TOKEN != ''
|
||||
timeout-minutes: 3
|
||||
continue-on-error: true
|
||||
uses: trunk-io/dynamic-ci@5bcddbf3615c2b032c21eed1b514c91164bda4a8 # v0.1.7
|
||||
env:
|
||||
TRUNK_DISABLE_TELEMETRY: 'true'
|
||||
with:
|
||||
token: ${{ env.TRUNK_API_TOKEN }}
|
||||
job-keys: windows-setup,held-install-journey
|
||||
github-token: ''
|
||||
enable-annotation: 'true'
|
||||
|
||||
windows-scope:
|
||||
# The paths filter above starts this workflow; this job decides whether
|
||||
# the Windows jobs run. A workflow_dispatch run always runs them.
|
||||
|
||||
@@ -31,6 +31,37 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Trunk Dynamic CI, LEARNING ONLY (GST-1847; free-tests.yml has the full
|
||||
# rationale). Trunk records whether it would have run or skipped windows-smoke
|
||||
# on same-repository pull requests; every job still runs. Trunk keys verdicts
|
||||
# by workflow file, so each observed workflow carries its own observer. No job
|
||||
# needs it, no condition reads it and it exports no outputs, so a mode change
|
||||
# in Trunk's dashboard cannot skip anything. It checks out nothing, runs no
|
||||
# pull-request code, holds no GITHUB_TOKEN scopes, never fails the run, and
|
||||
# skips fork pull requests, pushes and dispatches. docs/TESTING_INTERNALS.md.
|
||||
trunk-dynamic-ci:
|
||||
name: Trunk Dynamic CI (learning only, windows-smoke)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
permissions: {}
|
||||
env:
|
||||
TRUNK_API_TOKEN: ${{ secrets.TRUNK_API_TOKEN }}
|
||||
steps:
|
||||
- name: Record Trunk's verdicts for windows-smoke (nothing reads them)
|
||||
if: env.TRUNK_API_TOKEN != ''
|
||||
timeout-minutes: 3
|
||||
continue-on-error: true
|
||||
uses: trunk-io/dynamic-ci@5bcddbf3615c2b032c21eed1b514c91164bda4a8 # v0.1.7
|
||||
env:
|
||||
TRUNK_DISABLE_TELEMETRY: 'true'
|
||||
with:
|
||||
token: ${{ env.TRUNK_API_TOKEN }}
|
||||
job-keys: windows-smoke
|
||||
github-token: ''
|
||||
enable-annotation: 'true'
|
||||
|
||||
windows-smoke:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 15
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- **Trunk Dynamic CI now also watches the Windows checks, the quality gate and the eval jobs on pull requests, still in learning mode, and it still can't skip anything.** Trunk answers only for jobs in the workflow that asks, so `windows-smoke.yml`, `windows-scoped-tests.yml`, `windows-setup-e2e.yml`, `quality-gate.yml` and `evals.yml` each get the same learning-only `trunk-dynamic-ci` job that `free-tests.yml` already has. Trunk records whether it would have run or skipped `windows-smoke`, `windows-scoped-shard`, `windows-setup`, `held-install-journey`, `quality`, `build-image` and `eval-slices`. No job reads the answer, so every check still runs with its own name and result. Each observer runs one pinned action, checks out no code, holds no GitHub token scopes, skips fork pull requests, skips its step when the token is missing, and never fails the run. `test/trunk-dynamic-ci-observers.test.ts` holds every observer to that shape.
|
||||
+33
-12
@@ -1704,9 +1704,25 @@ whether it would have run or skipped `free-suite` and
|
||||
free-suite shards share one verdict. The verdicts show up in the job summary,
|
||||
on the pull request's Dynamic CI tab and at app.trunk.io/dynamic-ci.
|
||||
|
||||
Trunk keys verdicts by workflow file, so it answers only for jobs in the
|
||||
workflow that ran the action. So since GST-1847, the other costly pull-request
|
||||
workflows carry the same observer job, each named "Trunk Dynamic CI (learning
|
||||
only, <workflow>)":
|
||||
|
||||
| Workflow | Observed jobs |
|
||||
|---|---|
|
||||
| `free-tests.yml` | `free-suite`, `macos-named-regressions` |
|
||||
| `windows-smoke.yml` | `windows-smoke` |
|
||||
| `windows-scoped-tests.yml` | `windows-scoped-shard` |
|
||||
| `windows-setup-e2e.yml` | `windows-setup`, `held-install-journey` |
|
||||
| `quality-gate.yml` | `quality` |
|
||||
| `evals.yml` | `build-image`, `eval-slices` |
|
||||
|
||||
Everything below holds for every observer.
|
||||
|
||||
It never skips anything. No job needs the observer, no condition reads its
|
||||
verdicts, and it exports no outputs. Switching Trunk to enforcement in its
|
||||
dashboard would still skip nothing, because nothing in this workflow reads
|
||||
dashboard would still skip nothing, because nothing in its workflow reads
|
||||
the verdicts. Every test runs, and every required check keeps its name and
|
||||
its result. Merge evidence never reads Trunk: the `free-tests` aggregate,
|
||||
the CI reuse profile, `gstack-ci-carry`, the pregate, the train gate
|
||||
@@ -1722,28 +1738,33 @@ or commit disappears), the observer's own check reports failure, and
|
||||
`gstack-ship-queue ready` counts every check at the head. Rerun it once;
|
||||
if it fails again, delete the job. Fork pull requests, pushes and dispatches
|
||||
skip the job. A missing token skips only its step, so the job still fetches
|
||||
the action. It is the only job in the workflow that sees a secret
|
||||
(`TRUNK_API_TOKEN`), so it checks out nothing and runs none of the pull
|
||||
request's code. It holds no GITHUB_TOKEN scopes (`permissions: {}`). At the
|
||||
the action. Its only secret is `TRUNK_API_TOKEN` (in free-tests.yml it is
|
||||
the only job that sees any secret; evals.yml's other jobs hold their own), so
|
||||
it checks out nothing and runs none of the pull request's code. It holds no GITHUB_TOKEN scopes (`permissions: {}`). At the
|
||||
pinned commit (`src/config.ts`, `src/changed-files.ts`), the action makes one
|
||||
network call: a request for verdicts to
|
||||
`api.trunk.io/v2/dynamic-ci/generate-plan`, at most 3 attempts with a
|
||||
30-second budget each. Telemetry is off. With no checkout and an empty
|
||||
`github-token`, the action sends no file list, and Trunk reads the diff itself
|
||||
through its GitHub App.
|
||||
`test/free-tests-workflow-wiring.test.ts` and
|
||||
`test/ci-reuse-workflow-wiring.test.ts` lock that shape in place.
|
||||
`test/free-tests-workflow-wiring.test.ts`,
|
||||
`test/ci-reuse-workflow-wiring.test.ts` and
|
||||
`test/trunk-dynamic-ci-observers.test.ts` lock that shape in place.
|
||||
|
||||
**Review date: one week after it merges (2026-11-12 if it merges on
|
||||
2026-11-05).** Compare Trunk's would-skip verdicts with what the jobs it
|
||||
would have skipped actually found, and look hardest at any would-skip on a run
|
||||
where that job failed. Leave out runs where `reuse` skipped the suite: Trunk
|
||||
still records a verdict there, but there is no result to compare it with.
|
||||
Trunk's `required-check` vote treats both jobs as not
|
||||
required, because branch protection names the `free-tests` aggregate and
|
||||
where that job failed. Leave out runs where the workflow itself skipped the
|
||||
observed job, because Trunk still records a verdict there but there is no
|
||||
result to compare it with: `reuse` skipping the Free Tests suite,
|
||||
`windows-scope` skipping the scoped-test and setup Windows jobs on a pull
|
||||
request that touches no Windows path (`windows-smoke` always runs), and `eval-slices` on pull-request pushes, which run evals.yml
|
||||
in its screening-summary mode and plan no slices.
|
||||
In free-tests.yml, Trunk's `required-check` vote treats both observed jobs as
|
||||
not required, because branch protection names the `free-tests` aggregate and
|
||||
not the jobs under it. But `free-tests` needs both jobs and every shard
|
||||
receipt, so discount that vote when you judge the would-skip rate. Any skip
|
||||
added later also has to pass the aggregate's receipt check. Turning on
|
||||
receipt, so discount that vote when you judge their would-skip rate. Any skip
|
||||
added there later also has to pass the aggregate's receipt check. Turning on
|
||||
skipping is a separate decision for Garry, made after that review.
|
||||
|
||||
### Windows free lane
|
||||
|
||||
@@ -147,7 +147,7 @@ const dispatch = (ref = 'main') => ({ event_name: 'workflow_dispatch', actor: 'g
|
||||
event: { repository: { default_branch: 'main' } } });
|
||||
const INPUTS = { evals_all: true, base_ref: '', base_sha: '', expected_sha: '', nonce: '', pr_receipts: '', cases: '', prefix_shas: '', screen_pr: '', screen_merge_sha: '', screen_policy: '' };
|
||||
// rerun-plan is the free rule-16 planner (F1); its paid eval-rerun runs only when it plans a rerun.
|
||||
const TODAY_PR_JOBS = ['build-image', 'eval-slices', 'plan-slices', 'readiness', 'recover-receipts', 'rerun-plan', 'slices-comment', 'slices-report'];
|
||||
const TODAY_PR_JOBS = ['build-image', 'eval-slices', 'plan-slices', 'readiness', 'recover-receipts', 'rerun-plan', 'slices-comment', 'slices-report', 'trunk-dynamic-ci'];
|
||||
const TODAY_DISPATCH_JOBS = ['build-image', 'eval-slices', 'plan-slices', 'rerun-plan', 'slices-report'];
|
||||
const TRAIN_STEPS = ['Write the train record', 'Upload the train record', "Check the screen's PR head (Free Tests)", 'Plan the bisect (one executor per prefix)', 'Hand the bisect plan to its executors'];
|
||||
const train = { record_artifact: 'gstack-train-record', full_gate_check: 'gstack/full-gate' };
|
||||
@@ -188,14 +188,14 @@ describe('without a train block nothing changes (the inert proof)', () => {
|
||||
const fork = pr();
|
||||
(fork.event.pull_request.head.repo as { full_name: string }).full_name = 'fork/gstack';
|
||||
expect(simulate(evals, { github: fork, inputs: {}, outputs: { route: { ...ROUTE, mode: 'pr' } } }).jobs).toEqual(['build-image', 'route']);
|
||||
expect(simulate(evals, { github: { ...pr(), actor: 'dependabot[bot]' }, inputs: {} }).jobs).toEqual([]);
|
||||
expect(simulate(evals, { github: { ...pr(), actor: 'dependabot[bot]' }, inputs: {} }).jobs).toEqual(['trunk-dynamic-ci']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('under a train block (the gate edits, each only on its mode)', () => {
|
||||
test('a PR push starts no paid slice and waits for nothing: route, the image and the free screening summary only', () => {
|
||||
const sim = simulate(evals, { github: pr(), inputs: {}, outputs: { route: { ...ROUTE, ...train, mode: 'pr-summary' } } });
|
||||
expect(sim.jobs).toEqual(['build-image', 'route', 'screen-summary']);
|
||||
expect(sim.jobs).toEqual(['build-image', 'route', 'screen-summary', 'trunk-dynamic-ci']);
|
||||
expect(simulate(evals, { github: pr(), inputs: {}, outputs: { route: { ...ROUTE, mode: 'pr' } } }).jobs).toContain('readiness');
|
||||
const summary = evals.jobs['screen-summary']!;
|
||||
expect(summary.permissions).toEqual({ contents: 'read' });
|
||||
@@ -284,7 +284,7 @@ describe('under a train block (the gate edits, each only on its mode)', () => {
|
||||
describe('GSTA-48 open items', () => {
|
||||
test('a head the readiness job blocked replaces the stale verdict comment and plans nothing (P3-1)', () => {
|
||||
const sim = simulate(evals, { github: pr(), inputs: {}, outputs: { route: { ...ROUTE, mode: 'pr' } }, results: { readiness: 'failure' } });
|
||||
expect(sim.jobs).toEqual(['build-image', 'readiness', 'readiness-comment', 'route']);
|
||||
expect(sim.jobs).toEqual(['build-image', 'readiness', 'readiness-comment', 'route', 'trunk-dynamic-ci']);
|
||||
const comment = evals.jobs['readiness-comment']!;
|
||||
expect(comment.steps.some(st => st.uses)).toBe(false);
|
||||
expect(comment.steps[0]!.run).toContain('if [ -z "$COMMENT_ID" ]; then exit 0; fi');
|
||||
@@ -316,7 +316,7 @@ describe('rule-16 rerun wiring (F1)', () => {
|
||||
const sim = simulate(evals, { github: pr(), inputs: {}, outputs: { route: { ...ROUTE, mode: 'pr' }, 'rerun-plan': planned }, results });
|
||||
expect(sim.jobs, JSON.stringify(results)).toContain('slices-report');
|
||||
}
|
||||
for (const [id, job] of Object.entries(evals.jobs)) expect((job as Record<string, unknown>)['continue-on-error'], id).toBeUndefined();
|
||||
for (const [id, job] of Object.entries(evals.jobs).filter(([id]) => id !== 'trunk-dynamic-ci')) expect((job as Record<string, unknown>)['continue-on-error'], id).toBeUndefined();
|
||||
const report = evals.jobs['slices-report']!;
|
||||
const reconcile = report.steps.find(st => st.id === 'reconcile')!;
|
||||
expect(reconcile.run).toContain('echo "exit=${PIPESTATUS[0]}"');
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Trunk Dynamic CI observers (GST-1400, GST-1847; docs/TESTING_INTERNALS.md).
|
||||
*
|
||||
* Trunk keys its verdicts by workflow file, so every observed workflow carries
|
||||
* its own learning-only `trunk-dynamic-ci` job. Each one must stay an observer:
|
||||
* same-repository pull requests only, one pinned action, no checkout, no
|
||||
* GITHUB_TOKEN scopes, fail-open at step and job level, no outputs, and no job
|
||||
* that needs it or reads it. Then a mode change in Trunk's dashboard can never
|
||||
* skip, gate or fail a check, and the token never reaches fork pull requests or
|
||||
* pull-request code.
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const OBSERVED: Record<string, string> = {
|
||||
'free-tests.yml': 'free-suite,macos-named-regressions',
|
||||
'windows-smoke.yml': 'windows-smoke',
|
||||
'windows-scoped-tests.yml': 'windows-scoped-shard',
|
||||
'windows-setup-e2e.yml': 'windows-setup,held-install-journey',
|
||||
'quality-gate.yml': 'quality',
|
||||
'evals.yml': 'build-image,eval-slices',
|
||||
};
|
||||
const SECRET_REF = /secrets\s*(?:\.|\[)\s*['"]?[A-Za-z0-9_-]*|toJSON\(\s*secrets\b/gi;
|
||||
|
||||
describe('Trunk Dynamic CI observers', () => {
|
||||
// Value: protects=the learning-only contract for every observed workflow; fails_when=an observer runs on forks or non-PR events, checks out or runs code, gains token scopes, loses fail-open, exports outputs, names a job its workflow lacks, or any job needs or reads it, or the action is unpinned; seam=.github/workflows/*.yml; no_claim=what Trunk recommends
|
||||
for (const [file, keys] of Object.entries(OBSERVED)) {
|
||||
test(`${file}: observes ${keys} and nothing reads it`, () => {
|
||||
const wf = Bun.YAML.parse(fs.readFileSync(path.join(ROOT, '.github/workflows', file), 'utf8')) as any;
|
||||
expect(Object.keys(wf.on)).toContain('pull_request');
|
||||
const trunk = wf.jobs['trunk-dynamic-ci'];
|
||||
expect(trunk.if).toBe("github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository");
|
||||
expect(trunk['runs-on']).toBe('ubuntu-24.04');
|
||||
expect(trunk.permissions).toEqual({});
|
||||
expect(trunk['continue-on-error']).toBe(true);
|
||||
expect(trunk['timeout-minutes']).toBe(5);
|
||||
expect(trunk.outputs).toBeUndefined();
|
||||
expect(trunk.needs).toBeUndefined();
|
||||
expect(trunk.env).toEqual({ TRUNK_API_TOKEN: '${{ secrets.TRUNK_API_TOKEN }}' });
|
||||
expect(trunk.steps).toHaveLength(1);
|
||||
const [step] = trunk.steps;
|
||||
expect(step.uses).toBe('trunk-io/dynamic-ci@5bcddbf3615c2b032c21eed1b514c91164bda4a8');
|
||||
expect(step.run).toBeUndefined();
|
||||
expect(step.if).toBe("env.TRUNK_API_TOKEN != ''");
|
||||
expect(step['continue-on-error']).toBe(true);
|
||||
expect(step['timeout-minutes']).toBe(3);
|
||||
expect(step.env).toEqual({ TRUNK_DISABLE_TELEMETRY: 'true' });
|
||||
expect(step.with).toEqual({ token: '${{ env.TRUNK_API_TOKEN }}', 'job-keys': keys, 'github-token': '', 'enable-annotation': 'true' });
|
||||
for (const key of keys.split(',')) expect([key, wf.jobs[key] !== undefined]).toEqual([key, true]);
|
||||
for (const [name, job] of Object.entries(wf.jobs as Record<string, any>).filter(([n]) => n !== 'trunk-dynamic-ci')) {
|
||||
expect([name, [job.needs ?? []].flat().includes('trunk-dynamic-ci')]).toEqual([name, false]);
|
||||
expect([name, /trunk|dynamic-ci|TRUNK_API_TOKEN/i.test(JSON.stringify(job))]).toEqual([name, false]);
|
||||
}
|
||||
const trunkRefs = (JSON.stringify(wf.jobs).match(SECRET_REF) ?? []).filter(ref => ref === 'secrets.TRUNK_API_TOKEN');
|
||||
expect(trunkRefs).toEqual(['secrets.TRUNK_API_TOKEN']);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -112,12 +112,12 @@ test('a record_durations dispatch times each Windows-safe file alone and uploads
|
||||
expect(job.steps.at(-1).if).toBe("always() && steps.seed.outcome == 'success'");
|
||||
});
|
||||
|
||||
test('windows-smoke is the PR gate: every pull request and main push, one windows-latest job', () => {
|
||||
test('windows-smoke is the PR gate: every pull request and main push, one windows-latest job beside the Trunk observer', () => {
|
||||
expect(Object.keys(smoke.on).sort()).toEqual(['pull_request', 'push', 'workflow_dispatch']);
|
||||
expect(smoke.on.pull_request).toEqual({ branches: ['main'] });
|
||||
expect(smoke.on.push).toEqual({ branches: ['main'] });
|
||||
expect(smoke.permissions).toEqual({ contents: 'read' });
|
||||
expect(Object.keys(smoke.jobs)).toEqual(['windows-smoke']);
|
||||
expect(Object.keys(smoke.jobs)).toEqual(['trunk-dynamic-ci', 'windows-smoke']);
|
||||
const job = smoke.jobs['windows-smoke'];
|
||||
expect(job['runs-on']).toBe('windows-latest');
|
||||
expect(job.if).toBeUndefined();
|
||||
|
||||
@@ -130,10 +130,10 @@ function prRepo(files: string[], lines: Record<string, string> = {}) {
|
||||
}
|
||||
|
||||
describe('windows-smoke stays on every pull request', () => {
|
||||
test('no paths filter, no job condition, one job', () => {
|
||||
test('no paths filter, no job condition, one test job beside the Trunk observer', () => {
|
||||
expect(smoke.on.pull_request).toEqual({ branches: ['main'] });
|
||||
expect(smoke.on.push).toEqual({ branches: ['main'] });
|
||||
expect(Object.keys(smoke.jobs)).toEqual(['windows-smoke']);
|
||||
expect(Object.keys(smoke.jobs)).toEqual(['trunk-dynamic-ci', 'windows-smoke']);
|
||||
expect(smoke.jobs['windows-smoke'].if).toBeUndefined();
|
||||
expect(smoke.jobs['windows-smoke'].needs).toBeUndefined();
|
||||
});
|
||||
@@ -159,7 +159,7 @@ describe('Windows Setup E2E runs by install path', () => {
|
||||
const scope = setupE2e.jobs['windows-scope'];
|
||||
expect(scope['runs-on']).toBe('ubuntu-24.04');
|
||||
expect(scope.steps[0].with).toEqual({ 'fetch-depth': 2, 'persist-credentials': false });
|
||||
const windowsJobs = Object.keys(setupE2e.jobs).filter(name => name !== 'windows-scope');
|
||||
const windowsJobs = Object.keys(setupE2e.jobs).filter(name => name !== 'windows-scope' && name !== 'trunk-dynamic-ci');
|
||||
expect(windowsJobs).toEqual(['windows-setup', 'held-install-journey']);
|
||||
for (const name of windowsJobs) {
|
||||
expect([name, setupE2e.jobs[name]['runs-on'], setupE2e.jobs[name].needs, setupE2e.jobs[name].if])
|
||||
@@ -198,12 +198,12 @@ describe('Windows Setup E2E runs by install path', () => {
|
||||
});
|
||||
|
||||
describe('the path-scoped Windows shard on pull requests', () => {
|
||||
test('every pull request runs the cheap scope job; a dispatch names its files; read-only and secretless', () => {
|
||||
test('every pull request runs the cheap scope job; a dispatch names its files; read-only and secretless outside the Trunk observer', () => {
|
||||
expect(Object.keys(scoped.on).sort()).toEqual(['pull_request', 'workflow_dispatch']);
|
||||
expect(scoped.on.pull_request).toEqual({ branches: ['main'] });
|
||||
expect(scoped.on.workflow_dispatch.inputs.files).toMatchObject({ type: 'string', required: true });
|
||||
expect(scoped.permissions).toEqual({ contents: 'read' });
|
||||
expect(scopedText).not.toContain('secrets.');
|
||||
expect(scopedText.match(/secrets\s*(?:\.|\[)\s*['"]?[A-Za-z0-9_-]*|toJSON\(\s*secrets\b/gi)).toEqual(['secrets.TRUNK_API_TOKEN']);
|
||||
expect(scopedText).not.toContain('pull_request_target');
|
||||
expect(scoped.concurrency.group).toBe('windows-scoped-tests-${{ github.event.pull_request.number || github.run_id }}');
|
||||
const scope = scoped.jobs['windows-scope'];
|
||||
|
||||
Reference in new issue
Block a user