feat: CLI backend auth and same-account iroh auto-trust #1775
Workflow file for this run
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
| name: Preview Deploy | |
| # Creates an ephemeral preview stack on AWS Fargate for each pull request opened | |
| # from a branch in this repo. Five Cloudflare subdomains are created per PR, | |
| # mirroring prod's subdomain structure: | |
| # thunderbolt-pr-{N}.preview.thunderbolt.io → marketing | |
| # app-pr-{N}.preview.thunderbolt.io → app frontend | |
| # api-pr-{N}.preview.thunderbolt.io → backend API | |
| # auth-pr-{N}.preview.thunderbolt.io → keycloak OIDC | |
| # powersync-pr-{N}.preview.thunderbolt.io → powersync | |
| # | |
| # Fork PRs are supported via the `pull_request_target` trigger, gated behind | |
| # the `fork-preview-approval` environment so a maintainer must explicitly | |
| # approve each run (including each new push). Internal (same-repo) PRs use | |
| # `pull_request` and auto-deploy with no human gate, same as before. | |
| # | |
| # Trust boundary: under `pull_request_target`, the workflow file is read from | |
| # main (so a fork cannot modify how the deploy behaves) and the default | |
| # `actions/checkout` in `stack-deploy.yml` picks up main's Pulumi program — | |
| # meaning fork PRs deploy main's infrastructure code with the fork's built | |
| # images. The maintainer-approval gate is the human check on what's in those | |
| # images; nothing on the runner ever executes fork shell scripts with secrets. | |
| # | |
| # Also supports workflow_dispatch for manual testing from any branch. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| branches: [main] | |
| pull_request_target: | |
| types: [opened, synchronize, reopened] | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to deploy a preview for' | |
| type: string | |
| required: true | |
| concurrency: | |
| # event_name in the key so the no-op pull_request run for a fork doesn't | |
| # cancel the approval-pending pull_request_target run on the same PR. | |
| group: preview-pr-${{ github.event.pull_request.number || inputs.pr_number }}-${{ github.event_name }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| packages: write # GHCR image push | |
| id-token: write # AWS OIDC federation for Pulumi | |
| pull-requests: write # sticky PR comment with preview URL | |
| jobs: | |
| resolve: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| pr_number: ${{ steps.r.outputs.pr_number }} | |
| head_sha: ${{ steps.r.outputs.head_sha }} | |
| proceed: ${{ steps.r.outputs.proceed }} | |
| is_fork: ${{ steps.r.outputs.is_fork }} | |
| steps: | |
| - name: Resolve PR + route event | |
| id: r | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} | |
| EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| EVENT_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} | |
| EVENT_BASE_REPO: ${{ github.repository }} | |
| INPUT_PR_NUMBER: ${{ inputs.pr_number }} | |
| run: | | |
| set -euo pipefail | |
| # Both pull_request and pull_request_target fire for every PR. We | |
| # claim each event for exactly one path so the same PR doesn't try | |
| # to deploy twice: | |
| # pull_request → handle ONLY internal (same-repo) PRs | |
| # pull_request_target → handle ONLY fork PRs (with approval gate) | |
| # workflow_dispatch → handle either; forks still hit the gate | |
| if [ "$EVENT_NAME" = "workflow_dispatch" ]; then | |
| PR_NUM="$INPUT_PR_NUMBER" | |
| HEAD_SHA=$(gh pr view "$PR_NUM" --repo "$EVENT_BASE_REPO" --json headRefOid -q .headRefOid) | |
| HEAD_REPO=$(gh pr view "$PR_NUM" --repo "$EVENT_BASE_REPO" --json headRepository -q '.headRepository.nameWithOwner') | |
| else | |
| PR_NUM="$EVENT_PR_NUMBER" | |
| HEAD_SHA="$EVENT_HEAD_SHA" | |
| HEAD_REPO="$EVENT_HEAD_REPO" | |
| fi | |
| IS_FORK=false | |
| if [ "$HEAD_REPO" != "$EVENT_BASE_REPO" ]; then | |
| IS_FORK=true | |
| fi | |
| if [ "$EVENT_NAME" = "pull_request" ] && [ "$IS_FORK" = "true" ]; then | |
| echo "proceed=false" >> $GITHUB_OUTPUT | |
| echo "Fork PR — pull_request_target will handle this; exiting." | |
| exit 0 | |
| fi | |
| if [ "$EVENT_NAME" = "pull_request_target" ] && [ "$IS_FORK" = "false" ]; then | |
| echo "proceed=false" >> $GITHUB_OUTPUT | |
| echo "Internal PR — pull_request already handled this; exiting." | |
| exit 0 | |
| fi | |
| echo "pr_number=$PR_NUM" >> $GITHUB_OUTPUT | |
| echo "head_sha=$HEAD_SHA" >> $GITHUB_OUTPUT | |
| echo "is_fork=$IS_FORK" >> $GITHUB_OUTPUT | |
| echo "proceed=true" >> $GITHUB_OUTPUT | |
| echo "PR #$PR_NUM @ $HEAD_SHA (fork=$IS_FORK, event=$EVENT_NAME)" | |
| approve-fork: | |
| # Required-reviewer gate for fork PRs. Configure required reviewers on the | |
| # `fork-preview-approval` environment in repo Settings → Environments. | |
| # Each new push to a fork PR re-enters this gate (concurrency cancels the | |
| # prior pending run), so approval is per-commit, not per-PR. | |
| needs: resolve | |
| if: needs.resolve.outputs.proceed == 'true' && needs.resolve.outputs.is_fork == 'true' | |
| runs-on: ubuntu-latest | |
| environment: fork-preview-approval | |
| steps: | |
| - run: | | |
| echo "Fork preview approved for PR #${{ needs.resolve.outputs.pr_number }} @ ${{ needs.resolve.outputs.head_sha }}" | |
| publish: | |
| needs: [resolve, approve-fork] | |
| # Runs when resolve told us to proceed AND either: | |
| # - approve-fork ran and succeeded (fork path), OR | |
| # - approve-fork was skipped (internal path; no gate needed) | |
| if: | | |
| always() && | |
| needs.resolve.outputs.proceed == 'true' && | |
| (needs.approve-fork.result == 'success' || needs.approve-fork.result == 'skipped') | |
| uses: ./.github/workflows/images-publish.yml | |
| with: | |
| tag_override: pr-${{ needs.resolve.outputs.pr_number }}-${{ needs.resolve.outputs.head_sha }} | |
| ref: ${{ needs.resolve.outputs.head_sha }} | |
| vite_backend_url: https://api-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io/v1 | |
| deploy: | |
| needs: [resolve, publish] | |
| if: always() && needs.publish.result == 'success' | |
| uses: ./.github/workflows/stack-deploy.yml | |
| with: | |
| action: deploy | |
| platform: fargate | |
| region: us-east-1 | |
| stack_name: preview-pr-${{ needs.resolve.outputs.pr_number }} | |
| version: ${{ needs.publish.outputs.version }} | |
| marketing_hostname: thunderbolt-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | |
| app_hostname: app-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | |
| api_hostname: api-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | |
| auth_hostname: auth-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | |
| powersync_hostname: powersync-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | |
| cloudflare_zone_id: ${{ vars.CLOUDFLARE_ZONE_ID }} | |
| thunderbolt_inference_url: ${{ vars.THUNDERBOLT_INFERENCE_URL }} | |
| # Per-PR stacks read VPC/ALB/postgres/keycloak/powersync from the long-lived | |
| # `previews-shared` stack via Pulumi StackReference. The shared stack must | |
| # exist before this runs; deploy it once via the `Previews Shared Deploy` | |
| # workflow. See deploy/pulumi/SHARED.md. | |
| shared_stack_name: previews-shared | |
| secrets: inherit | |
| comment: | |
| needs: [resolve, deploy] | |
| if: always() && needs.deploy.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Sticky PR comment with preview URLs | |
| uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 | |
| with: | |
| header: preview-env | |
| number: ${{ needs.resolve.outputs.pr_number }} | |
| message: | | |
| **Preview environment deployed** 🚀 | |
| | Service | URL | | |
| |---|---| | |
| | Marketing / blog / docs | https://thunderbolt-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | | |
| | App | https://app-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | | |
| | API | https://api-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | | |
| | Keycloak | https://auth-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | | |
| | PowerSync | https://powersync-pr-${{ needs.resolve.outputs.pr_number }}.preview.thunderbolt.io | | |
| Stack: `preview-pr-${{ needs.resolve.outputs.pr_number }}` · Commit: `${{ needs.resolve.outputs.head_sha }}` | |
| Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — `demo@thunderbolt.io` / `demo` by default. |