Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a44a7ec
ci: enable PR Preview workflow with security hardening
piyalbasu May 11, 2026
66af2ce
ci: drop source-map strip step (public repo, source already public)
piyalbasu May 11, 2026
a8a3a73
ci: reframe preview-install warning to focus on review-stage risk
piyalbasu May 11, 2026
c0326c3
ci: reframe release-notes warning consistently with PR comment
piyalbasu May 11, 2026
e300b3f
ci: use folded block scalar (>-) for multi-line if:
piyalbasu May 11, 2026
5996e09
ci: drop author_association gate (private org memberships fail it)
piyalbasu May 11, 2026
64df9d0
ci: move install instructions to release description, slim PR comment
piyalbasu May 12, 2026
1e06787
ci: write release notes to file instead of $(cat <<EOF) capture
piyalbasu May 12, 2026
08196be
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu May 12, 2026
a83f88e
ci: address Copilot review feedback
piyalbasu May 12, 2026
458d573
ci: point preview at prod freighter-backend (not staging/beta)
piyalbasu May 14, 2026
b284f50
ci: V1 prod, V2 beta (revert V2 to beta only)
piyalbasu May 14, 2026
d2bffe7
ci: apply PR Preview security audit fixes
piyalbasu May 15, 2026
a325207
fix(ci): handle draft releases in pre-create + cleanup delete steps
piyalbasu May 15, 2026
4839a74
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu May 15, 2026
5d32ec3
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu May 15, 2026
614ced5
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu May 18, 2026
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
92 changes: 0 additions & 92 deletions .github/workflow-drafts/prPreview.yml

This file was deleted.

203 changes: 203 additions & 0 deletions .github/workflows/prPreview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# ----------------------------------------------------------------------
# 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.
Comment thread
piyalbasu marked this conversation as resolved.
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.
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

- name: Assert 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: Zip extension build
working-directory: ./extension/build
run: zip -qq -r ./build.zip *

Comment thread
piyalbasu marked this conversation as resolved.
- name: Delete existing preview release (idempotent)
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: gh release delete "pr-preview-${PR_NUMBER}" --yes --cleanup-tag || true

- 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:** staging

### 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.
# Pass --target so if the draft is ever manually published, the
# resulting git tag points at the PR's HEAD commit, not master.
gh release create "pr-preview-${PR_NUMBER}" \
./extension/build/build.zip \
--title "PR Preview #${PR_NUMBER}" \
--notes-file /tmp/release-notes.md \
--target "${PR_HEAD_SHA}" \
--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 }}
run: gh release delete "pr-preview-${PR_NUMBER}" --yes --cleanup-tag || true
Loading