forked from stellar/freighter
-
Notifications
You must be signed in to change notification settings - Fork 0
264 lines (239 loc) · 12.7 KB
/
Copy pathprPreview.yml
File metadata and controls
264 lines (239 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# ----------------------------------------------------------------------
# SECURITY INVARIANT — read before editing.
#
# Do NOT add any of these triggers without a strict author-association
# gate at the very first job step:
# issue_comment, pull_request_target, pull_request_review,
# pull_request_review_comment, workflow_run
# These triggers run with FULL repo secrets and a writable GITHUB_TOKEN
# even when activity originates from a fork PR. Combined with checkout-
# of-PR-head + execute-code-from-PR (which any build inherently does),
# they enable Remote Code Execution by anyone who can comment on a PR
# or open one.
#
# Background: the PR Preview design and a pre-rollout security review
# explicitly forbid these triggers. See the design doc Security section.
# ----------------------------------------------------------------------
name: PR Preview
on:
pull_request:
types: [opened, synchronize, reopened, closed]
# Default for all jobs is no permissions; each job opts in to the
# narrowest scope it needs below.
permissions: {}
# Single concurrency group across build and cleanup. When a PR closes
# mid-build, the close-event cleanup job cancels the still-running
# build (cancel-in-progress: true) so the build can't finish and
# create an orphaned release after the cleanup has already deleted
# whatever was there. The next build's "Delete existing preview
# release" step is idempotent, so a cancelled cleanup leaves no
# permanent half-state.
concurrency:
group: pr-preview-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
build-and-release:
name: Build and Release PR Preview
# Two-layer gate evaluated BEFORE any secret is injected:
# 1. Skip on PR close (handled by cleanup-release job)
# 2. Reject fork PRs (defense-in-depth — platform also withholds secrets/token)
# NOTE: we don't gate on author_association because the field in the
# webhook event payload only reflects PUBLIC org membership; SDF members
# with private memberships show as CONTRIBUTOR, which would lock them
# out. Non-SDF gating is handled by the org-level "Require approval
# for outside collaborators" setting plus the platform-level fork-PR
# secret-withholding.
if: >-
github.event.action != 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: write # release create/delete + tag operations
pull-requests: write # sticky preview-link comment on the PR
env:
# Secrets injected at job level (not workflow level) so a future job
# added without the same fork-guard if: doesn't inherit them.
#
# V1: PROD. V1 staging/beta does not have a public DNS entry — the
# only v1 stg ingress is kube-internal and would require sshuttle.
# V2: BETA. V2 beta is publicly reachable and is the right target
# for preview-stage testing.
# freighter-backend is a read-side indexer (balances, assets,
# history); the wallet submits txs directly to Horizon/RPC, so the
# backend choice here doesn't affect write paths.
INDEXER_URL: ${{ secrets.INDEXER_URL }}
INDEXER_V2_URL: ${{ secrets.INDEXER_V2_BETA_URL }}
steps:
- name: Validate required secrets
if: ${{ env.INDEXER_URL == '' || env.INDEXER_V2_URL == '' }}
run: |
echo "::error::INDEXER_URL or INDEXER_V2_URL is empty. Verify repo Secrets are configured."
exit 1
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# Default leaves a GITHUB_TOKEN auth header in .git/config for the
# rest of the job. With contents:write granted, any subsequent
# code execution (yarn lifecycle scripts, build scripts) could
# `git push` using those persisted creds. `gh` uses GH_TOKEN env
# separately so disabling this doesn't affect the release flow.
persist-credentials: false
- name: Assert source manifest has no top-level `key` field
run: |
if jq -e 'has("key")' ./extension/public/static/manifest/v3.json > /dev/null; then
echo "::error::manifest/v3.json contains a top-level 'key' field. Preview installs would share a Chromium extension ID with Web Store Freighter, leaking storage."
exit 1
fi
- name: Rewrite preview manifest identity (Chromium + Firefox)
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
PREVIEW_NAME="Freighter PR Preview #${PR_NUMBER}"
PREVIEW_VERSION_NAME="pr-preview-${PR_NUMBER}"
PREVIEW_GECKO_ID="freighter-pr-preview-${PR_NUMBER}@stellar.org"
jq --arg name "$PREVIEW_NAME" --arg vn "$PREVIEW_VERSION_NAME" \
'.name = $name | .version_name = $vn' \
./extension/public/static/manifest/v3.json > /tmp/v3.json
mv /tmp/v3.json ./extension/public/static/manifest/v3.json
jq --arg name "$PREVIEW_NAME" --arg vn "$PREVIEW_VERSION_NAME" --arg gid "$PREVIEW_GECKO_ID" \
'.name = $name | .version_name = $vn | .browser_specific_settings.gecko.id = $gid' \
./extension/public/static/manifest/v2.json > /tmp/v2.json
mv /tmp/v2.json ./extension/public/static/manifest/v2.json
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 22
- name: Enable Corepack
run: corepack enable
- name: Install + build extension (production)
run: yarn && yarn build:freighter-api && yarn build:extension:production
- name: Use BETA icons
run: |
rm -rf ./extension/build/images
mv ./extension/build/beta_images ./extension/build/images
- name: Assert built manifest has no top-level `key` field
# The source-manifest assertion above catches a hand-edited `key`,
# but a webpack plugin / postinstall script / transitive dep could
# still inject one into the built output. Re-check post-build,
# pre-zip, so the shipping artifact is what we asserted on.
run: |
if jq -e 'has("key")' ./extension/build/manifest.json > /dev/null; then
echo "::error::Built manifest.json contains a top-level 'key' field. The build pipeline injected an extension-ID-fixing key after the source assertion; preview install would collide with Web Store Freighter."
exit 1
fi
- name: Zip extension build
working-directory: ./extension/build
run: zip -qq -r ./build.zip *
- name: Delete existing preview release (idempotent)
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# Check-then-delete instead of `|| true`. `|| true` would also
# swallow transient API errors (network, 422 tag-conflict) and let
# the subsequent `gh release create` silently reuse a stale tag.
run: |
if gh release view "pr-preview-${PR_NUMBER}" > /dev/null 2>&1; then
# `--cleanup-tag` 422s on draft releases because drafts don't
# create the git tag until publish. Branch on isDraft so we
# only ask for tag-cleanup when there is actually a tag.
IS_DRAFT=$(gh release view "pr-preview-${PR_NUMBER}" --json isDraft --jq '.isDraft')
if [ "$IS_DRAFT" = "true" ]; then
gh release delete "pr-preview-${PR_NUMBER}" --yes
else
gh release delete "pr-preview-${PR_NUMBER}" --yes --cleanup-tag
fi
fi
- name: Create draft preview release
id: release
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
# Write notes to a file rather than $(cat <<EOF) because bash's
# command-substitution parser tokenizes single quotes inside the
# heredoc body, breaking on apostrophes in prose.
cat > /tmp/release-notes.md <<EOF
Internal preview build for PR [#${PR_NUMBER}](${PR_URL}). SDF collaborators only — non-SDF GitHub users get 404 on this page. Auto-deleted when the PR is closed.
**Commit:** ${PR_HEAD_SHA}
**Backend:** V1 production, V2 beta (read-only indexer; wallet writes go direct to Horizon/RPC)
### How to install (Chromium)
1. Download \`build.zip\` from the Assets section below and unzip it
2. Open \`chrome://extensions\` in Chrome, Edge, or Brave
3. Enable Developer Mode (toggle in the top-right)
4. Click "Load Unpacked" and select the unzipped folder
5. The extension installs as "Freighter PR Preview #${PR_NUMBER}" with beta icons
### Important
This code is still under review and may contain bugs that have not been caught yet. **Use caution before signing transactions with real funds** — consider testing with a testnet wallet instead.
EOF
# Don't capture stdout from `gh release create` — it can include
# progress/status lines, not just the URL. Query the URL with a
# dedicated `gh release view --json url` call instead.
#
# No --target: if the draft is ever manually published, the tag
# falls back to master HEAD (reviewed code) rather than the PR's
# HEAD commit (unreviewed). Dropping the targeted-tag property
# closes a one-click escalation path where any account that can
# click "Publish release" on a draft mints an official-looking
# release containing arbitrary PR-head code; branch-protection
# rules do not extend to release publication.
gh release create "pr-preview-${PR_NUMBER}" \
./extension/build/build.zip \
--title "PR Preview #${PR_NUMBER}" \
--notes-file /tmp/release-notes.md \
--draft > /dev/null
URL=$(gh release view "pr-preview-${PR_NUMBER}" --json url --jq '.url')
echo "url=${URL}" >> "$GITHUB_OUTPUT"
- name: Post/update sticky PR comment with preview link
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RELEASE_URL: ${{ steps.release.outputs.url }}
run: |
MARKER="<!-- pr-preview-comment -->"
BODY="${MARKER}"$'\n'"PR Preview build is ready: ${RELEASE_URL} (SDF collaborators only — install instructions in the release description)"
# --paginate so this works on PRs with >30 comments (default page
# size). Without it, the marker comment can fall off a later page
# and we'd post a duplicate instead of editing in place.
EXISTING=$(gh api --paginate "repos/${GH_REPO}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.body | startswith(\"${MARKER}\")) | .id" | head -1)
if [ -n "$EXISTING" ]; then
gh api -X PATCH "repos/${GH_REPO}/issues/comments/${EXISTING}" -f body="$BODY"
else
gh pr comment "${PR_NUMBER}" --body "$BODY"
fi
cleanup-release:
name: Cleanup PR Preview Release
if:
github.event.action == 'closed' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: write
# Shares the workflow-level concurrency group with the build job so a
# close-event cancels any in-flight build before deleting the release —
# prevents the build from finishing AFTER cleanup and re-creating an
# orphaned release.
steps:
- name: Delete draft release and tag
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# Skip silently if the release doesn't exist (race with a cancelled
# build), but fail loudly on any other delete error rather than
# masking it with `|| true`.
run: |
if gh release view "pr-preview-${PR_NUMBER}" > /dev/null 2>&1; then
# `--cleanup-tag` 422s on draft releases because drafts don't
# create the git tag until publish. Branch on isDraft so we
# only ask for tag-cleanup when there is actually a tag.
IS_DRAFT=$(gh release view "pr-preview-${PR_NUMBER}" --json isDraft --jq '.isDraft')
if [ "$IS_DRAFT" = "true" ]; then
gh release delete "pr-preview-${PR_NUMBER}" --yes
else
gh release delete "pr-preview-${PR_NUMBER}" --yes --cleanup-tag
fi
fi