fix(autofix): pin resolver binary, make image check digest-aware (#9527)

This commit is contained in:
qwen-code-dev-bot 2026-08-20 18:30:13 +00:00
parent 9c5e7282d3
commit 495a13c8e2
4 changed files with 76 additions and 1 deletions

View file

@ -678,6 +678,17 @@ test('every sandbox-image consumer binds the resolver step output', () => {
resolver.id,
`${name} job '${jobName}': the resolver step needs an id so its image output is addressable`,
);
// The export is derived from this binary's stdout: an unpinned
// SANDBOX_COMMAND (or the bare `docker` default) is steerable
// through the same $GITHUB_ENV-append channel the DOCKER_HOST pin
// closes, and a PATH shadow in the runner-writable qwen-bin dir
// defeats a bare-name pin — so step env must bind an absolute
// path (#9527 review).
assert.equal(
resolver.env?.SANDBOX_COMMAND,
'/usr/bin/docker',
`${name} job '${jobName}': pin SANDBOX_COMMAND to an absolute docker path so neither an appended $GITHUB_ENV value nor a $GITHUB_PATH shadow can steer the exported digest`,
);
}
const bindings = resolvers.map(
(resolver) => `\${{ steps.${resolver.id}.outputs.image }}`,

View file

@ -1099,6 +1099,13 @@ jobs:
id: 'sandbox_image'
if: |-
${{ steps.scan.outputs.has_candidates == 'true' }}
env:
# The export is derived from this binary's stdout, so step env
# pins it the way the DOCKER_HOST pins do: step env outranks an
# appended $GITHUB_ENV value, and the absolute path cannot be
# shadowed by a '${RUNNER_TEMP}/qwen-bin' or $GITHUB_PATH plant
# (#9527 review).
SANDBOX_COMMAND: '/usr/bin/docker'
run: |-
node .github/scripts/resolve-sandbox-image.mjs \
"$(node -p "require('./package.json').config.sandboxImageUri")"
@ -3926,6 +3933,13 @@ jobs:
- name: 'Resolve sandbox image'
id: 'sandbox_image'
env:
# The export is derived from this binary's stdout, so step env
# pins it the way the DOCKER_HOST pins do: step env outranks an
# appended $GITHUB_ENV value, and the absolute path cannot be
# shadowed by a '${RUNNER_TEMP}/qwen-bin' or $GITHUB_PATH plant
# (#9527 review).
SANDBOX_COMMAND: '/usr/bin/docker'
run: |-
node .github/scripts/resolve-sandbox-image.mjs \
"$(node -p "require('./package.json').config.sandboxImageUri")"

View file

@ -97,6 +97,51 @@ describe('start_sandbox', () => {
child.emit('close', 0);
await expect(result).resolves.toBe(0);
});
it('checks image presence with an offline inspect that sees digest references', async () => {
vi.stubEnv('SANDBOX_SET_UID_GID', 'false');
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'realpathSync').mockImplementation((filePath) =>
String(filePath),
);
execSyncMock.mockReturnValue(Buffer.from(''));
const digestImage =
'ghcr.io/qwenlm/qwen-code@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
const imageCheck = Object.assign(new EventEmitter(), {
stdout: new EventEmitter(),
});
const child = new EventEmitter();
spawnMock
.mockImplementationOnce(() => {
queueMicrotask(() => {
imageCheck.stdout.emit('data', Buffer.from('sha256:local'));
imageCheck.emit('close', 0);
});
return imageCheck;
})
.mockReturnValueOnce(child);
const result = start_sandbox({ command: 'docker', image: digestImage }, []);
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledTimes(2));
// Digest references never appear in a `docker images -q <image>` listing
// even when the content is local, which forced a needless network pull
// (and a FatalSandboxError whenever the registry was unreachable) at
// every consumer startup; the presence check must be the offline
// `image inspect` (#9527 review).
expect(spawnMock.mock.calls[0]).toEqual([
'docker',
['image', 'inspect', '--format', '{{.Id}}', digestImage],
]);
// Content found locally: no pull is attempted — the next spawn is the
// sandbox run itself.
expect((spawnMock.mock.calls[1]?.[1] as string[])[0]).toBe('run');
child.emit('close', 0);
await expect(result).resolves.toBe(0);
});
});
describe('resolveSeatbeltProfileFile', () => {

View file

@ -947,7 +947,12 @@ export async function start_sandbox(
// Helper functions to ensure sandbox image is present
async function imageExists(sandbox: string, image: string): Promise<boolean> {
return new Promise((resolve) => {
const args = ['images', '-q', image];
// `images -q` lists repository:tag entries only, so a digest reference
// (`repo@sha256:…`) lists empty even when its content is local — forcing
// a needless registry round-trip for content already present. `image
// inspect` resolves digest references against local content offline
// (#9527).
const args = ['image', 'inspect', '--format', '{{.Id}}', image];
const checkProcess = spawn(sandbox, args);
let stdoutData = '';