From e5df517ddc0118e1e0f5278bf34ce79065994001 Mon Sep 17 00:00:00 2001 From: Pascal THUET Date: Wed, 24 Jun 2026 22:08:16 +0200 Subject: [PATCH] ci: pin actions to commit SHAs and add shellcheck (#3126) * ci: pin actions to commit SHAs and add shellcheck Pin actions/github-script in catalog-assign.yml to a full commit SHA; all other workflows were already pinned. Add a repo-wide regression test that every workflow `uses:` ref is pinned to a 40-char commit SHA. Add a shellcheck job to lint.yml (--severity=error over scripts/bash/*.sh) and document the local command in CONTRIBUTING.md. * ci: use repo-standard actions/checkout v7.0.0 in shellcheck job * ci: shellcheck all tracked shell scripts Assisted-by: Codex (model: GPT-5, autonomous) * ci: address workflow hygiene review feedback Assisted-by: Codex (model: GPT-5, autonomous) --- .github/workflows/catalog-assign.yml | 2 +- .github/workflows/lint.yml | 12 ++++++++ CONTRIBUTING.md | 10 +++++++ tests/test_github_workflows.py | 41 ++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/test_github_workflows.py diff --git a/.github/workflows/catalog-assign.yml b/.github/workflows/catalog-assign.yml index 78b4f552f..f82879486 100644 --- a/.github/workflows/catalog-assign.yml +++ b/.github/workflows/catalog-assign.yml @@ -19,7 +19,7 @@ jobs: permissions: issues: write steps: - - uses: actions/github-script@v9 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: script: | const issue = context.payload.issue; diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 59a02702a..84074b479 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -42,3 +42,15 @@ jobs: globs: | '**/*.md' !extensions/**/*.md + + shellcheck: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + # shellcheck is preinstalled on ubuntu-latest runners. + # Start at --severity=error to block real bugs without flagging style + # (notably SC2155). Tighten in a follow-up after cleanup. + - name: Run shellcheck on shell scripts + run: git ls-files -z -- '*.sh' | xargs -0 shellcheck --severity=error diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cf5514a0..7cc6d28f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -113,6 +113,16 @@ uv pip install -e ".[test]" > `specify_cli` to this checkout's `src/`. This matches the gotcha documented in > `AGENTS.md` (Common Pitfalls). +#### Shell scripts + +```bash +git ls-files -z -- '*.sh' | xargs -0 shellcheck --severity=error +``` + +The CI `lint.yml` `shellcheck` job currently reports and blocks only +error-severity findings. Warnings such as SC2155 are intentionally outside this +job until a follow-up cleanup tightens the threshold. + ### Manual testing #### Testing setup diff --git a/tests/test_github_workflows.py b/tests/test_github_workflows.py new file mode 100644 index 000000000..b6ee409fb --- /dev/null +++ b/tests/test_github_workflows.py @@ -0,0 +1,41 @@ +"""Static checks for repository GitHub Actions workflows.""" + +from __future__ import annotations + +import re +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parent.parent +WORKFLOWS_DIR = REPO_ROOT / ".github" / "workflows" +# Match both the dedicated-step form (` uses: x@sha`) and the +# inline shorthand (` - uses: x@sha`) used in catalog-assign.yml. +USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s*(?P\S+)", re.MULTILINE) +PINNED_SHA_RE = re.compile(r"@[0-9a-f]{40}$", re.IGNORECASE) + + +def test_github_actions_are_pinned_to_full_commit_shas(): + unpinned_refs = [] + + workflows = sorted( + list(WORKFLOWS_DIR.glob("*.yml")) + list(WORKFLOWS_DIR.glob("*.yaml")) + ) + assert workflows + + for workflow in workflows: + workflow_text = workflow.read_text(encoding="utf-8") + for match in USES_RE.finditer(workflow_text): + uses_ref = match.group("ref") + if uses_ref.startswith(("./", "../")): + continue + if PINNED_SHA_RE.search(uses_ref): + continue + unpinned_refs.append(f"{workflow.relative_to(REPO_ROOT)}: {uses_ref}") + + assert unpinned_refs == [] + + +def test_pinned_action_ref_accepts_uppercase_hex_sha(): + assert PINNED_SHA_RE.search( + "actions/example@0123456789ABCDEF0123456789ABCDEF01234567" + )