code-foundry 1.17.0 → 1.19.0

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.
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ import { createHash } from 'node:crypto'
4
+ import { spawnSync } from 'node:child_process'
5
+ import {
6
+ lstatSync,
7
+ mkdirSync,
8
+ mkdtempSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ realpathSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from 'node:fs'
15
+ import { join, resolve } from 'node:path'
16
+ import { pathToFileURL } from 'node:url'
17
+ import { tmpdir } from 'node:os'
18
+
19
+ export const REQUIRED_NODES = ['20', '22', '24']
20
+ export const REQUIRED_FIXTURES = [
21
+ 'npm-direct',
22
+ 'pnpm-direct',
23
+ 'yarn-staging',
24
+ 'bun-mise',
25
+ 'nested-rust',
26
+ 'nested-python',
27
+ ]
28
+ /** @typedef {{ repository: string, tag: string, sourceSha: string, asset: string, directory: string }} Candidate */
29
+ /** @typedef {(command:string, args:string[], cwd?:string) => string} Run */
30
+ /** @param {unknown} condition @param {string} message @returns {asserts condition} */
31
+ function ensure(condition, message) {
32
+ if (!condition) throw new Error(message)
33
+ }
34
+ /** @type {Run} */
35
+ export function runCommand(command, args, cwd) {
36
+ const result = spawnSync(command, args, {
37
+ cwd,
38
+ encoding: 'utf8',
39
+ timeout: 180_000,
40
+ maxBuffer: 8 * 1024 * 1024,
41
+ env: { ...process.env, GH_HOST: 'github.com', GH_PROMPT_DISABLED: '1' },
42
+ })
43
+ ensure(
44
+ !result.error && result.status === 0,
45
+ `${command} failed (${result.status ?? result.error?.message}); publication remains blocked`
46
+ )
47
+ return result.stdout
48
+ }
49
+ /** @param {Candidate} candidate */
50
+ export function validateCandidate(candidate) {
51
+ ensure(
52
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(candidate.repository) &&
53
+ !candidate.repository.split('/').some((part) => ['.', '..'].includes(part)),
54
+ 'Expected an owner/repository'
55
+ )
56
+ ensure(
57
+ /^v\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(candidate.tag),
58
+ 'Expected an explicit version tag'
59
+ )
60
+ ensure(/^[a-f0-9]{40}$/.test(candidate.sourceSha), 'Expected an exact source SHA')
61
+ ensure(
62
+ /^[A-Za-z0-9][A-Za-z0-9._-]*\.tgz$/.test(candidate.asset),
63
+ 'Expected one exact tarball asset name'
64
+ )
65
+ }
66
+ /** @param {string} file */
67
+ export function sha256(file) {
68
+ ensure(
69
+ lstatSync(file).isFile() && !lstatSync(file).isSymbolicLink(),
70
+ 'Expected a regular non-symlink file'
71
+ )
72
+ const bytes = readFileSync(file)
73
+ ensure(bytes.length > 0, 'Empty artifact or report')
74
+ return createHash('sha256').update(bytes).digest('hex')
75
+ }
76
+ /** Pure policy check. Only reports from this workflow run are accepted by the caller.
77
+ * @param {unknown[]} reports @param {string} sourceSha @param {string} artifactSha256
78
+ */
79
+ export function validateQualificationReports(reports, sourceSha, artifactSha256) {
80
+ ensure(
81
+ reports.length === REQUIRED_NODES.length,
82
+ 'Every required Node qualification report is mandatory'
83
+ )
84
+ const seen = new Set()
85
+ for (const value of reports) {
86
+ const report = /** @type {Record<string, any>} */ (value)
87
+ ensure(
88
+ report?.schema_version === 1 && report.complete === true && report.actionlint === true,
89
+ 'Incomplete or unlinted qualification report'
90
+ )
91
+ ensure(
92
+ report.source_sha === sourceSha && report.artifact_sha256 === artifactSha256,
93
+ 'Qualification source or archive identity mismatch'
94
+ )
95
+ const node = /^v(\d+)\.\d+\.\d+$/.exec(report.node ?? '')?.[1]
96
+ ensure(
97
+ node && REQUIRED_NODES.includes(node) && !seen.has(node),
98
+ 'Unexpected or duplicate Node qualification'
99
+ )
100
+ seen.add(node)
101
+ ensure(
102
+ Array.isArray(report.fixtures) && report.fixtures.length === REQUIRED_FIXTURES.length,
103
+ 'Incomplete consumer fixture matrix'
104
+ )
105
+ for (const fixture of REQUIRED_FIXTURES)
106
+ ensure(
107
+ report.fixtures.filter(
108
+ (/** @type {Record<string,any>} */ entry) =>
109
+ entry.name === fixture && entry.status === 'passed'
110
+ ).length === 1,
111
+ `Missing successful fixture: ${fixture}`
112
+ )
113
+ }
114
+ return {
115
+ nodes: REQUIRED_NODES.filter((node) => seen.has(node)),
116
+ fixtures: [...REQUIRED_FIXTURES],
117
+ }
118
+ }
119
+ /** @param {Candidate} candidate */
120
+ async function verifier(candidate) {
121
+ // Supplied by the separately reviewed release-integrity feature (#537).
122
+ const { verifyRelease } = await import('./release-integrity.mjs')
123
+ return verifyRelease({
124
+ repository: candidate.repository,
125
+ tag: candidate.tag,
126
+ expectedSha: candidate.sourceSha,
127
+ root: candidate.directory,
128
+ assets: [candidate.asset],
129
+ })
130
+ }
131
+ /** @typedef {(candidate:Candidate) => Promise<any>} Verify */
132
+ /** @param {Candidate} candidate @param {{run?:Run, verify?:Verify}} [adapters] */
133
+ export async function downloadVerifiedArchive(candidate, adapters = {}) {
134
+ validateCandidate(candidate)
135
+ const run = adapters.run ?? runCommand
136
+ const verify = adapters.verify ?? verifier
137
+ mkdirSync(candidate.directory, { recursive: true })
138
+ ensure(
139
+ lstatSync(candidate.directory).isDirectory() &&
140
+ !lstatSync(candidate.directory).isSymbolicLink(),
141
+ 'Download directory must be a real directory'
142
+ )
143
+ ensure(readdirSync(candidate.directory).length === 0, 'Use a fresh empty download directory')
144
+ run('gh', [
145
+ 'release',
146
+ 'download',
147
+ candidate.tag,
148
+ '--repo',
149
+ candidate.repository,
150
+ '--pattern',
151
+ candidate.asset,
152
+ '--dir',
153
+ candidate.directory,
154
+ ])
155
+ ensure(
156
+ readdirSync(candidate.directory).length === 1 &&
157
+ readdirSync(candidate.directory)[0] === candidate.asset,
158
+ 'Unexpected downloaded release files'
159
+ )
160
+ const file = join(realpathSync(candidate.directory), candidate.asset)
161
+ const digest = sha256(file)
162
+ const identity = await verify(candidate)
163
+ ensure(
164
+ identity.status === 'passed' &&
165
+ identity.immutable === true &&
166
+ identity.sourceSha === candidate.sourceSha,
167
+ 'Release verification did not establish candidate identity'
168
+ )
169
+ ensure(
170
+ identity.assets?.length === 1 && identity.assets[0].digest === `sha256:${digest}`,
171
+ 'Verified release asset differs from downloaded bytes'
172
+ )
173
+ ensure(sha256(file) === digest, 'Archive changed during verification')
174
+ return { file, digest, identity }
175
+ }
176
+ /** @param {Candidate} candidate @param {string[]} reportPaths @param {Run} [run] */
177
+ export function qualifyArchive(candidate, reportPaths, run = runCommand) {
178
+ validateCandidate(candidate)
179
+ const file = join(realpathSync(candidate.directory), candidate.asset)
180
+ const digest = sha256(file)
181
+ const reports = reportPaths.map((path) => {
182
+ sha256(path)
183
+ return JSON.parse(readFileSync(path, 'utf8'))
184
+ })
185
+ const qualification = validateQualificationReports(reports, candidate.sourceSha, digest)
186
+ // Read one manifest without extracting archive paths into the workspace.
187
+ const manifest = JSON.parse(run('tar', ['-xOf', file, 'package/package.json']))
188
+ ensure(
189
+ manifest.name === 'code-foundry' && manifest.version === candidate.tag.slice(1),
190
+ 'Archive package identity does not match this Code Foundry release'
191
+ )
192
+ return {
193
+ schemaVersion: 1,
194
+ kind: 'code-foundry-publication-eligibility',
195
+ repository: candidate.repository,
196
+ tag: candidate.tag,
197
+ sourceSha: candidate.sourceSha,
198
+ asset: candidate.asset,
199
+ digest: `sha256:${digest}`,
200
+ qualification,
201
+ }
202
+ }
203
+ /** @param {Candidate} candidate @param {string[]} reports @param {{run?:Run, verify?:Verify}} [adapters] */
204
+ export async function publishQualifiedArchive(candidate, reports, adapters = {}) {
205
+ const run = adapters.run ?? runCommand
206
+ const before = qualifyArchive(candidate, reports, run)
207
+ const identity = await (adapters.verify ?? verifier)(candidate)
208
+ ensure(
209
+ identity.status === 'passed' &&
210
+ identity.immutable === true &&
211
+ identity.sourceSha === before.sourceSha &&
212
+ identity.assets?.length === 1 &&
213
+ identity.assets[0].digest === before.digest,
214
+ 'Current release verification does not match qualified bytes'
215
+ )
216
+ const file = join(realpathSync(candidate.directory), candidate.asset)
217
+ ensure(`sha256:${sha256(file)}` === before.digest, 'Qualified archive changed before publication')
218
+ // Never publish a directory, rebuild, or invoke package lifecycle hooks here.
219
+ run(
220
+ 'npm',
221
+ ['publish', file, '--ignore-scripts', '--provenance', '--access', 'public'],
222
+ candidate.directory
223
+ )
224
+ ensure(`sha256:${sha256(file)}` === before.digest, 'Archive changed during publication')
225
+ return { ...before, status: 'published' }
226
+ }
227
+
228
+ /** Stage only a previously-created draft: never move tags, overwrite assets, or
229
+ * treat an API/permissions failure as an absent release. Authentication needs
230
+ * immutable-settings read and release write; no setting is mutated.
231
+ * @param {Candidate} candidate @param {string[]} reports @param {{run?:Run, verify?:Verify}} [adapters]
232
+ */
233
+ export async function stageQualifiedRelease(candidate, reports, adapters = {}) {
234
+ const run = adapters.run ?? runCommand
235
+ const qualification = qualifyArchive(candidate, reports, run)
236
+ const api = (/** @type {string} */ endpoint) =>
237
+ JSON.parse(run('gh', ['api', '--hostname', 'github.com', endpoint]))
238
+ const prefix = `repos/${candidate.repository}`
239
+ ensure(
240
+ api(`${prefix}/immutable-releases`).enabled === true,
241
+ 'Release immutability is not confirmed enabled; no release write performed'
242
+ )
243
+ const checkTag = () => {
244
+ let object = api(`${prefix}/git/ref/tags/${encodeURIComponent(candidate.tag)}`).object
245
+ for (let depth = 0; object?.type === 'tag' && depth < 8; depth++) {
246
+ ensure(/^[a-f0-9]{40}$/.test(object.sha ?? ''), 'Invalid annotated tag')
247
+ object = api(`${prefix}/git/tags/${object.sha}`).object
248
+ }
249
+ ensure(
250
+ object?.type === 'commit' && object.sha === candidate.sourceSha,
251
+ 'Release tag does not identify the qualified commit'
252
+ )
253
+ }
254
+ checkTag()
255
+ const endpoint = `${prefix}/releases/tags/${encodeURIComponent(candidate.tag)}`
256
+ const release = api(endpoint)
257
+ ensure(
258
+ Number.isSafeInteger(release.id) &&
259
+ release.draft === true &&
260
+ release.tag_name === candidate.tag &&
261
+ Array.isArray(release.assets),
262
+ 'An existing draft release with the exact tag is required'
263
+ )
264
+ const temporary = mkdtempSync(join(tmpdir(), 'foundry-release-receipt-'))
265
+ try {
266
+ const receipt = join(temporary, 'qualification.json')
267
+ writeFileSync(receipt, JSON.stringify(qualification, null, 2) + '\n')
268
+ const files = [join(realpathSync(candidate.directory), candidate.asset), receipt]
269
+ const expected = [qualification.digest, `sha256:${sha256(receipt)}`]
270
+ const names = [candidate.asset, 'qualification.json']
271
+ for (let index = 0; index < files.length; index++) {
272
+ const existing = release.assets.filter(
273
+ (/** @type {Record<string, any>} */ asset) => asset.name === names[index]
274
+ )
275
+ ensure(existing.length <= 1, 'Ambiguous existing release asset')
276
+ if (existing.length)
277
+ ensure(
278
+ existing[0].state === 'uploaded' && existing[0].digest === expected[index],
279
+ 'Existing draft asset differs; refusing to overwrite it'
280
+ )
281
+ else
282
+ run('gh', [
283
+ 'release',
284
+ 'upload',
285
+ candidate.tag,
286
+ files[index],
287
+ '--repo',
288
+ candidate.repository,
289
+ ])
290
+ }
291
+ const uploaded = api(endpoint)
292
+ ensure(
293
+ uploaded.id === release.id && uploaded.draft === true && uploaded.tag_name === candidate.tag,
294
+ 'Draft changed during staging'
295
+ )
296
+ for (let index = 0; index < names.length; index++) {
297
+ const assets =
298
+ uploaded.assets?.filter(
299
+ (/** @type {Record<string, any>} */ asset) => asset.name === names[index]
300
+ ) ?? []
301
+ ensure(
302
+ assets.length === 1 &&
303
+ assets[0].state === 'uploaded' &&
304
+ assets[0].digest === expected[index],
305
+ 'Uploaded release asset digest mismatch'
306
+ )
307
+ ensure(
308
+ `sha256:${sha256(files[index])}` === expected[index],
309
+ 'Local asset changed during staging'
310
+ )
311
+ }
312
+ checkTag()
313
+ run('gh', [
314
+ 'release',
315
+ 'edit',
316
+ candidate.tag,
317
+ '--draft=false',
318
+ '--verify-tag',
319
+ '--repo',
320
+ candidate.repository,
321
+ ])
322
+ const identity = await (adapters.verify ?? verifier)(candidate)
323
+ ensure(
324
+ identity.status === 'passed' &&
325
+ identity.immutable === true &&
326
+ identity.sourceSha === qualification.sourceSha &&
327
+ identity.assets?.length === 1 &&
328
+ identity.assets[0].digest === qualification.digest,
329
+ 'Published release verification failed; npm remains blocked'
330
+ )
331
+ return { ...qualification, status: 'release-published-and-verified' }
332
+ } finally {
333
+ rmSync(temporary, { recursive: true, force: true })
334
+ }
335
+ }
336
+
337
+ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
338
+ try {
339
+ const [command, repository, tag, sourceSha, asset, directory, ...reports] =
340
+ process.argv.slice(2)
341
+ if (!directory || !['download', 'qualify', 'stage', 'publish'].includes(command))
342
+ throw new Error(
343
+ 'Usage: qualified-publication.mjs download|qualify|stage|publish REPO TAG SOURCE_SHA ASSET DIRECTORY [REPORT.json ...]'
344
+ )
345
+ const candidate = { repository, tag, sourceSha, asset, directory: resolve(directory) }
346
+ if (command === 'download' && reports.length)
347
+ throw new Error('download does not take report paths')
348
+ const result =
349
+ command === 'download'
350
+ ? await downloadVerifiedArchive(candidate)
351
+ : command === 'qualify'
352
+ ? qualifyArchive(candidate, reports)
353
+ : command === 'stage'
354
+ ? await stageQualifiedRelease(candidate, reports)
355
+ : await publishQualifiedArchive(candidate, reports)
356
+ console.log(JSON.stringify(result, null, 2))
357
+ } catch (error) {
358
+ console.error(error instanceof Error ? error.message : String(error))
359
+ process.exitCode = 1
360
+ }
361
+ }
@@ -0,0 +1,245 @@
1
+ // @ts-check
2
+ import { createHash } from 'node:crypto'
3
+ import { spawnSync } from 'node:child_process'
4
+ import { lstatSync, readFileSync } from 'node:fs'
5
+ import { join } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ export const RELEASE_POLICY_FILE = '.code-foundry-release-policy.json'
9
+ /** @typedef {(command:string, args:string[]) => string} Run */
10
+ /** @param {unknown} condition @param {string} message @returns {asserts condition} */
11
+ function ensure(condition, message) {
12
+ if (!condition) throw new Error(message)
13
+ }
14
+ /** @type {Run} */
15
+ function runCommand(command, args) {
16
+ const result = spawnSync(command, args, {
17
+ encoding: 'utf8',
18
+ timeout: 120_000,
19
+ maxBuffer: 16 * 1024 * 1024,
20
+ env: { ...process.env, GH_HOST: 'github.com', GH_PROMPT_DISABLED: '1' },
21
+ })
22
+ ensure(
23
+ !result.error && result.status === 0,
24
+ `Fleet eligibility is unverified: ${command} failed (${result.status ?? result.error?.message})`
25
+ )
26
+ return result.stdout.trim()
27
+ }
28
+ /** @param {Buffer} bytes */
29
+ function hash(bytes) {
30
+ return createHash('sha256').update(bytes).digest('hex')
31
+ }
32
+ /** @param {unknown} value */
33
+ export function validateReleasePolicy(value) {
34
+ const policy = /** @type {Record<string, any>} */ (value)
35
+ ensure(policy?.schema_version === 1, 'Expected a version-1 release policy')
36
+ ensure(
37
+ typeof policy.repository === 'string' &&
38
+ /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(policy.repository) &&
39
+ !policy.repository
40
+ .split('/')
41
+ .some((/** @type {string} */ part) => ['.', '..'].includes(part)),
42
+ 'Invalid release-policy repository'
43
+ )
44
+ ensure(
45
+ typeof policy.workflow === 'string' &&
46
+ /^\.github\/workflows\/[A-Za-z0-9_-]+\.ya?ml$/.test(policy.workflow),
47
+ 'Expected an exact qualification workflow path'
48
+ )
49
+ ensure(
50
+ policy.branch === 'main' || policy.branch === 'staging',
51
+ 'Select a protected main or staging qualification branch'
52
+ )
53
+ ensure(
54
+ Array.isArray(policy.required_jobs) &&
55
+ policy.required_jobs.length > 0 &&
56
+ policy.required_jobs.every(
57
+ (/** @type {unknown} */ job) =>
58
+ typeof job === 'string' && job.trim() === job && job.length > 0
59
+ ) &&
60
+ new Set(policy.required_jobs).size === policy.required_jobs.length,
61
+ 'Expected unique nonempty required qualification job names'
62
+ )
63
+ return policy
64
+ }
65
+ /** @param {unknown} value @param {string} property */
66
+ function pages(value, property) {
67
+ ensure(Array.isArray(value) && value.length > 0, 'Missing paginated qualification evidence')
68
+ /** @type {Record<string, any>[]} */
69
+ const items = []
70
+ let expected = 0
71
+ for (const page of value) {
72
+ ensure(
73
+ Number.isInteger(page.total_count) && page.total_count >= 0 && Array.isArray(page[property]),
74
+ 'Malformed paginated qualification evidence'
75
+ )
76
+ expected = Math.max(expected, page.total_count)
77
+ items.push(...page[property])
78
+ }
79
+ ensure(items.length >= expected, 'Incomplete qualification evidence pagination')
80
+ return items
81
+ }
82
+ /** @param {string} root @param {string} source @param {string} version @param {Run} [run] */
83
+ export function assertFleetEligibility(root, source, version, run = runCommand) {
84
+ const path = join(root, RELEASE_POLICY_FILE)
85
+ let entry
86
+ try {
87
+ entry = lstatSync(path)
88
+ } catch (error) {
89
+ if (/** @type {NodeJS.ErrnoException} */ (error).code === 'ENOENT') return null
90
+ throw error
91
+ }
92
+ ensure(
93
+ entry.isFile() && !entry.isSymbolicLink(),
94
+ 'Release policy must be a regular non-symlink file'
95
+ )
96
+ const bytes = readFileSync(path)
97
+ const policy = validateReleasePolicy(JSON.parse(bytes.toString('utf8')))
98
+ ensure(
99
+ /^v\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version),
100
+ 'Qualified upgrades require an explicit version tag'
101
+ )
102
+ const git = (/** @type {string[]} */ args) => run('git', ['-C', source, ...args]).trim()
103
+ const sourceSha = git(['rev-parse', 'HEAD'])
104
+ ensure(
105
+ /^[a-f0-9]{40}$/.test(sourceSha),
106
+ 'Qualified upgrades require a source Git checkout with an exact commit'
107
+ )
108
+ ensure(
109
+ git(['status', '--porcelain', '--untracked-files=all']) === '',
110
+ 'Qualification cannot authorize a dirty runtime source'
111
+ )
112
+ // Execute the installed/trusted verifier, never code from the proposed source checkout.
113
+ const verifier = fileURLToPath(new URL('../commands/release-integrity.mjs', import.meta.url))
114
+ const verified = JSON.parse(
115
+ run(process.execPath, [
116
+ verifier,
117
+ 'release',
118
+ '--repo',
119
+ policy.repository,
120
+ '--tag',
121
+ version,
122
+ '--expected-sha',
123
+ sourceSha,
124
+ ])
125
+ )
126
+ ensure(
127
+ verified.status === 'passed' &&
128
+ verified.check === 'release-attestation' &&
129
+ verified.immutable === true &&
130
+ verified.repository === policy.repository &&
131
+ verified.tag === version &&
132
+ verified.sourceSha === sourceSha,
133
+ 'Release identity was not cryptographically verified'
134
+ )
135
+ const api = (/** @type {string} */ endpoint, paginate = false) =>
136
+ JSON.parse(
137
+ run('gh', [
138
+ 'api',
139
+ '--hostname',
140
+ 'github.com',
141
+ endpoint,
142
+ ...(paginate ? ['--paginate', '--slurp'] : []),
143
+ ])
144
+ )
145
+ const workflow = api(
146
+ `repos/${policy.repository}/actions/workflows/${encodeURIComponent(policy.workflow.split('/').at(-1))}`
147
+ )
148
+ ensure(
149
+ Number.isSafeInteger(workflow.id) &&
150
+ workflow.path === policy.workflow &&
151
+ workflow.state === 'active',
152
+ 'Qualification workflow is missing, disabled, or has a different identity'
153
+ )
154
+ const runs = pages(
155
+ api(
156
+ `repos/${policy.repository}/actions/workflows/${workflow.id}/runs?head_sha=${sourceSha}&per_page=100`,
157
+ true
158
+ ),
159
+ 'workflow_runs'
160
+ )
161
+ .filter(
162
+ (item) =>
163
+ ['push', 'workflow_dispatch'].includes(item.event) && item.head_branch === policy.branch
164
+ )
165
+ // oxlint-disable-next-line unicorn/no-array-sort
166
+ .sort((a, b) => b.run_number - a.run_number || b.run_attempt - a.run_attempt)
167
+ ensure(
168
+ runs.every(
169
+ (item) => Number.isSafeInteger(item.run_number) && Number.isSafeInteger(item.run_attempt)
170
+ ),
171
+ 'Malformed qualification run ordering'
172
+ )
173
+ const current = runs[0]
174
+ ensure(
175
+ current &&
176
+ Number.isSafeInteger(current.id) &&
177
+ Number.isSafeInteger(current.run_attempt) &&
178
+ current.run_attempt > 0 &&
179
+ current.workflow_id === workflow.id &&
180
+ current.path === policy.workflow &&
181
+ current.head_sha === sourceSha &&
182
+ current.repository?.full_name === policy.repository &&
183
+ current.head_repository?.full_name === policy.repository,
184
+ 'No trusted qualification run matches the exact source and workflow'
185
+ )
186
+ ensure(
187
+ current.status === 'completed' && current.conclusion === 'success',
188
+ 'Latest matching qualification has not passed; older successes do not authorize rollout'
189
+ )
190
+ const jobs = pages(
191
+ api(
192
+ `repos/${policy.repository}/actions/runs/${current.id}/attempts/${current.run_attempt}/jobs?per_page=100`,
193
+ true
194
+ ),
195
+ 'jobs'
196
+ )
197
+ for (const name of policy.required_jobs) {
198
+ const matches = jobs.filter((job) => job.name === name)
199
+ ensure(
200
+ matches.length === 1 &&
201
+ matches[0].status === 'completed' &&
202
+ matches[0].conclusion === 'success',
203
+ `Required qualification job did not pass: ${name}`
204
+ )
205
+ }
206
+ const latest = api(`repos/${policy.repository}/actions/runs/${current.id}`)
207
+ ensure(
208
+ latest.run_attempt === current.run_attempt &&
209
+ latest.head_sha === sourceSha &&
210
+ latest.status === 'completed' &&
211
+ latest.conclusion === 'success',
212
+ 'Qualification run changed during verification'
213
+ )
214
+ ensure(
215
+ git(['rev-parse', 'HEAD']) === sourceSha &&
216
+ git(['status', '--porcelain', '--untracked-files=all']) === '',
217
+ 'Runtime source changed during eligibility verification'
218
+ )
219
+ ensure(
220
+ !lstatSync(path).isSymbolicLink() && hash(readFileSync(path)) === hash(bytes),
221
+ 'Release policy changed during verification'
222
+ )
223
+ return {
224
+ schema_version: 1,
225
+ kind: 'code-foundry-fleet-eligibility',
226
+ status: 'passed',
227
+ repository: policy.repository,
228
+ tag: version,
229
+ source_sha: sourceSha,
230
+ policy_sha256: hash(bytes),
231
+ workflow_id: workflow.id,
232
+ run_id: current.id,
233
+ run_attempt: current.run_attempt,
234
+ required_jobs: policy.required_jobs,
235
+ }
236
+ }
237
+ /** Every public upgrade path, including dry run, passes the same opt-in guard.
238
+ * @template T @param {string} root @param {string} source @param {string} version
239
+ * @param {() => T} operation @param {Run} [run]
240
+ */
241
+ export function guardedFleetUpgrade(root, source, version, operation, run) {
242
+ const eligibility = assertFleetEligibility(root, source, version, run)
243
+ if (eligibility) console.error(JSON.stringify(eligibility))
244
+ return operation()
245
+ }