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
@@ -36,6 +36,11 @@ on:
36
36
  required: false
37
37
  type: string
38
38
  default: '.'
39
+ turbo-filter:
40
+ description: 'Optional Turbo package name override; auto detects it from working-directory by default.'
41
+ required: false
42
+ type: string
43
+ default: auto
39
44
  build-script:
40
45
  required: false
41
46
  type: string
@@ -115,16 +120,143 @@ concurrency:
115
120
  cancel-in-progress: false
116
121
 
117
122
  jobs:
118
- candidate:
123
+ affected:
124
+ name: Check Turbo delivery impact
119
125
  if: >-
126
+ inputs['turbo-filter'] != '' &&
127
+ (github.event_name == 'pull_request' || github.event_name == 'push') &&
120
128
  vars.CI_BILLING_PAUSED != 'true' &&
121
129
  (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
130
+ (github.event_name != 'pull_request' || inputs.mode != 'preview' || !startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')) &&
122
131
  github.event_name != 'pull_request_target' &&
123
132
  (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
124
133
  runs-on: ubuntu-latest
134
+ timeout-minutes: 10
135
+ permissions:
136
+ contents: read
137
+ outputs:
138
+ should_deploy: ${{ steps.check.outputs.should_deploy || steps.resolve.outputs.should_deploy }}
139
+ steps:
140
+ - name: Checkout full history
141
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
142
+ with:
143
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
144
+ fetch-depth: 0
145
+ persist-credentials: false
146
+ - name: Resolve Turbo package
147
+ id: resolve
148
+ working-directory: ${{ inputs.install-working-directory }}
149
+ env:
150
+ TURBO_FILTER: ${{ inputs.turbo-filter }}
151
+ WORKSPACE: ${{ github.workspace }}
152
+ WORKING_DIRECTORY: ${{ inputs.working-directory }}
153
+ run: |
154
+ node --input-type=module <<'NODE'
155
+ import { appendFileSync, existsSync, readFileSync } from 'node:fs'
156
+ import { isAbsolute, relative, resolve, sep } from 'node:path'
157
+ const output = (name, value) => appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`)
158
+ const conservative = (reason) => {
159
+ console.log(`${reason}; deploying conservatively.`)
160
+ output('turbo_enabled', 'false')
161
+ output('should_deploy', 'true')
162
+ }
163
+ const filter = process.env.TURBO_FILTER
164
+ if (!filter || /[\r\n\0]/.test(filter)) throw new Error('Invalid turbo-filter')
165
+ const workspace = process.env.WORKSPACE
166
+ const appDirectory = resolve(workspace, process.env.WORKING_DIRECTORY)
167
+ const workspaceRelative = relative(workspace, appDirectory)
168
+ const outsideWorkspace =
169
+ isAbsolute(workspaceRelative) ||
170
+ workspaceRelative === '..' ||
171
+ workspaceRelative.startsWith(`..${sep}`)
172
+ if (!existsSync('turbo.json') && !existsSync('turbo.jsonc')) {
173
+ conservative('Turbo config not found')
174
+ } else if (outsideWorkspace) {
175
+ conservative('working-directory is outside the workspace')
176
+ } else if (filter !== 'auto') {
177
+ output('turbo_package', filter)
178
+ output('turbo_enabled', 'true')
179
+ } else {
180
+ const manifest = resolve(appDirectory, 'package.json')
181
+ let packageName = ''
182
+ try {
183
+ packageName = JSON.parse(readFileSync(manifest, 'utf8')).name ?? ''
184
+ } catch {
185
+ packageName = ''
186
+ }
187
+ if (typeof packageName !== 'string' || !packageName || /[\s\r\n\0]/.test(packageName)) {
188
+ conservative('Application package name could not be resolved')
189
+ } else {
190
+ console.log(`Resolved Turbo package: ${packageName}`)
191
+ output('turbo_package', packageName)
192
+ output('turbo_enabled', 'true')
193
+ }
194
+ }
195
+ NODE
196
+ - name: Set up Bun
197
+ if: steps.resolve.outputs.turbo_enabled == 'true'
198
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
199
+ with:
200
+ bun-version: ${{ inputs.bun-version }}
201
+ - name: Install dependencies for Turbo
202
+ if: steps.resolve.outputs.turbo_enabled == 'true'
203
+ working-directory: ${{ inputs.install-working-directory }}
204
+ run: bun install --frozen-lockfile --ignore-scripts
205
+ - name: Check affected build task
206
+ if: steps.resolve.outputs.turbo_enabled == 'true'
207
+ id: check
208
+ working-directory: ${{ inputs.install-working-directory }}
209
+ env:
210
+ TURBO_PACKAGE: ${{ steps.resolve.outputs.turbo_package }}
211
+ TURBO_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
212
+ TURBO_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
213
+ run: |
214
+ set -euo pipefail
215
+ [[ "$TURBO_PACKAGE" != *$'\n'* && "$TURBO_PACKAGE" != *$'\r'* && "$TURBO_PACKAGE" != *$'\0'* ]]
216
+ [[ "$TURBO_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]
217
+ if [[ ! "$TURBO_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ || "$TURBO_BASE_SHA" =~ ^0+$ ]]; then
218
+ echo 'No comparable base commit; deploying conservatively.'
219
+ echo 'should_deploy=true' >> "$GITHUB_OUTPUT"
220
+ exit 0
221
+ fi
222
+ bunx --bun --no-install turbo query affected \
223
+ --packages \
224
+ --base="$TURBO_BASE_SHA" \
225
+ --head="$TURBO_HEAD_SHA" \
226
+ --no-update-notifier > "$RUNNER_TEMP/turbo-affected.json"
227
+ node --input-type=module <<'NODE'
228
+ import { appendFileSync, readFileSync } from 'node:fs'
229
+ const result = JSON.parse(readFileSync(process.env.RUNNER_TEMP + '/turbo-affected.json', 'utf8'))
230
+ const items = result?.data?.affectedPackages?.items ?? result?.data?.affectedTasks?.items
231
+ if (!Array.isArray(items)) throw new Error('Turbo affected query returned no package/task items')
232
+ const packageName = process.env.TURBO_PACKAGE
233
+ const affected = items.some((item) => item?.name === packageName || item?.package?.name === packageName || item?.fullName?.startsWith(`${packageName}#`))
234
+ console.log(`${packageName}: ${affected ? 'affected' : 'unaffected'}`)
235
+ appendFileSync(process.env.GITHUB_OUTPUT, `should_deploy=${affected}\n`)
236
+ NODE
237
+
238
+ candidate:
239
+ needs: affected
240
+ if: >-
241
+ always() &&
242
+ vars.CI_BILLING_PAUSED != 'true' &&
243
+ (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
244
+ (github.event_name != 'pull_request' || inputs.mode != 'preview' || !startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')) &&
245
+ github.event_name != 'pull_request_target' &&
246
+ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
247
+ (
248
+ inputs['turbo-filter'] == '' ||
249
+ needs.affected.result == 'skipped' ||
250
+ needs.affected.outputs.should_deploy == 'true'
251
+ )
252
+ runs-on: ubuntu-latest
125
253
  timeout-minutes: 30
254
+ # The explicit deployment record below is the sole GitHub deployment record.
255
+ # Keep the environment for approvals, secrets, and branch protection without
256
+ # creating a second, competing PR timeline entry.
126
257
  environment:
127
258
  name: Preview
259
+ deployment: false
128
260
  url: ${{ steps.upload.outputs.url }}
129
261
  defaults:
130
262
  run:
@@ -251,6 +383,7 @@ jobs:
251
383
  timeout-minutes: 30
252
384
  environment:
253
385
  name: Production
386
+ deployment: false
254
387
  url: ${{ inputs.production-url }}
255
388
  defaults:
256
389
  run:
@@ -21,6 +21,11 @@ on:
21
21
  required: false
22
22
  type: string
23
23
  default: '.'
24
+ turbo-filter:
25
+ description: 'Optional Turbo package name override; auto detects it from working-directory by default.'
26
+ required: false
27
+ type: string
28
+ default: auto
24
29
  build-script:
25
30
  required: false
26
31
  type: string
@@ -69,17 +74,144 @@ concurrency:
69
74
  cancel-in-progress: false
70
75
 
71
76
  jobs:
77
+ affected:
78
+ name: Check Turbo deployment impact
79
+ if: >-
80
+ inputs['turbo-filter'] != '' &&
81
+ (github.event_name == 'pull_request' || github.event_name == 'push') &&
82
+ vars.CI_BILLING_PAUSED != 'true' &&
83
+ (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
84
+ (github.event_name != 'pull_request' || inputs.mode != 'preview' || !startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')) &&
85
+ github.event_name != 'pull_request_target' &&
86
+ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
87
+ runs-on: ${{ inputs.runner }}
88
+ timeout-minutes: 10
89
+ permissions:
90
+ contents: read
91
+ outputs:
92
+ should_deploy: ${{ steps.check.outputs.should_deploy || steps.resolve.outputs.should_deploy }}
93
+ steps:
94
+ - name: Checkout full history
95
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
96
+ with:
97
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
98
+ fetch-depth: 0
99
+ persist-credentials: false
100
+ - name: Resolve Turbo package
101
+ id: resolve
102
+ working-directory: ${{ inputs.install-working-directory }}
103
+ env:
104
+ TURBO_FILTER: ${{ inputs.turbo-filter }}
105
+ WORKSPACE: ${{ github.workspace }}
106
+ WORKING_DIRECTORY: ${{ inputs.working-directory }}
107
+ run: |
108
+ node --input-type=module <<'NODE'
109
+ import { appendFileSync, existsSync, readFileSync } from 'node:fs'
110
+ import { isAbsolute, relative, resolve, sep } from 'node:path'
111
+ const output = (name, value) => appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`)
112
+ const conservative = (reason) => {
113
+ console.log(`${reason}; deploying conservatively.`)
114
+ output('turbo_enabled', 'false')
115
+ output('should_deploy', 'true')
116
+ }
117
+ const filter = process.env.TURBO_FILTER
118
+ if (!filter || /[\r\n\0]/.test(filter)) throw new Error('Invalid turbo-filter')
119
+ const workspace = process.env.WORKSPACE
120
+ const appDirectory = resolve(workspace, process.env.WORKING_DIRECTORY)
121
+ const workspaceRelative = relative(workspace, appDirectory)
122
+ const outsideWorkspace =
123
+ isAbsolute(workspaceRelative) ||
124
+ workspaceRelative === '..' ||
125
+ workspaceRelative.startsWith(`..${sep}`)
126
+ if (!existsSync('turbo.json') && !existsSync('turbo.jsonc')) {
127
+ conservative('Turbo config not found')
128
+ } else if (outsideWorkspace) {
129
+ conservative('working-directory is outside the workspace')
130
+ } else if (filter !== 'auto') {
131
+ output('turbo_package', filter)
132
+ output('turbo_enabled', 'true')
133
+ } else {
134
+ const manifest = resolve(appDirectory, 'package.json')
135
+ let packageName = ''
136
+ try {
137
+ packageName = JSON.parse(readFileSync(manifest, 'utf8')).name ?? ''
138
+ } catch {
139
+ packageName = ''
140
+ }
141
+ if (typeof packageName !== 'string' || !packageName || /[\s\r\n\0]/.test(packageName)) {
142
+ conservative('Application package name could not be resolved')
143
+ } else {
144
+ console.log(`Resolved Turbo package: ${packageName}`)
145
+ output('turbo_package', packageName)
146
+ output('turbo_enabled', 'true')
147
+ }
148
+ }
149
+ NODE
150
+ - name: Set up Bun
151
+ if: steps.resolve.outputs.turbo_enabled == 'true'
152
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
153
+ with:
154
+ bun-version: ${{ inputs.bun-version }}
155
+ - name: Install dependencies for Turbo
156
+ if: steps.resolve.outputs.turbo_enabled == 'true'
157
+ working-directory: ${{ inputs.install-working-directory }}
158
+ run: bun install --frozen-lockfile --ignore-scripts
159
+ - name: Check affected build task
160
+ if: steps.resolve.outputs.turbo_enabled == 'true'
161
+ id: check
162
+ working-directory: ${{ inputs.install-working-directory }}
163
+ env:
164
+ TURBO_PACKAGE: ${{ steps.resolve.outputs.turbo_package }}
165
+ TURBO_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
166
+ TURBO_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
167
+ run: |
168
+ set -euo pipefail
169
+ [[ "$TURBO_PACKAGE" != *$'\n'* && "$TURBO_PACKAGE" != *$'\r'* && "$TURBO_PACKAGE" != *$'\0'* ]]
170
+ [[ "$TURBO_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]
171
+ if [[ ! "$TURBO_BASE_SHA" =~ ^[0-9a-fA-F]{40}$ || "$TURBO_BASE_SHA" =~ ^0+$ ]]; then
172
+ echo 'No comparable base commit; deploying conservatively.'
173
+ echo 'should_deploy=true' >> "$GITHUB_OUTPUT"
174
+ exit 0
175
+ fi
176
+ bunx --bun --no-install turbo query affected \
177
+ --packages \
178
+ --base="$TURBO_BASE_SHA" \
179
+ --head="$TURBO_HEAD_SHA" \
180
+ --no-update-notifier > "$RUNNER_TEMP/turbo-affected.json"
181
+ node --input-type=module <<'NODE'
182
+ import { appendFileSync, readFileSync } from 'node:fs'
183
+ const result = JSON.parse(readFileSync(process.env.RUNNER_TEMP + '/turbo-affected.json', 'utf8'))
184
+ const items = result?.data?.affectedPackages?.items ?? result?.data?.affectedTasks?.items
185
+ if (!Array.isArray(items)) throw new Error('Turbo affected query returned no package/task items')
186
+ const packageName = process.env.TURBO_PACKAGE
187
+ const affected = items.some((item) => item?.name === packageName || item?.package?.name === packageName || item?.fullName?.startsWith(`${packageName}#`))
188
+ console.log(`${packageName}: ${affected ? 'affected' : 'unaffected'}`)
189
+ appendFileSync(process.env.GITHUB_OUTPUT, `should_deploy=${affected}\n`)
190
+ NODE
191
+
72
192
  deploy:
73
193
  name: Cloudflare Deploy / ${{ inputs.mode }}
194
+ needs: affected
74
195
  if: >-
196
+ always() &&
75
197
  vars.CI_BILLING_PAUSED != 'true' &&
76
198
  (inputs['draft-protection'] != true || github.event_name != 'pull_request' || github.event.pull_request.draft == false) &&
199
+ (github.event_name != 'pull_request' || inputs.mode != 'preview' || !startsWith(github.event.pull_request.head.ref, 'release-please--branches--main')) &&
77
200
  github.event_name != 'pull_request_target' &&
78
- (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
201
+ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) &&
202
+ (
203
+ inputs['turbo-filter'] == '' ||
204
+ needs.affected.result == 'skipped' ||
205
+ needs.affected.outputs.should_deploy == 'true'
206
+ )
79
207
  runs-on: ${{ inputs.runner }}
80
208
  timeout-minutes: 20
209
+ # The explicit deployment record below is the sole GitHub deployment record.
210
+ # Keep the environment for approvals, secrets, and branch protection without
211
+ # creating a second, competing PR timeline entry.
81
212
  environment:
82
213
  name: ${{ inputs.environment || (inputs.mode == 'production' && 'Production' || 'Preview') }}
214
+ deployment: false
83
215
  url: ${{ steps.deploy.outputs.url }}
84
216
  outputs:
85
217
  url: ${{ steps.deploy.outputs.url }}
@@ -182,10 +314,17 @@ jobs:
182
314
  DEPLOYMENT_ID: ${{ steps.start.outputs.id }}
183
315
  DEPLOY_URL: ${{ steps.deploy.outputs.url }}
184
316
  JOB_STATUS: ${{ job.status }}
317
+ MODE: ${{ inputs.mode }}
185
318
  run: |
186
319
  set -euo pipefail
187
320
  state=failure
188
321
  if [ "$JOB_STATUS" = success ]; then state=success; fi
189
322
  args=(-f "state=$state" -F auto_inactive=false)
323
+ # Preview environments are persistent per application. Once a new
324
+ # preview succeeds, retire the previous successful preview record;
325
+ # failed previews must not take a working preview offline.
326
+ if [ "$MODE" = preview ] && [ "$state" = success ]; then
327
+ args+=(-F auto_inactive=true)
328
+ fi
190
329
  if [ -n "$DEPLOY_URL" ]; then args+=(-f "environment_url=$DEPLOY_URL"); fi
191
330
  gh api -X POST "repos/$GITHUB_REPOSITORY/deployments/$DEPLOYMENT_ID/statuses" "${args[@]}" --silent
@@ -76,7 +76,7 @@ jobs:
76
76
  strategy:
77
77
  fail-fast: false
78
78
  matrix:
79
- node: ['20', '22', '24']
79
+ node: ['24', '26']
80
80
  env:
81
81
  PACKAGE: ${{ needs.pack.outputs.filename }}
82
82
  SOURCE_SHA: ${{ needs.pack.outputs.source-sha }}
@@ -162,7 +162,7 @@ jobs:
162
162
  gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
163
163
  --name "qualification-candidate-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT" \
164
164
  --dir "$RUNNER_TEMP/candidate"
165
- for node in 20 22 24; do
165
+ for node in 24 26; do
166
166
  gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
167
167
  --name "consumer-qualification-$GITHUB_RUN_ATTEMPT-node-$node" \
168
168
  --dir "$RUNNER_TEMP/reports/$node"
@@ -182,7 +182,7 @@ jobs:
182
182
  sourceSha: env.SOURCE_SHA, asset: env.PACKAGE,
183
183
  directory: join(env.RUNNER_TEMP, 'candidate'),
184
184
  }
185
- const result = qualifyArchive(candidate, ['20', '22', '24'].map(node => join(env.RUNNER_TEMP, 'reports', node, 'report.json')))
185
+ const result = qualifyArchive(candidate, ['24', '26'].map(node => join(env.RUNNER_TEMP, 'reports', node, 'report.json')))
186
186
  assert.equal(result.digest, `sha256:${env.CANDIDATE_SHA256}`)
187
187
  const artifact = `qualification-candidate-${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}`
188
188
  writeFileSync(join(env.RUNNER_TEMP, 'qualification.json'), JSON.stringify({ ...result, run_id: env.GITHUB_RUN_ID, run_attempt: env.GITHUB_RUN_ATTEMPT }, null, 2) + '\n')
@@ -0,0 +1,116 @@
1
+ name: Code Foundry Eval
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ runtime-repository:
7
+ description: Repository containing the Code Foundry runtime.
8
+ required: false
9
+ type: string
10
+ default: 0xPlayerOne/code-foundry
11
+ runtime-ref:
12
+ description: Code Foundry runtime tag or ref.
13
+ required: false
14
+ type: string
15
+ default: v1.0.5
16
+ runner:
17
+ description: >-
18
+ Runner used by the eval harness. Browser evals need a
19
+ Chrome-capable runner such as ubuntu-latest; ubuntu-slim does not
20
+ ship a browser.
21
+ required: false
22
+ type: string
23
+ default: ubuntu-latest
24
+ artifact-prefix:
25
+ description: Prefix for task receipts; use a unique value when calling this workflow more than once.
26
+ required: false
27
+ type: string
28
+ default: task-result
29
+
30
+ permissions:
31
+ contents: read
32
+
33
+ env:
34
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
35
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
36
+ REPO_FOUNDRY_PROFILE: ${{ vars.REPO_FOUNDRY_PROFILE }}
37
+ REPO_FOUNDRY_LANGUAGES: ${{ vars.REPO_FOUNDRY_LANGUAGES }}
38
+ REPO_FOUNDRY_FEATURES: ${{ vars.REPO_FOUNDRY_FEATURES }}
39
+ REPO_FOUNDRY_PACKAGE_MANAGER: ${{ vars.REPO_FOUNDRY_PACKAGE_MANAGER }}
40
+ REPO_FOUNDRY_RUNNER: ${{ vars.REPO_FOUNDRY_RUNNER }}
41
+ REPO_FOUNDRY_CACHE_PACKAGES: ${{ vars.REPO_FOUNDRY_CACHE_PACKAGES || 'auto' }}
42
+ REPO_FOUNDRY_CACHE_BUILD: ${{ vars.REPO_FOUNDRY_CACHE_BUILD || 'auto' }}
43
+
44
+ concurrency:
45
+ group: code-foundry-eval-${{ github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}
46
+ cancel-in-progress: true
47
+
48
+ jobs:
49
+ eval:
50
+ name: Eval
51
+ if: vars.CI_BILLING_PAUSED != 'true'
52
+ runs-on: ${{ inputs.runner }}
53
+ steps:
54
+ - name: Checkout
55
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
56
+ with:
57
+ persist-credentials: false
58
+ - name: Runtime
59
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
60
+ with:
61
+ persist-credentials: false
62
+ repository: ${{ inputs.runtime-repository }}
63
+ ref: ${{ inputs.runtime-ref }}
64
+ path: .github/.code-foundry
65
+ sparse-checkout: |
66
+ .github/actions
67
+ src/lib
68
+ src/runtime-core.mjs
69
+ src/runtime.mjs
70
+ - name: Install runtime
71
+ run: |
72
+ mkdir -p .github/actions
73
+ mv .github/.code-foundry "$RUNNER_TEMP/code-foundry"
74
+ cp -R "$RUNNER_TEMP/code-foundry/.github/actions/." .github/actions/
75
+ - name: Detect
76
+ id: applicability
77
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci should_run eval >> "$GITHUB_OUTPUT"
78
+ - name: Setup
79
+ if: steps.applicability.outputs.applicable == 'true'
80
+ uses: ./.github/actions/setup
81
+ with:
82
+ # Eval harnesses run through the repository's own package scripts and
83
+ # need their dependencies installed before the task executes.
84
+ install: 'true'
85
+ cache-build: 'true'
86
+ task: eval
87
+ - name: Eval
88
+ id: execute
89
+ if: steps.applicability.outputs.applicable == 'true'
90
+ run: node "$RUNNER_TEMP/code-foundry/src/runtime.mjs" ci eval
91
+ - name: Retain task receipt
92
+ if: >-
93
+ always() &&
94
+ (steps.execute.outcome == 'success' || steps.execute.outcome == 'failure' ||
95
+ steps.applicability.outputs.applicable == 'false') &&
96
+ hashFiles('.code-foundry/results/eval.json') != ''
97
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
98
+ with:
99
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval
100
+ path: .code-foundry/results/eval.json
101
+ include-hidden-files: true
102
+ if-no-files-found: error
103
+ retention-days: 14
104
+ - name: Retain eval report
105
+ if: >-
106
+ always() &&
107
+ steps.execute.outcome == 'failure' &&
108
+ hashFiles('eval-results/result.json') != ''
109
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
110
+ with:
111
+ name: ${{ inputs.artifact-prefix }}-${{ github.run_id }}-${{ github.run_attempt }}-eval-report
112
+ path: |
113
+ eval-results/result.json
114
+ eval-results/summary.json
115
+ if-no-files-found: error
116
+ retention-days: 14
@@ -33,7 +33,7 @@ jobs:
33
33
  strategy:
34
34
  fail-fast: false
35
35
  matrix:
36
- node: ['20', '22', '24']
36
+ node: ['24', '26']
37
37
  steps:
38
38
  - name: Checkout trusted qualification runtime
39
39
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -113,7 +113,7 @@ jobs:
113
113
  GH_TOKEN: ${{ github.token }}
114
114
  run: |
115
115
  set -euo pipefail
116
- for node in 20 22 24; do
116
+ for node in 24 26; do
117
117
  gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
118
118
  --name "qualified-$GITHUB_RUN_ATTEMPT-node-$node" \
119
119
  --dir "$RUNNER_TEMP/reports/$node"
@@ -130,9 +130,8 @@ jobs:
130
130
  node src/commands/qualified-publication.mjs publish \
131
131
  "$GITHUB_REPOSITORY" "$RELEASE_TAG" "$GITHUB_SHA" "$RELEASE_ASSET" \
132
132
  "$RUNNER_TEMP/candidate" \
133
- "$RUNNER_TEMP/reports/20/report.json" \
134
- "$RUNNER_TEMP/reports/22/report.json" \
135
- "$RUNNER_TEMP/reports/24/report.json" > "$RUNNER_TEMP/publication.json"
133
+ "$RUNNER_TEMP/reports/24/report.json" \
134
+ "$RUNNER_TEMP/reports/26/report.json" > "$RUNNER_TEMP/publication.json"
136
135
  - name: Retain publication identity
137
136
  if: always()
138
137
  uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
@@ -22,7 +22,13 @@ concurrency:
22
22
  jobs:
23
23
  qualification:
24
24
  name: Qualify consumers
25
- if: github.ref == 'refs/heads/main' && (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true))
25
+ # Qualification feeds staging and publication, so it runs only when this
26
+ # push created a release or recovery found a stuck one. Feature merges
27
+ # only pay for the cheap release-please and recovery probes below; the
28
+ # content they merge was already audited on its pull request, and the
29
+ # release merge re-qualifies the exact tree it publishes.
30
+ needs: [release, recovery]
31
+ if: github.ref == 'refs/heads/main' && (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && (needs.release.outputs.release_created == 'true' || needs.recovery.outputs.found == 'true')
26
32
  uses: ./.github/workflows/consumer-qualification.yml
27
33
  with:
28
34
  billing-pause-bypass: ${{ github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true }}
@@ -65,7 +71,11 @@ jobs:
65
71
 
66
72
  release:
67
73
  name: Release
68
- needs: [qualification, preflight]
74
+ # Release Please consumes no qualification outputs; it runs first so the
75
+ # qualification below only executes when a release (or a stuck draft) needs
76
+ # it. Version pull requests keep flowing even when the candidate is broken;
77
+ # publication stays gated on qualification success in the stage job.
78
+ needs: [preflight]
69
79
  if: github.ref == 'refs/heads/main' && (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true))
70
80
  uses: ./.github/workflows/release.yml
71
81
  with:
@@ -87,8 +97,8 @@ jobs:
87
97
 
88
98
  recovery:
89
99
  name: Recover qualified draft identity
90
- needs: [qualification, release]
91
- if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && needs.qualification.result == 'success' && needs.release.result == 'success'
100
+ needs: [release]
101
+ if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && needs.release.result == 'success'
92
102
  runs-on: ubuntu-24.04
93
103
  timeout-minutes: 5
94
104
  permissions:
@@ -99,7 +109,9 @@ jobs:
99
109
  sha: ${{ steps.recover.outputs.sha }}
100
110
  env:
101
111
  RELEASE_CREATED: ${{ needs.release.outputs.release_created }}
102
- SOURCE_SHA: ${{ needs.qualification.outputs.source-sha }}
112
+ # The qualification source SHA is git rev-parse HEAD of the push, so
113
+ # recovery computes it directly instead of depending on qualification.
114
+ SOURCE_SHA: ${{ github.sha }}
103
115
  steps:
104
116
  - name: Checkout trusted recovery runtime
105
117
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
@@ -156,7 +168,10 @@ jobs:
156
168
  stage:
157
169
  name: Stage qualified release
158
170
  needs: [qualification, release, recovery]
159
- if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && github.ref == 'refs/heads/main' && (needs.release.outputs.release_created == 'true' || needs.recovery.outputs.found == 'true')
171
+ # Qualification only runs when this push created a release or recovery
172
+ # found a stuck one; staging additionally requires its success so an
173
+ # unqualified tree can never reach publication.
174
+ if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && github.ref == 'refs/heads/main' && needs.qualification.result == 'success' && (needs.release.outputs.release_created == 'true' || needs.recovery.outputs.found == 'true')
160
175
  runs-on: ubuntu-24.04
161
176
  timeout-minutes: 15
162
177
  permissions:
@@ -203,7 +218,7 @@ jobs:
203
218
  gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
204
219
  --name "$CANDIDATE_ARTIFACT" --dir "$RUNNER_TEMP/candidate"
205
220
  test "$(sha256sum "$RUNNER_TEMP/candidate/$PACKAGE" | cut -d' ' -f1)" = "$CANDIDATE_SHA256"
206
- for node in 20 22 24; do
221
+ for node in 24 26; do
207
222
  gh run download "$GITHUB_RUN_ID" --repo "$GITHUB_REPOSITORY" \
208
223
  --name "consumer-qualification-$GITHUB_RUN_ATTEMPT-node-$node" \
209
224
  --dir "$RUNNER_TEMP/reports/$node"
@@ -216,9 +231,8 @@ jobs:
216
231
  node src/commands/qualified-publication.mjs stage \
217
232
  "$GITHUB_REPOSITORY" "$RELEASE_TAG" "$SOURCE_SHA" "$PACKAGE" \
218
233
  "$RUNNER_TEMP/candidate" \
219
- "$RUNNER_TEMP/reports/20/report.json" \
220
- "$RUNNER_TEMP/reports/22/report.json" \
221
- "$RUNNER_TEMP/reports/24/report.json" > "$RUNNER_TEMP/staging.json"
234
+ "$RUNNER_TEMP/reports/24/report.json" \
235
+ "$RUNNER_TEMP/reports/26/report.json" > "$RUNNER_TEMP/staging.json"
222
236
  - name: Retain staging identity
223
237
  if: always()
224
238
  uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7