code-foundry 1.22.1 → 1.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.github/workflows/cloudflare-delivery.yml +134 -1
  2. package/.github/workflows/cloudflare-deploy.yml +140 -1
  3. package/.github/workflows/consumer-qualification.yml +3 -3
  4. package/.github/workflows/eval.yml +116 -0
  5. package/.github/workflows/qualified-foundry-publish.yml +4 -5
  6. package/.github/workflows/release_self-ci.yml +24 -10
  7. package/.github/workflows/validation-no-codeql.yml +34 -6
  8. package/.github/workflows/validation.yml +32 -5
  9. package/.github/workflows/validation_audit_self-ci.yml +1 -0
  10. package/.github/workflows/validation_self-ci.yml +7 -1
  11. package/AGENTS.md +10 -0
  12. package/CHANGELOG.md +77 -0
  13. package/README.md +4 -1
  14. package/docs/CONFIGURATION.md +20 -13
  15. package/docs/EVALS.md +139 -0
  16. package/docs/PUBLISHING.md +6 -1
  17. package/docs/WORKFLOWS.md +27 -4
  18. package/docs/cloudflare-delivery.md +47 -25
  19. package/docs/consumer-qualification.md +1 -1
  20. package/docs/fleet-release-eligibility.md +1 -5
  21. package/docs/qualified-publication.md +1 -1
  22. package/docs/required-capabilities.md +6 -4
  23. package/package.json +3 -3
  24. package/src/commands/doctor.mjs +5 -2
  25. package/src/commands/fleet-core.mjs +1 -1
  26. package/src/commands/qualified-publication.mjs +81 -3
  27. package/src/commands/release-integrity.mjs +37 -14
  28. package/src/commands/sync.mjs +3 -2
  29. package/src/lib/docs-only.mjs +39 -0
  30. package/src/lib/eval-envelope.mjs +234 -0
  31. package/src/lib/fleet-manifest.mjs +9 -5
  32. package/src/lib/merge-queue.mjs +1 -0
  33. package/src/lib/overlay.mjs +1 -1
  34. package/src/lib/release-manifest.mjs +1 -1
  35. package/src/lib/release-policy.mjs +2 -2
  36. package/src/lib/task-policy.mjs +7 -0
  37. package/src/lib/validation-policy.mjs +10 -6
  38. package/src/runtime-core.mjs +187 -10
  39. package/src/runtime.mjs +2 -0
  40. package/src/templates/gitignore +2 -0
@@ -37,7 +37,18 @@ jobs:
37
37
  ```
38
38
 
39
39
  `artifact-path` is relative to `working-directory`; `install-working-directory`
40
- selects the lockfile root. The selected output tree is hashed before and after
40
+ selects the lockfile root. For Turbo monorepos, filtering is automatic when
41
+ `working-directory` points to the application package and the lockfile root
42
+ contains `turbo.json` or `turbo.jsonc`. The workflow resolves the package name,
43
+ then compares pull-request base/head commits or push before/after commits with
44
+ `turbo query affected --packages`; shared-package changes therefore retain
45
+ dependent app deployments. Unaffected app calls skip the build and Cloudflare
46
+ deployment. Set `turbo-filter` only to override the detected package name; set
47
+ it to an empty string to disable filtering. Full history is fetched for the
48
+ comparison. Manual dispatches deploy conservatively because they do not provide
49
+ a reliable comparison range.
50
+
51
+ The selected output tree is hashed before and after
41
52
  upload; changes during upload fail. Include all built code/assets in that tree
42
53
  and disable duplicate custom build steps. This digest identifies the declared
43
54
  local build tree, not a Cloudflare-signed digest of every uploaded configuration
@@ -71,12 +82,16 @@ not sufficient.
71
82
  ## Approvals, ordering, and evidence
72
83
 
73
84
  Create and protect the fixed `Preview` and `Production` GitHub environments
74
- before adoption. The workflow references them at the **job** level; naming an
75
- environment does not itself configure reviewers or branch restrictions. Configure
76
- required reviewers and branch restrictions on `Production` separately. Production
77
- execution requires the current default-branch commit and
78
- rechecks freshness after approval and before final promotion. Fork PRs and
79
- `pull_request_target` execution are excluded.
85
+ before adoption. The workflow references them at the **job** level and disables
86
+ GitHub's implicit deployment record; the explicit application record is the sole
87
+ entry in deployment history. Required reviewers, wait timers, and branch
88
+ restrictions still apply. GitHub custom deployment protection rules are not
89
+ compatible with `deployment: false`; use the explicit record without that rule or
90
+ keep automatic deployment records enabled. Configure required reviewers and
91
+ branch restrictions on `Production` separately. Production execution requires the
92
+ current default-branch commit and rechecks freshness after approval and before
93
+ final promotion. Fork PRs, release-please PRs, and `pull_request_target` execution
94
+ are excluded from preview delivery.
80
95
 
81
96
  Per-repository/Worker/mode concurrency never cancels a running promotion. GitHub
82
97
  concurrency is not a FIFO queue; stale-source rejection is still necessary.
@@ -88,14 +103,15 @@ and new production workflows active.
88
103
 
89
104
  Outputs include preview URL, exact version ID, source SHA, declared build-tree
90
105
  SHA-256 digest, Cloudflare deployment ID, and GitHub application deployment ID.
91
- Explicit application deployment records receive in-progress and success/failure
92
- statuses, in addition to GitHub's environment-job records. For pull requests,
93
- the deployment record is associated with `github.event.pull_request.head.sha`,
94
- not GitHub Actions' merge commit, so GitHub can display the preview in the PR's
95
- Deployments section. Direct pushes use `github.sha`. Sanitized candidate and
96
- production JSON evidence is uploaded even on failure. Binding values, API bodies,
97
- and credentials are not copied into those reports. Forced runner termination may
98
- prevent final status steps; the GitHub job still reflects cancellation/failure.
106
+ The workflows disable GitHub's automatic environment deployment object and use
107
+ one explicit application deployment record, which receives in-progress and
108
+ success/failure statuses. For pull requests, that record is associated with
109
+ `github.event.pull_request.head.sha`, not GitHub Actions' merge commit, so GitHub
110
+ can display the preview in the PR's Deployments section. Direct pushes use
111
+ `github.sha`. Sanitized candidate and production JSON evidence is uploaded even
112
+ on failure. Binding values, API bodies, and credentials are not copied into those
113
+ reports. Forced runner termination may prevent final status steps; the GitHub job
114
+ still reflects cancellation/failure.
99
115
 
100
116
  ## Stateful resources and isolation
101
117
 
@@ -171,18 +187,24 @@ the deployment API intentionally changes version routing only.
171
187
 
172
188
  ## Legacy workflow hardening
173
189
 
174
- `cloudflare-deploy.yml` uses real job environments, non-cancelling concurrency,
175
- structured Wrangler output, exact/local Wrangler selection, reusable outputs, and
176
- in-progress/failure deployment records. Its preview deployment records use the
177
- pull request head SHA when called from a PR, so completed previews appear in that
178
- PR's Deployments section; direct pushes use the workflow SHA. Its compatibility
190
+ `cloudflare-deploy.yml` uses real job environments with automatic deployment
191
+ history disabled, non-cancelling concurrency, structured Wrangler output,
192
+ exact/local Wrangler selection, reusable outputs, and one explicit
193
+ in-progress/failure deployment record. Preview records use the pull request head
194
+ SHA when called from a PR, so completed previews appear in that PR's Deployments
195
+ section; direct pushes use `github.sha`. Preview delivery skips release-please PRs,
196
+ which are merged and closed by release automation rather than being application
197
+ previews. Its compatibility
179
198
  default remains `latest`; callers should prefer `local` or an exact version for
180
199
  reproducibility. A production URL can be supplied with `deployment-url` when API
181
- output contains only route patterns. It is still a **direct, unverified deployment**;
182
- adopt `cloudflare-delivery.yml` for candidate verification and
183
- guarded promotion. Consumers pinned to an older Code Foundry release must update
184
- their reusable workflow reference; existing deployments are not retroactively
185
- re-associated with the PR head commit.
200
+ output contains only route patterns. Turbo filtering also applies to production
201
+ pushes, which use the previous and current commit to skip unaffected
202
+ applications. It is still a **direct, unverified
203
+ deployment**; adopt `cloudflare-delivery.yml` for candidate verification and
204
+ guarded promotion.
205
+ Consumers pinned to an older Code Foundry release must update their reusable
206
+ workflow reference; existing deployments are not retroactively re-associated
207
+ with the PR head commit.
186
208
 
187
209
  ## References and testing
188
210
 
@@ -6,7 +6,7 @@ Release-candidate compatibility checks against the distributable package.
6
6
  **Scope:** Package installation, CLI initialization/synchronization, generated workflow contracts.
7
7
 
8
8
  `consumer-qualification.yml` packs the checked-out candidate once, shares that
9
- archive across Node 20, 22, and 24 using an artifact scoped to the same workflow
9
+ archive across Node 24 and 26 using an artifact scoped to the same workflow
10
10
  run **and attempt**, and installs it offline with lifecycle scripts disabled,
11
11
  and executes its public CLI from the installed package. The harness does not
12
12
  import the source checkout's initializer as a substitute for package testing.
@@ -16,11 +16,7 @@ guard, and `--force` cannot bypass source identity or qualification.
16
16
  "repository": "owner/code-foundry",
17
17
  "workflow": ".github/workflows/release_self-ci.yml",
18
18
  "branch": "main",
19
- "required_jobs": [
20
- "Qualify consumers / Node 20",
21
- "Qualify consumers / Node 22",
22
- "Qualify consumers / Node 24"
23
- ]
19
+ "required_jobs": ["Qualify consumers / Node 24", "Qualify consumers / Node 26"]
24
20
  }
25
21
  ```
26
22
 
@@ -9,7 +9,7 @@ consumer release callers keep the ordinary `release.yml` behavior.
9
9
  The self-release pipeline is intentionally staged:
10
10
 
11
11
  1. `consumer-qualification.yml` packs the candidate once and qualifies it across
12
- Node 20, 22, and 24.
12
+ Node 24 and 26.
13
13
  2. Release Please creates a draft release for the qualified source.
14
14
  3. The staging job attaches the exact archive and a digest-bound qualification
15
15
  receipt, publishes the immutable GitHub Release, and verifies its identity.
@@ -5,8 +5,9 @@ Optional task discovery remains available. Configure scalar values in
5
5
  `.github/code-foundry.yml`:
6
6
 
7
7
  ```yaml
8
- required_capabilities: type_check,unit,e2e,performance,coverage
8
+ required_capabilities: type_check,unit,e2e,eval,performance,coverage
9
9
  performance: true
10
+ eval: true
10
11
  coverage_enforcement: required
11
12
  coverage_minimum: 80
12
13
  coverage_metrics: lines,branches
@@ -14,10 +15,11 @@ coverage_report: coverage/coverage-summary.json
14
15
  ```
15
16
 
16
17
  Supported task capabilities are `format`, `lint`, `type_check`, `build`, `unit`,
17
- `integration`, `e2e`, `smoke`, and `performance`. `coverage` additionally requires
18
+ `integration`, `e2e`, `smoke`, `eval`, and `performance`. `coverage` additionally requires
18
19
  unit tests. Unknown names, contradictory requirements, and invalid thresholds
19
- are errors. `performance: true` means required, not merely enabled when a
20
- script happens to exist. Use `performance: auto` to retain optional discovery.
20
+ are errors. `performance: true` and `eval: true` mean required, not merely
21
+ enabled when a script happens to exist. Use `performance: auto` or
22
+ `eval: auto` to retain optional discovery.
21
23
 
22
24
  The public `src/runtime.mjs` entrypoint delegates ecosystem execution to the
23
25
  private `src/runtime-core.mjs`. Keep both files and `src/lib` when vendoring the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-foundry",
3
- "version": "1.22.1",
3
+ "version": "1.28.2",
4
4
  "description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
5
5
  "homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
6
6
  "bugs": {
@@ -42,7 +42,7 @@
42
42
  "build": "node --check src/cli.mjs",
43
43
  "format:check": "oxfmt --check .",
44
44
  "fmt": "oxfmt .",
45
- "lint": "oxlint",
45
+ "lint": "oxlint --deny-warnings",
46
46
  "performance:check": "node scripts/performance-check.mjs",
47
47
  "test": "node --test",
48
48
  "test:unit": "node --test",
@@ -57,7 +57,7 @@
57
57
  "typescript": "^7.0.2"
58
58
  },
59
59
  "engines": {
60
- "node": ">=20"
60
+ "node": ">=24"
61
61
  },
62
62
  "packageManager": "bun@1.4.0"
63
63
  }
@@ -9,6 +9,11 @@ import { packageManagerForLockfile } from '../lib/lockfiles.mjs'
9
9
  import { doctorGithub } from '../lib/github-doctor.mjs'
10
10
  import { isGeneratedEventCaller } from './sync.mjs'
11
11
 
12
+ /** @param {string} message */
13
+ function warn(message) {
14
+ console.warn(`WARN: ${message}`)
15
+ }
16
+
12
17
  /** @param {string} root @param {{ github?: boolean }} [options] */
13
18
  export function doctor(root, options = {}) {
14
19
  const target = resolve(root)
@@ -21,8 +26,6 @@ export function doctor(root, options = {}) {
21
26
  console.error(`ERROR: ${message}`)
22
27
  errors += 1
23
28
  }
24
- /** @param {string} message */
25
- const warn = (message) => console.warn(`WARN: ${message}`)
26
29
 
27
30
  const toolchain = config.toolchain ?? 'auto'
28
31
  if (!['auto', 'native', 'mise'].includes(toolchain)) error(`unsupported toolchain: ${toolchain}`)
@@ -33,7 +33,7 @@ export function discoverRepositories(root) {
33
33
  gitWorkflow: config.git_workflow ?? 'direct',
34
34
  })
35
35
  }
36
- return result.sort((a, b) => a.path.localeCompare(b.path))
36
+ return result.toSorted((a, b) => a.path.localeCompare(b.path))
37
37
  }
38
38
 
39
39
  /** @param {string} root @param {string} source @param {{ createPr?: boolean, dryRun?: boolean, force?: boolean, version: string, exclude?: string[] }} options */
@@ -16,7 +16,7 @@ import { join, resolve } from 'node:path'
16
16
  import { pathToFileURL } from 'node:url'
17
17
  import { tmpdir } from 'node:os'
18
18
 
19
- export const REQUIRED_NODES = ['20', '22', '24']
19
+ export const REQUIRED_NODES = ['24', '26']
20
20
  export const REQUIRED_FIXTURES = [
21
21
  'npm-direct',
22
22
  'pnpm-direct',
@@ -46,6 +46,24 @@ export function runCommand(command, args, cwd) {
46
46
  )
47
47
  return result.stdout
48
48
  }
49
+
50
+ /** Default tolerant runner: GitHub CLI failures surface as retryable misses
51
+ * instead of aborting the publication flow.
52
+ * @param {string} command
53
+ * @param {string[]} args
54
+ * @param {string} [cwd]
55
+ * @returns {{status: number, stdout: string}}
56
+ */
57
+ function defaultRunTolerant(command, args, cwd) {
58
+ const result = spawnSync(command, args, {
59
+ cwd,
60
+ encoding: 'utf8',
61
+ timeout: 180_000,
62
+ maxBuffer: 8 * 1024 * 1024,
63
+ env: { ...process.env, GH_HOST: 'github.com', GH_PROMPT_DISABLED: '1' },
64
+ })
65
+ return { status: result.error ? 1 : (result.status ?? 1), stdout: result.stdout ?? '' }
66
+ }
49
67
  /** @param {Candidate} candidate */
50
68
  export function validateCandidate(candidate) {
51
69
  ensure(
@@ -225,10 +243,50 @@ export async function publishQualifiedArchive(candidate, reports, adapters = {})
225
243
  return { ...before, status: 'published' }
226
244
  }
227
245
 
246
+ /** @typedef {(command:string, args:string[], cwd?:string) => {status: number, stdout: string}} RunTolerant */
247
+
248
+ /** Poll the releases-by-tag endpoint through its eventual-consistency window.
249
+ * Immediately after `gh release edit --draft=false` the endpoint can still
250
+ * return 404; a strict verification in that window aborts an otherwise
251
+ * healthy publication. Bounded retries keep the flow strict: it only proceeds
252
+ * once the endpoint serves the published release with the exact tag.
253
+ * @param {Candidate} candidate
254
+ * @param {{runTolerant?: RunTolerant, delay?: (ms:number)=>Promise<void>, attempts?: number}} [adapters]
255
+ * @returns {Promise<Record<string, any>>}
256
+ */
257
+ async function waitForPublishedRelease(candidate, adapters = {}) {
258
+ const runTolerant = adapters.runTolerant ?? defaultRunTolerant
259
+ const delay = adapters.delay ?? ((ms) => new Promise((done) => setTimeout(done, ms)))
260
+ const attempts = adapters.attempts ?? 15
261
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
262
+ const result = runTolerant('gh', [
263
+ 'api',
264
+ '--hostname',
265
+ 'github.com',
266
+ `repos/${candidate.repository}/releases/tags/${encodeURIComponent(candidate.tag)}`,
267
+ ])
268
+ if (result.status === 0) {
269
+ /** @type {Record<string, any> | null} */
270
+ let release = null
271
+ try {
272
+ release = JSON.parse(result.stdout)
273
+ } catch {
274
+ release = null
275
+ }
276
+ if (release?.draft === false && release?.tag_name === candidate.tag) return release
277
+ }
278
+ if (attempt === attempts) break
279
+ await delay(2_000)
280
+ }
281
+ throw new Error(
282
+ 'The published release was not retrievable via the tag endpoint within the consistency window'
283
+ )
284
+ }
285
+
228
286
  /** Stage only a previously-created draft: never move tags, overwrite assets, or
229
287
  * treat an API/permissions failure as an absent release. Authentication needs
230
288
  * immutable-settings read and release write; no setting is mutated.
231
- * @param {Candidate} candidate @param {string[]} reports @param {{run?:Run, verify?:Verify}} [adapters]
289
+ * @param {Candidate} candidate @param {string[]} reports @param {{run?:Run, runTolerant?:RunTolerant, delay?:(ms:number)=>Promise<void>, attempts?:number, verify?:Verify}} [adapters]
232
290
  */
233
291
  export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
234
292
  const run = adapters.run ?? runCommand
@@ -332,7 +390,27 @@ export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
332
390
  '--repo',
333
391
  candidate.repository,
334
392
  ])
335
- const identity = await (adapters.verify ?? verifier)(candidate)
393
+ // GitHub's release index is eventually consistent right after a draft is
394
+ // published; a strict verification a second later can observe a 404 and
395
+ // abort the publication even though the release is live. Poll until the
396
+ // index serves the release, then verify; the verification cycle itself
397
+ // retries through the same window and still fails closed in the end.
398
+ await waitForPublishedRelease(candidate, {
399
+ runTolerant: adapters.runTolerant,
400
+ delay: adapters.delay,
401
+ attempts: 10,
402
+ })
403
+ /** @type {Record<string, any>} */
404
+ let identity
405
+ for (let attempt = 1; ; attempt += 1) {
406
+ try {
407
+ identity = await (adapters.verify ?? verifier)(candidate)
408
+ break
409
+ } catch (error) {
410
+ if (attempt >= 10) throw error
411
+ await (adapters.delay ?? ((ms) => new Promise((done) => setTimeout(done, ms))))(5_000)
412
+ }
413
+ }
336
414
  ensure(
337
415
  identity.status === 'passed' &&
338
416
  identity.immutable === true &&
@@ -7,7 +7,7 @@ import { basename, isAbsolute, relative, resolve, sep } from 'node:path'
7
7
  import { spawnSync } from 'node:child_process'
8
8
  import { pathToFileURL } from 'node:url'
9
9
 
10
- /** @typedef {(argv: string[]) => {status: number|null, stdout: string}} Runner */
10
+ /** @typedef {(argv: string[]) => {status: number|null, stdout: string, stderr?: string}} Runner */
11
11
 
12
12
  /** @type {Runner} */
13
13
  export function runGh(argv) {
@@ -18,7 +18,7 @@ export function runGh(argv) {
18
18
  env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_HOST: 'github.com' },
19
19
  })
20
20
  if (result.error) throw new Error(`GitHub CLI unavailable: ${result.error.name}`)
21
- return { status: result.status, stdout: result.stdout ?? '' }
21
+ return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
22
22
  }
23
23
 
24
24
  /** @param {string} repository */
@@ -56,7 +56,7 @@ function json(run, argv) {
56
56
  const result = run(argv)
57
57
  if (result.status !== 0)
58
58
  throw new Error(
59
- `GitHub verification command failed (${result.status ?? 'signal'}); state is unverified`
59
+ `GitHub verification command failed (${result.status ?? 'signal'}); state is unverified: ${String(result.stderr ?? '').slice(0, 300) || 'no stderr captured'}`
60
60
  )
61
61
  try {
62
62
  return JSON.parse(result.stdout)
@@ -197,6 +197,17 @@ export function verifyRelease(options, run = runGh) {
197
197
  }
198
198
  }
199
199
 
200
+ /** Sync pause used by the post-publication verifier while the release index
201
+ * catches up with a just-published release.
202
+ * @param {number} ms
203
+ */
204
+ function sleepSync(ms) {
205
+ if (ms <= 0) return
206
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
207
+ }
208
+
209
+ const RELEASE_VERIFICATION_ATTEMPTS = 6
210
+
200
211
  /** @param {string[]} argv @param {Runner} [run] */
201
212
  export function integrityCommand(argv, run = runGh) {
202
213
  const [command, ...args] = argv
@@ -228,17 +239,29 @@ export function integrityCommand(argv, run = runGh) {
228
239
  throw new Error('An argument is not supported by this integrity command')
229
240
  if (command === 'settings') return verifyImmutableSetting(values['--repo'] ?? '', run)
230
241
  if (command === 'manifest') return assetManifest(values['--root'] ?? process.cwd(), assets)
231
- if (command === 'release')
232
- return verifyRelease(
233
- {
234
- repository: values['--repo'] ?? '',
235
- tag: values['--tag'] ?? '',
236
- root: values['--root'],
237
- expectedSha: values['--expected-sha'],
238
- assets,
239
- },
240
- run
241
- )
242
+ if (command === 'release') {
243
+ const options = {
244
+ repository: values['--repo'] ?? '',
245
+ tag: values['--tag'] ?? '',
246
+ root: values['--root'],
247
+ expectedSha: values['--expected-sha'],
248
+ assets,
249
+ }
250
+ // The lane fires the instant GitHub marks the release published, while the
251
+ // releases-by-tag index can still lag behind. Retry through that window;
252
+ // every strict check still runs on every attempt and the command fails
253
+ // closed once the bounded window is exhausted.
254
+ let lastError
255
+ for (let attempt = 1; attempt <= RELEASE_VERIFICATION_ATTEMPTS; attempt += 1) {
256
+ try {
257
+ return verifyRelease(options, run)
258
+ } catch (error) {
259
+ lastError = error
260
+ if (attempt < RELEASE_VERIFICATION_ATTEMPTS) sleepSync(5_000)
261
+ }
262
+ }
263
+ throw lastError
264
+ }
242
265
  throw new Error('Use release-integrity settings, release, or manifest')
243
266
  }
244
267
 
@@ -354,7 +354,7 @@ function synchronize(options) {
354
354
  const mergedManifest = {}
355
355
  for (const directory of [
356
356
  ...new Set([...Object.keys(releaseManifest), ...Object.keys(existingManifest)]),
357
- ].sort()) {
357
+ ].toSorted()) {
358
358
  mergedManifest[directory] = existingManifest[directory] ?? releaseManifest[directory]
359
359
  }
360
360
  const content = `${JSON.stringify(mergedManifest, null, 2)}\n`
@@ -652,7 +652,7 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
652
652
  // self-only jobs whose outputs and environments do not exist for them.
653
653
  for (const job of ['qualification', 'preflight', 'recovery', 'stage', 'publish'])
654
654
  rendered = removeWorkflowBlock(rendered, job)
655
- rendered = rendered.replace(/^ needs: \[qualification, preflight\]\n/m, '')
655
+ rendered = rendered.replace(/^ needs: \[preflight\]\n/m, '')
656
656
  rendered = rendered.replace(/^ config-file: \.github\/release-please-foundry\.json\n/m, '')
657
657
  rendered = rendered.replace(/^ defer-publication: true\n/m, '')
658
658
  rendered = rendered.replace(
@@ -714,6 +714,7 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
714
714
  'codeql-runner': config.codeql_runner ?? config.runner,
715
715
  'unit-runner': config.unit_runner,
716
716
  'performance-runner': config.performance_runner ?? config.test_runner ?? config.runner,
717
+ 'eval-runner': config.eval_runner ?? config.runner,
717
718
  }
718
719
  for (const [input, value] of Object.entries(runnerInputs)) {
719
720
  if (!value) continue
@@ -0,0 +1,39 @@
1
+ // @ts-check
2
+
3
+ import { execFileSync } from 'node:child_process'
4
+
5
+ /** Docs-only allowlist: markdown, docs/, and license roots. Code, lockfiles,
6
+ * workflows, configuration, and packaging always keep the audit tier. */
7
+ const DOCS_ONLY_PATTERNS = [/\.md$/i, /^docs\//, /^LICENSE/i]
8
+
9
+ /** @param {string} path */
10
+ export function isDocsOnlyPath(path) {
11
+ return DOCS_ONLY_PATTERNS.some((pattern) => pattern.test(path))
12
+ }
13
+
14
+ /**
15
+ * Report whether a pull request changes docs only. Fail-closed: any git
16
+ * failure, unresolvable base, or non-docs path keeps the audit tier.
17
+ * @param {string} root
18
+ * @param {string} baseRef
19
+ */
20
+ export function docsOnlyPullRequest(root, baseRef) {
21
+ try {
22
+ execFileSync('git', ['fetch', '--no-tags', '--depth=100', 'origin', baseRef], {
23
+ cwd: root,
24
+ stdio: 'pipe',
25
+ })
26
+ const output = execFileSync('git', ['diff', '--name-only', 'FETCH_HEAD...HEAD'], {
27
+ cwd: root,
28
+ encoding: 'utf8',
29
+ stdio: ['ignore', 'pipe', 'pipe'],
30
+ })
31
+ const paths = output
32
+ .split('\n')
33
+ .map((line) => line.trim())
34
+ .filter((line) => line.length > 0)
35
+ return paths.length > 0 && paths.every(isDocsOnlyPath)
36
+ } catch {
37
+ return false
38
+ }
39
+ }