-
Notifications
You must be signed in to change notification settings - Fork 50
ci: enable PR Preview workflow with security hardening (Phase 1) #2772
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+262
−92
Merged
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 66af2ce
ci: drop source-map strip step (public repo, source already public)
piyalbasu a8a3a73
ci: reframe preview-install warning to focus on review-stage risk
piyalbasu c0326c3
ci: reframe release-notes warning consistently with PR comment
piyalbasu e300b3f
ci: use folded block scalar (>-) for multi-line if:
piyalbasu 5996e09
ci: drop author_association gate (private org memberships fail it)
piyalbasu 64df9d0
ci: move install instructions to release description, slim PR comment
piyalbasu 1e06787
ci: write release notes to file instead of $(cat <<EOF) capture
piyalbasu 08196be
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu a83f88e
ci: address Copilot review feedback
piyalbasu 458d573
ci: point preview at prod freighter-backend (not staging/beta)
piyalbasu b284f50
ci: V1 prod, V2 beta (revert V2 to beta only)
piyalbasu d2bffe7
ci: apply PR Preview security audit fixes
piyalbasu a325207
fix(ci): handle draft releases in pre-create + cleanup delete steps
piyalbasu 4839a74
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu 5d32ec3
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu 614ced5
Merge branch 'master' into enable-pr-preview-workflow
piyalbasu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
| 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 * | ||
|
|
||
|
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 | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.