Skip to content

ci: align release-plz with the standard fg-labs publish workflow#35

Closed
nh13 wants to merge 1 commit into
mainfrom
nh_release-plz-publish-workflow
Closed

ci: align release-plz with the standard fg-labs publish workflow#35
nh13 wants to merge 1 commit into
mainfrom
nh_release-plz-publish-workflow

Conversation

@nh13

@nh13 nh13 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings redskull's release automation in line with the canonical setup used across the other fg-labs / fulcrumgenomics Rust crates (tricord, mako, primer3-rs, refget-rs, bwa-mem3-rs, fg-sra, …).

Previously redskull used the upstream-style release-plz.yml, which ran release-plz's release command directly with the default GITHUB_TOKEN. This PR replaces it with the standard publish.yml workflow and a matching release-plz.toml.

What changed

  • .github/workflows/release-plz.yml.github/workflows/publish.yml ("Manage Release PRs and Publish Crates"), split into two jobs:
    • release-pr — release-plz opens/updates the version-bump + changelog PR.
    • publish — on merge (when Cargo.toml's version no longer matches crates.io), publishes via crates.io Trusted Publishing (OIDC) and idempotently tags + creates the GitHub release.
    • Both jobs authenticate with the FG_LABS_BOT GitHub App token instead of the default GITHUB_TOKEN, so the release PR and tags it creates can trigger other workflows.
  • release-plz.toml — delegates publishing/tagging/GitHub releases to the publish job (publish, git_tag_enable, git_release_enable all disabled), enables dependencies_update (cargo update) and the release PR label, and keeps semver_check = true (redskull is already past v0.1.0).
  • README.md — "Releasing a New Version" section updated to describe the two-job publish.yml, the app-token auth, and the new Trusted Publishing workflow filename.

Notes

  • redskull is a single crate, so this mirrors the single-crate variant of the pattern (tricord) — no lockstep workspace version checks or per-crate publish loop.
  • Intentionally did not adopt cliff.toml: the existing CHANGELOG.md is in release-plz's default "Keep a Changelog" format (with PR links + compare URLs), which is richer than the cliff.toml output other repos use, so keeping the default avoids a mid-file format break.
  • Action required before this can publish: a Trusted Publisher must be configured on crates.io for owner fg-labs, repo redskull, workflow publish.yml (the README documents this), and the repo needs the FG_LABS_BOT_APP_ID / FG_LABS_BOT_PRIVATE_KEY secrets.

Summary by CodeRabbit

  • Chores
    • Added a new GitHub Actions workflow to manage release PRs and publish crates on main, with concurrency controls and idempotent tagging/release creation.
    • Removed the prior release-plz workflow to stop the old release automation behavior.
    • Updated release-plz configuration to disable git tagging, GitHub releases, and crates.io publishing (publishing now handled by the new workflow).
  • Documentation
    • Updated “Releasing a New Version” instructions to reflect the new publish.yml-based process and Trusted Publishing details.

@nh13

nh13 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Replaces .github/workflows/release-plz.yml with a new publish.yml workflow containing two jobs: release-pr (release-plz PR creation via GitHub App token) and publish (version-gated crates.io Trusted Publishing plus idempotent Git tag and GitHub Release creation). release-plz.toml is narrowed to changelog and semver checks only. README docs are updated to match.

Changes

Release Automation Overhaul

Layer / File(s) Summary
Workflow trigger, permissions, concurrency, and release-plz.toml narrowing
.github/workflows/publish.yml, release-plz.toml
publish.yml establishes the main-push trigger (owner-gated), pull-requests/contents permissions, and per-ref concurrency. release-plz.toml disables git_tag_enable, git_release_enable, and publish, keeping only changelog and semver checks, and adds pr_labels = ["release"].
release-pr job
.github/workflows/publish.yml
Authenticates via a short-lived GitHub App token (FG_LABS_BOT_APP_ID/FG_LABS_BOT_PRIVATE_KEY), checks out the repo with that token, installs stable Rust, and invokes release-plz/action with command: release-pr.
publish job: version-gating and crates.io Trusted Publishing
.github/workflows/publish.yml
GitHub App token auth, stable Rust install, crates.io OIDC Trusted Publishing auth, then a shell script compares the local version from cargo metadata against the crates.io API max version for redskull and runs cargo publish -p redskull only when they differ, emitting published=true/false.
Idempotent Git tag and GitHub Release creation + documentation updates
.github/workflows/publish.yml, README.md
When the version is non-empty, creates and pushes a Git tag if absent, then uses gh release create to idempotently create a GitHub Release with generated notes and --latest. README is updated to name publish.yml, document both jobs and GitHub App token usage, explain the version-gating and Trusted Publishing flow, and update the Trusted Publisher config bullet. Old release-plz.yml is removed.

Sequence Diagram(s)

sequenceDiagram
  participant Event as main push event
  participant ReleasePR as release-pr job
  participant Publish as publish job
  participant ReleaseAction as release-plz/action
  participant CargoMeta as cargo metadata
  participant CratesIO as crates.io API
  participant CargoPublish as cargo publish
  participant GitOps as git & gh release
  
  Event->>ReleasePR: trigger on main push
  ReleasePR->>ReleaseAction: GitHub App token + release-pr command
  ReleaseAction-->>Event: PR with version/changelog
  Event->>Publish: trigger on main push
  Publish->>CargoMeta: extract local version
  CargoMeta-->>Publish: local version
  Publish->>CratesIO: fetch current max version
  CratesIO-->>Publish: published version
  Publish->>CargoPublish: publish if versions differ
  CargoPublish-->>Publish: published flag
  Publish->>GitOps: create tag and GitHub Release
  GitOps-->>Event: idempotent tag/release
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hippity-hop, the old workflow hops out,
A new publish.yml skips and bounds about!
Two jobs in tandem — PR then release,
Version-gated magic brings the team peace.
With OIDC tokens and a trusted key,
The rabbit publishes crates with glee! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: aligning release automation with fg-labs standard workflow by replacing release-plz.yml with publish.yml and updating configuration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (4)
.github/workflows/publish.yml (4)

92-93: 💤 Low value

Gate release creation on published == 'true' instead of version != ''.

The current condition runs the release-creation step on every push, even when no publish occurred. While the idempotent checks skip duplicate work, conditioning on published == 'true' makes the intent clearer and avoids unnecessary API calls.

Suggested change
       - name: Create GitHub release
-        if: steps.publish.outputs.version != ''
+        if: steps.publish.outputs.published == 'true'
         env:
           GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 92 - 93, The "Create GitHub
release" step currently gates its execution on steps.publish.outputs.version not
being empty, which causes the step to run even when no actual publish occurred.
Replace the if condition from steps.publish.outputs.version != '' to
steps.publish.outputs.published == 'true' to make the intent explicit and
prevent unnecessary API calls when no publish has taken place.

96-99: 💤 Low value

Use environment variable indirection for version to avoid template expansion in shell.

While Cargo enforces semver format (limiting injection risk), passing workflow outputs through env: rather than direct template expansion in the script is cleaner and follows defense-in-depth.

Suggested change
       - name: Create GitHub release
         if: steps.publish.outputs.version != ''
         env:
           GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+          VERSION: ${{ steps.publish.outputs.version }}
         run: |
           set -euo pipefail
-          VERSION="${{ steps.publish.outputs.version }}"
           TAG="v${VERSION}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 96 - 99, Move the VERSION
variable assignment from the inline shell script to the workflow's env section
to avoid direct template expansion in the shell. Define VERSION as an
environment variable in the `env:` section using the workflow output from
steps.publish.outputs.version, then reference it directly as an environment
variable in the shell script instead of using template expansion syntax. This
provides better separation of concerns and follows defense-in-depth practices.

Source: Linters/SAST tools


3-5: ⚡ Quick win

Move permissions to job level for least-privilege.

Workflow-level pull-requests: write and contents: write grant these permissions to all jobs. Since the publish job already declares its own permissions block (lines 44-46), consider moving pull-requests: write down to the release-pr job only. This limits each job's token to only the scopes it actually needs.

Suggested change
 name: Manage Release PRs and Publish Crates

-permissions:
-  pull-requests: write
-  contents: write
-
 on:
   push:
     branches:
@@ -17,6 +14,9 @@ concurrency:
 jobs:
   release-pr:
     runs-on: ubuntu-latest
+    permissions:
+      pull-requests: write
+      contents: read
     if: ${{ github.repository_owner == 'fg-labs' }}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 3 - 5, The workflow-level
permissions at the top of the file grant pull-requests and contents write access
to all jobs, violating the principle of least privilege. Since the publish job
already declares its own permissions block at lines 44-46, move the
pull-requests write permission from the workflow-level permissions block (lines
3-5) down to the release-pr job level only. Ensure each job declares exactly the
permissions it needs by reviewing what tokens each job actually uses and
adjusting the permission blocks accordingly.

Source: Linters/SAST tools


27-31: ⚡ Quick win

Add persist-credentials: false to checkout steps.

When passing a custom token to checkout, the credential is stored in git config and remains available to later steps or artifact uploads. Setting persist-credentials: false removes the credential after checkout, limiting exposure.

Suggested change
       - name: Checkout code
         uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
           fetch-depth: 0
           token: ${{ steps.app-token.outputs.token }}
+          persist-credentials: false

Apply the same change to the checkout step in the publish job (lines 54-58).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 27 - 31, The checkout step in the
workflow uses a custom token from the app-token output but does not set
persist-credentials to false, which leaves the credential stored in git config
and available to later steps. Add persist-credentials: false to the with section
of the checkout step (which uses
actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd) to ensure the
credential is removed after checkout, limiting exposure to subsequent steps or
artifact uploads.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In @.github/workflows/publish.yml:
- Around line 92-93: The "Create GitHub release" step currently gates its
execution on steps.publish.outputs.version not being empty, which causes the
step to run even when no actual publish occurred. Replace the if condition from
steps.publish.outputs.version != '' to steps.publish.outputs.published == 'true'
to make the intent explicit and prevent unnecessary API calls when no publish
has taken place.
- Around line 96-99: Move the VERSION variable assignment from the inline shell
script to the workflow's env section to avoid direct template expansion in the
shell. Define VERSION as an environment variable in the `env:` section using the
workflow output from steps.publish.outputs.version, then reference it directly
as an environment variable in the shell script instead of using template
expansion syntax. This provides better separation of concerns and follows
defense-in-depth practices.
- Around line 3-5: The workflow-level permissions at the top of the file grant
pull-requests and contents write access to all jobs, violating the principle of
least privilege. Since the publish job already declares its own permissions
block at lines 44-46, move the pull-requests write permission from the
workflow-level permissions block (lines 3-5) down to the release-pr job level
only. Ensure each job declares exactly the permissions it needs by reviewing
what tokens each job actually uses and adjusting the permission blocks
accordingly.
- Around line 27-31: The checkout step in the workflow uses a custom token from
the app-token output but does not set persist-credentials to false, which leaves
the credential stored in git config and available to later steps. Add
persist-credentials: false to the with section of the checkout step (which uses
actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd) to ensure the
credential is removed after checkout, limiting exposure to subsequent steps or
artifact uploads.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c1f4b9b1-5104-4a1d-821b-43d400f4eed7

📥 Commits

Reviewing files that changed from the base of the PR and between c48ebcf and d27dbaf.

📒 Files selected for processing (4)
  • .github/workflows/publish.yml
  • .github/workflows/release-plz.yml
  • README.md
  • release-plz.toml
💤 Files with no reviewable changes (1)
  • .github/workflows/release-plz.yml

Replace the upstream-style `release-plz.yml` (which ran release-plz's
`release` command with the default `GITHUB_TOKEN`) with the canonical
`publish.yml` used across the other fg-labs/fulcrumgenomics Rust crates.

The new workflow splits work into two jobs:

- `release-pr`: release-plz opens/updates the version-bump + changelog PR.
- `publish`: on merge, publishes to crates.io via Trusted Publishing
  (OIDC) and tags + creates the GitHub release idempotently.

Both jobs authenticate with the `FG_LABS_BOT` GitHub App token instead of
the default `GITHUB_TOKEN`, so the release PR and tags can trigger other
workflows.

`release-plz.toml` now delegates publishing, tagging, and GitHub releases
to the `publish` job (`publish`/`git_tag_enable`/`git_release_enable` are
disabled), enables `cargo update` and a `release` PR label, and keeps
`semver_check` on (redskull is already past v0.1.0). The existing
"Keep a Changelog" format is retained.
@nh13 nh13 force-pushed the nh_release-plz-publish-workflow branch from d27dbaf to 622abcb Compare June 16, 2026 16:16
@nh13

nh13 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit nitpicks on publish.yml:

  • Moved permissions to job level — dropped the workflow-level block; release-pr now declares contents: read + pull-requests: write (release-plz writes via the app token, not the default GITHUB_TOKEN).
  • persist-credentials: false on the release-pr checkout — release-plz uses the app token via the GITHUB_TOKEN env, so the checkout credential need not persist. Matches upstream release-plz's own workflow.
  • VERSION moved into the env: block of the "Create GitHub release" step instead of inline template expansion.

Two suggestions intentionally not applied:

  • Gate release on published == 'true' — kept version != '' on purpose. The step has an implicit success() (no status function in the if), so a failed publish never creates a release. Gating on version preserves idempotent recovery: if a prior run publishes the crate but dies before tagging/releasing, a re-run (where published == false because the crate is already up) still creates the missing tag/release. published == 'true' would skip it permanently.
  • persist-credentials: false on the publish checkout — that job's git push origin "$TAG" relies on the checkout-persisted credential, so disabling it there would break the tag push.

@nh13

nh13 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/publish.yml:
- Around line 23-28: The GitHub App token generation steps using
`actions/create-github-app-token` are not constraining token scopes with
`permission-*` inputs, causing tokens to inherit the full app installation's
repo permission set. This creates unnecessary security risk. Add appropriate
`permission-*` input parameters to both token generation steps (at lines 23-28
and lines 53-58) to scope each token to only the minimum permissions required
for its respective job's downstream steps, reducing the blast radius if a step
is compromised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9675ee2a-8872-4348-a453-3f577b162ce6

📥 Commits

Reviewing files that changed from the base of the PR and between d27dbaf and 622abcb.

📒 Files selected for processing (4)
  • .github/workflows/publish.yml
  • .github/workflows/release-plz.yml
  • README.md
  • release-plz.toml
💤 Files with no reviewable changes (1)
  • .github/workflows/release-plz.yml
✅ Files skipped from review due to trivial changes (1)
  • release-plz.toml

Comment on lines +23 to +28
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.FG_LABS_BOT_APP_ID }}
private-key: ${{ secrets.FG_LABS_BOT_PRIVATE_KEY }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the workflow file exists and examine the token generation steps
cd /root/repo && cat -n .github/workflows/publish.yml

Repository: fg-labs/redskull

Length of output: 121


🌐 Web query:

actions/create-github-app-token v3.1.1 input parameters permission-contents permission-pull-requests

💡 Result:

In version 3.1.1 of the actions/create-github-app-token action, permissions are managed via optional input parameters prefixed with permission- [1][2]. These inputs allow you to explicitly define the scopes granted to the generated token, which is a recommended practice to follow the principle of least privilege [1][2]. Regarding your specific inquiries: - permission-contents: This input allows you to specify the 'contents' permission scope [3][4]. It accepts 'read' or 'write' as values [5][6]. When provided, it controls the token's ability to access repository contents, commits, and branches [6]. - permission-pull-requests: This input allows you to specify the 'pull-requests' permission scope [3][4]. It accepts 'read' or 'write' as values [5][6]. When provided, it controls the token's ability to interact with pull requests, reviews, and merges [6]. Key points regarding these permissions: - Default Behavior: If no permission-* inputs are specified, the generated token defaults to inheriting all permissions granted to the GitHub App's installation [1][2][6]. - Validation: You must ensure that any permission requested via these inputs has already been granted to the GitHub App installation [2]. Requesting a permission that the app does not possess will result in an error [5][2]. - Normalization: The action internally maps these input keys (e.g., permission-pull-requests) to the corresponding GitHub API permission format (e.g., pull_requests) [6].

Citations:


🏁 Script executed:

# Find the repository root and examine the workflow file
find / -name "publish.yml" -path "*/.github/workflows/*" 2>/dev/null | head -5

Repository: fg-labs/redskull

Length of output: 109


🏁 Script executed:

# Try listing git repositories to locate the correct path
git ls-files .github/workflows/publish.yml 2>/dev/null || find . -name "publish.yml" 2>/dev/null | grep -E "\.github/workflows"

Repository: fg-labs/redskull

Length of output: 90


🏁 Script executed:

cat -n .github/workflows/publish.yml

Repository: fg-labs/redskull

Length of output: 5483


Scope GitHub App tokens to least privilege per job.

Both token-generation steps mint installation tokens without explicit permission narrowing, so each token inherits the app installation's full repo permission set. Constrain token scopes with permission-* inputs (and optionally explicit repo scoping) to reduce blast radius if a downstream step is compromised.

Suggested hardening diff
       - name: Generate app token
         id: app-token
         uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
         with:
           app-id: ${{ secrets.FG_LABS_BOT_APP_ID }}
           private-key: ${{ secrets.FG_LABS_BOT_PRIVATE_KEY }}
+          owner: fg-labs
+          repositories: redskull
+          permission-contents: write
+          permission-pull-requests: write
@@
       - name: Generate app token
         id: app-token
         uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
         with:
           app-id: ${{ secrets.FG_LABS_BOT_APP_ID }}
           private-key: ${{ secrets.FG_LABS_BOT_PRIVATE_KEY }}
+          owner: fg-labs
+          repositories: redskull
+          permission-contents: write

Also applies to: 53-58

🧰 Tools
🪛 zizmor (1.25.2)

[error] 25-25: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions

(github-app)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 23 - 28, The GitHub App token
generation steps using `actions/create-github-app-token` are not constraining
token scopes with `permission-*` inputs, causing tokens to inherit the full app
installation's repo permission set. This creates unnecessary security risk. Add
appropriate `permission-*` input parameters to both token generation steps (at
lines 23-28 and lines 53-58) to scope each token to only the minimum permissions
required for its respective job's downstream steps, reducing the blast radius if
a step is compromised.

Source: Linters/SAST tools

@nh13

nh13 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

Closing: redskull already has a fully working release-plz setup (release-plz.yml). It opened release PR #34 and has published v0.1.0–v0.1.2 to crates.io via trusted publishing, with GH releases, on every push to main. CI also triggers on its release PRs, so the app-token rework offers no functional gain here, and renaming the workflow to publish.yml would break the existing crates.io Trusted Publisher (bound to the release-plz.yml filename). Not worth the churn.

@nh13 nh13 closed this Jun 16, 2026
@nh13 nh13 deleted the nh_release-plz-publish-workflow branch June 16, 2026 16:35
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