Pin release pipeline actions and enforce least-privilege permissions #1379
|
|
@ -7,12 +7,15 @@ on:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||||
with: { python-version: "3.11" }
|
with: { python-version: "3.11" }
|
||||||
- run: pip install -r requirements.txt
|
- run: pip install -r requirements.txt
|
||||||
- run: pip install -r requirements-audit.txt
|
- run: pip install -r requirements-audit.txt
|
||||||
|
|
@ -23,7 +26,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: lint
|
needs: lint
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Build deterministic release bundle
|
- name: Build deterministic release bundle
|
||||||
run: |
|
run: |
|
||||||
SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA")"
|
SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA")"
|
||||||
|
|
@ -33,7 +36,7 @@ jobs:
|
||||||
--commit "$GITHUB_SHA" \
|
--commit "$GITHUB_SHA" \
|
||||||
--source-date-epoch "$SOURCE_DATE_EPOCH"
|
--source-date-epoch "$SOURCE_DATE_EPOCH"
|
||||||
- name: Upload tested release bundle
|
- name: Upload tested release bundle
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: release-bundle
|
name: release-bundle
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|
@ -44,11 +47,11 @@ jobs:
|
||||||
env:
|
env:
|
||||||
STACKCHAIN_RUN_RELEASE_E2E: "1"
|
STACKCHAIN_RUN_RELEASE_E2E: "1"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||||
with: { python-version: "3.11" }
|
with: { python-version: "3.11" }
|
||||||
- name: Download assembled release bundle
|
- name: Download assembled release bundle
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||||
with:
|
with:
|
||||||
name: release-bundle
|
name: release-bundle
|
||||||
path: dist
|
path: dist
|
||||||
|
|
@ -66,9 +69,9 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Download tested release bundle
|
- name: Download tested release bundle
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||||
with:
|
with:
|
||||||
name: release-bundle
|
name: release-bundle
|
||||||
path: dist
|
path: dist
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,44 @@
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
WORKFLOW = Path(".gitea/workflows/ci.yml")
|
WORKFLOW = Path(".gitea/workflows/ci.yml")
|
||||||
|
ACTION_REFERENCE = re.compile(
|
||||||
|
r"^\s*- uses: (?P<action>[^\s@]+)@(?P<revision>[^\s#]+)\s+# (?P<version>v\d+(?:\.\d+){1,2})$",
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
DOWNLOAD_ARTIFACT = (
|
||||||
|
"actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ci_uses_gitea_compatible_artifact_action():
|
def test_ci_uses_gitea_compatible_artifact_action():
|
||||||
text = WORKFLOW.read_text()
|
text = WORKFLOW.read_text()
|
||||||
assert "actions/upload-artifact@v4" not in 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():
|
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 "needs: [lint, build-release, browser-journey]" in release
|
||||||
assert "github.event_name == 'push'" in release
|
assert "github.event_name == 'push'" in release
|
||||||
assert "actions/download-artifact@v3" in release
|
assert DOWNLOAD_ARTIFACT in release
|
||||||
assert (
|
assert (
|
||||||
'python3 scripts/verify_release.py --input-dir dist --commit "$TARGET" --repository .'
|
'python3 scripts/verify_release.py --input-dir dist --commit "$TARGET" --repository .'
|
||||||
in release
|
in release
|
||||||
|
|
@ -54,7 +85,7 @@ def test_release_promotion_waits_for_every_packaged_mobile_journey():
|
||||||
release = text[text.index(" release-candidate:") :]
|
release = text[text.index(" release-candidate:") :]
|
||||||
|
|
||||||
assert "needs: build-release" in browser
|
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 "pip install -r requirements-e2e.txt" in browser
|
||||||
assert "python3 -m playwright install --with-deps chromium" in browser
|
assert "python3 -m playwright install --with-deps chromium" in browser
|
||||||
assert 'STACKCHAIN_RUN_RELEASE_E2E: "1"' in browser
|
assert 'STACKCHAIN_RUN_RELEASE_E2E: "1"' in browser
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user