Spaces:
Running
Running
| import { dirname, resolve } from 'node:path'; | |
| import { fileURLToPath } from 'node:url'; | |
| import { isMainModule, pickFailureOutput, printJson, runCommand } from './command-center-utils.mjs'; | |
| const LEGACY_FIXTURE_CONTAINER = 'gipc-local-image-fixture'; | |
| const LEGACY_FIXTURE_DESTINATION = '/workspace'; | |
| const DOCKER_TIMEOUT_MS = 30_000; | |
| const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); | |
| export function hasLegacyFixtureRepoMount(mounts, repoRoot = REPO_ROOT) { | |
| const resolvedRepoRoot = resolve(repoRoot); | |
| return mounts.some( | |
| (mount) => | |
| mount?.Type === 'bind' && | |
| typeof mount.Source === 'string' && | |
| resolve(mount.Source) === resolvedRepoRoot && | |
| mount.Destination === LEGACY_FIXTURE_DESTINATION | |
| ); | |
| } | |
| export function parseDockerInspectContainer(output) { | |
| const parsed = JSON.parse(output); | |
| if (!Array.isArray(parsed) || !parsed[0] || typeof parsed[0] !== 'object') { | |
| throw new Error('docker inspect did not return a container object'); | |
| } | |
| return parsed[0]; | |
| } | |
| export function summarizeDockerMounts(mounts) { | |
| return mounts.map((mount) => ({ | |
| type: mount.Type, | |
| source: mount.Source, | |
| destination: mount.Destination, | |
| mode: mount.Mode, | |
| writable: mount.RW | |
| })); | |
| } | |
| function parseArgs(argv) { | |
| const options = { | |
| dryRun: false, | |
| help: false, | |
| containerName: LEGACY_FIXTURE_CONTAINER | |
| }; | |
| for (let index = 0; index < argv.length; index += 1) { | |
| const arg = argv[index]; | |
| if (arg === '--dry-run') { | |
| options.dryRun = true; | |
| continue; | |
| } | |
| if (arg === '--help' || arg === '-h') { | |
| options.help = true; | |
| continue; | |
| } | |
| if (arg === '--container') { | |
| const value = argv[index + 1]; | |
| if (!value) throw new Error('--container requires a value'); | |
| options.containerName = value; | |
| index += 1; | |
| continue; | |
| } | |
| throw new Error(`Unknown option: ${arg}`); | |
| } | |
| return options; | |
| } | |
| function printHelp() { | |
| console.log(`用法:npm run docker:cleanup-fixtures -- [--dry-run] [--container <name>] | |
| 清理遗留的本地图片上游 Docker fixture 容器。该脚本只会删除同时满足以下条件的容器: | |
| - 容器名匹配,默认 ${LEGACY_FIXTURE_CONTAINER} | |
| - 存在 ${REPO_ROOT} -> ${LEGACY_FIXTURE_DESTINATION} 的整仓 bind mount | |
| 推荐使用 npm run smoke:image-upstream-local 启动本地 fixture。不要用 docker run -v "$PWD:${LEGACY_FIXTURE_DESTINATION}" 启动 fixture。`); | |
| } | |
| function isMissingContainer(result) { | |
| const output = `${result.stdout}\n${result.stderr}`; | |
| return /No such object|No such container/i.test(output); | |
| } | |
| export function buildAbsentReport(containerName) { | |
| return { | |
| ok: true, | |
| command: 'docker:cleanup-fixtures', | |
| container: containerName, | |
| present: false, | |
| unsafe_repo_mount: false, | |
| removed: false | |
| }; | |
| } | |
| export function buildSkippedReport(containerName, mounts) { | |
| return { | |
| ok: true, | |
| command: 'docker:cleanup-fixtures', | |
| container: containerName, | |
| present: true, | |
| unsafe_repo_mount: false, | |
| removed: false, | |
| mounts: summarizeDockerMounts(mounts) | |
| }; | |
| } | |
| function cleanupLegacyFixtureContainer(options) { | |
| const inspect = runCommand('docker', ['inspect', options.containerName], { timeoutMs: DOCKER_TIMEOUT_MS }); | |
| if (!inspect.ok) { | |
| if (isMissingContainer(inspect)) return buildAbsentReport(options.containerName); | |
| return { | |
| ok: false, | |
| command: 'docker:cleanup-fixtures', | |
| phase: 'inspect', | |
| container: options.containerName, | |
| error: pickFailureOutput(inspect) | |
| }; | |
| } | |
| let container; | |
| try { | |
| container = parseDockerInspectContainer(inspect.stdout); | |
| } catch (error) { | |
| return { | |
| ok: false, | |
| command: 'docker:cleanup-fixtures', | |
| phase: 'parse-inspect', | |
| container: options.containerName, | |
| error: error instanceof Error ? error.message : String(error) | |
| }; | |
| } | |
| const mounts = Array.isArray(container.Mounts) ? container.Mounts : []; | |
| if (!hasLegacyFixtureRepoMount(mounts, REPO_ROOT)) return buildSkippedReport(options.containerName, mounts); | |
| if (options.dryRun) { | |
| return { | |
| ok: true, | |
| command: 'docker:cleanup-fixtures', | |
| container: options.containerName, | |
| present: true, | |
| unsafe_repo_mount: true, | |
| removed: false, | |
| dry_run: true, | |
| mounts: summarizeDockerMounts(mounts) | |
| }; | |
| } | |
| const remove = runCommand('docker', ['rm', '-f', options.containerName], { timeoutMs: DOCKER_TIMEOUT_MS }); | |
| return { | |
| ok: remove.ok, | |
| command: 'docker:cleanup-fixtures', | |
| container: options.containerName, | |
| present: true, | |
| unsafe_repo_mount: true, | |
| removed: remove.ok, | |
| mounts: summarizeDockerMounts(mounts), | |
| ...(remove.ok ? {} : { phase: 'remove', error: pickFailureOutput(remove) }) | |
| }; | |
| } | |
| function main() { | |
| const options = parseArgs(process.argv.slice(2)); | |
| if (options.help) { | |
| printHelp(); | |
| return; | |
| } | |
| const report = cleanupLegacyFixtureContainer(options); | |
| printJson(report); | |
| if (!report.ok) process.exitCode = 1; | |
| } | |
| if (isMainModule(import.meta.url, process.argv[1])) main(); | |