镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Commit 22aeb78

Browse files
committed
fix: retain candidate locks after frozen install failures
Keep failed frozen installs failed and restore committed inputs after diagnostic regeneration. Retain the normal installer candidate and status for cross-platform investigation. Refs #3223 Refs stacksjs/ts-cloud#210 Refs stacksjs/hq.training#346
1 parent f18c351 commit 22aeb78

3 files changed

Lines changed: 74 additions & 1 deletion

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { expect, test } from 'bun:test'
2+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
3+
import { tmpdir } from 'node:os'
4+
import { join } from 'node:path'
5+
import { diagnosePantryLock } from './diagnose-pantry-lock'
6+
7+
for (const exitCode of [0, 1]) {
8+
test(`retains the actual candidate and restores inputs when regeneration exits ${exitCode}`, async () => {
9+
const root = await mkdtemp(join(tmpdir(), 'pantry-lock-diagnostic-'))
10+
try {
11+
await writeFile(join(root, 'pantry.lock'), 'original lock\n')
12+
const fixture = join(root, 'installer.ts')
13+
await writeFile(fixture, `await Bun.write('pantry.lock', 'generated candidate\\n'); console.log('normal installer output'); process.exit(${exitCode})`)
14+
expect(await diagnosePantryLock(root, [process.execPath, fixture])).toBe(exitCode)
15+
expect(await readFile(join(root, 'pantry.lock'), 'utf8')).toBe('original lock\n')
16+
const output = join(root, 'artifacts/pantry-lock-diagnostic')
17+
expect(await readFile(join(output, 'committed.lock'), 'utf8')).toBe('original lock\n')
18+
expect(await readFile(join(output, 'candidate.lock'), 'utf8')).toBe('generated candidate\n')
19+
expect(await readFile(join(output, 'regeneration.stdout.log'), 'utf8')).toContain('normal installer output')
20+
expect(JSON.parse(await readFile(join(output, 'result.json'), 'utf8')).exitCode).toBe(exitCode)
21+
}
22+
finally {
23+
await rm(root, { recursive: true, force: true })
24+
}
25+
})
26+
}
27+
28+
test('restores the committed input even when the diagnostic installer cannot start', async () => {
29+
const root = await mkdtemp(join(tmpdir(), 'pantry-lock-diagnostic-'))
30+
try {
31+
await writeFile(join(root, 'pantry.lock'), 'original lock\n')
32+
await expect(diagnosePantryLock(root, [join(root, 'missing-installer')])).rejects.toThrow()
33+
expect(await readFile(join(root, 'pantry.lock'), 'utf8')).toBe('original lock\n')
34+
}
35+
finally {
36+
await rm(root, { recursive: true, force: true })
37+
}
38+
})
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
2+
import { join, resolve } from 'node:path'
3+
4+
/** A failed frozen check remains failed. This only retains the normal installer's candidate. */
5+
export async function diagnosePantryLock(root: string, command: string[] = ['pantry']): Promise<number> {
6+
const lockPath = join(root, 'pantry.lock')
7+
const original = await readFile(lockPath)
8+
const output = join(root, 'artifacts', 'pantry-lock-diagnostic')
9+
await mkdir(output, { recursive: true })
10+
await writeFile(join(output, 'committed.lock'), original)
11+
try {
12+
const child = Bun.spawn([...command, 'install', '--ignore-scripts', '--quiet'], {
13+
cwd: root,
14+
stdout: Bun.file(join(output, 'regeneration.stdout.log')),
15+
stderr: Bun.file(join(output, 'regeneration.stderr.log')),
16+
})
17+
const exitCode = await child.exited
18+
await writeFile(join(output, 'candidate.lock'), await readFile(lockPath))
19+
await writeFile(join(output, 'result.json'), `${JSON.stringify({ exitCode, platform: process.platform, arch: process.arch }, null, 2)}\n`)
20+
return exitCode
21+
}
22+
finally {
23+
// Diagnostic regeneration must never change the failed build's inputs.
24+
await writeFile(lockPath, original)
25+
}
26+
}
27+
28+
if (import.meta.main)
29+
process.exitCode = await diagnosePantryLock(resolve(import.meta.dir, '../..'))

‎.github/workflows/ci.yml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -477,16 +477,22 @@ jobs:
477477
run: bun --no-env-file check-pantry-lock.ts
478478

479479
- name: Install the pantry tree
480+
id: pantry-tree
480481
run: pantry install --frozen-lockfile --ignore-scripts --quiet
481482

482-
- name: Retain the dependency lockfiles generated by the normal installers
483+
- name: Retain the candidate after a failed frozen Pantry check
484+
if: failure() && steps.pantry-tree.outcome == 'failure'
485+
run: bun .github/scripts/diagnose-pantry-lock.ts
486+
487+
- name: Retain dependency lockfiles and failure diagnostics
483488
if: always()
484489
uses: actions/upload-artifact@v7
485490
with:
486491
name: dependency-lockfiles-${{ github.sha }}
487492
path: |
488493
bun.lock
489494
pantry.lock
495+
artifacts/pantry-lock-diagnostic/
490496
if-no-files-found: error
491497

492498
# The frozen install must still leave both committed lockfiles untouched.

0 commit comments

Comments
 (0)