Skip to content
Merged
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
64 changes: 34 additions & 30 deletions .github/workflows/bumpy-check.yaml
Original file line number Diff line number Diff line change
@@ -1,55 +1,59 @@
name: Bumpy check

# Posts/updates the release-plan comment on every PR (including forks).
# PR half of bumpy's fork-safe release-plan comment (the 1.18+ split pattern).
#
# Uses `pull_request_target` so the comment can be posted on fork PRs (a plain
# `pull_request` gives forks a read-only token, so `pull-requests: write` would
# be ineffective). Because that runs with the base repo's elevated token, the PR
# code is treated as untrusted DATA only:
# - the base branch is checked out at the root (trusted bunfig.toml / lockfile
# + the base package.json we read the pinned bumpy version from);
# - the PR head is checked out into ./pr, isolated and never executed;
# - bumpy is run from the root (so the PR's bunfig.toml/.npmrc can't redirect
# package resolution) and only reads the PR files via `--cwd ./pr`.
# ⚠️ DO NOT bun install / npm install / run any script from ./pr ⚠️
on: pull_request_target
# Runs on plain `pull_request`, so on fork PRs this job gets a READ-ONLY token
# and cannot post comments — that's intentional. It only computes the release
# plan, gates on missing bump files, and writes the rendered comment to the
# `bumpy-comment` artifact (`ci check --emit-comment`). The privileged half that
# actually posts lives in bumpy-comment.yaml, which runs on `workflow_run`
# (trusted context, never executes PR code) and resolves the target PR from the
# trusted event — never from this artifact.
#
# Because the token here is read-only, it is safe to run bumpy against PR code:
# the worst a malicious fork's bunfig.toml/.npmrc/preload can do is corrupt its
# own release-plan output, with no write capability to abuse.
on: pull_request

permissions:
pull-requests: write
contents: read

concurrency:
group: bumpy-check-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check:
# Distinct job name (not "check") so the status context reads
# "Bumpy check / bumpy-check" and doesn't collide with other `check` jobs —
# this is the context to require in branch protection.
bumpy-check:
runs-on: ubuntu-latest
steps:
# Trusted base checkout at the root: provides the bunfig.toml / lockfile
# that govern how `bunx` below resolves and runs bumpy, plus the base
# package.json we read the pinned bumpy version from.
- uses: actions/checkout@v7
with:
ref: main
fetch-depth: 0
persist-credentials: false

- uses: oven-sh/setup-bun@v2

# Untrusted PR head, isolated in ./pr — read as data, never executed.
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.head.sha }}
path: pr
persist-credentials: false

# Resolve bumpy from the base package.json (trusted) and run it from the
# root against the PR files. The version is read straight into the bunx
# invocation (never written to $GITHUB_ENV) so there is no env-injection
# sink even though this is a pull_request_target workflow.
# Resolve the bumpy version pinned in package.json and run the PR check.
# A direct comment post is attempted but fails-soft on the read-only token
# (bumpy just warns); the rendered comment is written to ./bumpy-comment
# for the poster regardless. Exits non-zero only on the gate: a changed
# publishable package missing its bump file.
- 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
bunx "@varlock/bumpy@$VERSION" ci check --emit-comment ./bumpy-comment
env:
GH_TOKEN: ${{ github.token }}

# Hand the rendered comment to the privileged poster. `always()` so the
# plan still posts even when the check fails the gate (missing bump file);
# bumpy always writes a (possibly empty) comment.md, so the artifact exists.
- name: Upload release-plan comment
if: always()
uses: actions/upload-artifact@v4
with:
name: bumpy-comment
path: ./bumpy-comment
62 changes: 62 additions & 0 deletions .github/workflows/bumpy-comment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Bumpy comment

# Privileged half of bumpy's fork-safe release-plan comment (the 1.18+ split).
#
# The unprivileged "Bumpy check" workflow runs on `pull_request` (executing PR
# code with a read-only token) and uploads the rendered comment as the
# `bumpy-comment` artifact. This workflow runs on `workflow_run` — a trusted
# context that NEVER checks out or executes PR code — and posts that comment.
#
# SECURITY: the artifact body is untrusted (a fork produced it). Two things keep
# this safe: (1) it is downloaded to runner.temp, OUTSIDE the checkout, so it
# can't overwrite the trusted files (package.json etc.) we read below; and (2)
# bumpy uses the body only as comment TEXT and resolves the target PR from the
# trusted `workflow_run` event (head_sha) — never from the artifact — so a fork
# can't redirect the comment onto another PR.
on:
workflow_run:
workflows: ["Bumpy check"]
types: [completed]

permissions:
contents: read
pull-requests: write

concurrency:
group: bumpy-comment-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true

jobs:
comment:
# Only act on check runs that came from a PR; skip anything else.
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
# Trusted base checkout (default branch) — provides the package.json we
# read the pinned bumpy version from. PR code is never checked out here.
- uses: actions/checkout@v7
with:
persist-credentials: false

- uses: oven-sh/setup-bun@v2

# Pull the rendered comment from the triggering check run. Download to
# runner.temp (OUTSIDE the checkout) so the untrusted artifact can't
# overwrite trusted files — this is what CodeQL's actions/artifact-poisoning
# query wants. `continue-on-error` so a missing artifact just means there's
# nothing to post (ci comment no-ops).
- name: Download release-plan comment
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: bumpy-comment
path: ${{ runner.temp }}/bumpy-comment
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Post release-plan comment
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
run: |
VERSION=$(jq -r '.devDependencies["@varlock/bumpy"] // .dependencies["@varlock/bumpy"]' package.json | sed 's/[\^~]//')
bunx "@varlock/bumpy@$VERSION" ci comment --body-file "$RUNNER_TEMP/bumpy-comment/comment.md"
env:
GH_TOKEN: ${{ github.token }}
7 changes: 4 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ jobs:
- name: Enable turborepo build cache
uses: rharkor/caching-for-turbo@56219402aacc0d06b650d898c222996dbc1191ec # v2.3.14

# Note: the bumpy release-plan comment now lives in its own hardened
# workflow (.github/workflows/bumpy-check.yaml) so it can post on fork PRs
# without running PR-defined code with write permissions.
# Note: the bumpy release-plan check + comment live in their own workflows
# (bumpy-check.yaml emits the comment on `pull_request`; bumpy-comment.yaml
# posts it on `workflow_run`) so it can post on fork PRs without running
# PR-defined code with write permissions.

# lint, build, tests ---------------------------------
- name: ESLint
Expand Down
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@types/node": "catalog:",
"@typescript-eslint/eslint-plugin": "^8.56.1",
"@typescript-eslint/parser": "^8.56.1",
"@varlock/bumpy": "^1.17.0",
"@varlock/bumpy": "^1.18.0",
"@varlock/tsconfig": "workspace:*",
"eslint": "^10.0.2",
"eslint-plugin-es-x": "^9.5.0",
Expand Down
Loading