ci: workflow overhaul — path gating, benchmark signal, cargo-deny v2, naming convention - #757
Merged
Conversation
…ed Rust setup Path-gate the PR workflows so unrelated changes stop running the full Rust suite, fix the benchmark job for pull requests from forks, and adopt the Rust CI practices used in dfinity/cdk-rs. Required-check gating --------------------- `rust` is the only required status check on master, which rules out a workflow-level `paths:` filter: a workflow that never starts produces no check run, and branch protection then waits forever on a check that will never arrive. rust.yml therefore always starts, and a `changes` job decides whether the work runs. Gated jobs are skipped, which GitHub reports as passing for required checks. If detection itself fails the gate falls open and the jobs run, so a broken filter cannot silently green-light the check. tools.yml and bench.yml are not required, so they use plain workflow-level `paths:` filters. license.yml: renamed the job from `license-check:required` to `license-check`. The `:required` suffix is this repo's marker for checks in branch protection, and this one is no longer among them. Benchmark on forks ------------------ Every fork pull request failed at the "Post comment" step with "Resource not accessible by integration" (most recently #755). Fork pull requests get a read-only GITHUB_TOKEN no matter what `permissions:` declares, so the comment can never be posted from the same job. Split the comment into bench-comment.yml, triggered on `workflow_run`, which runs in the base repository context with a writable token. It downloads a text artifact and posts it, and never executes pull request code. The PR number travels in the artifact because `github.event.workflow_run.pull_requests` is empty for fork PRs, and is validated as digits-only before use. bench.yml is now `permissions: contents: read` and path-filtered, so it no longer runs — or comments — on documentation-only pull requests. Shared Rust setup ----------------- Adopted from dfinity/cdk-rs for every job that runs Rust: remove the runner image's pre-installed stable toolchain (its version varies between image releases and makes the rust-cache hash non-deterministic), then use actions-rust-lang/setup-rust-toolchain, which installs the toolchain and handles caching internally. Dropped the hand-rolled `actions/cache` steps. Also: cargo-fuzz now installs as a prebuilt binary via taiki-e/install-action instead of compiling from source; canbench is cached on its own version rather than the Cargo.lock hash; runners are pinned to ubuntu-24.04; concurrency groups added to rust.yml and tools.yml; and the actions flagged by the Node 20 deprecation warning were bumped. All actions remain pinned to full commit SHAs. Not touched: didc-release.yml is generated by cargo-dist (regenerate with `dist generate`), and coq.yml is already path-gated and runs no Rust. Verified with actionlint: no findings outside the generated didc-release.yml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comments now carry only what someone editing the workflow needs; the rationale lives in the PR description. rust/bench was outside every lint gate: it is its own workspace, so the root `cargo fmt --all` and `cargo clippy` never reached bench.rs and nns.rs, and its manifest had no `[lints]` table. Added fmt and clippy steps to bench.yml, mirrored the root clippy policy into the bench workspace, and fixed the two `manual_repeat_n` errors this surfaced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cargo-fuzz derives its default `--target` from its own build-time triple. taiki-e/install-action fetches the upstream prebuilt binary, and the only Linux asset published is x86_64-unknown-linux-musl, so the build was sent to a musl target with no std installed and where sanitizers cannot link against a static libc. The previous `cargo install cargo-fuzz` compiled a gnu binary locally, which is why this did not surface until the switch to the prebuilt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the PR comment with a job summary and adds a 10% regression threshold, deleting bench-comment.yml and everything that supported it. The comment was unconditional, so on most pull requests it was noise — nothing failed on a regression, so the table was output nobody had a reason to read. Inverting that: the job summary always carries the numbers, and the job fails only when an instruction count regresses past the threshold, which is when someone has a reason to open it. This also removes the fork problem rather than routing around it. A job summary needs no token, so it behaves identically for fork pull requests. Gone with it: the workflow_run companion holding a writable token, the artifact handoff, the cross-run download, and the untrusted PR-number scrub. Only instruction counts are gated. They are deterministic under canbench, so a move past the threshold is a real change rather than noise. Memory is still reported but not gated: heap_increase is page-granular, so a one-page move on a small footprint is a large percentage meaning nothing. diff.py takes the threshold as an optional third argument and exits 1 when breached. The workflow captures that, writes the summary, and only then fails, so the numbers are readable on a failing run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pinned action was cargo-deny-action v1.6.3, and deny.toml still used v1 syntax. `default`, `unlicensed` and the explicit GPL `deny` list were all removed in cargo-deny 0.16, so the config was a hard validation error on anything current — bumping the action alone would have broken it. Migrated to the shape icp-cli uses: action v2.0.15, `rm rust-toolchain.toml` so cargo-deny does not install a toolchain and the wasm32 target just to read the dependency graph, and `unused-allowed-license = "allow"`. The GPL deny list is dropped because anything absent from `allow` is now denied. The allow list itself is unchanged, and `bans ok, licenses ok, sources ok` both before and after, so this is a config migration with no policy change. Two things are deliberately not copied from icp-cli: * Their broader allow list (BlueOak, CDLA, ISC, MPL-2.0, Unicode-3.0, OpenSSL) covers their dependency graph, not ours. Keeping candid's list minimal is what makes it evident that the migration found nothing new. * Their `[sources] allow-git` exists because they depend on agent-rs. candid has no git dependency and should not gain one — a git revision is mutable and never reaches crates.io, so it cannot ship in a published release. Since cargo-deny only *warns* about unknown git and registry sources by default, `check sources` here could never fail; both are now set to deny. Verified by temporarily pointing logos at a git tag, which produces `error[source-not-allowed]` and `sources FAILED`. Also rewrote the header comment. It linked to two archived dfinity-lab repos and justified allowing MPL-2.0, which is not and should not be on candid's list — it described icp-cli's policy, not this one. Replaced with the actual rule: candid is a library, so allow only licenses that place no obligation on downstream users beyond attribution. The workflow now runs only when the dependency graph or the policy changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Half the workflow files could not be guessed from their `name:` and vice versa: bench.yml was "Rust Bench", license.yml "License Check", candid-ui.yml "Release candid_ui", publish.yml "Publish crates to crates.io". Casing varied three ways, job ids mixed camelCase with kebab, and coq.yml's job was called `build`, which says nothing in a list of a dozen checks. The rule is now one line: the filename is the identity and `name:` is its Title Case rendering, as `<subject>[-<qualifier>].yml`. Subject first, including for release and publish, so related workflows group together and so the one file we cannot edit sets the pattern instead of being an exception to it. Written down in .github/workflows/README.md. bench.yml -> rust-bench.yml Rust Bench license.yml -> dependencies.yml Dependencies candid-ui.yml -> candid-ui-release.yml Candid UI Release publish.yml -> crates-publish.yml Crates Publish license.yml becomes dependencies.yml because it does not only check licenses -- it runs bans, licenses and sources -- and because that is where the missing advisories check would later belong. Job names now come from the id unless the id is insufficient, since GitHub falls back to the id and a `name:` repeating it is duplicated state. Dropped from rust, fuzzing, cargo-deny, publish and candid-ui; kept on `detect changes`. coq.yml's `build` is renamed `coq`, and bench.yml's camelCase `runBenchMark` is renamed `bench`. The `:required` suffix convention is dropped rather than corrected. GitHub already labels required checks in the merge box, so the suffix duplicated settings held elsewhere, and it had already drifted: it sat on license-check, which is not required, while rust, which is, never had it. No branch protection change is needed. rust.yml keeps both its filename and its `name:`, so the required `rust` context and the README badge (workflows/Rust/badge.svg) are untouched, and every renamed workflow is non-required. Also documents what testing dist established: didc-release.yml's filename comes from `tag-namespace` and cannot be set independently of the release tag prefix, and its `name: Release` is not configurable at all. Verified that `dist generate` still reproduces the committed file byte for byte. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`rust` was the job that built and tested; it is now an aggregate that reports on the jobs above it, and the work moved to a `build` job. Same shape as `test:required` in icp-cli's test.yml. The reason to prefer it here is that the required check stops depending on how GitHub scores a skipped job. Gating `rust` by a job rather than a workflow-level `paths:` filter meant that on an unrelated pull request the required check reported *skipped*, and merging relied on that counting as success. The aggregate runs under a bare `if: always()`, so it executes and exits 0 even when everything it watches was skipped. Nothing to assume. icp-cli's aggregate is additionally gated on its own paths filter, so it skips too and still leans on that behaviour; ours deliberately does not. It also means future work jobs gate merging by being added to `needs:`, with no branch protection change — the configured context stays `rust` either way, so no settings change is needed for this commit. `skipped` counts as a pass, since it means the paths filter ruled the job out. The aggregate does not gate on `changes`: the work jobs already fall open when detection fails, so the work ran regardless, and a red `detect changes` is the signal by itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It runs cargo build, cargo test three ways, cargo fmt, cargo clippy and cargo doc. `build` described the first step and nothing else — the same problem as coq.yml's `build`, which this branch renamed two commits ago for the same reason. Kept as one job rather than split into build/test/fmt/clippy/doc the way cdk-rs and icp-cli do. rust-cache keys per job, so splitting would compile the workspace several times over for a job that currently takes about a minute end to end. Worth revisiting if it grows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lwshang
marked this pull request as ready for review
July 28, 2026 01:15
|
✅ No security or compliance issues detected. Reviewed everything up to 4248ef2. Security Overview
Detected Code Changes
|
raymondk
approved these changes
Jul 28, 2026
lwshang
added a commit
that referenced
this pull request
Jul 28, 2026
Picks up the CI workflow overhaul (#757). candid-ui.yml was renamed to candid-ui-release.yml there; the reference in REWRITE.md is fixed in the next commit.
lwshang
added a commit
that referenced
this pull request
Jul 28, 2026
candid-ui.yml became candid-ui-release.yml under the naming convention adopted in #757. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
General cleanup of
.github/workflows, adopting the Rust CI practices from icp-cli and cdk-rs.On a documentation-only PR,
rust,fuzzing,tools,benchanddependenciesnow all stop running, instead of all five running and one of them posting a table.Changes
cargo-deny: upgrade to v2 and make thesourcescheck enforceable.rustcheck an aggregate job.actions-rust-lang/setup-rust-toolchainfor install and caching. Removes the hand-rolledactions/cachesteps.rust/bench, which no lint gate reached.ubuntu-24.04, add concurrency groups, installcargo-fuzzprebuilt rather than from source, cachecanbenchon its own version, bump the actions flagged by the Node 20 deprecation warning.Behavior changes worth knowing
rust.ymlis gated per job, not bypaths:. A required check whose workflow never starts is never created, and branch protection then waits on it forever. Achangesjob decides instead. Non-required workflows use plainpaths:filters.The required
rustcheck is now an aggregate. The build and test work moved to acheckjob;rustonlyneeds: [check, fuzzing]and fails if either did. It runs under a bareif: always(), so it reports even when everything it watches was skipped — merging no longer depends on how GitHub scores a skipped job. Future work jobs gate by joining itsneeds:, with no branch protection change.Benchmarks post to the job summary instead of commenting. The comment was unconditional while nothing ever failed on a regression, so it was output nobody had reason to read — and it broke on every fork PR (fix(candid): newtype elements and binary format in the primitive-vec fast path + release candid 0.10.34 #755 failed four times), since forks get a read-only token whatever
permissions:says. Step summaries need no token, so forks behave identically.A >10% instruction-count regression now fails the bench job. Green means no regression, red means open the summary. Only instruction counts are gated: they are deterministic under canbench, whereas
heap_increaseis page-granular and would flake on small footprints.cargo-denymoved to v2.deny.tomluseddefault,unlicensedanddeny, all removed in cargo-deny 0.16, so bumping the action alone would have broken the config. Allow-list unchanged, no new findings.An unexpected dependency source now fails. cargo-deny only warns about unknown git and registry sources by default, so
check sourcescould never fail here. Both are nowdeny: a git revision is mutable and never reaches crates.io, so it cannot ship in a published release.Four workflows renamed, following
<subject>[-<qualifier>].ymlwithname:as its Title Case rendering.license.ymlbecomesdependencies.ymlbecause it runs bans, licenses and sources — and because that is where an advisories check would later belong.bench.ymlrust-bench.ymllicense.ymldependencies.ymlcandid-ui.ymlcandid-ui-release.ymlpublish.ymlcrates-publish.ymlNo branch protection change is needed. The required context is still
rust, and every renamed workflow is non-required. The:requiredsuffix convention is dropped: GitHub already labels required checks, and the marker had drifted ontolicense-check, which is not required, whilerust, which is, never carried it.Notes for review
rust.ymlproduces four checks —detect changes,check,fuzzing, and the requiredrustaggregate. Only the last gates merging; the rest are informational.rust.ymlin the diff. The aggregate makes that moot for merging, but the filter's behaviour on an unrelated PR is unverified.rust/benchsurfaced two real clippy errors, fixed here.actionlintis clean on every workflow. Its only output is pre-existing shellcheck style noise in the generateddidc-release.yml, which is written bydistand must not be hand-edited.advisoriescheck, nodependabot.yml. Out of scope here, but worth a follow-up.