Skip to content

Reusable workflows->composite actions - #89

Merged
samuelburnham merged 9 commits into
mainfrom
ci-hardening
Sep 30, 2026
Merged

samuelburnham merged 9 commits into
mainfrom
ci-hardening

Conversation

@samuelburnham

@samuelburnham samuelburnham commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Converts the reusable workflows in this repo to composite actions, drops the org GitHub App token in favour of the scoped GITHUB_TOKEN, and hardens the lint job so the actions can't regress silently.

Breaking change for callers

Every reusable workflow except docs.yml and licenses-audits.yml is gone. Callers move from a job-level

uses: argumentcomputer/ci-workflows/.github/workflows/<name>.yml@main

to job steps: checkout, actions-rust-lang/setup-rust-toolchain where Rust is involved, then

uses: argumentcomputer/ci-workflows/.github/actions/<name>@<ref>

Each action's description documents the permissions the calling job needs and what it assumes is already set up. check-lurk-compiles and bench-pr-comment are removed outright; ix superseded both.

What changed

Composite actions. repo-sync, rust-version-check, unused-deps, links-check, typos, lints, msrv, wasm, codecov, gpu-ci (CUDA and OpenCL merged behind a gpu-framework input) and gpu-bench. Sibling actions are referenced with $/ self-repository refs, which resolve to this repo at the running commit, so the self-checkouts and their default-branch skew are gone. gpu-bench reports regressions through a step output because composite steps ignore continue-on-error.

No more App token. Automation runs on GITHUB_TOKEN with job-level permissions and fail-closed workflow defaults. repo-sync opens an issue asking for a manual sync when a push touches workflow files, which that token cannot write. JasonEtco/create-an-issue and peter-evans/create-issue-from-file are replaced by a first-party create-issue action: stdlib Python around the gh CLI that dedupes by title and updates the existing open issue.

Toolchain setup. Standardised on actions-rust-lang/setup-rust-toolchain. Rust CI actions assume the caller provisions the toolchain and cache; release-pr and typos provision their own. ci-env is removed since the toolchain action sets the same variables.

Supply chain. Every third-party action is pinned to a full commit SHA with a version comment. taiki-e/install-action's per-tool tags move and can't be pinned, so those sites use its tool: input against a pinnable release. lint-workflows now runs pinact with verify, which fails the job on an unpinned action or on a SHA whose version comment names a different release. Dependabot covers .github/actions/* as well as workflows, since the github-actions ecosystem's / directory only means .github/workflows/.

Lint coverage for composite actions. actionlint can't parse composite actions, so a script beside lint-workflows extracts every bash run: block via yq, masks ${{ }} expressions the way actionlint does, and runs them through shellcheck. This is the class of bug behind the NUM_VCPUS typo and the unguarded cds in release-pr, both fixed here.

Smoke tests. A new test.yml exercises create-issue against a stub gh that records its calls (create, update-by-title, and invalid-input paths), runs lints, wasm and unused-deps on a generated fixture crate through the documented caller pattern, and runs rust-version-check with an up-to-date pin and an outdated pin.

Fixes surfaced by the smoke tests

  • actionlint's -ignore pattern for $/ was being split on whitespace by the wrapper action; it now contains no literal spaces.
  • cargo clippy -Dwarnings was missing the -- separator, on main too.
  • rust-version-check parsed the toolchain from rustup show, which stopped printing a rustc line in rustup 1.28; it now reads rustc --version.
  • zizmor findings that only appear once actionlint stops failing first are suppressed inline with a reason; the stale .github/zizmor.yml that disabled unpinned-uses is removed now that everything is pinned.

Not in this PR

  • Consumer repos still calling the removed reusable workflows by @main will break on merge and need their own migration PRs.
  • msrv, codecov, the GPU actions, typos, links-check, repo-sync, release-pr and tag-release have no smoke coverage yet.
  • rust-version-check assumes the pinned toolchain is installed; rustup 1.28+ no longer installs it on demand.

- Drop the GitHub App token everywhere: automation runs on the scoped
  `GITHUB_TOKEN` with job-level `permissions` blocks and fail-closed
  workflow defaults. repo-sync falls back to opening an issue asking for
  a manual sync when a push touches workflow files, which the token
  cannot write.
- Replace JasonEtco/create-an-issue and peter-evans/create-issue-from-file
  with a first-party `create-issue` action: stdlib Python around the gh
  CLI that dedupes by title and updates the existing open issue.
- Convert reusable workflows to composite actions: repo-sync,
  rust-version-check, unused-deps, links-check, typos, lints, msrv, wasm,
  codecov, gpu-ci (cuda and opencl merged behind a `gpu-framework`
  input), and gpu-bench. Keep docs and licenses-audits as reusable
  workflows since they take no configuration. Remove check-lurk-compiles
  and bench-pr-comment, superseded in ix.
- Reference sibling actions and same-repo actions with `$/`
  self-repository refs, deleting all ci-workflows self-checkouts along
  with their default-branch version skew; actionlint gets an ignore for
  its `$/` false positive (rhysd/actionlint#711), and zizmor's
  self-repository audit endorses the syntax.
- Standardize on actions-rust-lang/setup-rust-toolchain: Rust CI actions
  assume the caller provisions the toolchain and cache (or a wrapper like
  ix's), while release-pr and typos provision their own since Rust is
  incidental there. ci-env is gone; the toolchain action covers its env
  vars.
- gpu-bench reports regressions via a step output because composite
  steps silently ignore `continue-on-error`; also fix its NUM_VCPUS
  typo and release-pr's unguarded `cd`s.

Callers migrate from `uses: .../.github/workflows/<x>.yml@main` to job
steps calling `.../.github/actions/<x>@<ref>`; each action description
documents its required permissions and setup assumptions.
The github-actions ecosystem's "/" directory only covers
`.github/workflows/`, so the third-party actions that moved into
composite actions would no longer receive update PRs without listing
their directories explicitly.
- create-issue runs end-to-end against a stub `gh` on PATH that records
  its calls: the create + label path, the update-existing-by-title path,
  and rejection when both `body` and `body-file` are set, asserted
  against the recorded call log
- lints, wasm, and unused-deps run on a generated fixture crate through
  the documented caller pattern (checkout, then setup-rust-toolchain,
  then the action)
- rust-version-check runs twice against the stub `gh`: an up-to-date
  pin must not open an issue, and an outdated pin (installed so
  `rustup show` reports it as active) must open exactly one with the
  parsed version in the body
actionlint can't parse composite actions (rhysd/actionlint#46), so their
bash `run:` blocks otherwise go unchecked — the class of bug that
produced the NUM_VCPUS typo and release-pr's unguarded `cd`s. A Python
script beside the action extracts each block via `yq -o=json`, masks
`${{ }}` expressions the way actionlint does, and batches everything
through one shellcheck invocation. Both tools are preinstalled on
GitHub-hosted runners.
The workarounds covered `actions/create-github-app-token`'s `app-id` to
`client-id` rename, and no workflow calls that action any more.
Every `uses:` now carries a full commit SHA with a version comment, which
is the form Dependabot reads to offer updates.

`taiki-e/install-action`'s per-tool tags (`@nextest`, `@cargo-udeps`,
`@cargo-llvm-cov`) move and cannot be pinned, so those call sites switch
to the action's `tool:` input against a pinnable release.
pinact fails the lint job when an action is not pinned to a full commit
SHA. `verify` additionally rejects a pinned SHA whose version comment
names a different release — the form an attacker would use to make a
malicious commit look like an innocuous tag.

The comments pinact maintains are also what Dependabot reads, so pinned
actions still receive update PRs.

`fix: "false"` implies `skip_push`, so the check runs no git commands and
needs no write permissions. Wired into lint-workflows, so callers pick it
up when they bump their pin.
Dependabot documents `directories` globbing with `*`, not `**`, and every
composite action is one level under `.github/actions/`. An unsupported
pattern would match nothing and silently stop update PRs.
@samuelburnham
samuelburnham marked this pull request as ready for review September 30, 2026 16:17
@samuelburnham
samuelburnham merged commit 4416580 into main Sep 30, 2026
5 checks passed
@samuelburnham
samuelburnham deleted the ci-hardening branch September 30, 2026 16:18
samuelburnham added a commit to argumentcomputer/multi-stark that referenced this pull request Oct 1, 2026
ci-workflows removed its ci-env composite action (argumentcomputer/ci-workflows#89),
so both jobs that checked it out failed at that step. The toolchain action's v2
defaults cover what ci-env set: incremental off, colored output, short
backtraces, and warnings denied through cargo's build.warnings instead of
RUSTFLAGS. With RUSTFLAGS no longer exported, the .cargo/config.toml codegen
flags now apply to the ubuntu-latest jobs as they already did to cuda-compile.

Pin every action to a full commit SHA with pinact, replacing the moving
`install-action@nextest` alias with the release pin plus `tool: nextest`.
Add the ci-workflows lint-workflows step (actionlint, shellcheck, pinact
verify, zizmor) to the lints job ahead of the toolchain setup. ci-workflows
publishes no tags, so .github/pinact.yaml exempts its commit-pinned actions
from version-comment verification.

Also set persist-credentials: false on every checkout, pass the cargo-deny
Rust version as a step output instead of through GITHUB_ENV, use
github.token in the Nix workflow, and give Dependabot a 3-day cooldown.
samuelburnham added a commit to argumentcomputer/multi-stark that referenced this pull request Oct 1, 2026
ci-workflows removed its ci-env composite action (argumentcomputer/ci-workflows#89),
so both jobs that checked it out failed at that step. The toolchain action's v2
defaults cover what ci-env set: incremental off, colored output, short
backtraces, and warnings denied through cargo's build.warnings instead of
RUSTFLAGS. With RUSTFLAGS no longer exported, the .cargo/config.toml codegen
flags now apply to the ubuntu-latest jobs as they already did to cuda-compile.

Pin every action to a full commit SHA with pinact, replacing the moving
`install-action@nextest` alias with the release pin plus `tool: nextest`.
Add the ci-workflows lint-workflows step (actionlint, shellcheck, pinact
verify, zizmor) to the lints job ahead of the toolchain setup. ci-workflows
publishes no tags, so .github/pinact.yaml exempts its commit-pinned actions
from version-comment verification.

Also set persist-credentials: false on every checkout, pass the cargo-deny
Rust version as a step output instead of through GITHUB_ENV, use
github.token in the Nix workflow, and raise Dependabot's cooldown from the
3-day default to the 7 days zizmor's dependabot-cooldown audit requires.
samuelburnham added a commit to argumentcomputer/multi-stark that referenced this pull request Oct 1, 2026
ci-workflows removed its ci-env composite action (argumentcomputer/ci-workflows#89),
so both jobs that checked it out failed at that step. The toolchain action's v2
defaults cover what ci-env set: incremental off, colored output, short
backtraces, and warnings denied through cargo's build.warnings instead of
RUSTFLAGS. With RUSTFLAGS no longer exported, the .cargo/config.toml codegen
flags now apply to the ubuntu-latest jobs as they already did to cuda-compile.

Pin every action to a full commit SHA with pinact, replacing the moving
`install-action@nextest` alias with the release pin plus `tool: nextest`.
Add the ci-workflows lint-workflows step (actionlint, shellcheck, pinact
verify, zizmor) to the lints job ahead of the toolchain setup. ci-workflows
publishes no tags, so .github/pinact.yaml exempts its commit-pinned actions
from version-comment verification.

Also set persist-credentials: false on every checkout, pass the cargo-deny
Rust version as a step output instead of through GITHUB_ENV, use
github.token in the Nix workflow, and raise Dependabot's cooldown from the
3-day default to the 7 days zizmor's dependabot-cooldown audit requires.
samuelburnham added a commit that referenced this pull request Oct 1, 2026
… rust-version-check

The toolchain parse piped `rustup show` through `grep rustc`, but rustup
1.28 reworked that output and no longer prints the rustc line, so the
grep found nothing and pipefail failed the step. `rustc --version` under
the `rust-toolchain.toml` override reports the pinned version directly.

Nightlies were dated by the commit date rustc and `rustup check` report,
which is the day before the nightly cut from it, so a dated pin read as
one day older than it was and the "latest" date was one day behind the
real latest. A dated pin now supplies its own date, and the latest date
comes from the nightly channel manifest, the file the fenix hash is
taken from. Only stable needs the channel installed for `rustup check`.

rust-version-check is removed: rust-version covers the same check and can
open the pull request itself. Its only remaining callers use the reusable
workflow form that #89 already removed.
samuelburnham added a commit that referenced this pull request Oct 2, 2026
)

* rust-version: update flake.nix and open the PR; fix version checks

rust-version now detects the release channel from the pinned toolchain
instead of taking it as an input, so a nightly pin can never be rewritten
to a stable release or the other way round; beta pins are rejected. With
`update-flake` it also rewrites the `sha256` of the fenix
`fromToolchainFile` call in flake.nix. fenix fetches the release's channel
manifest with `pkgs.fetchurl` at evaluation time, so the hash is that
manifest's, computed with curl and openssl rather than Nix. Flake inputs
are deliberately left alone. With `pr` it opens the pull request itself
through create-pull-request, adding only the files it edited, so callers
no longer wire that up. The PR uses GITHUB_TOKEN by default, so a
maintainer approves its CI run.

Also installs the channel before `rustup check` and anchors the channel
line it parses, so a repository that pins a version or date no longer
reads as up to date, and drops the stale cleanup step from the pre-`$/`
checkout convention.

rust-version-check compared versions as strings, which orders 1.100 before
1.99 and would have stopped reporting once the minor version reached three
digits; it now sorts them as versions and anchors the same parse.

The lint-workflows shellcheck helper also covers a repository that is
itself an action, i.e. has action.yml at its root, and skips manifests
without `runs.steps`.

The test workflow exercises rust-version on an up-to-date pin, an outdated
stable pin with a flake, an outdated nightly pin, and a beta pin.

* Update Rust flake inputs with the toolchain

* rust-version: read versions from rustc and the nightly manifest; drop rust-version-check

The toolchain parse piped `rustup show` through `grep rustc`, but rustup
1.28 reworked that output and no longer prints the rustc line, so the
grep found nothing and pipefail failed the step. `rustc --version` under
the `rust-toolchain.toml` override reports the pinned version directly.

Nightlies were dated by the commit date rustc and `rustup check` report,
which is the day before the nightly cut from it, so a dated pin read as
one day older than it was and the "latest" date was one day behind the
real latest. A dated pin now supplies its own date, and the latest date
comes from the nightly channel manifest, the file the fenix hash is
taken from. Only stable needs the channel installed for `rustup check`.

rust-version-check is removed: rust-version covers the same check and can
open the pull request itself. Its only remaining callers use the reusable
workflow form that #89 already removed.

* rust-version: open the flake.nix sed range at the call's brace

The range opened at any `fromToolchainFile` mention and closed at the next
`}`. In a flake that also calls lean4-nix's `fromToolchainFile` on a
`${system}` line, that brace closed the range before the fenix block, so
its `sha256` was never rewritten and the action failed on ix, lean-ffi and
Blake3.lean. The range now opens at `fromToolchainFile {` and closes at the
`sha256` line itself.
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