diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index fc7058413..f72a542be 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -13,6 +13,14 @@ on: permissions: contents: read +concurrency: + # First-attempt PR validations share a generation key so a newer event + # supersedes only another first attempt. Manual reruns add their stable + # run_id and therefore cannot cancel, or be cancelled by, a newer PR event. + # Tag publication is unique per run and cancel-in-progress remains false. + group: docker-publish-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}-${{ github.event_name == 'pull_request' && github.run_attempt == 1 && 'first-attempt' || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: REGISTRY: ghcr.io # Keep the explicit opt-in as belt-and-suspenders. The real warning removal diff --git a/backend/tests/test_docker_workflow_concurrency.py b/backend/tests/test_docker_workflow_concurrency.py new file mode 100644 index 000000000..d90ee5786 --- /dev/null +++ b/backend/tests/test_docker_workflow_concurrency.py @@ -0,0 +1,28 @@ +"""Regression coverage for Docker workflow concurrency identity.""" + +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_docker_pr_concurrency_isolates_reruns_from_first_attempts() -> None: + """Keep manual reruns out of the first-attempt PR cancellation group.""" + workflow = (REPO_ROOT / ".github/workflows/docker-publish.yml").read_text( + encoding="utf-8" + ) + header = workflow.split("jobs:", 1)[0] + expected_group = ( + "group: docker-publish-${{ github.repository }}-" + "${{ github.event.pull_request.number || github.ref }}-" + "${{ github.event_name == 'pull_request' && github.run_attempt == 1 " + "&& 'first-attempt' || github.run_id }}" + ) + bare_group = ( + "group: docker-publish-${{ github.repository }}-" + "${{ github.event.pull_request.number || github.ref }}" + ) + + assert expected_group in header + assert bare_group not in header.splitlines() + assert "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in header diff --git a/backend/tests/test_release_governance.py b/backend/tests/test_release_governance.py index a23c70746..3b2eb610c 100644 --- a/backend/tests/test_release_governance.py +++ b/backend/tests/test_release_governance.py @@ -738,6 +738,15 @@ def test_docker_publish_validates_pr_images_and_publishes_semver_images_only_on_ assert ( "needs.deploy_preflight.outputs.aks_kubeconfig_configured == 'true'" in workflow ) + assert "concurrency:" in workflow + assert ( + "docker-publish-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }}" + in workflow + ) + assert ( + "cancel-in-progress: ${{ github.event_name == 'pull_request' }}" in workflow + ) + assert "cancel-in-progress: true" not in workflow.split("jobs:", 1)[0] def test_frontend_dockerfile_builds_and_starts_production_artifact() -> None: