From c8bbd03836af9e74035bd076576daeae4f1782a5 Mon Sep 17 00:00:00 2001 From: Scott Converse Date: Sun, 24 May 2026 15:31:08 -0600 Subject: [PATCH] ci: add civiccore cleanroom release rehearsal --- .github/workflows/release.yml | 217 +++++++++++++++++++++++++++++++-- tests/test_github_workflows.py | 23 +++- 2 files changed, 224 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe666ef..dc2817f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,14 +5,90 @@ on: tags: - "v*" - "civiccore-*-freeze" + workflow_dispatch: + inputs: + release_tag: + description: "Published release tag to rehearse from cold caches" + required: true + default: "v1.2.0" permissions: contents: write id-token: write jobs: + cleanroom-rehearsal: + name: cleanroom-rehearsal + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: scott-desktop-cleanroom-${{ github.repository }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + + - name: Set up Python for direct-pip cleanroom + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Cleanroom cache strip (scoped, CivicSuite-only) + shell: bash + run: | + python - <<'PY' + import os + import shutil + + runner_temp = os.environ["RUNNER_TEMP"] + for rel in (".npm", ".cache/pip"): + shutil.rmtree(os.path.join(runner_temp, rel), ignore_errors=True) + PY + docker builder prune --all --force --filter "label=civicsuite-cleanroom=1" || true + docker network ls --filter "label=civicsuite-cleanroom=1" -q \ + | xargs -r docker network rm || true + docker volume ls --filter "label=civicsuite-cleanroom=1" -q \ + | xargs -r docker volume rm || true + + - name: Install published wheel from release URL + shell: bash + run: | + TAG="${{ inputs.release_tag }}" + VERSION="${TAG#v}" + WHEEL_URL="https://github.com/CivicSuite/civiccore/releases/download/${TAG}/civiccore-${VERSION}-py3-none-any.whl" + echo "Cleanroom wheel URL: ${WHEEL_URL}" + python -m venv .cleanroom-venv + . .cleanroom-venv/bin/activate + python -m pip install --upgrade pip + python -m pip install --no-cache-dir --force-reinstall "${WHEEL_URL}" + TESTING=1 python - < release-notes.md <<'EOF' - CivicCore ${{ github.ref_name }} release. + export VERSION="${GITHUB_REF_NAME#v}" + export REPO="${GITHUB_REPOSITORY}" + export TAG="${GITHUB_REF_NAME}" + export WORKFLOW_RUN_ID="${GITHUB_RUN_ID}" + export WHEEL_URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/civiccore-${VERSION}-py3-none-any.whl" + export CLEANROOM_TIMESTAMP="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + cat > release-notes.md < None: Path(".github/workflows/release.yml").read_text(encoding="utf-8") ) tag_triggers = workflow[True]["push"]["tags"] - release_steps = workflow["jobs"]["release"]["steps"] - download_step = release_steps[0] - create_release_script = release_steps[1]["run"] + draft_steps = workflow["jobs"]["create-draft-release"]["steps"] + download_step = draft_steps[0] + create_release_script = draft_steps[1]["run"] + release_cleanroom = workflow["jobs"]["release-cleanroom-rehearsal"] + publish_script = workflow["jobs"]["publish-release"]["steps"][0]["run"] assert "v*" in tag_triggers assert "civiccore-*-freeze" in tag_triggers + assert workflow["jobs"]["cleanroom-rehearsal"]["if"] == "github.event_name == 'workflow_dispatch'" + assert release_cleanroom["needs"] == ["create-draft-release"] + assert workflow["jobs"]["publish-release"]["needs"] == ["release-cleanroom-rehearsal"] assert download_step["uses"] == "actions/download-artifact@v8" assert download_step["with"]["name"] == "civiccore-dist" assert download_step["with"]["path"] == "release-assets/" assert "civiccore-*-freeze) latest_flag=(--latest=false)" in create_release_script + assert "Cleanroom rehearsal: PASSED in workflow run ${WORKFLOW_RUN_ID}" in create_release_script + assert "Verified clean install of ${WHEEL_URL} from cold caches" in create_release_script assert "docs/evidence/co8-civiccore-procurement-evidence-pack/index.md" in create_release_script assert "docs/ops/co-9-civiccore-v1-closeout.md" in create_release_script - assert "python scripts/verify-release-provenance.py ${{ github.ref_name }}" in create_release_script + assert "python scripts/verify-release-provenance.py ${TAG}" in create_release_script assert '"${latest_flag[@]}" \\' in create_release_script + assert "--draft" in create_release_script assert "release-assets/dist/*" in create_release_script assert "release-assets/release-attestation.json \\" in create_release_script assert "release-assets/release-attestation.json.bundle \\" in create_release_script assert "release-assets/*" not in create_release_script + assert "gh release download" in release_cleanroom["steps"][3]["run"] + assert "python -m pip install --no-cache-dir --force-reinstall" in release_cleanroom["steps"][4]["run"] + assert "TESTING=1 python" in release_cleanroom["steps"][4]["run"] + assert "docker builder prune --all --force --filter \"label=civicsuite-cleanroom=1\"" in release_cleanroom["steps"][2]["run"] + assert "system prune" not in release_cleanroom["steps"][2]["run"] + assert "--draft=false" in publish_script + assert "civiccore-*-freeze) latest_flag=(--latest=false)" in publish_script def test_ci_workflow_runs_full_release_verification_gate() -> None: