fix(release): query helper artifacts with native Node fetch

This commit is contained in:
creatixchu committed 2026-11-04 11:03:12 +08:00
1 parent 9e761119bb
commit 72b3dfdc6f
7 files changed
+310 -113

No files matched your search

+45 -33
View File
@@ -2,7 +2,7 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, symlinkSync, utimesSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { delimiter, dirname, join, resolve } from 'node:path'
import { dirname, join, resolve } from 'node:path'
import { expect, it, onTestFinished } from 'vitest'
import { loadDesktopPackageEnvironment } from '../../apps/desktop/scripts/desktop-package-environment.mjs'
import { cuaHelperBuildKey, cuaHelperSourceDigest, cuaHelperSourceMatches, readCuaSigningSettings } from './build-inputs.ts'
@@ -148,25 +148,25 @@ function producerSnapshot(root: string) {
const bytes = execFileSync('git', ['cat-file', 'blob', entry.sha], { cwd: root, stdio: 'pipe' })
responses[`repos/${repository}/git/blobs/${entry.sha}`] = { sha: entry.sha, encoding: 'base64', content: bytes.toString('base64') }
}
return { commit, response, responses, api: (path: string): unknown => {
return { commit, response, responses, api: async (path: string): Promise<unknown> => {
if (!(path in responses)) throw new Error(`Unexpected GitHub fixture request ${path}`)
return responses[path]
} }
}
it('compares GitHub commit objects independently of artifact claims while allowing a root version bump', () => {
it('compares GitHub commit objects independently of artifact claims while allowing a root version bump', async () => {
const f = fixture()
const previous = producerSnapshot(f.root)
expect(cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(true)
expect(await cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(true)
writeFileSync(join(f.root, 'package.json'), JSON.stringify({ version: '2.0.0', packageManager: '[email protected]' }))
writeFileSync(join(f.root, 'packages/client/unrelated/src/index.ts'), 'another unrelated feature')
expect(cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(true)
expect(await cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(true)
writeFileSync(f.source, 'changed native code')
expect(cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(false)
expect(await cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(false)
})
it.each(['helper workflow', 'publish workflow', 'packaging script', 'manifest lifecycle', 'npm config', 'patch', 'tracked dependency', 'deleted file'])(
'rejects independent producer source proof after a changed %s', (change) => {
'rejects independent producer source proof after a changed %s', async (change) => {
const f = fixture()
const previous = producerSnapshot(f.root)
const digest = cuaHelperSourceDigest(f.root)
@@ -189,15 +189,15 @@ it.each(['helper workflow', 'publish workflow', 'packaging script', 'manifest li
if (change === 'deleted file') rmSync(f.source)
f.git('add', '-A')
expect(cuaHelperSourceDigest(f.root)).not.toBe(digest)
expect(cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(false)
expect(await cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(false)
},
)
it('stops source comparison when GitHub returns an incomplete tree', () => {
it('stops source comparison when GitHub returns an incomplete tree', async () => {
const f = fixture()
const previous = producerSnapshot(f.root)
previous.response.truncated = true
expect(() => cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toThrow('incomplete commit tree')
await expect(cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).rejects.toThrow('incomplete commit tree')
})
it.each(['test', 'production'])('computes the same %s build key through selection and packaging configuration readers', (preset) => {
@@ -218,9 +218,9 @@ it.each(['test', 'production'])('computes the same %s build key through selectio
.toBe(cuaHelperBuildKey(source, loadDesktopPackageEnvironment('darwin', environment), run))
})
// The fake gh uses a POSIX executable shim; the production workflow runs on macOS.
it.skipIf(process.platform === 'win32').each(['match', 'miss', 'api failure', 'missing gh'])(
'runs the plain-Node selector entry with %s and no node_modules directory', (scenario) => {
// A private POSIX PATH exposes only Git, so successful CLI execution cannot depend on gh.
it.skipIf(process.platform === 'win32').each(['match', 'miss', 'api failure', 'missing token'])(
'runs the plain-Node selector entry with %s and without gh or node_modules', (scenario) => {
const f = fixture()
copyHelperScripts(f.root)
const snapshot = producerSnapshot(f.root)
@@ -231,35 +231,47 @@ it.skipIf(process.platform === 'win32').each(['match', 'miss', 'api failure', 'm
[`${base}/attempts/2`]: { id: 123, run_attempt: 2, status: 'completed', conclusion: 'success',
event: 'workflow_dispatch', path: '.github/workflows/cua-helper-package.yml', head_sha: snapshot.commit,
repository: { full_name: repository }, head_repository: { full_name: repository } },
[`${base}/jobs?filter=all&per_page=100`]: [{ jobs: [{ name: 'package', conclusion: 'success', run_attempt: 2 }] }],
[`${base}/artifacts?per_page=100`]: [{ artifacts: scenario === 'match'
? [{ id: 456, name: `cua-helper-123-2-${key}`, expired: false, size_in_bytes: 4096 }] : [] }],
[`${base}/jobs?filter=all&per_page=100`]: { jobs: [{ name: 'package', conclusion: 'success', run_attempt: 2 }] },
[`${base}/artifacts?per_page=100`]: { artifacts: scenario === 'match'
? [{ id: 456, name: `cua-helper-123-2-${key}`, expired: false, size_in_bytes: 4096 }] : [] },
})
const data = join(f.root, 'responses.json')
writeFileSync(data, JSON.stringify(snapshot.responses))
const bin = join(f.root, 'fake-bin')
const bin = join(f.root, 'git-only-bin')
mkdirSync(bin)
if (scenario !== 'missing gh') {
writeFileSync(join(bin, 'gh'), `#!/usr/bin/env node\nconst fs=require('node:fs');
if(process.env.FIXTURE_API_FAILURE==='1') { console.error('fixture API failure'); process.exit(1); }
const path=process.argv.at(-1); const data=JSON.parse(fs.readFileSync(process.env.FIXTURE_API_DATA,'utf8'));
if(path.includes('/workflows/')) console.log(JSON.stringify([{workflow_runs:path.includes('cua-helper-package.yml')?[{id:123}]:[]}]));
else if(path in data) console.log(JSON.stringify(data[path])); else {console.error('unexpected path '+path);process.exit(1);}
`, { mode: 0o755 })
}
const git = execFileSync('/usr/bin/which', ['git'], { encoding: 'utf8' }).trim()
symlinkSync(git, join(bin, 'git'))
const preload = join(f.root, 'mock-github.mjs')
writeFileSync(preload, `import { readFileSync } from 'node:fs';
const data = JSON.parse(readFileSync(process.env.FIXTURE_API_DATA, 'utf8'));
globalThis.fetch = async (input, options) => {
const url = new URL(input);
if (url.origin !== 'https://api.github.com' || new Headers(options.headers).get('authorization') !== 'Bearer fixture-token') {
throw new Error('unexpected GitHub request');
}
if (process.env.FIXTURE_API_FAILURE === '1') return new Response('fixture API failure', { status: 503 });
const path = url.pathname.slice(1) + url.search;
if (path.includes('/workflows/')) return Response.json({ workflow_runs: path.includes('cua-helper-package.yml') ? [{ id: 123 }] : [] });
if (!(path in data)) throw new Error('unexpected GitHub path');
return Response.json(data[path]);
};
`)
const output = join(f.root, 'outputs')
const summary = join(f.root, 'summary')
const result = spawnSync(process.execPath, [join(f.root, 'scripts/cua-helper/select-artifact.ts')], {
const result = spawnSync(process.execPath, ['--import', preload, join(f.root, 'scripts/cua-helper/select-artifact.ts')], {
cwd: f.root, encoding: 'utf8', timeout: 60_000,
env: { ...process.env, NODE_OPTIONS: '', PATH: scenario === 'missing gh' ? bin : `${bin}${delimiter}${process.env.PATH ?? ''}`,
GITHUB_REPOSITORY: repository, GITHUB_OUTPUT: output, GITHUB_STEP_SUMMARY: summary, CUA_RUN_ID: '', CUA_BUILD_KEY: key,
env: { ...process.env, NODE_OPTIONS: '', PATH: bin, GH_TOKEN: scenario === 'missing token' ? '' : 'fixture-token',
GITHUB_API_URL: 'https://api.github.com', GITHUB_REPOSITORY: repository, GITHUB_OUTPUT: output,
GITHUB_STEP_SUMMARY: summary, CUA_RUN_ID: '', CUA_BUILD_KEY: key,
FIXTURE_API_DATA: data, FIXTURE_API_FAILURE: scenario === 'api failure' ? '1' : '0' },
})
expect(result.error).toBeUndefined()
expect(result.signal).toBeNull()
if (scenario === 'api failure' || scenario === 'missing gh') {
expect(existsSync(join(bin, 'gh'))).toBe(false)
expect(existsSync(join(f.root, 'node_modules'))).toBe(false)
if (scenario === 'api failure' || scenario === 'missing token') {
expect(result.status).toBe(1)
expect(result.stderr).toContain(scenario === 'missing gh' ? 'install GitHub CLI (gh)' : 'check token permissions')
expect(result.stderr).toContain(scenario === 'missing token' ? 'GH_TOKEN is required' : 'HTTP 503')
expect(existsSync(output)).toBe(false)
expect(existsSync(summary)).toBe(false)
} else {
@@ -270,14 +282,14 @@ else if(path in data) console.log(JSON.stringify(data[path])); else {console.err
},
)
it('matches Git blobs when tracked attributes require CRLF checkout bytes', () => {
it('matches Git blobs when tracked attributes require CRLF checkout bytes', async () => {
const f = fixture()
const path = join(f.root, 'apps/desktop/scripts/fixture.cmd')
writeFileSync(join(f.root, '.gitattributes'), '*.cmd text eol=crlf\n')
writeFileSync(path, '@echo off\necho fixture\n')
const previous = producerSnapshot(f.root)
writeFileSync(path, '@echo off\r\necho fixture\r\n')
expect(cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(true)
expect(await cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(true)
writeFileSync(join(f.root, '.gitattributes'), '*.cmd text eol=lf\n')
expect(cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(false)
expect(await cuaHelperSourceMatches(f.root, repository, previous.commit, previous.api)).toBe(false)
})
+5 -5
View File
@@ -75,12 +75,12 @@ export function cuaHelperSourceDigest(root: string): string {
* @param repository - GitHub repository containing the authenticated producer run.
* @param commit - Producer head SHA returned by GitHub, never an artifact-supplied revision.
* @param api - Read-only GitHub JSON reader.
* @returns Whether all selected file paths, bytes and executable modes match, excluding product version fields in package manifests.
* @returns Resolves to whether selected paths, bytes and executable modes match, excluding package manifest version fields.
*/
export function cuaHelperSourceMatches(root: string, repository: string, commit: string,
api: (path: string) => unknown): boolean {
export async function cuaHelperSourceMatches(root: string, repository: string, commit: string,
api: (path: string) => Promise<unknown>): Promise<boolean> {
assert(/^[a-f0-9]{40}$/u.test(commit), 'CUA source: invalid producer commit')
const response = api(`repos/${repository}/git/trees/${commit}?recursive=1`)
const response = await api(`repos/${repository}/git/trees/${commit}?recursive=1`)
assert(response && typeof response === 'object' && 'truncated' in response && response.truncated === false
&& 'tree' in response && Array.isArray(response.tree), 'CUA source: GitHub returned an incomplete commit tree')
const tree = new Map<string, { sha: string; mode: string }>()
@@ -106,7 +106,7 @@ export function cuaHelperSourceMatches(root: string, repository: string, commit:
{ cwd: root, input: local.bytes, encoding: 'utf8' }).trim()
if (canonical === remote.sha) continue
if (file !== 'package.json' && !file.endsWith('/package.json')) return false
const blob = api(`repos/${repository}/git/blobs/${remote.sha}`)
const blob = await api(`repos/${repository}/git/blobs/${remote.sha}`)
assert(blob && typeof blob === 'object' && 'sha' in blob && blob.sha === remote.sha
&& 'encoding' in blob && blob.encoding === 'base64' && 'content' in blob && typeof blob.content === 'string',
'CUA source: invalid package manifest blob')
+159 -41
View File
@@ -1,6 +1,6 @@
/** Discovery and explicit selection require matching inputs and successful producer evidence. */
import { expect, it, vi } from 'vitest'
import { selectCuaHelperArtifact } from './select-artifact.ts'
import { expect, it, onTestFinished, vi } from 'vitest'
import { createGitHubJsonReader, selectCuaHelperArtifact } from './select-artifact.ts'
const repository = 'deepseek-harness/deepseek-harness'
const base = `repos/${repository}/actions/runs/123`
@@ -10,10 +10,10 @@ function fixture() {
const run = { id: 123, run_attempt: 2, status: 'completed', conclusion: 'success', event: 'workflow_dispatch',
path: '.github/workflows/cua-helper-package.yml', head_sha: 'c'.repeat(40), repository: { full_name: repository }, head_repository: { full_name: repository } }
const jobs = [{ jobs: [{ name: 'package', conclusion: 'success', run_attempt: 2 }] }]
const matchesSource = vi.fn((_commit: string) => true)
const matchesSource = vi.fn(async (_commit: string) => true)
const artifact = { id: 456, name: `cua-helper-123-2-${key}`, expired: false, size_in_bytes: 4096 }
const artifacts = [{ artifacts: [{ ...artifact, id: 455, name: `cua-helper-123-1-${key}` }] }, { artifacts: [artifact] }]
const api = vi.fn((path: string, paginate?: boolean) => {
const api = vi.fn(async (path: string, paginate?: boolean) => {
if (path === base) { expect(paginate).toBeUndefined(); return { run_attempt: 2 } }
if (path === `${base}/attempts/2`) { expect(paginate).toBeUndefined(); return run }
expect(paginate).toBe(true)
@@ -26,38 +26,38 @@ function fixture() {
return { run, jobs, artifact, artifacts, api, matchesSource }
}
it.each(['123', ''])('selects a matching artifact with run ID %j and pins the latest attempt', (runId) => {
it.each(['123', ''])('selects a matching artifact with run ID %j and pins the latest attempt', async (runId) => {
const f = fixture()
expect(selectCuaHelperArtifact(repository, runId, key, f.api, f.matchesSource))
expect(await selectCuaHelperArtifact(repository, runId, key, f.api, f.matchesSource))
.toEqual({ runId: '123', attempt: '2', artifactId: '456' })
expect(f.api.mock.calls.filter(([path]) => path === base)).toHaveLength(1)
expect(f.api.mock.calls.some(([path]) => path.includes('/workflows/'))).toBe(runId === '')
})
it('allows successful helper packaging when later npm publication failed', () => {
it('allows successful helper packaging when later npm publication failed', async () => {
const f = fixture()
f.run.path = '.github/workflows/release-publish.yml'
f.run.conclusion = 'failure'
f.jobs[0]!.jobs = [{ name: 'cua-helper / package', conclusion: 'success', run_attempt: 2 }, { name: 'Publish to npm', conclusion: 'failure', run_attempt: 2 }]
expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)?.artifactId).toBe('456')
expect((await selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource))?.artifactId).toBe('456')
})
it.each(['123/other', ' 123', '0', '-1', '2\nartifact-id=9', 'https://github.com/owner/repo/actions/runs/123'])(
'rejects invalid run ID %j before reading GitHub', (runId) => {
'rejects invalid run ID %j before reading GitHub', async (runId) => {
const f = fixture()
expect(() => selectCuaHelperArtifact(repository, runId, key, f.api, f.matchesSource)).toThrow('numeric run ID')
await expect(selectCuaHelperArtifact(repository, runId, key, f.api, f.matchesSource)).rejects.toThrow('numeric run ID')
expect(f.api).not.toHaveBeenCalled()
},
)
it.each(['wrong/repo/extra', '../repo', 'owner/..', 'owner/.'])('rejects invalid repository %s before reading GitHub', (repo) => {
it.each(['wrong/repo/extra', '../repo', 'owner/..', 'owner/.'])('rejects invalid repository %s before reading GitHub', async (repo) => {
const f = fixture()
expect(() => selectCuaHelperArtifact(repo, '123', key, f.api, f.matchesSource)).toThrow('invalid repository')
await expect(selectCuaHelperArtifact(repo, '123', key, f.api, f.matchesSource)).rejects.toThrow('invalid repository')
expect(f.api).not.toHaveBeenCalled()
})
it.each(['workflow', 'event', 'repository', 'head_repository', 'id', 'attempt', 'running', 'failed'] as const)(
'rejects explicit %s run evidence', (change) => {
'rejects explicit %s run evidence', async (change) => {
const f = fixture()
if (change === 'workflow') f.run.path = '.github/workflows/ci.yml'
if (change === 'event') f.run.event = 'pull_request'
@@ -67,40 +67,40 @@ it.each(['workflow', 'event', 'repository', 'head_repository', 'id', 'attempt',
if (change === 'attempt') f.run.run_attempt = 1
if (change === 'running') f.run.status = 'in_progress'
if (change === 'failed') f.run.conclusion = 'failure'
expect(() => selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).toThrow()
await expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).rejects.toThrow()
expect(f.api.mock.calls.some(([path]) => path.includes('/artifacts'))).toBe(false)
},
)
it.each(['failure', 'cancelled', 'skipped', 'missing', 'duplicate'])(
'rejects %s helper jobs without falling back to attempt 1', (status) => {
'rejects %s helper jobs without falling back to attempt 1', async (status) => {
const f = fixture()
f.run.path = '.github/workflows/release-publish.yml'
f.run.conclusion = 'failure'
f.jobs[0]!.jobs = status === 'missing' ? [] : status === 'duplicate'
? [{ name: 'cua-helper / package', conclusion: 'success', run_attempt: 2 }, { name: 'cua-helper / package', conclusion: 'success', run_attempt: 2 }]
: [{ name: 'cua-helper / package', conclusion: status, run_attempt: 2 }]
expect(() => selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).toThrow('last executed cua-helper / package job did not succeed')
await expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).rejects.toThrow('last executed cua-helper / package job did not succeed')
expect(f.api.mock.calls.some(([path]) => path.includes('/attempts/1') || path.includes('/artifacts'))).toBe(false)
},
)
it.each(['missing', 'expired', 'empty', 'mismatch', 'legacy'] as const)(
'rebuilds on automatic %s but rejects the same explicit selection', (change) => {
'rebuilds on automatic %s but rejects the same explicit selection', async (change) => {
const f = fixture()
if (change === 'missing') f.artifacts.pop()
if (change === 'expired') f.artifact.expired = true
if (change === 'empty') f.artifact.size_in_bytes = 0
if (change === 'mismatch') f.artifact.name = `cua-helper-123-2-${'b'.repeat(64)}`
if (change === 'legacy') f.artifact.name = 'cua-helper-123-2'
expect(selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
expect(() => selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).toThrow('CUA reuse: run 123:')
expect(await selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
await expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).rejects.toThrow('CUA reuse: run 123:')
},
)
it('selects a match across workflows and pages after a newer failed producer', () => {
it('selects a match across workflows and pages after a newer failed producer', async () => {
const f = fixture()
const api = vi.fn((path: string, paginate?: boolean): unknown => {
const api = vi.fn(async (path: string, paginate?: boolean): Promise<unknown> => {
if (path.includes('/workflows/cua-helper-package.yml/runs?')) return [{ workflow_runs: [] }]
if (path.includes('/workflows/release-publish.yml/runs?')) return [{ workflow_runs: [{ id: 124 }] }, { workflow_runs: [{ id: 123 }] }]
if (path.endsWith('/runs/124/artifacts?per_page=100')) return [{ artifacts: [{ ...f.artifact, name: `cua-helper-124-1-${key}` }] }]
@@ -108,65 +108,183 @@ it('selects a match across workflows and pages after a newer failed producer', (
if (path.endsWith('/runs/124/attempts/1')) return { ...f.run, id: 124, run_attempt: 1, conclusion: 'failure' }
return f.api(path, paginate)
})
expect(selectCuaHelperArtifact(repository, '', key, api, f.matchesSource, new Date('2026-11-03T00:00:00Z'))?.runId).toBe('123')
expect((await selectCuaHelperArtifact(repository, '', key, api, f.matchesSource, new Date('2026-11-03T00:00:00Z')))?.runId).toBe('123')
const paths = api.mock.calls.map(([path]) => path)
expect(paths.find(path => path.includes('/workflows/'))).toContain(encodeURIComponent('>=2026-10-20T00:00:00.000Z'))
expect(paths.indexOf(`${base.replace('123', '124')}/attempts/1`)).toBeLessThan(paths.indexOf(base))
})
it.each(['duplicate', 'invalid metadata'] as const)('stops automatic discovery on %s evidence', (change) => {
it.each(['duplicate', 'invalid metadata'] as const)('stops automatic discovery on %s evidence', async (change) => {
const f = fixture()
if (change === 'duplicate') f.artifacts.push({ artifacts: [{ ...f.artifact, id: 457 }] })
const api = (path: string, paginate?: boolean): unknown => change === 'invalid metadata' && path.endsWith('/artifacts?per_page=100')
const api = async (path: string, paginate?: boolean): Promise<unknown> => change === 'invalid metadata' && path.endsWith('/artifacts?per_page=100')
? [{ artifacts: [{ ...f.artifact, expired: 'false' }] }] : f.api(path, paginate)
expect(() => selectCuaHelperArtifact(repository, '', key, api, f.matchesSource)).toThrow()
await expect(selectCuaHelperArtifact(repository, '', key, api, f.matchesSource)).rejects.toThrow()
})
it.each(['', '123'])('propagates unavailable GitHub evidence with selection %j', (runId) => {
const api = vi.fn(() => { throw new Error('HTTP 403') })
expect(() => selectCuaHelperArtifact(repository, runId, key, api, () => true)).toThrow('HTTP 403')
it.each(['', '123'])('propagates unavailable GitHub evidence with selection %j', async (runId) => {
const api = vi.fn(async () => { throw new Error('HTTP 403') })
await expect(selectCuaHelperArtifact(repository, runId, key, api, async () => true)).rejects.toThrow('HTTP 403')
expect(api).toHaveBeenCalledTimes(1)
})
it('does not query job or attempt details when artifact names exclude the requested build key', () => {
it('does not query job or attempt details when artifact names exclude the requested build key', async () => {
const f = fixture()
f.artifact.name = 'cua-helper-123-2-legacy'
f.artifacts.shift()
expect(selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
expect(await selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
expect(f.api.mock.calls.map(([path]) => path).filter(path => !path.includes('/workflows/')))
.toEqual([`${base}/artifacts?per_page=100`])
})
it.each(['123', ''])('retains the original helper after a publish-only rerun with selection %j', (runId) => {
it.each(['123', ''])('retains the original helper after a publish-only rerun with selection %j', async (runId) => {
const f = fixture()
f.run.path = '.github/workflows/release-publish.yml'
f.jobs[0]!.jobs = [{ name: 'cua-helper / package', conclusion: 'success', run_attempt: 1 },
{ name: 'Publish to npm', conclusion: 'success', run_attempt: 2 }]
f.artifacts.pop()
expect(selectCuaHelperArtifact(repository, runId, key, f.api, f.matchesSource))
expect(await selectCuaHelperArtifact(repository, runId, key, f.api, f.matchesSource))
.toEqual({ runId: '123', attempt: '1', artifactId: '455' })
expect(f.matchesSource).toHaveBeenCalledExactlyOnceWith(f.run.head_sha)
})
it('does not reuse an older successful helper after a newer helper execution failed', () => {
it('does not reuse an older successful helper after a newer helper execution failed', async () => {
const f = fixture()
f.run.path = '.github/workflows/release-publish.yml'
f.jobs[0]!.jobs = [{ name: 'cua-helper / package', conclusion: 'success', run_attempt: 1 },
{ name: 'cua-helper / package', conclusion: 'failure', run_attempt: 2 }]
expect(selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
expect(() => selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).toThrow('did not succeed')
expect(await selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
await expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).rejects.toThrow('did not succeed')
expect(f.matchesSource).not.toHaveBeenCalled()
})
it('rejects forged artifact keys when independent producer source evidence differs', () => {
it('rejects forged artifact keys when independent producer source evidence differs', async () => {
const f = fixture()
f.matchesSource.mockReturnValue(false)
expect(selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
expect(() => selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).toThrow('producer commit')
f.matchesSource.mockResolvedValue(false)
expect(await selectCuaHelperArtifact(repository, '', key, f.api, f.matchesSource)).toBeUndefined()
await expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).rejects.toThrow('producer commit')
})
it('stops when a new attempt appears during selection', () => {
it('stops when a new attempt appears during selection', async () => {
const f = fixture()
f.jobs[0]!.jobs[0]!.run_attempt = 3
expect(() => selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).toThrow('run advanced')
await expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).rejects.toThrow('run advanced')
})
it('authenticates native HTTP requests and follows canonical GitHub pagination links', async () => {
const request = vi.fn<typeof fetch>()
.mockResolvedValueOnce(Response.json({ jobs: [{ id: 1 }] }, {
headers: { link: '<https://api.github.com/repositories/42/actions/jobs?page=2>; rel="next"' },
}))
.mockResolvedValueOnce(Response.json({ jobs: [{ id: 2 }] }))
const api = createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)
await expect(api('repos/owner/repo/actions/jobs?page=1', true)).resolves.toEqual([
{ jobs: [{ id: 1 }] }, { jobs: [{ id: 2 }] },
])
expect(request).toHaveBeenCalledTimes(2)
expect(request.mock.calls[1]![1]?.signal).toBe(request.mock.calls[0]![1]?.signal)
expect(String(request.mock.calls[1]![0])).toBe('https://api.github.com/repositories/42/actions/jobs?page=2')
for (const [, options] of request.mock.calls) {
const headers = new Headers(options?.headers)
expect(headers.get('authorization')).toBe('Bearer fixture-token')
expect(headers.get('accept')).toBe('application/vnd.github+json')
expect(options?.signal).toBeInstanceOf(AbortSignal)
expect(options?.redirect).toBe('error')
}
})
it('uses the configured GitHub API base path and leaves non-paginated JSON unwrapped', async () => {
const request = vi.fn<typeof fetch>().mockResolvedValue(Response.json({ run_attempt: 2 }))
const api = createGitHubJsonReader({ GH_TOKEN: 'fixture-token', GITHUB_API_URL: 'https://github.example.test/api/v3' }, request)
await expect(api('repos/owner/repo/actions/runs/123')).resolves.toEqual({ run_attempt: 2 })
expect(String(request.mock.calls[0]![0])).toBe('https://github.example.test/api/v3/repos/owner/repo/actions/runs/123')
})
it.each([401, 403, 429, 500, 503])('rejects HTTP %s without returning a partial page list or response credentials', async (status) => {
const request = vi.fn<typeof fetch>()
.mockResolvedValueOnce(Response.json({ jobs: [{ id: 1 }] }, {
headers: { link: '<https://api.github.com/repos/owner/repo/jobs?page=2>; rel="next"' },
}))
.mockResolvedValueOnce(new Response('private-response-value', { status }))
const api = createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)
const result = api('repos/owner/repo/jobs?page=1', true)
await expect(result).rejects.toThrow(`HTTP ${status}`)
await expect(result).rejects.not.toThrow('private-response-value')
await expect(result).rejects.not.toThrow('fixture-token')
})
it.each(['connection reset', 'request timeout'])('reports %s as a failed query', async (failure) => {
const request = vi.fn<typeof fetch>().mockRejectedValue(new Error(failure))
const api = createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)
await expect(api('repos/owner/repo')).rejects.toThrow('failed or timed out')
})
it('rejects invalid JSON without exposing its content', async () => {
const request = vi.fn<typeof fetch>().mockResolvedValue(new Response('private-response-value'))
const result = createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)('repos/owner/repo')
await expect(result).rejects.toThrow('invalid JSON')
await expect(result).rejects.not.toThrow('private-response-value')
})
it.each([
'https://outside.example.test/page/2',
'https://[email protected]/repos/owner/repo/jobs?page=2',
'https://api.github.com/repos/owner/repo/jobs?page=1',
])('rejects unsafe or cyclic pagination %s before sending another request', async (next) => {
const request = vi.fn<typeof fetch>().mockResolvedValue(Response.json({ jobs: [] }, { headers: { link: `<${next}>; rel="next"` } }))
const api = createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)
await expect(api('repos/owner/repo/jobs?page=1', true)).rejects.toThrow('CUA reuse: GitHub')
expect(request).toHaveBeenCalledTimes(1)
})
it('rejects a malformed next-page header', async () => {
const request = vi.fn<typeof fetch>().mockResolvedValue(Response.json({}, { headers: { link: 'missing-url; rel="next"' } }))
await expect(createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)('repos/owner/repo/jobs', true))
.rejects.toThrow('invalid pagination link')
})
it('stops reading an oversized response and cancels its stream', async () => {
let cancelled = false
const body = new ReadableStream<Uint8Array>({
start(controller) { controller.enqueue(new Uint8Array(32 * 1024 * 1024 + 1)) },
cancel() { cancelled = true },
})
const request = vi.fn<typeof fetch>().mockResolvedValue(new Response(body))
await expect(createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)('repos/owner/repo/git/trees/fixture'))
.rejects.toThrow('exceeds 32 MiB')
expect(cancelled).toBe(true)
})
it.each(['', 'not-a-url', 'http://api.github.com', 'https://user:[email protected]', 'https://api.github.com?token=private'])(
'rejects invalid API configuration %j before requesting data', (url) => {
const request = vi.fn<typeof fetch>()
expect(() => createGitHubJsonReader({ GH_TOKEN: 'fixture-token', GITHUB_API_URL: url }, request)).toThrow('GITHUB_API_URL')
expect(request).not.toHaveBeenCalled()
},
)
it('requires an explicit Actions token instead of relying on gh credentials', () => {
const request = vi.fn<typeof fetch>()
expect(() => createGitHubJsonReader({}, request)).toThrow('GH_TOKEN is required')
expect(request).not.toHaveBeenCalled()
})
it('propagates an asynchronous source verification failure', async () => {
const f = fixture()
f.matchesSource.mockRejectedValue(new Error('source evidence unavailable'))
await expect(selectCuaHelperArtifact(repository, '123', key, f.api, f.matchesSource)).rejects.toThrow('source evidence unavailable')
})
it('passes the 60-second abort signal to the HTTP transport', async () => {
const controller = new AbortController()
const timeout = vi.spyOn(AbortSignal, 'timeout').mockReturnValue(controller.signal)
onTestFinished(() => timeout.mockRestore())
const request: typeof fetch = async (_url, options) => {
expect(options?.signal).toBe(controller.signal)
controller.abort(new DOMException('fixture timeout', 'TimeoutError'))
options?.signal?.throwIfAborted()
throw new Error('Expected an aborted request')
}
await expect(createGitHubJsonReader({ GH_TOKEN: 'fixture-token' }, request)('repos/owner/repo')).rejects.toThrow('timed out')
expect(timeout).toHaveBeenCalledExactlyOnceWith(60_000)
})
+97 -30
View File
@@ -1,6 +1,5 @@
/** Find a matching retained helper on the Mac runner before installing dependencies. */
import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import { appendFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { isEntry } from '../release/process.ts'
@@ -43,19 +42,19 @@ export interface CuaHelperSelection {
* @param api - Read-only GitHub JSON reader; paginated responses are arrays of pages.
* @param matchesSource - Independent comparison of the producer head SHA with the consuming build inputs.
* @param now - Discovery time; automatic search covers the artifact's 14-day retention window.
* @returns Matching producer artifact, or undefined when automatic discovery has no match.
* @returns Resolves to the matching producer artifact, or undefined when automatic discovery has no match.
*/
export function selectCuaHelperArtifact(repository: string, runId: string, buildKey: string,
api: (path: string, paginate?: boolean) => unknown, matchesSource: (commit: string) => boolean,
now = new Date()): CuaHelperSelection | undefined {
export async function selectCuaHelperArtifact(repository: string, runId: string, buildKey: string,
api: (path: string, paginate?: boolean) => Promise<unknown>, matchesSource: (commit: string) => Promise<boolean>,
now = new Date()): Promise<CuaHelperSelection | undefined> {
assert(/^[\w.-]+\/[\w.-]+$/u.test(repository) && !repository.split('/').some(part => part === '.' || part === '..'),
'CUA reuse: invalid repository')
assert(runId === '' || /^[1-9]\d*$/u.test(runId), 'CUA reuse: enter the numeric run ID after /actions/runs/ in the run URL')
assert(/^[a-f0-9]{64}$/u.test(buildKey), 'CUA reuse: invalid build key')
const base = `repos/${repository}/actions`
const selectRun = (candidate: string, retained?: Record<string, unknown>[]): CuaHelperSelection | string => {
const attempt = id(record(api(`${base}/runs/${candidate}`)).run_attempt)
const run = record(api(`${base}/runs/${candidate}/attempts/${attempt}`))
const selectRun = async (candidate: string, retained?: Record<string, unknown>[]): Promise<CuaHelperSelection | string> => {
const attempt = id(record(await api(`${base}/runs/${candidate}`)).run_attempt)
const run = record(await api(`${base}/runs/${candidate}/attempts/${attempt}`))
assert(id(run.id) === candidate && id(run.run_attempt) === attempt
&& record(run.repository).full_name === repository && record(run.head_repository).full_name === repository,
'CUA reuse: selected run, attempt or repository does not match GitHub evidence')
@@ -64,7 +63,7 @@ export function selectCuaHelperArtifact(repository: string, runId: string, build
if (run.status !== 'completed') return 'the latest attempt has not completed'
if (workflow === 'cua-helper-package.yml' && run.conclusion !== 'success') return 'the latest packaging attempt did not succeed'
const jobName = workflow === 'cua-helper-package.yml' ? 'package' : 'cua-helper / package'
const jobs = rows(api(`${base}/runs/${candidate}/jobs?filter=all&per_page=100`, true), 'jobs')
const jobs = rows(await api(`${base}/runs/${candidate}/jobs?filter=all&per_page=100`, true), 'jobs')
.filter(job => job.name === jobName)
const attempts = jobs.map(job => Number(id(job.run_attempt)))
assert(attempts.every(value => value <= Number(attempt)), 'CUA reuse: run advanced during selection; retry with the same run ID')
@@ -72,7 +71,7 @@ export function selectCuaHelperArtifact(repository: string, runId: string, build
const producers = jobs.filter(job => job.run_attempt === producerAttempt)
if (producers.length !== 1 || producers[0]?.conclusion !== 'success') return `the last executed ${jobName} job did not succeed`
const name = `cua-helper-${candidate}-${producerAttempt}-${buildKey}`
const artifacts = (retained ?? rows(api(`${base}/runs/${candidate}/artifacts?per_page=100`, true), 'artifacts'))
const artifacts = (retained ?? rows(await api(`${base}/runs/${candidate}/artifacts?per_page=100`, true), 'artifacts'))
.filter(artifact => artifact.name === name)
if (artifacts.length === 0) return 'no artifact matches the current source bytes and build configuration; use the producer run linked in its summary or rebuild'
const [artifact] = artifacts
@@ -80,42 +79,109 @@ export function selectCuaHelperArtifact(repository: string, runId: string, build
assert(typeof artifact.expired === 'boolean' && typeof artifact.size_in_bytes === 'number', 'CUA reuse: invalid artifact metadata')
if (artifact.expired || artifact.size_in_bytes <= 0) return 'the matching artifact is expired or empty'
assert(typeof run.head_sha === 'string' && /^[a-f0-9]{40}$/u.test(run.head_sha), 'CUA reuse: invalid producer head SHA')
if (!matchesSource(run.head_sha)) return 'the producer commit has different helper build inputs'
if (!await matchesSource(run.head_sha)) return 'the producer commit has different helper build inputs'
return { runId: candidate, attempt: String(producerAttempt), artifactId: id(artifact.id) }
}
if (runId !== '') {
const selected = selectRun(runId)
const selected = await selectRun(runId)
if (typeof selected === 'string') throw new Error(`CUA reuse: run ${runId}: ${selected}`)
return selected
}
const since = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000).toISOString()
const candidates = workflows.flatMap(workflow => rows(api(
`${base}/workflows/${workflow}/runs?event=workflow_dispatch&status=completed&created=${encodeURIComponent(`>=${since}`)}&per_page=100`,
true), 'workflow_runs'))
.sort((left, right) => Number(id(right.id)) - Number(id(left.id)))
const candidates: Record<string, unknown>[] = []
for (const workflow of workflows) {
candidates.push(...rows(await api(
`${base}/workflows/${workflow}/runs?event=workflow_dispatch&status=completed&created=${encodeURIComponent(`>=${since}`)}&per_page=100`,
true), 'workflow_runs'))
}
candidates.sort((left, right) => Number(id(right.id)) - Number(id(left.id)))
for (const candidate of candidates) {
const candidateId = id(candidate.id)
const artifacts = rows(api(`${base}/runs/${candidateId}/artifacts?per_page=100`, true), 'artifacts')
const artifacts = rows(await api(`${base}/runs/${candidateId}/artifacts?per_page=100`, true), 'artifacts')
if (!artifacts.some(artifact => typeof artifact.name === 'string'
&& artifact.name.startsWith(`cua-helper-${candidateId}-`) && artifact.name.endsWith(`-${buildKey}`))) continue
const selected = selectRun(candidateId, artifacts)
const selected = await selectRun(candidateId, artifacts)
if (typeof selected !== 'string') return selected
}
return undefined
}
function githubJson(path: string, paginate = false): unknown {
let output: string
try {
output = execFileSync('gh', ['api', ...(paginate ? ['--paginate', '--slurp'] : []), path],
{ encoding: 'utf8', timeout: 60_000, maxBuffer: 32 * 1024 * 1024 })
} catch (error) {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
throw new Error('CUA reuse: install GitHub CLI (gh) on the Mac runner PATH, then retry this workflow')
/**
* Create authenticated, read-only GitHub requests using Node's built-in fetch.
* Each call, including pagination, has a 60-second deadline and a 32 MiB response limit.
* Pagination stays on the configured API origin. Request or parsing failures never return partial pages.
* @param environment - GH_TOKEN and optional GITHUB_API_URL supplied by Actions.
* @param request - HTTP transport receiving the authenticated URL and request options.
* @returns JSON reader; paginated calls resolve to an array of page objects.
*/
export function createGitHubJsonReader(environment: NodeJS.ProcessEnv, request: typeof fetch = fetch):
(path: string, paginate?: boolean) => Promise<unknown> {
const token = environment.GH_TOKEN?.trim()
if (!token) throw new Error('CUA reuse: GH_TOKEN is required for GitHub API access')
const configured = environment.GITHUB_API_URL ?? 'https://api.github.com'
let base: URL
try { base = new URL(configured.endsWith('/') ? configured : `${configured}/`) } catch (_error) {
// URL parser errors may include credential-bearing configuration.
throw new Error('CUA reuse: GITHUB_API_URL must be a valid HTTPS API URL')
}
assert(base.protocol === 'https:' && !base.username && !base.password && !base.search && !base.hash,
'CUA reuse: GITHUB_API_URL must be HTTPS without credentials, query or fragment')
return async (path, paginate = false) => {
let target = new URL(path, base)
const visited = new Set<string>()
const pages: unknown[] = []
let bytes = 0
const signal = AbortSignal.timeout(60_000)
while (true) {
assert(target.origin === base.origin && target.pathname.startsWith(base.pathname)
&& !target.username && !target.password && !target.hash, 'CUA reuse: GitHub pagination left the configured API')
assert(!visited.has(target.href), 'CUA reuse: GitHub returned a pagination cycle')
visited.add(target.href)
let response: Response
try {
response = await request(target, {
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2026-03-10', 'User-Agent': 'deepseek-harness-cua-helper' },
redirect: 'error', signal,
})
} catch (error) {
throw new Error('CUA reuse: GitHub request failed or timed out; check connectivity and retry this workflow', { cause: error })
}
if (!response.ok) {
try { await response.body?.cancel() } catch (_error) {
// A failed body stream must not hide the HTTP failure already received.
}
throw new Error(`CUA reuse: GitHub request failed (HTTP ${response.status}); check token permissions, rate limits or service availability, then retry`)
}
const reader = response.body?.getReader()
if (!reader) throw new Error('CUA reuse: GitHub returned an empty response')
const chunks: Uint8Array[] = []
try {
while (true) {
const chunk = await reader.read()
if (chunk.done) break
bytes += chunk.value.byteLength
if (bytes > 32 * 1024 * 1024) {
await reader.cancel()
throw new Error('CUA reuse: GitHub response exceeds 32 MiB')
}
chunks.push(chunk.value)
}
} finally { reader.releaseLock() }
let value: unknown
try { value = JSON.parse(Buffer.concat(chunks).toString('utf8')) } catch (_error) {
// JSON parser errors may include response bytes.
throw new Error('CUA reuse: GitHub returned invalid JSON; retry this workflow')
}
if (!paginate) return value
pages.push(value)
const link = response.headers.get('link') ?? ''
const next = /<([^>]+)>\s*;\s*rel="next"/u.exec(link)?.[1]
assert(next || !link.includes('rel="next"'), 'CUA reuse: GitHub returned an invalid pagination link')
if (!next) return pages
target = new URL(next, target)
}
throw new Error('CUA reuse: GitHub lookup failed; check token permissions, rate limits and connectivity, then retry this workflow', { cause: error })
}
return JSON.parse(output)
}
if (isEntry(import.meta.url)) {
@@ -123,13 +189,14 @@ if (isEntry(import.meta.url)) {
if (!env.GITHUB_REPOSITORY || !env.GITHUB_OUTPUT || !env.GITHUB_STEP_SUMMARY || !env.CUA_BUILD_KEY) {
throw new Error('CUA reuse: Actions context and CUA_BUILD_KEY are required')
}
const githubJson = createGitHubJsonReader(env)
const repository = env.GITHUB_REPOSITORY
const root = resolve(import.meta.dirname, '../..')
const sourceMatches = new Map<string, boolean>()
const selection = selectCuaHelperArtifact(repository, env.CUA_RUN_ID ?? '', env.CUA_BUILD_KEY, githubJson, (commit) => {
const selection = await selectCuaHelperArtifact(repository, env.CUA_RUN_ID ?? '', env.CUA_BUILD_KEY, githubJson, async (commit) => {
let matches = sourceMatches.get(commit)
if (matches === undefined) {
matches = cuaHelperSourceMatches(root, repository, commit, githubJson)
matches = await cuaHelperSourceMatches(root, repository, commit, githubJson)
sourceMatches.set(commit, matches)
}
return matches