Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Manage Release PRs and Publish Crates

on:
push:
branches:
- main

concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false

jobs:
release-pr:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'fg-labs' }}
permissions:
# release-plz authenticates with the app token (GITHUB_TOKEN env below),
# so the default token only needs read access plus PR write to open the
# release PR.
contents: read
pull-requests: write
steps:
- 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 }}
Comment on lines +23 to +28

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

- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
# release-plz uses the app token via GITHUB_TOKEN, so the checkout
# credential does not need to persist in git config.
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Create or update release PR
uses: release-plz/action@064f4d1e36c843611ddf013be726beaa4ad804db # v0.5.129
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}

publish:
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'fg-labs' }}
permissions:
id-token: write
contents: write
steps:
- 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 }}
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Authenticate to crates.io via Trusted Publishing
id: crates-auth
uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4
- name: Publish crate
id: publish
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
run: |
set -euo pipefail

VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r \
'.packages[] | select(.name == "redskull") | .version')

PUBLISHED_VERSION=$(curl -sS \
-H "User-Agent: redskull-ci (https://github.com/fg-labs/redskull)" \
"https://crates.io/api/v1/crates/redskull" | jq -r \
'.crate.max_version // "0.0.0"')

echo "version=$VERSION" >> "$GITHUB_OUTPUT"

if [ "$VERSION" = "$PUBLISHED_VERSION" ]; then
echo "redskull v$VERSION already on crates.io -- nothing to publish"
echo "published=false" >> "$GITHUB_OUTPUT"
exit 0
fi

echo "Publishing redskull v$VERSION..."
cargo publish -p redskull
echo "Published redskull v$VERSION"
echo "published=true" >> "$GITHUB_OUTPUT"

- name: Create GitHub release
# Gate on `version` rather than `published` so a re-run can still create
# the GitHub release idempotently if a prior run published the crate but
# failed before tagging/releasing.
if: steps.publish.outputs.version != ''
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
VERSION: ${{ steps.publish.outputs.version }}
run: |
set -euo pipefail
TAG="v${VERSION}"

# Idempotent: skip if tag/release already exist
if ! git rev-parse "$TAG" >/dev/null 2>&1; then
git tag "$TAG"
fi
if ! git ls-remote --tags origin "$TAG" | grep -q "$TAG"; then
git push origin "$TAG"
fi
if ! gh release view "$TAG" >/dev/null 2>&1; then
gh release create "$TAG" \
--title "v${VERSION}" \
--generate-notes \
--latest
else
echo "Release $TAG already exists -- skipping"
fi
55 changes: 0 additions & 55 deletions .github/workflows/release-plz.yml

This file was deleted.

16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,12 @@ Releases are automated with [`release-plz`][release-plz-link], driven by [Conven

### How it works

On every push to `main`, the `release-plz` workflow (`.github/workflows/release-plz.yml`) runs and does two things:
On every push to `main`, the `publish` workflow (`.github/workflows/publish.yml`) runs two jobs:

1. **Opens or updates a release PR** that bumps the version in `Cargo.toml` and `Cargo.lock` and updates `CHANGELOG.md` based on commit messages since the last tag.
2. **Publishes to crates.io and creates a GitHub release** when a release PR is merged, tagging the commit and pushing the crate to crates.io.
1. **`release-pr`** — `release-plz` opens or updates a release PR that bumps the version in `Cargo.toml` and `Cargo.lock` and updates `CHANGELOG.md` based on commit messages since the last tag.
2. **`publish`** — when a release PR is merged (so the version in `Cargo.toml` no longer matches the latest version on crates.io), the crate is published to crates.io via Trusted Publishing and a GitHub release is tagged and created.

Both jobs authenticate using a short-lived GitHub App token (`FG_LABS_BOT_APP_ID` / `FG_LABS_BOT_PRIVATE_KEY`) rather than the default `GITHUB_TOKEN`, so that the release PR and tags it creates can trigger other workflows.

### Conventional Commits

Expand All @@ -206,15 +208,15 @@ This tool follows [Semantic Versioning](https://semver.org/).

1. Merge your changes to `main` using Conventional Commit messages.
2. Review the release PR opened by `release-plz` (version bump + changelog).
3. Merge the release PR. `release-plz` publishes to crates.io and creates the GitHub release automatically.
3. Merge the release PR. The `publish` job then publishes to crates.io and creates the GitHub release automatically.

### Publishing credentials: Trusted Publishing

This repository uses [crates.io Trusted Publishing][trusted-publishing-link] (OIDC) instead of a long-lived
`CARGO_REGISTRY_TOKEN` secret. The `release-plz` workflow requests a short-lived token from crates.io via
`CARGO_REGISTRY_TOKEN` secret. The `publish` job requests a short-lived token from crates.io via
GitHub's OIDC provider at publish time — nothing needs to be stored in the repository.

The workflow already sets `id-token: write` on the release job, which is required for OIDC.
The workflow already sets `id-token: write` on the `publish` job, which is required for OIDC.

#### First release

Expand All @@ -236,7 +238,7 @@ After `0.1.0` is on crates.io, configure a Trusted Publisher for this repository

* Repository owner: `fg-labs`
* Repository name: `redskull`
* Workflow filename: `release-plz.yml`
* Workflow filename: `publish.yml`
* Environment: *(leave blank)*

Once configured, merging a release PR on `main` will publish automatically.
Expand Down
29 changes: 13 additions & 16 deletions release-plz.toml
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
[workspace]
# Enable changelog generation driven by Conventional Commits.
# Update the changelog from Conventional Commits when preparing a release.
changelog_update = true
# Create a GitHub release when a release PR is merged.
git_release_enable = true
# Tag each release in git.
git_tag_enable = true
# Publish the crate to crates.io on release.
publish = true
# Verify the package builds before publishing.
publish_allow_dirty = false
# Use semver-compatible version bumps.
# Update dependencies with `cargo update` in the release PR.
dependencies_update = true
# Label applied to the release PR.
pr_labels = ["release"]
# Verify semver compatibility with cargo-semver-checks before releasing.
semver_check = true

[[package]]
name = "redskull"
# Group pre-1.0 releases under a single major so breaking changes
# can still bump the minor version appropriately.
changelog_include = ["redskull"]
# Publishing (crates.io upload, git tag, and GitHub release) is handled by the
# `publish` job in .github/workflows/publish.yml so that we publish via
# crates.io Trusted Publishing (OIDC). release-plz only opens and maintains the
# release PR, so disable the release-side actions here.
publish = false
git_tag_enable = false
git_release_enable = false
Loading