diff --git a/.bumpy/fork-comment-workflow-run-split.md b/.bumpy/fork-comment-workflow-run-split.md new file mode 100644 index 0000000..b5aa73a --- /dev/null +++ b/.bumpy/fork-comment-workflow-run-split.md @@ -0,0 +1,5 @@ +--- +'@varlock/bumpy': minor +--- + +Add a `pull_request` + `workflow_run` option for commenting on fork PRs, so the privileged half never touches fork code. `bumpy ci check --emit-comment ` renders the release-plan comment to `/comment.md` for upload as an artifact, and a new `bumpy ci comment --body-file ` posts it from a `workflow_run` job. The target PR is resolved from the trusted `workflow_run` event (`head_sha`), never from the (untrusted) artifact. diff --git a/.github/workflows/bumpy-check.yaml b/.github/workflows/bumpy-check.yaml index 1633ef5..436e40b 100644 --- a/.github/workflows/bumpy-check.yaml +++ b/.github/workflows/bumpy-check.yaml @@ -1,69 +1,38 @@ -# 🐸 Bumpy CI check -# checks for missing bump files and posts/updates a PR comment with the release plan +# 🐸 Bumpy CI check (dogfood) +# Runs our own unreleased bumpy on every PR and renders the release-plan comment as an +# artifact. Runs on the UNPRIVILEGED `pull_request` event, so it's safe to build and run +# the PR's own bumpy (fork or not) — there's no write token or secrets to protect here. +# Posting the comment on fork PRs is the privileged half and lives in bumpy-comment.yaml +# (workflow_run). A normal project just adds +# bunx @varlock/bumpy ci check --emit-comment ./bumpy-comment +# to its existing CI workflow. # -# ⚠️ NOTE - DO NOT COPY THIS FILE -# instead look at the recommended workflow in the docs +# ⚠️ DO NOT COPY THIS FILE — see the recommended setup in the docs: # ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️ -# -# This repo builds and runs its OWN unreleased bumpy so we dogfood the current -# CLI on every PR. Two jobs, split by trust level: -# - non-fork PRs build and run the PR's OWN bumpy, so a PR previews its own -# ci-check changes. Safe because the code comes from this repo. -# - fork PRs build and run MAIN's bumpy and only READ the PR via `--cwd ./pr`, -# so no untrusted code ever touches the pull_request_target write token. -# A normal project just runs `bunx @varlock/bumpy@latest ci check --cwd ./pr`. - name: Bumpy Check -on: pull_request_target # < necessary so it can post comments on fork PRs +on: pull_request permissions: - pull-requests: write + pull-requests: write # same-repo PRs comment directly; fork PRs are read-only (the poster handles those) contents: read jobs: - # Non-fork PRs (trusted): build and run the PR's OWN bumpy so it dogfoods its - # own changes. `--cwd .` acknowledges that the current checkout is trusted. - check-local: - if: github.event.pull_request.head.repo.full_name == github.repository + check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: - ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 # history to diff bump files against the PR base branch - uses: oven-sh/setup-bun@v2 - run: bun install - # Build first since we run the local built version of bumpy. - run: bun run --filter @varlock/bumpy build - # Re-install so the freshly-built CLI bin is linked. - - run: bun install - - run: bunx @varlock/bumpy ci check --cwd . + - run: bun install # link the freshly-built CLI bin + - run: bunx @varlock/bumpy ci check --emit-comment ./bumpy-comment env: GH_TOKEN: ${{ github.token }} - - # Fork PRs (untrusted): build and run MAIN's bumpy from a trusted checkout and - # only READ the PR head via `--cwd ./pr` — never install/build/run fork code. - check-fork: - if: github.event.pull_request.head.repo.full_name != github.repository - runs-on: ubuntu-latest - steps: - # TRUSTED base checkout (main) — bumpy is built and run from here. - - uses: actions/checkout@v6 - with: - ref: main - persist-credentials: false - # UNTRUSTED PR head into ./pr — only READ via --cwd, never built or run. - - uses: actions/checkout@v6 + - uses: actions/upload-artifact@v4 + if: always() # upload even when the check fails — the comment explains why with: - ref: ${{ github.event.pull_request.head.sha }} - path: pr - persist-credentials: false - - uses: oven-sh/setup-bun@v2 - - run: bun install - - run: bun run --filter @varlock/bumpy build - - run: bun install - # bunx runs from the trusted root; bumpy reads the PR's bump files via --cwd. - - run: bunx @varlock/bumpy ci check --cwd ./pr - env: - GH_TOKEN: ${{ github.token }} + name: bumpy-comment + path: ./bumpy-comment diff --git a/.github/workflows/bumpy-comment.yaml b/.github/workflows/bumpy-comment.yaml new file mode 100644 index 0000000..f1ff705 --- /dev/null +++ b/.github/workflows/bumpy-comment.yaml @@ -0,0 +1,40 @@ +# 🐸 Bumpy PR comment (dogfood) +# Privileged half of the fork-comment split: posts the release-plan comment that +# bumpy-check.yaml rendered. Triggered after that workflow completes. It NEVER checks +# out or runs PR code — it builds MAIN's bumpy and posts a pre-rendered body to the PR +# it resolves from the trusted workflow_run.head_sha. A normal project would just run +# `bunx @varlock/bumpy ci comment` instead of building from source. +# +# ⚠️ DO NOT COPY THIS FILE — see the recommended setup in the docs: +# ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️ +name: Bumpy PR Comment + +on: + workflow_run: + workflows: ['Bumpy Check'] + types: [completed] + +permissions: + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + steps: + # TRUSTED: default branch (main) only — never the PR. Build our own bumpy. + - uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + - run: bun install + - run: bun run --filter @varlock/bumpy build + - run: bun install # link the freshly-built CLI bin + # Download AFTER checkout — checkout cleans the workspace and would wipe it. + - uses: actions/download-artifact@v4 + continue-on-error: true # the check may not have produced a comment + with: + name: bumpy-comment + path: ./bumpy-comment + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ github.token }} + - run: bunx @varlock/bumpy ci comment --body-file ./bumpy-comment/comment.md + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b05ffaf..da8b7e7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,5 +14,6 @@ jobs: - run: git config --global user.name "CI" && git config --global user.email "ci@example.com" - run: bun run test - # NOTE: `bumpy ci check` lives in .github/workflows/bumpy-check.yaml + # NOTE: `bumpy ci check` runs in .github/workflows/bumpy-check.yaml (pull_request), + # and the fork-PR comment is posted from .github/workflows/bumpy-comment.yaml (workflow_run). # see ➡️ https://bumpy.varlock.dev/blob/main/docs/github-actions.md ⬅️ diff --git a/docs/cli.md b/docs/cli.md index 7b79741..068224f 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -180,18 +180,35 @@ CI command for PR checks. Computes the release plan from bump files changed in t bumpy ci check bumpy ci check --strict bumpy ci check --no-fail +bumpy ci check --emit-comment ./bumpy-comment ``` -| Flag | Description | -| ----------- | ---------------------------------------------------------------- | -| `--comment` | Force PR comment on or off (default: auto-detect CI environment) | -| `--strict` | Fail if any changed package is not covered by a bump file | -| `--no-fail` | Warn only, never exit non-zero | +| Flag | Description | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `--comment` | Force PR comment on or off (default: auto-detect CI environment) | +| `--strict` | Fail if any changed package is not covered by a bump file | +| `--no-fail` | Warn only, never exit non-zero | +| `--emit-comment ` | Also write the rendered comment to `/comment.md` for a downstream [`ci comment`](#bumpy-ci-comment) to post (the fork-comment split) | Requires `GH_TOKEN` environment variable (automatically available in GitHub Actions). **On channel and promotion PRs:** for a PR targeting a [prerelease channel](prereleases.md) branch, the comment is channel-aware — it shows the prerelease plan (`-.x` versions) and the target dist-tag rather than implying a stable release. For a **promotion PR** (a channel branch → `main`, or a graduation like `alpha` → `beta`), the check reads the cycle's already-shipped bump files from `.bumpy//` and shows the consolidated stable plan, calling out that merging ends the prerelease cycle. (Feature PRs that _target_ a channel branch are checked against that branch, so only the PR's own new bump files count — see [`--base`](#bumpy-check) on the local `check` command for the equivalent locally.) +## `bumpy ci comment` + +Posts a pre-rendered comment (from `ci check --emit-comment`) to a PR. This is the privileged half of the [fork-comment split](github-actions.md#commenting-on-fork-prs): your `pull_request` check renders the comment as an artifact, and this command — run from a `workflow_run` job — posts it. + +```bash +bumpy ci comment --body-file ./bumpy-comment/comment.md +``` + +| Flag | Description | +| -------------------- | ------------------------------------------------------------------ | +| `--body-file ` | Path to the rendered comment body (required) | +| `--pr ` | Target PR number (default: resolved from the `workflow_run` event) | + +It needs no checkout and no bumpy project — it only posts. Under `workflow_run` it resolves the target PR from the **trusted** event (`head_sha`), never from the artifact, and treats the body as untrusted text. A missing or empty body file is a no-op. Requires `GH_TOKEN`. + ## `bumpy ci plan` CI command that reports what `ci release` would do, without acting. Outputs JSON to stdout and sets GitHub Actions step outputs so you can conditionally run expensive steps (builds, etc.) only when needed. diff --git a/docs/github-actions.md b/docs/github-actions.md index 9733d9f..a550c7f 100644 --- a/docs/github-actions.md +++ b/docs/github-actions.md @@ -16,131 +16,87 @@ These commands facilitate the following: > > The version-resolution shell snippets work as-is regardless of package manager — they only depend on `jq` and `git`, both preinstalled on GitHub-hosted runners. -## PR check workflow +## PR check -Posts/updates the release-plan comment on every PR, including PRs from forks. **Copy it as-is and you're covered** — it's structured so nothing in a fork PR can influence how bumpy is fetched or run. (If you change `main` to your own base branch, that's the only edit most repos need. See the security notes below before restructuring it.) +`bumpy ci check` confirms every PR carries a bump file and posts a release-plan comment showing what will be released. The simplest setup is one step in your existing PR workflow (or a new one triggered `on: pull_request`): ```yaml -# .github/workflows/bumpy-check.yaml -name: Bumpy Check - -on: pull_request_target # so it can post comments on fork PRs - -permissions: - pull-requests: write - contents: read - -jobs: - check: - runs-on: ubuntu-latest - steps: - # 1. TRUSTED checkout of the base branch — bumpy is fetched and run from - # here, so the package-manager config in effect (bunfig.toml, .npmrc) - # is YOURS, not the PR's. Hardcoded "main" (the PR controls its own - # base ref); change it to your base branch. - - uses: actions/checkout@v6 - with: - ref: main - persist-credentials: false - - # 2. UNTRUSTED PR head into ./pr — only READ from here, never run it. - - uses: actions/checkout@v6 - with: - ref: ${{ github.event.pull_request.head.sha }} - path: pr - persist-credentials: false - - - uses: oven-sh/setup-bun@v2 - - # ⚠️ DO NOT bun install / npm install / run any script from ./pr ⚠️ - - # Read bumpy's version from the TRUSTED base checkout (never from ./pr) and - # run it in one step. bunx runs from the trusted root; `--cwd ./pr` only - # points bumpy at the PR tree to read its bump files. The version is folded - # straight into the command (not written to $GITHUB_ENV) to avoid CodeQL's - # actions/envvar-injection sink; quote it so a malformed value can't inject. - - name: Bumpy release-plan check - run: | - VERSION=$(jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' package.json | sed 's/[\^~]//') - bunx "@varlock/bumpy@$VERSION" ci check --cwd ./pr - env: - GH_TOKEN: ${{ github.token }} -``` - -### ⚠️ Security essentials - -`pull_request_target` carries a **write token and secrets even on fork PRs** — that's what lets it comment on forks, and why a PR author must never be able to influence what runs. The workflow above handles this; two rules to preserve if you adapt it: - -- **Never execute PR code** — no `bun install` / `npm install` (postinstall scripts run), no `bun run