diff --git a/.github/workflows/catalog-assign.yml b/.github/workflows/catalog-assign.yml index 78b4f552f3..f828794864 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 59a02702a1..84074b4791 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 5cf5514a0a..7cc6d28f86 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 0000000000..b6ee409fb0 --- /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" + )