Skip to content

Commit 1fd94f8

Browse files
committed
fix(ci): post WASM size report via workflow_run, not pull_request
pull_request-triggered workflows get a read-only GITHUB_TOKEN for cross-repo (fork) PRs no matter what permissions: is declared, so the direct comment POST from wasm-size-report was failing with a 403. wasm-size-report now just writes the size data to JSON and uploads it as an artifact (no write permissions needed). A new workflow_run- triggered workflow (wasm-size-comment.yml), which runs in the base repo's context, downloads that artifact and posts/updates the PR comment. It never checks out or executes PR-supplied code, so it avoids the pull_request_target "pwn request" risk that would come from building untrusted contract source with a write-scoped token.
1 parent 6da48e4 commit 1fd94f8

2 files changed

Lines changed: 92 additions & 38 deletions

File tree

.github/workflows/contracts.yml

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,6 @@ jobs:
103103
runs-on: ubuntu-latest
104104
if: github.event_name == 'pull_request'
105105

106-
permissions:
107-
contents: read
108-
pull-requests: write
109-
110106
steps:
111107
- name: Checkout PR head
112108
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
@@ -163,38 +159,28 @@ jobs:
163159
SIZE=$(wc -c < "${{ github.workspace }}/.wasm-size-target/wasm32-unknown-unknown/release/stellar_contracts.wasm" | tr -d '[:space:]')
164160
echo "size=$SIZE" >> "$GITHUB_OUTPUT"
165161
166-
- name: Comment size report on PR
167-
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
162+
- name: Write size report
163+
run: |
164+
cat > wasm-size-report.json <<EOF
165+
{
166+
"pr_number": ${{ github.event.pull_request.number }},
167+
"base_sha": "${{ github.event.pull_request.base.sha }}",
168+
"head_sha": "${{ github.event.pull_request.head.sha }}",
169+
"base_size": ${{ steps.base_size.outputs.size }},
170+
"head_size": ${{ steps.head_size.outputs.size }},
171+
"limit": ${{ env.MAX_WASM_BYTES }}
172+
}
173+
EOF
174+
175+
# PRs from forks get a read-only GITHUB_TOKEN under the `pull_request`
176+
# event, so this job can't post a comment directly. The report is
177+
# handed off as an artifact to wasm-size-comment.yml, which runs via
178+
# `workflow_run` in the base repo's context (write-capable token) and
179+
# only ever reads this JSON — it never checks out or executes PR code,
180+
# so it stays safe from a malicious PR's build script.
181+
- name: Upload size report artifact
182+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
168183
with:
169-
script: |
170-
const headSize = ${{ steps.head_size.outputs.size }};
171-
const baseSize = ${{ steps.base_size.outputs.size }};
172-
const limit = ${{ env.MAX_WASM_BYTES }};
173-
const delta = headSize - baseSize;
174-
const deltaStr = `${delta > 0 ? '+' : ''}${delta.toLocaleString()} bytes`;
175-
const pct = ((headSize / limit) * 100).toFixed(1);
176-
const marker = '<!-- wasm-size-report -->';
177-
178-
const body = [
179-
marker,
180-
'### Contract WASM size report',
181-
'',
182-
'| | Bytes |',
183-
'|---|---|',
184-
`| Base (\`${context.payload.pull_request.base.sha.slice(0, 7)}\`) | ${baseSize.toLocaleString()} |`,
185-
`| Head (\`${context.payload.pull_request.head.sha.slice(0, 7)}\`) | ${headSize.toLocaleString()} |`,
186-
`| Delta | ${deltaStr} |`,
187-
`| Budget used | ${pct}% of ${limit.toLocaleString()} bytes |`,
188-
].join('\n');
189-
190-
const { owner, repo } = context.repo;
191-
const issue_number = context.issue.number;
192-
193-
const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
194-
const existing = comments.data.find((c) => c.body.includes(marker));
195-
196-
if (existing) {
197-
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
198-
} else {
199-
await github.rest.issues.createComment({ owner, repo, issue_number, body });
200-
}
184+
name: wasm-size-report
185+
path: wasm-size-report.json
186+
retention-days: 7
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: WASM Size Comment
2+
3+
# Runs in the base repo's context (not the PR head's), so it gets a
4+
# write-capable GITHUB_TOKEN even for PRs from forks. It never checks out
5+
# or executes any code from the PR — it only downloads the small JSON
6+
# artifact produced by the `wasm-size-report` job in contracts.yml and
7+
# posts/updates a PR comment from it.
8+
on:
9+
workflow_run:
10+
workflows: ["Smart Contract CI"]
11+
types: [completed]
12+
13+
permissions:
14+
contents: read
15+
pull-requests: write
16+
17+
jobs:
18+
comment:
19+
name: Post size comment
20+
runs-on: ubuntu-latest
21+
if: github.event.workflow_run.event == 'pull_request'
22+
23+
steps:
24+
- name: Download size report artifact
25+
id: download
26+
continue-on-error: true
27+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
28+
with:
29+
name: wasm-size-report
30+
run-id: ${{ github.event.workflow_run.id }}
31+
github-token: ${{ secrets.GITHUB_TOKEN }}
32+
33+
- name: Post or update comment
34+
if: steps.download.outcome == 'success'
35+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
36+
with:
37+
script: |
38+
const fs = require('fs');
39+
const report = JSON.parse(fs.readFileSync('wasm-size-report.json', 'utf8'));
40+
41+
const delta = report.head_size - report.base_size;
42+
const deltaStr = `${delta > 0 ? '+' : ''}${delta.toLocaleString()} bytes`;
43+
const pct = ((report.head_size / report.limit) * 100).toFixed(1);
44+
const marker = '<!-- wasm-size-report -->';
45+
46+
const body = [
47+
marker,
48+
'### Contract WASM size report',
49+
'',
50+
'| | Bytes |',
51+
'|---|---|',
52+
`| Base (\`${report.base_sha.slice(0, 7)}\`) | ${report.base_size.toLocaleString()} |`,
53+
`| Head (\`${report.head_sha.slice(0, 7)}\`) | ${report.head_size.toLocaleString()} |`,
54+
`| Delta | ${deltaStr} |`,
55+
`| Budget used | ${pct}% of ${report.limit.toLocaleString()} bytes |`,
56+
].join('\n');
57+
58+
const { owner, repo } = context.repo;
59+
const issue_number = report.pr_number;
60+
61+
const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
62+
const existing = comments.data.find((c) => c.body.includes(marker));
63+
64+
if (existing) {
65+
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
66+
} else {
67+
await github.rest.issues.createComment({ owner, repo, issue_number, body });
68+
}

0 commit comments

Comments
 (0)