basant307/AI_Governance_Project
048
1#!/usr/bin/env node2import { appendFileSync } from 'node:fs';3import { spawn } from 'node:child_process';4import { pathToFileURL } from 'node:url';5 6const GHCR_REPOSITORY = 'qwenlm/qwen-code';7const FETCH_TIMEOUT_MS = 30_000;8const PULL_TIMEOUT_MS = 10 * 60 * 1000;9 10async function responseError(response, label) {11 const body = await response.text();12 return new Error(13 `${label}: ${response.status} ${body.slice(0, 200)}`.trimEnd(),14 );15}16 17export function latestSemverTag(tags) {18 return tags19 .filter((tag) => /^\d+\.\d+\.\d+$/.test(tag))20 .sort((a, b) => {21 const left = a.split('.').map(Number);22 const right = b.split('.').map(Number);23 return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];24 })25 .at(-1);26}27 28export function validateRequestedImage(image) {29 const requestedImage = image?.trim();30 if (31 !requestedImage ||32 requestedImage === 'undefined' ||33 requestedImage === 'null'34 ) {35 throw new Error(36 'package.json config.sandboxImageUri must be set to a sandbox image.',37 );38 }39 return requestedImage;40}41 42async function fetchLatestGhcrSemver() {43 const tokenResponse = await fetch(44 `https://ghcr.io/token?service=ghcr.io&scope=repository:${GHCR_REPOSITORY}:pull`,45 { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) },46 );47 if (!tokenResponse.ok) {48 throw await responseError(tokenResponse, 'Failed to fetch GHCR token');49 }50 51 const { token } = await tokenResponse.json();52 const tagsResponse = await fetch(53 `https://ghcr.io/v2/${GHCR_REPOSITORY}/tags/list?n=1000`,54 {55 headers: { Authorization: `Bearer ${token}` },56 signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),57 },58 );59 if (!tagsResponse.ok) {60 throw await responseError(tagsResponse, 'Failed to fetch GHCR tags');61 }62 63 const { tags = [] } = await tagsResponse.json();64 if (tags.length >= 1000) {65 console.warn(66 '::warning::GHCR returned at least 1000 tags; latest semver may be inaccurate without pagination.',67 );68 }69 const latest = latestSemverTag(tags);70 if (!latest) {71 throw new Error('No semver GHCR tags found for qwen-code.');72 }73 return latest;74}75 76function pullImage(command, image) {77 return new Promise((resolve) => {78 const child = spawn(command, ['pull', image], { stdio: 'inherit' });79 let settled = false;80 let timer;81 const finish = (ok) => {82 if (settled) return;83 settled = true;84 clearTimeout(timer);85 resolve(ok);86 };87 timer = setTimeout(() => {88 console.error(89 `::error::Timed out pulling ${image} after ${PULL_TIMEOUT_MS / 1000}s.`,90 );91 child.kill('SIGKILL');92 finish(false);93 }, PULL_TIMEOUT_MS);94 95 child.on('error', (error) => {96 console.error(97 `::error::Failed to start '${command} pull ${image}': ${error.message}`,98 );99 finish(false);100 });101 child.on('close', (code) => {102 if (code !== 0) {103 console.error(104 `::error::'${command} pull ${image}' exited with code ${code}.`,105 );106 }107 finish(code === 0);108 });109 });110}111 112function exportImage(image) {113 if (process.env.GITHUB_ENV) {114 appendFileSync(process.env.GITHUB_ENV, `QWEN_SANDBOX_IMAGE=${image}\n`);115 }116 console.log(`QWEN_SANDBOX_IMAGE=${image}`);117}118 119async function main() {120 const requestedImage = validateRequestedImage(process.argv[2]);121 122 const command = process.env.SANDBOX_COMMAND || 'docker';123 if (await pullImage(command, requestedImage)) {124 exportImage(requestedImage);125 return;126 }127 128 const latest = await fetchLatestGhcrSemver();129 const fallbackImage = `ghcr.io/${GHCR_REPOSITORY}:${latest}`;130 if (fallbackImage === requestedImage) {131 throw new Error(132 `Requested sandbox image failed to pull: ${requestedImage}`,133 );134 }135 136 console.warn(137 `::warning::Falling back from ${requestedImage} to latest GHCR semver ${fallbackImage}; sandbox image version may differ from package version.`,138 );139 if (!(await pullImage(command, fallbackImage))) {140 throw new Error(`Fallback sandbox image failed to pull: ${fallbackImage}`);141 }142 exportImage(fallbackImage);143}144 145if (146 process.argv[1] &&147 import.meta.url === pathToFileURL(process.argv[1]).href148) {149 main().catch((error) => {150 console.error(error instanceof Error ? error.message : String(error));151 process.exit(1);152 });153}154 