code-foundry 1.25.3 → 1.28.3
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/code-foundry.yml +1 -1
- 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/qualified-foundry-publish.yml +4 -5
- package/.github/workflows/release_self-ci.yml +24 -10
- package/.github/workflows/validation_self-ci.yml +6 -1
- package/AGENTS.md +9 -0
- package/CHANGELOG.md +42 -0
- package/README.md +1 -1
- package/docs/PUBLISHING.md +6 -1
- package/docs/WORKFLOWS.md +15 -0
- 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/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 +3 -3
- package/src/commands/sync.mjs +2 -2
- package/src/lib/docs-only.mjs +39 -0
- package/src/lib/fleet-manifest.mjs +9 -5
- 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/runtime-core.mjs +34 -11
package/.github/code-foundry.yml
CHANGED
|
@@ -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')
|
|
@@ -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
|
|
@@ -64,7 +64,12 @@ jobs:
|
|
|
64
64
|
validation:
|
|
65
65
|
name: Validation
|
|
66
66
|
needs: mode
|
|
67
|
-
|
|
67
|
+
# Bot-authored pull requests (Dependabot and friends) validate only on an
|
|
68
|
+
# explicit ready transition: dependency pushes never allocate validation
|
|
69
|
+
# runners by themselves. A human pushing to a bot branch also runs, since
|
|
70
|
+
# the sender is no longer a bot. Release Please heads are managed by the
|
|
71
|
+
# release workflow and always validate.
|
|
72
|
+
if: vars.CI_BILLING_PAUSED != 'true' && github.event_name == 'pull_request' && github.event.pull_request.draft == false && (github.event.pull_request.user.type != 'Bot' || github.event.sender.type != 'Bot' || startsWith(github.event.pull_request.head.ref, 'release-please--branches--main'))
|
|
68
73
|
# Reusable workflows can only maintain or reduce the caller job's scopes.
|
|
69
74
|
# The audit tier needs security-events: write for CodeQL uploads.
|
|
70
75
|
permissions:
|
package/AGENTS.md
CHANGED
|
@@ -17,6 +17,15 @@ They complement `CONTRIBUTING.md`. More specific instructions in nested `AGENTS.
|
|
|
17
17
|
|
|
18
18
|
This repository may contain TypeScript, Rust, Python, or any combination of them. Detect the active stack from the files present; do not assume every check applies.
|
|
19
19
|
|
|
20
|
+
## Contributing back
|
|
21
|
+
|
|
22
|
+
Consumers are encouraged to help improve this open-source project. Open a small,
|
|
23
|
+
focused pull request for bug fixes, performance improvements, documentation, or
|
|
24
|
+
other narrowly scoped changes. For larger feature requests or architectural
|
|
25
|
+
changes, create an issue first so the proposal can be discussed and scoped.
|
|
26
|
+
Contributions should help make the tool as performant, reliable, and helpful as
|
|
27
|
+
possible for everyone.
|
|
28
|
+
|
|
20
29
|
## Read before acting
|
|
21
30
|
|
|
22
31
|
Before editing:
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.28.3](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.2...v1.28.3) (2026-09-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Maintenance
|
|
7
|
+
|
|
8
|
+
* **config:** pin Code Foundry runtime to v1.28.2 ([#599](https://github.com/0xPlayerOne/code-foundry/issues/599)) ([ed61358](https://github.com/0xPlayerOne/code-foundry/commit/ed6135813e12e1f5410066a92183f2f2d18dcc8c))
|
|
9
|
+
|
|
10
|
+
## [1.28.2](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.1...v1.28.2) (2026-09-10)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **release:** pass one qualification report per required node ([#597](https://github.com/0xPlayerOne/code-foundry/issues/597)) ([5ceb92c](https://github.com/0xPlayerOne/code-foundry/commit/5ceb92cab3afc97145ef068e6b5e33399d99b9e5))
|
|
16
|
+
|
|
17
|
+
## [1.28.1](https://github.com/0xPlayerOne/code-foundry/compare/v1.28.0...v1.28.1) (2026-09-10)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **cloudflare:** remove duplicate deployment records ([#595](https://github.com/0xPlayerOne/code-foundry/issues/595)) ([d9a2a4b](https://github.com/0xPlayerOne/code-foundry/commit/d9a2a4b0363bb9a9ded91d3368bdb7b9b580330f))
|
|
23
|
+
|
|
24
|
+
## [1.28.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.27.0...v1.28.0) (2026-09-10)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* **release:** qualify only when a release or a stuck draft needs it ([#591](https://github.com/0xPlayerOne/code-foundry/issues/591)) ([4cce045](https://github.com/0xPlayerOne/code-foundry/commit/4cce045228648e6e705ad1e89c59eaf20a72a538))
|
|
30
|
+
|
|
31
|
+
## [1.27.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.26.0...v1.27.0) (2026-09-10)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
* **validation:** downgrade docs-only PRs to fast and gate bot pushes ([#590](https://github.com/0xPlayerOne/code-foundry/issues/590)) ([df774f5](https://github.com/0xPlayerOne/code-foundry/commit/df774f5d1e4a8a6c5f174219eb2b24ef012f6fd3))
|
|
37
|
+
|
|
38
|
+
## [1.26.0](https://github.com/0xPlayerOne/code-foundry/compare/v1.25.3...v1.26.0) (2026-09-10)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
### Features
|
|
42
|
+
|
|
43
|
+
* **node:** support Node 24 and 26, drop 20 and 22 ([#589](https://github.com/0xPlayerOne/code-foundry/issues/589)) ([ee3e294](https://github.com/0xPlayerOne/code-foundry/commit/ee3e2943889ffd06a8d4cd4ccb75a3f36c46c0fd))
|
|
44
|
+
|
|
3
45
|
## [1.25.3](https://github.com/0xPlayerOne/code-foundry/compare/v1.25.2...v1.25.3) (2026-09-10)
|
|
4
46
|
|
|
5
47
|
|
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ is merged; npm publication is opt-in through `npm_publish: true` and supports
|
|
|
135
135
|
npm trusted publishing or an `NPM_TOKEN` fallback.
|
|
136
136
|
|
|
137
137
|
Code Foundry's own release caller is stricter: it qualifies the package across
|
|
138
|
-
Node
|
|
138
|
+
Node 24 and 26, stages the exact qualified archive, publishes the
|
|
139
139
|
immutable GitHub Release, and publishes that archive through the verified
|
|
140
140
|
publisher. See
|
|
141
141
|
[Consumer qualification](docs/consumer-qualification.md) and [Qualified
|
package/docs/PUBLISHING.md
CHANGED
|
@@ -37,7 +37,7 @@ registry version and its provenance link before treating the setup as complete.
|
|
|
37
37
|
The self-hosted package follows a stricter contract than generated consumer
|
|
38
38
|
callers:
|
|
39
39
|
|
|
40
|
-
1. Pack the candidate once and qualify that archive across Node
|
|
40
|
+
1. Pack the candidate once and qualify that archive across Node 24 and 26.
|
|
41
41
|
2. Create a draft GitHub Release and attach the exact qualified archive plus its
|
|
42
42
|
qualification receipt.
|
|
43
43
|
3. Publish the immutable GitHub Release after verifying the tag, source commit,
|
|
@@ -50,6 +50,11 @@ the bytes selected from the same workflow run and attempt. See [Consumer
|
|
|
50
50
|
qualification](consumer-qualification.md) and [Qualified publication](qualified-publication.md)
|
|
51
51
|
for the complete contract, retries, and recovery rules.
|
|
52
52
|
|
|
53
|
+
The main-push pipeline runs Release Please first, then qualifies only when the
|
|
54
|
+
push created a release or recovery found a stuck draft. Feature merges pay for
|
|
55
|
+
the cheap release-please and recovery probes; the runner-heavy matrix runs on
|
|
56
|
+
release merges, which re-qualify the exact tree they publish.
|
|
57
|
+
|
|
53
58
|
## GitHub Releases and GitHub Packages
|
|
54
59
|
|
|
55
60
|
A GitHub Release is metadata attached to a Git tag. It is independent of npm and
|
package/docs/WORKFLOWS.md
CHANGED
|
@@ -31,6 +31,21 @@ pull request starts it again for the current head. Audit-mode runs additionally
|
|
|
31
31
|
execute the eval lane (`Validation / Eval`) for repositories that ship an eval
|
|
32
32
|
harness; see [Evals](EVALS.md).
|
|
33
33
|
|
|
34
|
+
Two waste-avoidance rules keep validation minutes honest without weakening
|
|
35
|
+
confidence:
|
|
36
|
+
|
|
37
|
+
- **Docs-only pull requests run the fast tier.** When an audit-classified pull
|
|
38
|
+
request changes only markdown, `docs/`, and license roots, the mode
|
|
39
|
+
classifier downgrades it to fast. Any code, lockfile, workflow,
|
|
40
|
+
configuration, or packaging change — or any diff that cannot be computed
|
|
41
|
+
deterministically — keeps the audit tier. Scheduled and manual audits always
|
|
42
|
+
run the full tier.
|
|
43
|
+
- **Bot-authored pull requests validate only on ready transitions.**
|
|
44
|
+
Dependabot and other bot pushes never allocate validation runners; a
|
|
45
|
+
maintainer marks the (Guard-drafted) pull request ready — or pushes to its
|
|
46
|
+
branch, which changes the sender — to run validation. Release Please heads
|
|
47
|
+
are exempt because the release workflow merges them through its own lane.
|
|
48
|
+
|
|
34
49
|
The separate `validation-audit.yml` caller is pinned to the configured released
|
|
35
50
|
runtime and handles scheduled and manual audits:
|
|
36
51
|
|
|
@@ -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.
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
not GitHub Actions' merge commit, so GitHub
|
|
95
|
-
Deployments section. Direct pushes use
|
|
96
|
-
production JSON evidence is uploaded even
|
|
97
|
-
and credentials are not copied into those
|
|
98
|
-
prevent final status steps; the GitHub job
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
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
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.3",
|
|
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": ">=
|
|
60
|
+
"node": ">=24"
|
|
61
61
|
},
|
|
62
62
|
"packageManager": "bun@1.4.0"
|
|
63
63
|
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -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.
|
|
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 = ['
|
|
19
|
+
export const REQUIRED_NODES = ['24', '26']
|
|
20
20
|
export const REQUIRED_FIXTURES = [
|
|
21
21
|
'npm-direct',
|
|
22
22
|
'pnpm-direct',
|
|
@@ -256,7 +256,7 @@ export async function publishQualifiedArchive(candidate, reports, adapters = {})
|
|
|
256
256
|
*/
|
|
257
257
|
async function waitForPublishedRelease(candidate, adapters = {}) {
|
|
258
258
|
const runTolerant = adapters.runTolerant ?? defaultRunTolerant
|
|
259
|
-
const delay = adapters.delay ?? ((ms) => new Promise((
|
|
259
|
+
const delay = adapters.delay ?? ((ms) => new Promise((done) => setTimeout(done, ms)))
|
|
260
260
|
const attempts = adapters.attempts ?? 15
|
|
261
261
|
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
262
262
|
const result = runTolerant('gh', [
|
|
@@ -408,7 +408,7 @@ export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
|
|
|
408
408
|
break
|
|
409
409
|
} catch (error) {
|
|
410
410
|
if (attempt >= 10) throw error
|
|
411
|
-
await (adapters.delay ?? ((ms) => new Promise((
|
|
411
|
+
await (adapters.delay ?? ((ms) => new Promise((done) => setTimeout(done, ms))))(5_000)
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
ensure(
|
package/src/commands/sync.mjs
CHANGED
|
@@ -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
|
-
].
|
|
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: \[
|
|
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(
|
|
@@ -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
|
+
}
|
|
@@ -277,16 +277,20 @@ export function verifiedPullRequest(pr, required) {
|
|
|
277
277
|
return false
|
|
278
278
|
const checks = pr.statusCheckRollup
|
|
279
279
|
if (!Array.isArray(checks) || !checks.length) return false
|
|
280
|
-
|
|
281
|
-
const state = (check) =>
|
|
282
|
-
check.status === 'COMPLETED' ? check.conclusion : (check.state ?? check.status)
|
|
283
|
-
if (checks.some((check) => !['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(state(check))))
|
|
280
|
+
if (checks.some((check) => !['SUCCESS', 'NEUTRAL', 'SKIPPED'].includes(checkState(check))))
|
|
284
281
|
return false
|
|
285
282
|
return required.every((name) =>
|
|
286
|
-
checks.some(
|
|
283
|
+
checks.some(
|
|
284
|
+
(check) => (check.name ?? check.context) === name && checkState(check) === 'SUCCESS'
|
|
285
|
+
)
|
|
287
286
|
)
|
|
288
287
|
}
|
|
289
288
|
|
|
289
|
+
/** @param {any} check */
|
|
290
|
+
function checkState(check) {
|
|
291
|
+
return check.status === 'COMPLETED' ? check.conclusion : (check.state ?? check.status)
|
|
292
|
+
}
|
|
293
|
+
|
|
290
294
|
/** @param {string} text */
|
|
291
295
|
function scalarConfig(text) {
|
|
292
296
|
return Object.fromEntries(
|
package/src/lib/overlay.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export function customWorkflowFiles(root, standardFiles) {
|
|
|
16
16
|
.filter((file) => file.endsWith('.yml') || file.endsWith('.yaml'))
|
|
17
17
|
.map((file) => `.github/workflows/${file}`)
|
|
18
18
|
.filter((file) => !isManagedPath(standardFiles, file))
|
|
19
|
-
.
|
|
19
|
+
.toSorted()
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** @param {string} root @param {Record<string, string>} config */
|
|
@@ -295,8 +295,8 @@ export function buildReleaseRecoveryPlan(input) {
|
|
|
295
295
|
const orphanGitHubReleases = releaseTags.filter(
|
|
296
296
|
(tag) => !tagSet.has(tag) && !tagSet.has(tag.replace(/^v/, ''))
|
|
297
297
|
)
|
|
298
|
-
const latestTag = [...tags].
|
|
299
|
-
const latestPackageVersion = [...input.packageVersions].
|
|
298
|
+
const latestTag = [...tags].toSorted(compareVersions).at(-1) ?? ''
|
|
299
|
+
const latestPackageVersion = [...input.packageVersions].toSorted(compareVersions).at(-1) ?? ''
|
|
300
300
|
return {
|
|
301
301
|
latestTag,
|
|
302
302
|
latestPackageVersion,
|
package/src/runtime-core.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
7
7
|
import { detectPackageManager, resolveProfile } from './lib/profile.mjs'
|
|
8
8
|
import { configured, readConfig } from './lib/config.mjs'
|
|
9
9
|
import { classifyTestFiles } from './lib/test-discovery.mjs'
|
|
10
|
+
import { docsOnlyPullRequest } from './lib/docs-only.mjs'
|
|
10
11
|
import { classifyValidationMode, evaluateValidationGate } from './lib/validation-policy.mjs'
|
|
11
12
|
import { readReleaseConfig, validateGeneratedReleaseDiff } from './lib/release-policy.mjs'
|
|
12
13
|
import { runNodePackagePerformance } from './lib/node-package-performance.mjs'
|
|
@@ -237,7 +238,8 @@ function runEval() {
|
|
|
237
238
|
report = JSON.parse(readFileSync(reportFile, 'utf8'))
|
|
238
239
|
} catch (error) {
|
|
239
240
|
throw new Error(
|
|
240
|
-
`Eval report is not valid JSON: ${error instanceof Error ? error.message : String(error)}
|
|
241
|
+
`Eval report is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
242
|
+
{ cause: error }
|
|
241
243
|
)
|
|
242
244
|
}
|
|
243
245
|
const envelope = validateEvalReport(report)
|
|
@@ -315,17 +317,37 @@ function runPerformance() {
|
|
|
315
317
|
}
|
|
316
318
|
}
|
|
317
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Audit-mode pull requests whose diff is docs-only run the fast tier instead.
|
|
322
|
+
* Scheduled and manual audits always keep the full tier; release pull requests
|
|
323
|
+
* are classified separately and always run their lane.
|
|
324
|
+
* @param {string} mode
|
|
325
|
+
*/
|
|
326
|
+
function downgradeDocsOnlyPullRequest(mode) {
|
|
327
|
+
if (mode !== 'audit') return mode
|
|
328
|
+
if ((process.env.FOUNDRY_EVENT_NAME ?? '') !== 'pull_request') return mode
|
|
329
|
+
const baseRef = process.env.FOUNDRY_BASE_REF ?? ''
|
|
330
|
+
if (!baseRef) return mode
|
|
331
|
+
try {
|
|
332
|
+
return docsOnlyPullRequest(root, baseRef) ? 'fast' : mode
|
|
333
|
+
} catch {
|
|
334
|
+
return mode
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
318
338
|
/** @param {string} task */
|
|
319
339
|
function validation(task) {
|
|
320
340
|
if (task === 'mode') {
|
|
321
341
|
writeOutput(
|
|
322
342
|
'mode',
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
343
|
+
downgradeDocsOnlyPullRequest(
|
|
344
|
+
classifyValidationMode({
|
|
345
|
+
eventName: process.env.FOUNDRY_EVENT_NAME ?? '',
|
|
346
|
+
baseRef: process.env.FOUNDRY_BASE_REF ?? '',
|
|
347
|
+
headRef: process.env.FOUNDRY_HEAD_REF ?? '',
|
|
348
|
+
stagingMode: config.staging_validation_mode,
|
|
349
|
+
})
|
|
350
|
+
)
|
|
329
351
|
)
|
|
330
352
|
return
|
|
331
353
|
}
|
|
@@ -692,10 +714,11 @@ function ci(task) {
|
|
|
692
714
|
hasRootJavascriptProject() &&
|
|
693
715
|
hasOxlintSetup()
|
|
694
716
|
) {
|
|
695
|
-
// Oxlint is the baseline's linter.
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
|
|
717
|
+
// Oxlint is the baseline's linter. Warnings are denied so lint debt
|
|
718
|
+
// never accumulates silently. Repositories that use a different linter
|
|
719
|
+
// keep full control through their own `lint` script, which the runScript
|
|
720
|
+
// fallback above already honors.
|
|
721
|
+
runTool('oxlint', ['--deny-warnings'])
|
|
699
722
|
}
|
|
700
723
|
if (hasLanguage('python') && hasRootPythonProject()) runTool('ruff', ['check', '.'])
|
|
701
724
|
if (hasLanguage('rust') && hasRootRustProject())
|