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
|
@@ -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.
|
|
@@ -5,8 +5,9 @@ Optional task discovery remains available. Configure scalar values in
|
|
|
5
5
|
`.github/code-foundry.yml`:
|
|
6
6
|
|
|
7
7
|
```yaml
|
|
8
|
-
required_capabilities: type_check,unit,e2e,performance,coverage
|
|
8
|
+
required_capabilities: type_check,unit,e2e,eval,performance,coverage
|
|
9
9
|
performance: true
|
|
10
|
+
eval: true
|
|
10
11
|
coverage_enforcement: required
|
|
11
12
|
coverage_minimum: 80
|
|
12
13
|
coverage_metrics: lines,branches
|
|
@@ -14,10 +15,11 @@ coverage_report: coverage/coverage-summary.json
|
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
Supported task capabilities are `format`, `lint`, `type_check`, `build`, `unit`,
|
|
17
|
-
`integration`, `e2e`, `smoke`, and `performance`. `coverage` additionally requires
|
|
18
|
+
`integration`, `e2e`, `smoke`, `eval`, and `performance`. `coverage` additionally requires
|
|
18
19
|
unit tests. Unknown names, contradictory requirements, and invalid thresholds
|
|
19
|
-
are errors. `performance: true`
|
|
20
|
-
script happens to exist. Use `performance: auto`
|
|
20
|
+
are errors. `performance: true` and `eval: true` mean required, not merely
|
|
21
|
+
enabled when a script happens to exist. Use `performance: auto` or
|
|
22
|
+
`eval: auto` to retain optional discovery.
|
|
21
23
|
|
|
22
24
|
The public `src/runtime.mjs` entrypoint delegates ecosystem execution to the
|
|
23
25
|
private `src/runtime-core.mjs`. Keep both files and `src/lib` when vendoring the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-foundry",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.2",
|
|
4
4
|
"description": "A fast, language-aware repository factory for agent-ready workflows, testing, security, and release automation.",
|
|
5
5
|
"homepage": "https://github.com/0xPlayerOne/code-foundry#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"build": "node --check src/cli.mjs",
|
|
43
43
|
"format:check": "oxfmt --check .",
|
|
44
44
|
"fmt": "oxfmt .",
|
|
45
|
-
"lint": "oxlint",
|
|
45
|
+
"lint": "oxlint --deny-warnings",
|
|
46
46
|
"performance:check": "node scripts/performance-check.mjs",
|
|
47
47
|
"test": "node --test",
|
|
48
48
|
"test:unit": "node --test",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"typescript": "^7.0.2"
|
|
58
58
|
},
|
|
59
59
|
"engines": {
|
|
60
|
-
"node": ">=
|
|
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',
|
|
@@ -46,6 +46,24 @@ export function runCommand(command, args, cwd) {
|
|
|
46
46
|
)
|
|
47
47
|
return result.stdout
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/** Default tolerant runner: GitHub CLI failures surface as retryable misses
|
|
51
|
+
* instead of aborting the publication flow.
|
|
52
|
+
* @param {string} command
|
|
53
|
+
* @param {string[]} args
|
|
54
|
+
* @param {string} [cwd]
|
|
55
|
+
* @returns {{status: number, stdout: string}}
|
|
56
|
+
*/
|
|
57
|
+
function defaultRunTolerant(command, args, cwd) {
|
|
58
|
+
const result = spawnSync(command, args, {
|
|
59
|
+
cwd,
|
|
60
|
+
encoding: 'utf8',
|
|
61
|
+
timeout: 180_000,
|
|
62
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
63
|
+
env: { ...process.env, GH_HOST: 'github.com', GH_PROMPT_DISABLED: '1' },
|
|
64
|
+
})
|
|
65
|
+
return { status: result.error ? 1 : (result.status ?? 1), stdout: result.stdout ?? '' }
|
|
66
|
+
}
|
|
49
67
|
/** @param {Candidate} candidate */
|
|
50
68
|
export function validateCandidate(candidate) {
|
|
51
69
|
ensure(
|
|
@@ -225,10 +243,50 @@ export async function publishQualifiedArchive(candidate, reports, adapters = {})
|
|
|
225
243
|
return { ...before, status: 'published' }
|
|
226
244
|
}
|
|
227
245
|
|
|
246
|
+
/** @typedef {(command:string, args:string[], cwd?:string) => {status: number, stdout: string}} RunTolerant */
|
|
247
|
+
|
|
248
|
+
/** Poll the releases-by-tag endpoint through its eventual-consistency window.
|
|
249
|
+
* Immediately after `gh release edit --draft=false` the endpoint can still
|
|
250
|
+
* return 404; a strict verification in that window aborts an otherwise
|
|
251
|
+
* healthy publication. Bounded retries keep the flow strict: it only proceeds
|
|
252
|
+
* once the endpoint serves the published release with the exact tag.
|
|
253
|
+
* @param {Candidate} candidate
|
|
254
|
+
* @param {{runTolerant?: RunTolerant, delay?: (ms:number)=>Promise<void>, attempts?: number}} [adapters]
|
|
255
|
+
* @returns {Promise<Record<string, any>>}
|
|
256
|
+
*/
|
|
257
|
+
async function waitForPublishedRelease(candidate, adapters = {}) {
|
|
258
|
+
const runTolerant = adapters.runTolerant ?? defaultRunTolerant
|
|
259
|
+
const delay = adapters.delay ?? ((ms) => new Promise((done) => setTimeout(done, ms)))
|
|
260
|
+
const attempts = adapters.attempts ?? 15
|
|
261
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
262
|
+
const result = runTolerant('gh', [
|
|
263
|
+
'api',
|
|
264
|
+
'--hostname',
|
|
265
|
+
'github.com',
|
|
266
|
+
`repos/${candidate.repository}/releases/tags/${encodeURIComponent(candidate.tag)}`,
|
|
267
|
+
])
|
|
268
|
+
if (result.status === 0) {
|
|
269
|
+
/** @type {Record<string, any> | null} */
|
|
270
|
+
let release = null
|
|
271
|
+
try {
|
|
272
|
+
release = JSON.parse(result.stdout)
|
|
273
|
+
} catch {
|
|
274
|
+
release = null
|
|
275
|
+
}
|
|
276
|
+
if (release?.draft === false && release?.tag_name === candidate.tag) return release
|
|
277
|
+
}
|
|
278
|
+
if (attempt === attempts) break
|
|
279
|
+
await delay(2_000)
|
|
280
|
+
}
|
|
281
|
+
throw new Error(
|
|
282
|
+
'The published release was not retrievable via the tag endpoint within the consistency window'
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
228
286
|
/** Stage only a previously-created draft: never move tags, overwrite assets, or
|
|
229
287
|
* treat an API/permissions failure as an absent release. Authentication needs
|
|
230
288
|
* immutable-settings read and release write; no setting is mutated.
|
|
231
|
-
* @param {Candidate} candidate @param {string[]} reports @param {{run?:Run, verify?:Verify}} [adapters]
|
|
289
|
+
* @param {Candidate} candidate @param {string[]} reports @param {{run?:Run, runTolerant?:RunTolerant, delay?:(ms:number)=>Promise<void>, attempts?:number, verify?:Verify}} [adapters]
|
|
232
290
|
*/
|
|
233
291
|
export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
|
|
234
292
|
const run = adapters.run ?? runCommand
|
|
@@ -332,7 +390,27 @@ export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
|
|
|
332
390
|
'--repo',
|
|
333
391
|
candidate.repository,
|
|
334
392
|
])
|
|
335
|
-
|
|
393
|
+
// GitHub's release index is eventually consistent right after a draft is
|
|
394
|
+
// published; a strict verification a second later can observe a 404 and
|
|
395
|
+
// abort the publication even though the release is live. Poll until the
|
|
396
|
+
// index serves the release, then verify; the verification cycle itself
|
|
397
|
+
// retries through the same window and still fails closed in the end.
|
|
398
|
+
await waitForPublishedRelease(candidate, {
|
|
399
|
+
runTolerant: adapters.runTolerant,
|
|
400
|
+
delay: adapters.delay,
|
|
401
|
+
attempts: 10,
|
|
402
|
+
})
|
|
403
|
+
/** @type {Record<string, any>} */
|
|
404
|
+
let identity
|
|
405
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
406
|
+
try {
|
|
407
|
+
identity = await (adapters.verify ?? verifier)(candidate)
|
|
408
|
+
break
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (attempt >= 10) throw error
|
|
411
|
+
await (adapters.delay ?? ((ms) => new Promise((done) => setTimeout(done, ms))))(5_000)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
336
414
|
ensure(
|
|
337
415
|
identity.status === 'passed' &&
|
|
338
416
|
identity.immutable === true &&
|
|
@@ -7,7 +7,7 @@ import { basename, isAbsolute, relative, resolve, sep } from 'node:path'
|
|
|
7
7
|
import { spawnSync } from 'node:child_process'
|
|
8
8
|
import { pathToFileURL } from 'node:url'
|
|
9
9
|
|
|
10
|
-
/** @typedef {(argv: string[]) => {status: number|null, stdout: string}} Runner */
|
|
10
|
+
/** @typedef {(argv: string[]) => {status: number|null, stdout: string, stderr?: string}} Runner */
|
|
11
11
|
|
|
12
12
|
/** @type {Runner} */
|
|
13
13
|
export function runGh(argv) {
|
|
@@ -18,7 +18,7 @@ export function runGh(argv) {
|
|
|
18
18
|
env: { ...process.env, GH_PROMPT_DISABLED: '1', GH_HOST: 'github.com' },
|
|
19
19
|
})
|
|
20
20
|
if (result.error) throw new Error(`GitHub CLI unavailable: ${result.error.name}`)
|
|
21
|
-
return { status: result.status, stdout: result.stdout ?? '' }
|
|
21
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? '' }
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/** @param {string} repository */
|
|
@@ -56,7 +56,7 @@ function json(run, argv) {
|
|
|
56
56
|
const result = run(argv)
|
|
57
57
|
if (result.status !== 0)
|
|
58
58
|
throw new Error(
|
|
59
|
-
`GitHub verification command failed (${result.status ?? 'signal'}); state is unverified`
|
|
59
|
+
`GitHub verification command failed (${result.status ?? 'signal'}); state is unverified: ${String(result.stderr ?? '').slice(0, 300) || 'no stderr captured'}`
|
|
60
60
|
)
|
|
61
61
|
try {
|
|
62
62
|
return JSON.parse(result.stdout)
|
|
@@ -197,6 +197,17 @@ export function verifyRelease(options, run = runGh) {
|
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/** Sync pause used by the post-publication verifier while the release index
|
|
201
|
+
* catches up with a just-published release.
|
|
202
|
+
* @param {number} ms
|
|
203
|
+
*/
|
|
204
|
+
function sleepSync(ms) {
|
|
205
|
+
if (ms <= 0) return
|
|
206
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const RELEASE_VERIFICATION_ATTEMPTS = 6
|
|
210
|
+
|
|
200
211
|
/** @param {string[]} argv @param {Runner} [run] */
|
|
201
212
|
export function integrityCommand(argv, run = runGh) {
|
|
202
213
|
const [command, ...args] = argv
|
|
@@ -228,17 +239,29 @@ export function integrityCommand(argv, run = runGh) {
|
|
|
228
239
|
throw new Error('An argument is not supported by this integrity command')
|
|
229
240
|
if (command === 'settings') return verifyImmutableSetting(values['--repo'] ?? '', run)
|
|
230
241
|
if (command === 'manifest') return assetManifest(values['--root'] ?? process.cwd(), assets)
|
|
231
|
-
if (command === 'release')
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
if (command === 'release') {
|
|
243
|
+
const options = {
|
|
244
|
+
repository: values['--repo'] ?? '',
|
|
245
|
+
tag: values['--tag'] ?? '',
|
|
246
|
+
root: values['--root'],
|
|
247
|
+
expectedSha: values['--expected-sha'],
|
|
248
|
+
assets,
|
|
249
|
+
}
|
|
250
|
+
// The lane fires the instant GitHub marks the release published, while the
|
|
251
|
+
// releases-by-tag index can still lag behind. Retry through that window;
|
|
252
|
+
// every strict check still runs on every attempt and the command fails
|
|
253
|
+
// closed once the bounded window is exhausted.
|
|
254
|
+
let lastError
|
|
255
|
+
for (let attempt = 1; attempt <= RELEASE_VERIFICATION_ATTEMPTS; attempt += 1) {
|
|
256
|
+
try {
|
|
257
|
+
return verifyRelease(options, run)
|
|
258
|
+
} catch (error) {
|
|
259
|
+
lastError = error
|
|
260
|
+
if (attempt < RELEASE_VERIFICATION_ATTEMPTS) sleepSync(5_000)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
throw lastError
|
|
264
|
+
}
|
|
242
265
|
throw new Error('Use release-integrity settings, release, or manifest')
|
|
243
266
|
}
|
|
244
267
|
|
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(
|
|
@@ -714,6 +714,7 @@ function renderWorkflow(content, config, repository, ref, rustCodeql, file, self
|
|
|
714
714
|
'codeql-runner': config.codeql_runner ?? config.runner,
|
|
715
715
|
'unit-runner': config.unit_runner,
|
|
716
716
|
'performance-runner': config.performance_runner ?? config.test_runner ?? config.runner,
|
|
717
|
+
'eval-runner': config.eval_runner ?? config.runner,
|
|
717
718
|
}
|
|
718
719
|
for (const [input, value] of Object.entries(runnerInputs)) {
|
|
719
720
|
if (!value) continue
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
/** Docs-only allowlist: markdown, docs/, and license roots. Code, lockfiles,
|
|
6
|
+
* workflows, configuration, and packaging always keep the audit tier. */
|
|
7
|
+
const DOCS_ONLY_PATTERNS = [/\.md$/i, /^docs\//, /^LICENSE/i]
|
|
8
|
+
|
|
9
|
+
/** @param {string} path */
|
|
10
|
+
export function isDocsOnlyPath(path) {
|
|
11
|
+
return DOCS_ONLY_PATTERNS.some((pattern) => pattern.test(path))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Report whether a pull request changes docs only. Fail-closed: any git
|
|
16
|
+
* failure, unresolvable base, or non-docs path keeps the audit tier.
|
|
17
|
+
* @param {string} root
|
|
18
|
+
* @param {string} baseRef
|
|
19
|
+
*/
|
|
20
|
+
export function docsOnlyPullRequest(root, baseRef) {
|
|
21
|
+
try {
|
|
22
|
+
execFileSync('git', ['fetch', '--no-tags', '--depth=100', 'origin', baseRef], {
|
|
23
|
+
cwd: root,
|
|
24
|
+
stdio: 'pipe',
|
|
25
|
+
})
|
|
26
|
+
const output = execFileSync('git', ['diff', '--name-only', 'FETCH_HEAD...HEAD'], {
|
|
27
|
+
cwd: root,
|
|
28
|
+
encoding: 'utf8',
|
|
29
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
})
|
|
31
|
+
const paths = output
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map((line) => line.trim())
|
|
34
|
+
.filter((line) => line.length > 0)
|
|
35
|
+
return paths.length > 0 && paths.every(isDocsOnlyPath)
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|