Skip to content

Add reusable Rust release workflows - #60

Draft
HDauven wants to merge 4 commits into
mainfrom
harden-rust-release-workflows
Draft

Add reusable Rust release workflows#60
HDauven wants to merge 4 commits into
mainfrom
harden-rust-release-workflows

Conversation

@HDauven

@HDauven HDauven commented May 10, 2026

Copy link
Copy Markdown
Member

Summary

This PR adds reusable Rust release automation workflows for Dusk crates:

  • rust-release-plan.yml creates or updates release-plz release PRs
  • rust-release-check.yml validates release PRs and runs release/publish dry-runs
  • rust-publish-crates.yml publishes through release-plz using crates.io trusted publishing

The publish flow uses OIDC trusted publishing directly through release-plz. It does not use CARGO_REGISTRY_TOKEN or rust-lang/crates-io-auth-action.

Release impact

This moves our Rust crate releases toward a release-plz owned flow:

  • release changes are prepared through explicit release PRs
  • version bumps, changelog entries, tags, GitHub releases and crates.io publishing are handled by release-plz
  • caller repos declare the exact crates that are allowed to publish via publish-packages
  • publish jobs validate the crate publish surface again immediately before publishing
  • changelogs should no longer be edited manually for release entries

Caller repos should use a local .github/workflows/release-plz.yml workflow and configure crates.io trusted publishing with workflow filename release-plz.yml and environment crates-io.

Caller flow

A caller repo has one local release workflow entry point, usually .github/workflows/release-plz.yml.

That workflow exposes two manual commands:

  • release-pr: creates or updates the release PR
  • publish: publishes after the release PR has been reviewed and merged

The intended sequence is:

  1. run release-pr
  2. review the generated release PR
  3. merge the release PR after checks pass
  4. run publish

Publishing is intentionally manual after merge. The publish job uses the crates-io GitHub Environment, so repository or organization rules can require approval before crates.io receives the OIDC-backed publish.

Versioning

Version bumps are owned by release-plz.

When release-pr runs, release-plz compares the repo with the latest published crate version and uses merged commit history plus release-plz.toml to prepare the release PR. That PR contains the version bump and changelog updates.

For normal releases:

  • do not manually bump crate versions
  • do not manually edit release changelog entries
  • use release-note-ready PR titles and descriptions
  • prefer conventional titles such as feat(crate): ..., fix(crate): ..., docs(crate): ..., chore(crate): ...
  • use ! or a BREAKING CHANGE: footer for breaking changes

After the release PR is merged, publish publishes the version already present on the default branch.'

For more details see release-plz

Registry setup required

Before using publish, configure crates.io trusted publishing for the particular crate:

  • owner: dusk-network
  • repository: REPO-NAME
  • workflow filename: release-plz.yml
  • environment: crates-io

Comment thread .github/CONTRIBUTING.md
- Any pull request that is not passing our CI tests & builds will not be merged. This implies incorrect formatting errors, compilation errors, clippy lints, or any other kind of inconsistency with your code.
- Do not open PRs that are not linked or related to a previously opened issue.
- Update the `Unreleased` section of the `CHANGELOG` if your PR includes anything that's worth mentioning in there. Avoid adding things like doc-nitpicks and similar changes which do not affect directly any added, fixed, removed or changed feature.
- Do not edit `CHANGELOG.md` files manually. Release changelogs are generated by the Rust release automation from merged PR metadata.

@herr-seppia herr-seppia May 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got the rationale behind this, but I strongly believe that having the unreleased section compiled by contributors will help to understand what's in there, and help us in a lot of different situations (keep track of what's unreleased, know possible breaking changes that needs major bump, etc etc)

Instead of letting the files to be automatically generated, it would be useful (imho) to have CI to check what devs put in the CHANGELOG against what's supposed to be there (according to the CI), maybe adding some RFC to the PR

This will help in 2 ways:

  • let the control of the CHANGELOG to developers
  • help the developers in case of something missing or not coherent

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be clear, I'm not against having it automatically generated. I'm against having it generated only when releasing it.

If it would be possible to autogenerate it while merging every single PR that would be awesome.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would need to look into that more. This release-plz flow is a fairly big change from how we do releases.

If we use their commit-based approach, we should not do tiny commits anymore because it would bloat generated changelogs.

If we want to do it based on PR titles/description, squash-merge would be the cleanest option.

We can also keep the old manual changelog approach though. I mostly care about the crate release/publishing side of release-plz. But let's discuss during the standup in the morning.

@xevisalle xevisalle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I don't have a strong preference, but it looks good to me the current approach, it allows not having to update the same changelog twice if for instance for any reason we revert changes before release. I'm OK though if we end up following @herr-seppia 's suggestion.

release_check:
name: Validate release PR
runs-on: ${{ inputs.runner }}
if: ${{ github.repository_owner == 'dusk-network' && (startsWith(github.head_ref, 'release-plz-') || startsWith(github.head_ref, 'release/')) && contains(github.event.pull_request.labels.*.name, 'release') }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKER: Inner gate requires a release PR label that release-plz does not add by default, so the dry-run job silently skips on every release PR for a freshly-adopted caller and reports green.

Context: README example caller (.github/workflows/README.md:240) gates only on startsWith(github.head_ref, 'release-plz-') and never tells adopters to set pr_labels = ["release"] in release-plz.toml. With no label set, the reusable workflow's only job evaluates the if: to false and is marked Skipped, which propagates to the caller job as Skipped. Skipped jobs satisfy required-status-check gates in branch protection.

Impact: Commit 072d477 ("Limit release checks to labeled release PRs") makes the check workflow non-functional by default for new adopters following the README — no cargo publish --dry-run, no surface validation, no release-plz dry-run runs. The regression is in the exact safety mechanism this PR is meant to deliver.

Fix: Either drop the label requirement and rely on the branch-prefix gate already enforced at both caller and reusable level, or keep the label gate and (a) document the required release-plz.toml setup (pr_labels = ["release"]) and (b) extend the README caller example with the same label predicate so callers don't ship a skipped-but-required check.

- name: Create or update release PR
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKER: release-plz release-pr runs with the default GITHUB_TOKEN, so the release PR it creates does not trigger any downstream pull_request workflow on the same repo — rust-release-check.yml never fires on a release-plz-generated PR, independent of the label-gate BLOCKER above.

Context: GitHub Actions loop-prevention rule (Actions docs, "Triggering a workflow from a workflow"): events triggered by GITHUB_TOKEN, except workflow_dispatch and repository_dispatch, do not create a new workflow run. release-plz's own docs (release-plz.dev/docs/github/trigger) flag this and list two workarounds — a Personal Access Token, or a GitHub App installation token. The reusable's workflow_call.inputs declares no PAT / App-token secret, and the README caller example (.github/workflows/README.md:230-260) passes no secrets: block. curves#17 — the only documented adoption in the org — inherits the gap.

Impact: The check workflow physically cannot fire on the PRs it is meant to guard. Combined with the label gate, the validation path has two independent dead-on-arrival failure modes; an adopter following the README sees a green release PR that ran neither cargo publish --dry-run nor the publish-surface validator.

Fix: Accept a token via secret on workflow_call (e.g. release-plz-token, or secrets: inherit) and pass it through to release-plz release-pr --git-token. Document the GitHub App / PAT setup in the README alongside the crates.io trusted-publisher setup, and update the caller example to pass the secret.

Comment thread .github/CONTRIBUTING.md
Comment on lines +138 to +139
- Do not edit `CHANGELOG.md` files manually. Release changelogs are generated by the Rust release automation from merged PR metadata.
- Use release-note-ready PR titles and descriptions. Prefer conventional titles such as `feat(crate): ...`, `fix(crate): ...`, `docs(crate): ...`, `chore(crate): ...`, or `refactor(crate): ...`. Use `!` or a `BREAKING CHANGE:` footer when the change is breaking.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Rule is phrased as an org-wide ban on manual CHANGELOG.md edits, but only the Rust release-plz flow generates them — applies as-is to JS/TS repos, Solidity repos, contract repos, and Rust repos not yet opted into release-plz, where no automation owns the file.

Context: This CONTRIBUTING.md lives in the org template and surfaces as the default contributing guide across the org. Many Dusk repos still use a hand-maintained [Unreleased] flow; flipping the guidance org-wide will leave those repos with no changelog updates at all. herr-seppia's open questions on per-PR generation and a developer-curated Unreleased section are also unresolved.

Fix: Scope the rule, e.g.: "In Rust repos that use the reusable release-plz workflows, do not edit CHANGELOG.md manually — release-plz owns it. In all other repos, continue to update the Unreleased section as described above." Resolve herr-seppia's open questions before broadcasting org-wide.

Comment on lines +28 to +32
workflow-scripts-ref:
description: "Ref of dusk-network/.github to checkout for helper scripts"
required: false
type: string
default: main

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: workflow-scripts-ref defaults to mutable main while the rest of the PR (README, third-party actions) insists on immutable SHA pinning. A caller that omits this input gets the workflow pinned to a SHA but fetches the helper script from a moving branch, defeating the pin.

Context: Same mutable default appears in rust-publish-crates.yml:33-37. The README example caller already passes <full_commit_sha> for this input.

Fix: Drop the default and make workflow-scripts-ref required in both rust-release-check.yml and rust-publish-crates.yml, so callers cannot accidentally combine a pinned workflow with a main-tracked script.

lines = cargo_toml.read_text(errors="replace").splitlines()

for line_number, line in enumerate(lines, 1):
if line.strip() == "[patch.crates-io]":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: Validator only matches [patch.crates-io] and misses [patch."https://github.com/..."] git-source overrides plus other [patch.<id>] tables, which can also redirect dependency resolution in non-obvious ways.

Fix: Match any [patch."..."] / [patch.<id>] header (regex like ^\s*\[patch\.[^\]]+\]\s*$) and report all, or document that the validator only covers the crates.io patch table and that other patch tables must be vetted manually.

Comment on lines +147 to +149
for path, line_number in find_crates_io_patches(Path(".")):
print(f"{path}:{line_number}: [patch.crates-io]")
errors += 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: Per-match output is plain stdout, not a GitHub annotation, so the PR check summary points only at the aggregated "Private patch not publishable" error at line 152 without per-file/line navigation. The error() helper already supports file= annotations.

Fix: Emit one ::error file=...,line=...:: per match using the existing helper inside the loop, and drop the aggregated final error.

release_check:
name: Validate release PR
runs-on: ${{ inputs.runner }}
if: ${{ github.repository_owner == 'dusk-network' && (startsWith(github.head_ref, 'release-plz-') || startsWith(github.head_ref, 'release/')) && contains(github.event.pull_request.labels.*.name, 'release') }}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: The release/ branch prefix is supported by this gate but never used by the documented caller (.github/workflows/README.md:240 only filters release-plz-). Speculative branch shapes that aren't reachable from the documented setup add cognitive load when debugging a skipped job.

Fix: Drop release/ from the branch predicate (release-plz only creates release-plz-* branches), or extend the README caller example to also invoke on release/* branches.

Comment on lines +39 to +62
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false

- name: Install Rust toolchain
uses: dsherret/rust-toolchain-file@3551321aa44dd44a0393eb3b6bdfbc5d25ecf621 # v1

- name: Create or update release PR
shell: bash
env:
GITHUB_TOKEN: ${{ github.token }}
MANIFEST_PATH: ${{ inputs.manifest-path }}
CONFIG: ${{ inputs.config }}
RELEASE_PLZ_VERSION: ${{ inputs.release-plz-version }}
run: |
set -euo pipefail
cargo install --locked release-plz --version "$RELEASE_PLZ_VERSION"
release-plz release-pr \
--git-token "$GITHUB_TOKEN" \
--manifest-path "$MANIFEST_PATH" \
--config "$CONFIG"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: Plan workflow does not run validate-rust-publish-surface.py before opening the release PR. A misconfigured publish-packages list, stray [patch.crates-io], or missing description/repository/license metadata is caught only at publish time (and at check time, if check ever fires — see BLOCKER on rust-release-plan.yml:52). A release PR can sit "looking ready" and fail mid-publish.

Context: rust-release-check.yml and rust-publish-crates.yml both invoke the validator after checking out the workflow scripts. The plan workflow has neither the scripts-checkout step nor the validator step, so adding the gate requires both.

Fix: Mirror the check/publish workflows: add a Checkout Dusk reusable workflow scripts step followed by a Validate release metadata and publish surface step running validate-rust-publish-surface.py, before the release-plz release-pr step. Fails the plan job (no PR created) when the workspace surface is broken — catches the same bug class earlier.

@moCello moCello left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST CHANGES — two independent BLOCKERs leave the validation path inert on a freshly-adopted caller: (1) rust-release-check.yml requires a release PR label that release-plz does not add by default, so the check is silently Skipped on every release PR following the README; (2) rust-release-plan.yml creates the PR with the default GITHUB_TOKEN, which by GitHub's loop-prevention rule cannot trigger downstream pull_request workflows at all — so even fixing the label gate does not make the check fire. Also: CONTRIBUTING.md changelog rule needs scoping to Rust release-plz repos only, and the plan workflow should validate the publish surface before opening the PR (currently only check + publish do).

@HDauven
HDauven marked this pull request as draft June 1, 2026 16:27
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.

4 participants