Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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.

184 changes: 184 additions & 0 deletions .github/workflows/prPreview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# ----------------------------------------------------------------------
# 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

env:
INDEXER_URL: ${{ secrets.INDEXER_URL }}
INDEXER_V2_URL: ${{ secrets.INDEXER_V2_BETA_URL }}

Comment thread
piyalbasu marked this conversation as resolved.
Outdated
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: {}

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
concurrency:
group: pr-preview-build-${{ github.event.pull_request.number }}
Comment thread
piyalbasu marked this conversation as resolved.
Outdated
cancel-in-progress: true
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"

Comment thread
piyalbasu marked this conversation as resolved.
Outdated
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
URL=$(gh release create "pr-preview-${PR_NUMBER}" \
./extension/build/build.zip \
--title "PR Preview #${PR_NUMBER}" \
--notes-file /tmp/release-notes.md \
--draft)
Comment thread
piyalbasu marked this conversation as resolved.
Outdated
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)"

EXISTING=$(gh api "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
concurrency:
group: pr-preview-cleanup-${{ github.event.pull_request.number }}
# Don't cancel cleanup runs — they need to complete to remove the draft.
cancel-in-progress: false
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