Skip to content

[CI update] Lock GitHub Actions with gh actions-lock - #269

Open
aboucaud wants to merge 6 commits into
mainfrom
ci/lock-github-actions
Open

aboucaud wants to merge 6 commits into
mainfrom
ci/lock-github-actions

Conversation

@aboucaud

@aboucaud aboucaud commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

Our workflows referenced actions by major tag (actions/checkout@v4). A tag is a pointer the action's owner, or anyone who compromises their account, can move to arbitrary code, and the next CI run executes it with our secrets in reach. GitHub's answer is the workflow lockfile: gh actions-lock resolves every action, transitive ones included, to a commit SHA and records it in .github/workflows/actions.lock; the runner then rejects any action whose code no longer matches. Workflows stay readable (exact release tags, no SHAs inline), and every dependency change shows up as a reviewable diff. See https://hugovk.dev/blog/2026/lock-github-actions/ and GitHub's docs.

A lock is only useful if it stays in sync and stays current, so this PR also verifies it in CI and hands version bumps to Renovate, which regenerates actions.lock itself when it bumps an action. Both bumps and manual pins wait out a seven-day cooldown, because a compromised or broken release is usually caught and pulled within days. While touching every uses:, it also moves off node20, whose deprecation warnings every run currently prints, and fixes what zizmor reports on the workflows.

Summary

  • Adds .github/workflows/actions.lock, generated by gh actions-lock; workflows reference exact release tags, the lock carries the SHAs.
  • Moves every action off node20 to its first node24 major: checkout v5, upload-artifact v6, setup-uv v7, github-script v8, setup-node v5. claude-code-action is composite and stays on v1, pinned to v1.0.268 (v1.0.271 was two days old). pypi-publish.yaml was already on node24 and is unchanged.
  • New lockfile job in lint.yml runs gh actions-lock --verify (after GitHub's own verify-lockfile.yml), so an edited uses: without a regenerated lock fails CI. Its --pin v0.4.1 of the extension is tracked by a Renovate custom regex manager.
  • .github/renovate.json5: weekly updates of uses: references and the extension pin, grouped (majors separately), with a 7-day minimumReleaseAge. Versions passed to setup actions (tests.yml's uv floor, Node) and runner labels are left alone. Validated with renovate-config-validator --strict, and dry-run locally (--platform=local --dry-run=lookup): it finds every action and the extension pin, and skips the uv floor, Node and runners.
  • CLAUDE.md records the workflow: run gh actions-lock after any uses: edit, and hand-pin only releases at least seven days old (gh actions-lock has no cooldown of its own).
  • zizmor fixes:
    • pypa/gh-action-pypi-publish v1.14.2 → v1.14.3 for GHSA-5xjx-3w67-w9x9 (critical: a revoked CA in Sigstore's chain breaks Trusted Publishing).
    • The release build no longer restores the uv cache, which runs on main could have filled (cache-poisoning).
    • persist-credentials: false on every checkout (artipacked): nothing pushes, and eval's agent could otherwise read the token from .git/config.
    • lint.yml and tests.yml get an explicit contents: read token instead of the repository default (excessive-permissions).
    • dco.yml's pull_request_target/issue_comment and claude.yml's issue_comment are marked zizmor: ignore[dangerous-triggers]: neither checks out PR code or interpolates untrusted input.

Test plan

  • gh actions-lock --verify passes locally
  • zizmor .github/ (1.32.0, online audits): no findings, 3 ignored
  • CI green with no Node deprecation warnings (needs the PR out of draft — the jobs are gated on it)
  • The Renovate GitHub App is installed for this repository (needs an org admin)
  • First Renovate PR regenerates actions.lock (the lockfile job checks it)

🤖 Generated with Claude Code

aboucaud and others added 3 commits November 12, 2026 12:41
Add .github/workflows/actions.lock, generated by `gh actions-lock`,
which records the commit SHA each action reference resolves to, and
pin every workflow's action references to the exact release the lock
records instead of a floating major tag.

See https://hugovk.dev/blog/2026/lock-github-actions/

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Move every action off node20 to its first node24 major: checkout v5,
upload-artifact v6, setup-uv v7, github-script v8, setup-node v5;
actions.lock regenerated to match.

A `lockfile` job in lint.yml runs `gh actions-lock --verify`, so a
`uses:` edit without a regenerated lock, or a locked commit that no
longer verifies upstream, fails CI. Dependabot proposes action bumps
weekly as one grouped PR, after a seven-day cooldown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gh actions-lock has no cooldown and pins the newest matching release,
which put claude-code-action at v1.0.271, two days old. Pin v1.0.268
instead, the newest past the seven days Dependabot waits.

Note beside the gh-actions-lock --pin that Dependabot cannot bump it,
and record the lock workflow in CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aboucaud aboucaud changed the title Lock GitHub Actions with gh actions-lock [CI update] Lock GitHub Actions with gh actions-lock Nov 12, 2026
@aboucaud
aboucaud marked this pull request as ready for review November 12, 2026 11:58
- Bump pypa/gh-action-pypi-publish to v1.14.3 for GHSA-5xjx-3w67-w9x9
  (a revoked CA in Sigstore's chain breaks Trusted Publishing).
- Build releases without the uv cache, which runs on main could have
  filled.
- Set persist-credentials: false on every checkout; nothing pushes, and
  eval's agent could otherwise read the token from .git/config.
- Give lint.yml and tests.yml an explicit read-only token instead of
  the repository default.
- Mark dco.yml's and claude.yml's privileged triggers as reviewed: no
  checkout of PR code, no expression interpolation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

✅ Eval

Metric Value
Outputs check success
Agent run success
Turns 7
Tool calls 5
Cost $0.11
Agent wall time 0m40s
Model claude-sonnet-5-5
lc status
  mode:    direct
  sandbox: landlock (fs: declared, network: allowed)
  crate:   up to date with the outputs

  · current  baseline/best_fit        4d1aa08
  · current  baseline/hubble_diagram  4d1aa08
  · current  baseline/residuals       4d1aa08

3 current
Confusion & pain points (Claude analysis)

Confusion & pain points

  • The run was essentially clean. There were no errored tool calls. Validation passed, lc materialize made all 3 outputs on the first try, and --check exited 0. The friction below is minor.
  • The agent edited astra.yaml blind. It rewrote the spec with a Python str.replace script that matched exact recipe snippets (recipe:\n command: python scripts/fit.py). It had read the file only through a truncated cat and had no schema reference in front of it. The edit worked, but a whitespace or ordering difference would have failed silently, because replace does nothing when nothing matches. The spec skeleton leaves format, inputs, decisions and the recipe placeholders to be filled in by hand. Fix: add a documented astra edit command, or put a worked recipe example in the skill. astra validate only caught structural errors, not a replace that matched nothing.
  • The agent guessed a license to turn on crate maintenance. It inserted license = "CC-BY-4.0" into pyproject.toml with sed. The task never named a license, and the agent flagged the choice to the user afterward. It also ran a no-op sed 's/^dependencies = \[\]/&/'. The cause is that lc init scaffolds no [project].license, and the crate only appears once one exists. This is by design, since intent is derived. Fix: have lc materialize print a one-line hint when no crate is being maintained, and tell the eval prompt which license to use, so agents don't invent one.
  • An allocation step stands between the spec and a run. The agent had to run lc compute launch --wait, pass the cluster name local to lc materialize, and then run lc compute down local. The task didn't mention any of this, so the agent presumably found it from the skill or --help. It worked, but it is overhead for a single-node job. Fix: document the launch, materialize, down sequence in the astra/lc skill so agents don't have to discover it.
  • The agent didn't look at the figures. It built hubble_diagram and residuals and checked only that they were made, not that they looked right. It also reported a reduced χ² of 0.37 and explained it as conservative errors, without checking that explanation. This is a harness gap rather than a tool failure. The eval doesn't verify plot content, and nothing prompted the agent to inspect it.

Full trace: agent-trace artifact on this run.

aboucaud and others added 2 commits November 12, 2026 17:00
ubuntu-latest moved to Ubuntu 26.04, whose Docker 29 defaults to the
containerd image store. There lc cannot find its images by config
digest, so the Docker smoke tests fail. That is a real bug, tracked in
#270; pin the runner so this PR's CI reports on its own changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renovate regenerates actions.lock itself when it bumps an action, and a
custom regex manager also bumps the gh-actions-lock extension lint.yml
pins, which Dependabot could not see. Same policy as before: weekly,
grouped, seven-day minimum release age. Only `uses:` references are
updated; versions passed to setup actions and runner labels are not,
since tests.yml's uv is the scaffold floor and its runner is pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant