Skip to content

Commit 7b33739

Browse files
committed
(fix): harden changelog-preview and downstream caller usage
1 parent 4898047 commit 7b33739

1 file changed

Lines changed: 57 additions & 125 deletions

File tree

Lines changed: 57 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: Changelog Preview
22

33
on:
4-
# Allow this workflow to be called from other repositories
4+
# Allow this workflow to be called from other repositories.
55
#
66
# USAGE REQUIREMENTS:
77
# When calling this workflow from another repository, you must:
@@ -10,15 +10,14 @@ on:
1010
# - This is required to post comments on PRs from forks
1111
# - pull_request event has read-only GITHUB_TOKEN for fork PRs
1212
#
13-
# 2. Grant required permissions:
14-
# - contents: read (to checkout repo and read git history)
15-
# - pull-requests: write (to post/update PR comments in comment mode)
16-
# - statuses: write (to create commit statuses in status check mode)
13+
# 2. Grant required permissions at the call site:
14+
# - contents: read (to checkout repo and read git history)
15+
# - pull-requests: write (to post/update PR comments)
1716
#
1817
# 3. Inherit secrets:
19-
# - secrets: inherit (ensures caller's GITHUB_TOKEN is used)
18+
# - secrets: inherit (ensures caller's GITHUB_TOKEN is used)
2019
#
21-
# Example caller workflow (comment mode):
20+
# Example caller workflow:
2221
#
2322
# on:
2423
# pull_request_target:
@@ -33,25 +32,16 @@ on:
3332
# uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2
3433
# secrets: inherit
3534
#
36-
# Example caller workflow (status check mode):
37-
#
38-
# permissions:
39-
# contents: read
40-
# statuses: write
41-
#
42-
# jobs:
43-
# changelog-preview:
44-
# uses: getsentry/craft/.github/workflows/changelog-preview.yml@v2
45-
# with:
46-
# comment: false
47-
# secrets: inherit
48-
#
49-
# SECURITY NOTE:
50-
# This workflow is safe to use with pull_request_target because:
51-
# - The Craft binary is downloaded from releases, NOT from the PR
52-
# - Only git metadata (commits, tags) and .craft.yml config are read
53-
# - No code from the PR is ever executed
54-
#
35+
# SECURITY NOTES FOR CALLERS:
36+
# Callers that invoke this workflow on pull_request_target are checking out
37+
# attacker-controlled code and running Craft against it. To reduce blast
38+
# radius, callers SHOULD:
39+
# - Pin this workflow to a commit SHA, not a moving tag like @v2.
40+
# - Grant only the minimum permissions shown above; never add
41+
# `contents: write` or other scopes to this job.
42+
# - Be aware that Craft reads `.craft.env` from the config-file directory
43+
# and copies its keys into the environment; hardening for that case is
44+
# tracked separately.
5545
workflow_call:
5646
inputs:
5747
working-directory:
@@ -63,39 +53,45 @@ on:
6353
description: 'Version of Craft to use (tag or "latest")'
6454
required: false
6555
type: string
66-
comment:
67-
description: 'Post changelog as PR comment (true) or as check run with job summary (false)'
68-
required: false
69-
type: boolean
70-
default: true
7156

72-
# Also run on PRs in this repository (dogfooding)
73-
pull_request_target:
57+
# Also run on PRs in this repository (dogfooding).
58+
#
59+
# We intentionally use `pull_request` (not `pull_request_target`) here so the
60+
# job runs with a read-only GITHUB_TOKEN for fork PRs. Same-repo branch PRs
61+
# still get the declared permissions and a working comment; fork PRs won't
62+
# get a preview comment, which is an acceptable trade-off for eliminating
63+
# the pull_request_target attack surface on this repository.
64+
pull_request:
7465
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
7566

76-
permissions:
77-
contents: read
78-
pull-requests: write # For comment mode
79-
statuses: write # For status check mode
80-
8167
jobs:
8268
preview:
8369
runs-on: ubuntu-latest
70+
permissions:
71+
contents: read
72+
pull-requests: write
8473
steps:
85-
# For pull_request_target, we must explicitly specify the ref to get the PR commits.
86-
# Try the merge ref first; fall back to head ref if PR has merge conflicts.
87-
- uses: actions/checkout@v6
74+
# For pull_request_target callers we must explicitly specify the ref to
75+
# get the PR commits. Try the merge ref first; fall back to head ref if
76+
# the PR has merge conflicts.
77+
#
78+
# `persist-credentials: false` ensures the GITHUB_TOKEN is not written
79+
# into `.git/config` on disk, which would otherwise leak if a later step
80+
# executes PR-controlled code.
81+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
8882
id: checkout-merge
8983
continue-on-error: true
9084
with:
9185
fetch-depth: 0
9286
ref: refs/pull/${{ github.event.pull_request.number }}/merge
87+
persist-credentials: false
9388

94-
- uses: actions/checkout@v6
89+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
9590
if: steps.checkout-merge.outcome == 'failure'
9691
with:
9792
fetch-depth: 0
9893
ref: ${{ github.event.pull_request.head.sha }}
94+
persist-credentials: false
9995

10096
- name: Install Craft
10197
shell: bash
@@ -165,9 +161,7 @@ jobs:
165161
# CalVer projects don't use semver bumps — skip the impact badge
166162
if [[ "$VERSIONING_POLICY" == "calver" ]]; then
167163
BUMP_BADGE=""
168-
BUMP_SHORT="CalVer"
169164
SECTION_HEADING="Changelog Preview"
170-
STATUS_CONTEXT="Changelog Preview"
171165
else
172166
case "$BUMP_TYPE" in
173167
major) BUMP_BADGE="🔴 **Major** (breaking changes)" ;;
@@ -176,72 +170,11 @@ jobs:
176170
*) BUMP_BADGE="⚪ **None** (no version bump detected)" ;;
177171
esac
178172
179-
case "$BUMP_TYPE" in
180-
major) BUMP_SHORT="Major" ;;
181-
minor) BUMP_SHORT="Minor" ;;
182-
patch) BUMP_SHORT="Patch" ;;
183-
*) BUMP_SHORT="None" ;;
184-
esac
185-
186173
SECTION_HEADING="Semver Impact of This PR"
187-
STATUS_CONTEXT="Changelog Preview / Semver Impact"
188174
fi
189175
190-
# Determine mode: use status check mode when comment is false OR when running internally (no input)
191-
USE_COMMENT_MODE="${{ inputs.comment }}"
192-
if [[ "$USE_COMMENT_MODE" == "false" ]] || [[ -z "$USE_COMMENT_MODE" ]]; then
193-
# Status check mode (new feature or internal dogfooding)
194-
echo "Using status check mode..."
195-
196-
HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}"
197-
TARGET_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
198-
PR_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}"
199-
200-
# Create commit status via GitHub API
201-
echo "Creating commit status..."
202-
gh api --method POST \
203-
-H "Accept: application/vnd.github+json" \
204-
-H "X-GitHub-Api-Version: 2022-11-28" \
205-
"repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \
206-
-f state="success" \
207-
-f context="$STATUS_CONTEXT" \
208-
-f description="$BUMP_SHORT" \
209-
-f target_url="$TARGET_URL"
210-
211-
echo "✓ Commit status created"
212-
213-
# 2. Write to job summary
214-
cat >> $GITHUB_STEP_SUMMARY << CRAFT_CHANGELOG_SUMMARY_END
215-
# Changelog Preview for PR #${PR_NUMBER}
216-
217-
[→ View PR #${PR_NUMBER}](${PR_URL})
218-
219-
## ${SECTION_HEADING}
220-
221-
${BUMP_BADGE}
222-
223-
<details>
224-
<summary>📋 Changelog Preview</summary>
225-
226-
This is how your changes will appear in the changelog.
227-
Entries from this PR are highlighted with a left border (blockquote style).
228-
229-
---
230-
231-
${CHANGELOG}
232-
233-
---
234-
235-
</details>
236-
CRAFT_CHANGELOG_SUMMARY_END
237-
238-
echo "✓ Job summary written"
239-
else
240-
# Comment mode (original behavior)
241-
echo "Using comment mode..."
242-
243-
COMMENT_FILE=$(mktemp)
244-
cat > "$COMMENT_FILE" << CRAFT_CHANGELOG_COMMENT_END
176+
COMMENT_FILE=$(mktemp)
177+
cat > "$COMMENT_FILE" << CRAFT_CHANGELOG_COMMENT_END
245178
<!-- craft-changelog-preview -->
246179
## ${SECTION_HEADING}
247180
@@ -264,23 +197,22 @@ jobs:
264197
<sub>🤖 This preview updates automatically when you update the PR.</sub>
265198
CRAFT_CHANGELOG_COMMENT_END
266199
267-
COMMENT_ID=$(gh api \
268-
"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
269-
--jq '.[] | select(.body | contains("<!-- craft-changelog-preview -->")) | .id' \
270-
| head -1)
271-
272-
if [[ -n "$COMMENT_ID" ]]; then
273-
echo "Updating existing comment $COMMENT_ID..."
274-
gh api -X PATCH \
275-
"repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" \
276-
-F body=@"$COMMENT_FILE"
277-
else
278-
echo "Creating new comment..."
279-
gh api -X POST \
280-
"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
281-
-F body=@"$COMMENT_FILE"
282-
fi
200+
COMMENT_ID=$(gh api \
201+
"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
202+
--jq '.[] | select(.body | contains("<!-- craft-changelog-preview -->")) | .id' \
203+
| head -1)
283204
284-
rm -f "$COMMENT_FILE"
285-
echo "✓ Comment posted"
205+
if [[ -n "$COMMENT_ID" ]]; then
206+
echo "Updating existing comment $COMMENT_ID..."
207+
gh api -X PATCH \
208+
"repos/$GITHUB_REPOSITORY/issues/comments/$COMMENT_ID" \
209+
-F body=@"$COMMENT_FILE"
210+
else
211+
echo "Creating new comment..."
212+
gh api -X POST \
213+
"repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
214+
-F body=@"$COMMENT_FILE"
286215
fi
216+
217+
rm -f "$COMMENT_FILE"
218+
echo "✓ Comment posted"

0 commit comments

Comments
 (0)