From 7b337394fcce7db0fb7d07b08e340c7615fd9cce Mon Sep 17 00:00:00 2001
From: geoffg-sentry <165922362+geoffg-sentry@users.noreply.github.com>
Date: Tue, 21 Apr 2026 11:44:46 -0400
Subject: [PATCH] (fix): harden changelog-preview and downstream caller usage
---
.github/workflows/changelog-preview.yml | 182 ++++++++----------------
1 file changed, 57 insertions(+), 125 deletions(-)
diff --git a/.github/workflows/changelog-preview.yml b/.github/workflows/changelog-preview.yml
index 99f721a92..cdc826fc6 100644
--- a/.github/workflows/changelog-preview.yml
+++ b/.github/workflows/changelog-preview.yml
@@ -1,7 +1,7 @@
name: Changelog Preview
on:
- # Allow this workflow to be called from other repositories
+ # Allow this workflow to be called from other repositories.
#
# USAGE REQUIREMENTS:
# When calling this workflow from another repository, you must:
@@ -10,15 +10,14 @@ on:
# - This is required to post comments on PRs from forks
# - pull_request event has read-only GITHUB_TOKEN for fork PRs
#
- # 2. Grant required permissions:
- # - contents: read (to checkout repo and read git history)
- # - pull-requests: write (to post/update PR comments in comment mode)
- # - statuses: write (to create commit statuses in status check mode)
+ # 2. Grant required permissions at the call site:
+ # - contents: read (to checkout repo and read git history)
+ # - pull-requests: write (to post/update PR comments)
#
# 3. Inherit secrets:
- # - secrets: inherit (ensures caller's GITHUB_TOKEN is used)
+ # - secrets: inherit (ensures caller's GITHUB_TOKEN is used)
#
- # Example caller workflow (comment mode):
+ # Example caller workflow:
#
# on:
# pull_request_target:
@@ -33,25 +32,16 @@ on:
# uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2
# secrets: inherit
#
- # Example caller workflow (status check mode):
- #
- # permissions:
- # contents: read
- # statuses: write
- #
- # jobs:
- # changelog-preview:
- # uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2
- # with:
- # comment: false
- # secrets: inherit
- #
- # SECURITY NOTE:
- # This workflow is safe to use with pull_request_target because:
- # - The Craft binary is downloaded from releases, NOT from the PR
- # - Only git metadata (commits, tags) and .craft.yml config are read
- # - No code from the PR is ever executed
- #
+ # SECURITY NOTES FOR CALLERS:
+ # Callers that invoke this workflow on pull_request_target are checking out
+ # attacker-controlled code and running Craft against it. To reduce blast
+ # radius, callers SHOULD:
+ # - Pin this workflow to a commit SHA, not a moving tag like @v2.
+ # - Grant only the minimum permissions shown above; never add
+ # `contents: write` or other scopes to this job.
+ # - Be aware that Craft reads `.craft.env` from the config-file directory
+ # and copies its keys into the environment; hardening for that case is
+ # tracked separately.
workflow_call:
inputs:
working-directory:
@@ -63,39 +53,45 @@ on:
description: 'Version of Craft to use (tag or "latest")'
required: false
type: string
- comment:
- description: 'Post changelog as PR comment (true) or as check run with job summary (false)'
- required: false
- type: boolean
- default: true
- # Also run on PRs in this repository (dogfooding)
- pull_request_target:
+ # Also run on PRs in this repository (dogfooding).
+ #
+ # We intentionally use `pull_request` (not `pull_request_target`) here so the
+ # job runs with a read-only GITHUB_TOKEN for fork PRs. Same-repo branch PRs
+ # still get the declared permissions and a working comment; fork PRs won't
+ # get a preview comment, which is an acceptable trade-off for eliminating
+ # the pull_request_target attack surface on this repository.
+ pull_request:
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
-permissions:
- contents: read
- pull-requests: write # For comment mode
- statuses: write # For status check mode
-
jobs:
preview:
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pull-requests: write
steps:
- # For pull_request_target, we must explicitly specify the ref to get the PR commits.
- # Try the merge ref first; fall back to head ref if PR has merge conflicts.
- - uses: actions/checkout@v6
+ # For pull_request_target callers we must explicitly specify the ref to
+ # get the PR commits. Try the merge ref first; fall back to head ref if
+ # the PR has merge conflicts.
+ #
+ # `persist-credentials: false` ensures the GITHUB_TOKEN is not written
+ # into `.git/config` on disk, which would otherwise leak if a later step
+ # executes PR-controlled code.
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
id: checkout-merge
continue-on-error: true
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/merge
+ persist-credentials: false
- - uses: actions/checkout@v6
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: steps.checkout-merge.outcome == 'failure'
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
+ persist-credentials: false
- name: Install Craft
shell: bash
@@ -165,9 +161,7 @@ jobs:
# CalVer projects don't use semver bumps — skip the impact badge
if [[ "$VERSIONING_POLICY" == "calver" ]]; then
BUMP_BADGE=""
- BUMP_SHORT="CalVer"
SECTION_HEADING="Changelog Preview"
- STATUS_CONTEXT="Changelog Preview"
else
case "$BUMP_TYPE" in
major) BUMP_BADGE="🔴 **Major** (breaking changes)" ;;
@@ -176,72 +170,11 @@ jobs:
*) BUMP_BADGE="⚪ **None** (no version bump detected)" ;;
esac
- case "$BUMP_TYPE" in
- major) BUMP_SHORT="Major" ;;
- minor) BUMP_SHORT="Minor" ;;
- patch) BUMP_SHORT="Patch" ;;
- *) BUMP_SHORT="None" ;;
- esac
-
SECTION_HEADING="Semver Impact of This PR"
- STATUS_CONTEXT="Changelog Preview / Semver Impact"
fi
- # Determine mode: use status check mode when comment is false OR when running internally (no input)
- USE_COMMENT_MODE="${{ inputs.comment }}"
- if [[ "$USE_COMMENT_MODE" == "false" ]] || [[ -z "$USE_COMMENT_MODE" ]]; then
- # Status check mode (new feature or internal dogfooding)
- echo "Using status check mode..."
-
- HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
- TARGET_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
- PR_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}"
-
- # Create commit status via GitHub API
- echo "Creating commit status..."
- gh api --method POST \
- -H "Accept: application/vnd.github+json" \
- -H "X-GitHub-Api-Version: 2022-11-28" \
- "repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \
- -f state="success" \
- -f context="$STATUS_CONTEXT" \
- -f description="$BUMP_SHORT" \
- -f target_url="$TARGET_URL"
-
- echo "✓ Commit status created"
-
- # 2. Write to job summary
- cat >> $GITHUB_STEP_SUMMARY << CRAFT_CHANGELOG_SUMMARY_END
- # Changelog Preview for PR #${PR_NUMBER}
-
- [→ View PR #${PR_NUMBER}](${PR_URL})
-
- ## ${SECTION_HEADING}
-
- ${BUMP_BADGE}
-
-
- 📋 Changelog Preview
-
- This is how your changes will appear in the changelog.
- Entries from this PR are highlighted with a left border (blockquote style).
-
- ---
-
- ${CHANGELOG}
-
- ---
-
-
- CRAFT_CHANGELOG_SUMMARY_END
-
- echo "✓ Job summary written"
- else
- # Comment mode (original behavior)
- echo "Using comment mode..."
-
- COMMENT_FILE=$(mktemp)
- cat > "$COMMENT_FILE" << CRAFT_CHANGELOG_COMMENT_END
+ COMMENT_FILE=$(mktemp)
+ cat > "$COMMENT_FILE" << CRAFT_CHANGELOG_COMMENT_END
## ${SECTION_HEADING}
@@ -264,23 +197,22 @@ jobs:
🤖 This preview updates automatically when you update the PR.
CRAFT_CHANGELOG_COMMENT_END
- COMMENT_ID=$(gh api \
- "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
- --jq '.[] | select(.body | contains("")) | .id' \
- | head -1)
-
- if [[ -n "$COMMENT_ID" ]]; then
- echo "Updating existing comment $COMMENT_ID..."
- gh api -X PATCH \
- "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" \
- -F body=@"$COMMENT_FILE"
- else
- echo "Creating new comment..."
- gh api -X POST \
- "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
- -F body=@"$COMMENT_FILE"
- fi
+ COMMENT_ID=$(gh api \
+ "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
+ --jq '.[] | select(.body | contains("")) | .id' \
+ | head -1)
- rm -f "$COMMENT_FILE"
- echo "✓ Comment posted"
+ if [[ -n "$COMMENT_ID" ]]; then
+ echo "Updating existing comment $COMMENT_ID..."
+ gh api -X PATCH \
+ "repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" \
+ -F body=@"$COMMENT_FILE"
+ else
+ echo "Creating new comment..."
+ gh api -X POST \
+ "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
+ -F body=@"$COMMENT_FILE"
fi
+
+ rm -f "$COMMENT_FILE"
+ echo "✓ Comment posted"