From 6e3e434b68a60191a39f4f24315cce0bcb73e355 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 5 Aug 2026 14:32:19 +0200 Subject: [PATCH] ci: pin every GitHub Action to a commit SHA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI has failed on every pull request since 2026-07-01. The last green run was 2026-06-30 (run 28426521860). Nothing in this repo changed: the only commits touching `.github/workflows/` since then are Dependabot tag bumps, and the `uses:` refs have looked like this for the repo's whole life. What changed is outside the repo. `tools/semgrep` runs `semgrep --config=auto`, which resolves the Community rule registry **at run time**, and that registry now carries `yaml.github-actions.security.github-actions-mutable-action-tag`. It reports 7 blocking findings — every `uses:` names a mutable tag instead of a 40-character commit SHA: | file | lines | |---|---| | `.github/workflows/ci.yaml` | 19, 20, 27, 43 | | `.github/workflows/release.yml` | 16, 17, 25 | That single red job is why ruff 0.15.19 → 0.16.0, actions/checkout, actions/setup-python and even Mergify's own configuration-deprecation PRs are all stuck. The loop is self-sustaining: Dependabot writes *mutable tags* when it bumps an action, so its own GHA bumps trip the rule they just re-introduced. #291 is literally "bump actions/checkout from 7.0.0 to 7.0.1" failing on the tag it wrote. ## The one that is not a lint nit `release.yml:25` was `pypa/gh-action-pypi-publish@release/v1` — a mutable **branch**, not even a tag, on the job that holds this project's PyPI publish rights (`environment: release`, `id-token: write`). A push to that branch upstream — by its owner or by anyone who compromises the repository — runs with permission to publish `sql-compare` to PyPI on the next release, and there is no version number to notice it by. This is the real supply-chain exposure here; it is pinned deliberately, not incidentally. ## What this does and does not cost Every ref is pinned to the SHA its current tag resolves to, so behaviour is unchanged today — including `gh-action-pypi-publish`, where `dc37677` (v1.14.2) is exactly where `release/v1` points right now. The trailing `# vX.Y.Z` comment keeps the file readable and is the field Dependabot rewrites. No new maintenance: Dependabot updates SHA-pinned actions natively and `.github/dependabot.yml` already enables the `github-actions` ecosystem. Verified locally against the exact CI image — `semgrep/semgrep:1.138.0` running `semgrep --config=auto --error --timeout=15 .` over the same 18 targets with the same 329 rules: **7 blocking findings before, 0 after.** ## What this does NOT fix — a call for @jd Pinning the semgrep *image* pins the **engine**, not the **rules**. `--config=auto` still resolves the registry live, which is precisely how this repo went red with zero commits, and it will happen again the next time the Community ruleset gains a rule this repo trips. Green today does not mean green tomorrow. Three options, none of them free: 1. **Pin a ruleset** — `--config=p/` or a versioned registry pin. Keeps CI reproducible; new upstream rules only arrive when someone bumps them, which is also the downside. 2. **Vendor the rules** — copy the rule YAML into the repo. Fully reproducible and diffable; someone has to own refreshing it. 3. **Accept the drift** — status quo. Zero effort, and the fleet occasionally goes red with no local cause. Note the monorepo has explicitly made this choice and recorded it in its root `CLAUDE.md`. Changing the security posture is a maintainer call, so this PR does not make it. It only unblocks CI. Change-Id: Ica52caa915bdbf1cde52e1849ca89f8fad0537db --- .github/workflows/ci.yaml | 8 ++++---- .github/workflows/release.yml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 2ee906e..ce66f91 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,15 +16,15 @@ jobs: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v7.0.0 - - uses: actions/setup-python@v6.3.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 id: target-python with: python-version: "${{ matrix.python-version }}" # poetry >=2.3 requires Python >=3.10, so install it under a fixed # interpreter and target the matrix Python via `poetry env use`. This keeps # the project tested on every matrix version (including 3.9). - - uses: actions/setup-python@v6.3.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.12" - run: | @@ -40,5 +40,5 @@ jobs: container: image: semgrep/semgrep:1.138.0 steps: - - uses: actions/checkout@v7.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - run: tools/semgrep diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09307a8..5aa6bcb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,8 @@ jobs: id-token: write contents: write steps: - - uses: actions/checkout@v7.0.0 - - uses: actions/setup-python@v6.3.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: 3.12 - run: | @@ -22,4 +22,4 @@ jobs: poetry sync poetry build - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2