From 22fe5dd1d862cd1caee0d58db0cba17645d690c9 Mon Sep 17 00:00:00 2001 From: timmy Date: Tue, 25 Aug 2026 03:57:29 +0000 Subject: [PATCH] ci: pin release pipeline actions (Closes #1378) --- .gitea/workflows/ci.yml | 21 ++++++++++++--------- tests/test_ci_workflow.py | 37 ++++++++++++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 3acf077..e9de1c8 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -7,12 +7,15 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: read + jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: { python-version: "3.11" } - run: pip install -r requirements.txt - run: pip install -r requirements-audit.txt @@ -23,7 +26,7 @@ jobs: runs-on: ubuntu-latest needs: lint steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build deterministic release bundle run: | SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA")" @@ -33,7 +36,7 @@ jobs: --commit "$GITHUB_SHA" \ --source-date-epoch "$SOURCE_DATE_EPOCH" - name: Upload tested release bundle - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 with: name: release-bundle path: dist/ @@ -44,11 +47,11 @@ jobs: env: STACKCHAIN_RUN_RELEASE_E2E: "1" steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: { python-version: "3.11" } - name: Download assembled release bundle - uses: actions/download-artifact@v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: release-bundle path: dist @@ -66,9 +69,9 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download tested release bundle - uses: actions/download-artifact@v3 + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: release-bundle path: dist diff --git a/tests/test_ci_workflow.py b/tests/test_ci_workflow.py index 09e286c..3cf978c 100644 --- a/tests/test_ci_workflow.py +++ b/tests/test_ci_workflow.py @@ -1,13 +1,44 @@ from pathlib import Path +import re WORKFLOW = Path(".gitea/workflows/ci.yml") +ACTION_REFERENCE = re.compile( + r"^\s*- uses: (?P[^\s@]+)@(?P[^\s#]+)\s+# (?Pv\d+(?:\.\d+){1,2})$", + re.MULTILINE, +) +DOWNLOAD_ARTIFACT = ( + "actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2" +) def test_ci_uses_gitea_compatible_artifact_action(): text = WORKFLOW.read_text() assert "actions/upload-artifact@v4" not in text - assert "actions/upload-artifact@v3" in text + assert re.search( + r"actions/upload-artifact@[0-9a-f]{40}\s+# v3\.\d+\.\d+", text + ) + + +def test_ci_pins_every_external_action_to_a_reviewable_commit(): + text = WORKFLOW.read_text() + uses_lines = [line for line in text.splitlines() if line.lstrip().startswith("- uses:")] + references = list(ACTION_REFERENCE.finditer(text)) + + assert len(references) == len(uses_lines) + assert references + for reference in references: + assert re.fullmatch(r"[0-9a-f]{40}", reference["revision"]) + + +def test_ci_grants_repository_write_access_only_to_release_job(): + text = WORKFLOW.read_text() + jobs = text[text.index("jobs:") :] + before_release, release = jobs.split(" release-candidate:", 1) + + assert "permissions:\n contents: read" in text[: text.index("jobs:")] + assert "contents: write" not in before_release + assert "permissions:\n contents: write" in release def test_ci_installs_declared_requirements_before_tests(): @@ -40,7 +71,7 @@ def test_release_promotion_waits_for_tests_and_bundle(): assert "needs: [lint, build-release, browser-journey]" in release assert "github.event_name == 'push'" in release - assert "actions/download-artifact@v3" in release + assert DOWNLOAD_ARTIFACT in release assert ( 'python3 scripts/verify_release.py --input-dir dist --commit "$TARGET" --repository .' in release @@ -54,7 +85,7 @@ def test_release_promotion_waits_for_every_packaged_mobile_journey(): release = text[text.index(" release-candidate:") :] assert "needs: build-release" in browser - assert "actions/download-artifact@v3" in browser + assert DOWNLOAD_ARTIFACT in browser assert "pip install -r requirements-e2e.txt" in browser assert "python3 -m playwright install --with-deps chromium" in browser assert 'STACKCHAIN_RUN_RELEASE_E2E: "1"' in browser