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.
- package/.github/workflows/cloudflare-delivery.yml +134 -1
- package/.github/workflows/cloudflare-deploy.yml +140 -1
- package/.github/workflows/consumer-qualification.yml +3 -3
- package/.github/workflows/eval.yml +116 -0
- package/.github/workflows/qualified-foundry-publish.yml +4 -5
- package/.github/workflows/release_self-ci.yml +24 -10
- package/.github/workflows/validation-no-codeql.yml +34 -6
- package/.github/workflows/validation.yml +32 -5
- package/.github/workflows/validation_audit_self-ci.yml +1 -0
- package/.github/workflows/validation_self-ci.yml +7 -1
- package/AGENTS.md +10 -0
- package/CHANGELOG.md +77 -0
- package/README.md +4 -1
- package/docs/CONFIGURATION.md +20 -13
- package/docs/EVALS.md +139 -0
- package/docs/PUBLISHING.md +6 -1
- package/docs/WORKFLOWS.md +27 -4
- package/docs/cloudflare-delivery.md +47 -25
- package/docs/consumer-qualification.md +1 -1
- package/docs/fleet-release-eligibility.md +1 -5
- package/docs/qualified-publication.md +1 -1
- package/docs/required-capabilities.md +6 -4
- package/package.json +3 -3
- package/src/commands/doctor.mjs +5 -2
- package/src/commands/fleet-core.mjs +1 -1
- package/src/commands/qualified-publication.mjs +81 -3
- package/src/commands/release-integrity.mjs +37 -14
- package/src/commands/sync.mjs +3 -2
- package/src/lib/docs-only.mjs +39 -0
- package/src/lib/eval-envelope.mjs +234 -0
- package/src/lib/fleet-manifest.mjs +9 -5
- package/src/lib/merge-queue.mjs +1 -0
- package/src/lib/overlay.mjs +1 -1
- package/src/lib/release-manifest.mjs +1 -1
- package/src/lib/release-policy.mjs +2 -2
- package/src/lib/task-policy.mjs +7 -0
- package/src/lib/validation-policy.mjs +10 -6
- package/src/runtime-core.mjs +187 -10
- package/src/runtime.mjs +2 -0
- 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
|
-
|
|
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: ['
|
|
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
|
|
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, ['
|
|
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: ['
|
|
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
|
|
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/
|
|
134
|
-
"$RUNNER_TEMP/reports/
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
91
|
-
if: (vars.CI_BILLING_PAUSED != 'true' || (github.event_name == 'workflow_dispatch' && inputs['release-while-paused'] == true)) && needs.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
220
|
-
"$RUNNER_TEMP/reports/
|
|
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
|