diff --git a/.github/scripts/run-release-docker-integration.sh b/.github/scripts/run-release-docker-integration.sh index 3769fee8796..409070ad4ee 100755 --- a/.github/scripts/run-release-docker-integration.sh +++ b/.github/scripts/run-release-docker-integration.sh @@ -1,6 +1,26 @@ #!/usr/bin/env bash set -euo pipefail +cleanup_release_containers() { + container_ids="$(timeout 30 docker ps -aq --filter "label=org.qwen-code.ci.owner=${RELEASE_CONTAINER_OWNER}" 2>/dev/null)" || container_ids='' + if [ -n "$container_ids" ]; then + printf '%s\n' "$container_ids" | xargs -r timeout 60 docker rm -f > /dev/null 2>&1 || echo "::warning::failed to remove release containers for ${RELEASE_CONTAINER_OWNER}" + fi +} + +if [ "${1:-}" = 'cleanup' ]; then + cleanup_release_containers + remaining="$(timeout 30 docker ps -aq --filter "label=org.qwen-code.ci.owner=${RELEASE_CONTAINER_OWNER}")" + if [ -n "$remaining" ]; then + echo "::error::release containers remain for ${RELEASE_CONTAINER_OWNER}: ${remaining//$'\n'/,}" + exit 1 + fi + exit 0 +fi + +trap cleanup_release_containers EXIT +trap 'exit 1' INT TERM + sandbox_revision="$(git rev-parse HEAD)" sandbox_image="$(node -p "require('./packages/cli/package.json').config.sandboxImageUri")-release-${sandbox_revision}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4c90a84357b..3e46304d06f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -546,9 +546,17 @@ jobs: env: QWEN_SANDBOX: 'docker' BUILD_SANDBOX_FLAGS: '--label org.qwen-code.ci.sandbox=true' + RELEASE_CONTAINER_OWNER: '${{ github.run_id }}-${{ github.run_attempt }}-release' + SANDBOX_FLAGS: '--label org.qwen-code.ci.owner=${RELEASE_CONTAINER_OWNER}' RUNNER_ENVIRONMENT: '${{ runner.environment }}' run: '.release-workflow/.github/scripts/run-release-docker-integration.sh' + - name: 'Remove job-owned release containers' + if: "${{ always() && runner.environment == 'self-hosted' }}" + env: + RELEASE_CONTAINER_OWNER: '${{ github.run_id }}-${{ github.run_attempt }}-release' + run: '.release-workflow/.github/scripts/run-release-docker-integration.sh cleanup' + audio_capture_prebuilds: name: 'Audio Capture Prebuilds' needs: 'prepare' diff --git a/scripts/tests/release-workflow.test.js b/scripts/tests/release-workflow.test.js index 7b4e41a6104..3c9bd534eca 100644 --- a/scripts/tests/release-workflow.test.js +++ b/scripts/tests/release-workflow.test.js @@ -2077,6 +2077,37 @@ describe('release workflow', () => { ); }); + it('reaps only the Docker integration containers owned by its job', () => { + const owner = '${{ github.run_id }}-${{ github.run_attempt }}-release'; + const steps = releaseYaml.jobs.integration_docker.steps; + const testStep = steps.find( + (step) => step.name === 'Run Docker Integration Tests', + ); + const cleanupStep = steps.find( + (step) => step.name === 'Remove job-owned release containers', + ); + + expect(testStep.env.RELEASE_CONTAINER_OWNER).toBe(owner); + expect(testStep.env.SANDBOX_FLAGS).toContain( + 'org.qwen-code.ci.owner=${RELEASE_CONTAINER_OWNER}', + ); + expect(dockerIntegrationScript).toContain( + 'trap cleanup_release_containers EXIT', + ); + expect(dockerIntegrationScript).toContain("trap 'exit 1' INT TERM"); + expect(dockerIntegrationScript).toContain( + '--filter "label=org.qwen-code.ci.owner=${RELEASE_CONTAINER_OWNER}"', + ); + expect(cleanupStep.if).toContain('always()'); + expect(cleanupStep.env.RELEASE_CONTAINER_OWNER).toBe(owner); + expect(cleanupStep.run).toBe( + '.release-workflow/.github/scripts/run-release-docker-integration.sh cleanup', + ); + expect(dockerIntegrationScript).toContain('docker rm -f > /dev/null'); + expect(dockerIntegrationScript.match(/docker ps -aq/g)).toHaveLength(2); + expect(dockerIntegrationScript).toContain('release containers remain'); + }); + it('digest-pins every sandbox base image', () => { // integration_docker builds on the shared pool, whose docker daemon // store persists across jobs: a co-resident job can retag a mutable @@ -2530,11 +2561,15 @@ describe('release lane runner routing', () => { ]) { const job = releaseYaml.jobs[name]; expect(job.env.RUNNER_ENVIRONMENT, name).toBeUndefined(); - const testSteps = job.steps.filter((step) => - /(?:vitest|test:integration|run-release-docker-integration)/.test( - String(step.run ?? ''), - ), - ); + const testSteps = job.steps.filter((step) => { + const command = String(step.run ?? ''); + return ( + !command.endsWith(' cleanup') && + /(?:vitest|test:integration|run-release-docker-integration)/.test( + command, + ) + ); + }); expect(testSteps, name).toHaveLength(expectedSteps); for (const step of testSteps) { expect(step.env.RUNNER_ENVIRONMENT, `${name}: ${step.name}`).toBe(