qwen-code/.github/scripts/resolve-sandbox-image.mjs
易良 e1fc45d508
ci(autofix): restore sandbox image flow (#6261)
* ci(autofix): restore sandbox image flow

* test(ci): update autofix workflow assertions

* ci(autofix): restore tracked output before review checkout

* test(ci): clarify autofix workspace assertion

* style(ci): format sandbox image resolver

* ci(autofix): address sandbox review feedback

* test(ci): cover sandbox image publish gating

* ci(autofix): validate sandbox image config

* ci(autofix): address workflow review comments
2026-07-03 15:30:58 +00:00

153 lines
4.2 KiB
JavaScript

#!/usr/bin/env node
import { appendFileSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { pathToFileURL } from 'node:url';
const GHCR_REPOSITORY = 'qwenlm/qwen-code';
const FETCH_TIMEOUT_MS = 30_000;
const PULL_TIMEOUT_MS = 10 * 60 * 1000;
async function responseError(response, label) {
const body = await response.text();
return new Error(
`${label}: ${response.status} ${body.slice(0, 200)}`.trimEnd(),
);
}
export function latestSemverTag(tags) {
return tags
.filter((tag) => /^\d+\.\d+\.\d+$/.test(tag))
.sort((a, b) => {
const left = a.split('.').map(Number);
const right = b.split('.').map(Number);
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
})
.at(-1);
}
export function validateRequestedImage(image) {
const requestedImage = image?.trim();
if (
!requestedImage ||
requestedImage === 'undefined' ||
requestedImage === 'null'
) {
throw new Error(
'package.json config.sandboxImageUri must be set to a sandbox image.',
);
}
return requestedImage;
}
async function fetchLatestGhcrSemver() {
const tokenResponse = await fetch(
`https://ghcr.io/token?service=ghcr.io&scope=repository:${GHCR_REPOSITORY}:pull`,
{ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) },
);
if (!tokenResponse.ok) {
throw await responseError(tokenResponse, 'Failed to fetch GHCR token');
}
const { token } = await tokenResponse.json();
const tagsResponse = await fetch(
`https://ghcr.io/v2/${GHCR_REPOSITORY}/tags/list?n=1000`,
{
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
},
);
if (!tagsResponse.ok) {
throw await responseError(tagsResponse, 'Failed to fetch GHCR tags');
}
const { tags = [] } = await tagsResponse.json();
if (tags.length >= 1000) {
console.warn(
'::warning::GHCR returned at least 1000 tags; latest semver may be inaccurate without pagination.',
);
}
const latest = latestSemverTag(tags);
if (!latest) {
throw new Error('No semver GHCR tags found for qwen-code.');
}
return latest;
}
function pullImage(command, image) {
return new Promise((resolve) => {
const child = spawn(command, ['pull', image], { stdio: 'inherit' });
let settled = false;
let timer;
const finish = (ok) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(ok);
};
timer = setTimeout(() => {
console.error(
`::error::Timed out pulling ${image} after ${PULL_TIMEOUT_MS / 1000}s.`,
);
child.kill('SIGKILL');
finish(false);
}, PULL_TIMEOUT_MS);
child.on('error', (error) => {
console.error(
`::error::Failed to start '${command} pull ${image}': ${error.message}`,
);
finish(false);
});
child.on('close', (code) => {
if (code !== 0) {
console.error(
`::error::'${command} pull ${image}' exited with code ${code}.`,
);
}
finish(code === 0);
});
});
}
function exportImage(image) {
if (process.env.GITHUB_ENV) {
appendFileSync(process.env.GITHUB_ENV, `QWEN_SANDBOX_IMAGE=${image}\n`);
}
console.log(`QWEN_SANDBOX_IMAGE=${image}`);
}
async function main() {
const requestedImage = validateRequestedImage(process.argv[2]);
const command = process.env.SANDBOX_COMMAND || 'docker';
if (await pullImage(command, requestedImage)) {
exportImage(requestedImage);
return;
}
const latest = await fetchLatestGhcrSemver();
const fallbackImage = `ghcr.io/${GHCR_REPOSITORY}:${latest}`;
if (fallbackImage === requestedImage) {
throw new Error(
`Requested sandbox image failed to pull: ${requestedImage}`,
);
}
console.warn(
`::warning::Falling back from ${requestedImage} to latest GHCR semver ${fallbackImage}; sandbox image version may differ from package version.`,
);
if (!(await pullImage(command, fallbackImage))) {
throw new Error(`Fallback sandbox image failed to pull: ${fallbackImage}`);
}
exportImage(fallbackImage);
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}