Repository navigation
ci(relay): deploy labeled PRs to relay preview stages #23872
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
Changes from 8 commits
ea5dea2
ac68c39
0d37f4b
554f456
e06f4f7
5d543a2
1474563
4a02715
9187428
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| name: Clean up relay preview | ||
|
|
||
| # Tears down stage `pr-<n>` when `relay:preview` is removed or the PR closes, | ||
| # and retries teardown for closed labeled PRs on a schedule. A label stays on a | ||
| # PR only while its teardown is outstanding, so the scheduled retry lists | ||
| # exactly the failures. | ||
| # | ||
| # This runs under pull_request_target, so the workflow and the Alchemy program | ||
| # come from the default branch. No PR code runs here, which is why it needs no | ||
| # reviewer approval and why a PR cannot edit its own teardown to reach secrets. | ||
| on: | ||
| pull_request_target: | ||
| types: [unlabeled, closed] | ||
| schedule: | ||
| - cron: "17 7 * * *" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| issues: read | ||
| pull-requests: read | ||
|
|
||
| jobs: | ||
| cleanup_pr: | ||
| name: Tear down relay preview | ||
| if: >- | ||
| github.ref == 'refs/heads/main' && | ||
| github.event_name == 'pull_request_target' && | ||
| ( | ||
| (github.event.action == 'unlabeled' && github.event.label.name == 'relay:preview') || | ||
| (github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'relay:preview')) | ||
| ) | ||
| # Job-level so that events which skip this job, such as a closed PR without | ||
| # the label, never take the group's pending slot. GitHub replaces a pending | ||
| # run in a group with a newer one, which would drop a queued teardown. | ||
| concurrency: | ||
| group: relay-preview-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: false | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
| runs-on: blacksmith-8vcpu-ubuntu-2404 | ||
| timeout-minutes: 30 | ||
| environment: | ||
| name: relay-preview-cleanup | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| STAGE: pr-${{ github.event.pull_request.number }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | ||
| PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }} | ||
| AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }} | ||
| RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }} | ||
| RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }} | ||
| CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} | ||
| CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }} | ||
| APNS_ENABLED: "false" | ||
| ALCHEMY_TELEMETRY_DISABLED: "1" | ||
| steps: | ||
| - name: Checkout default branch | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ github.event.repository.default_branch }} | ||
| sparse-checkout: | | ||
| /* | ||
| !/.repos/ | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Setup Vite+ | ||
| uses: voidzero-dev/setup-vp@v1 | ||
| with: | ||
| node-version-file: package.json | ||
| cache: true | ||
| run-install: | | ||
| args: | ||
| - --filter=t3code-relay... | ||
|
|
||
| # Destroy also runs the preview tunnel sweep, which is part of the stack. | ||
| - name: Destroy preview stage | ||
| run: vp run --filter t3code-relay destroy --stage "$STAGE" --yes --no-input | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }} | ||
| PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }} | ||
| AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} | ||
| CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} | ||
|
|
||
| # Only reached after a successful destroy. A failed one keeps the label, | ||
| # so the scheduled retry picks the PR up again. | ||
| - name: Clear the teardown marker | ||
| if: github.event.action == 'closed' | ||
| run: gh pr edit "${{ github.event.pull_request.number }}" --repo "$GITHUB_REPOSITORY" --remove-label relay:preview | ||
|
|
||
| list_closed_previews: | ||
| name: List closed relay previews | ||
| if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request_target' | ||
| runs-on: blacksmith-8vcpu-ubuntu-2404 | ||
| timeout-minutes: 10 | ||
| outputs: | ||
| numbers: ${{ steps.list.outputs.numbers }} | ||
| steps: | ||
| # The issues endpoint is paginated by gh, so there is no listing cap. Pull | ||
| # requests appear there with a `pull_request` key. | ||
| - name: List closed PRs labeled relay:preview | ||
| id: list | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| numbers=$(gh api --paginate --method GET "repos/$GITHUB_REPOSITORY/issues" \ | ||
| -f labels=relay:preview -f state=closed -f per_page=100 \ | ||
| --jq '.[] | select(.pull_request) | .number') \ | ||
| || { echo "Could not list closed relay:preview PRs" >&2; exit 1; } | ||
| json=$(printf '%s\n' "$numbers" | jq -Rsc 'split("\n") | map(select(length > 0) | tonumber)') | ||
| echo "Closed relay:preview PRs: $json" | ||
| echo "numbers=$json" >> "$GITHUB_OUTPUT" | ||
|
|
||
| sweep_pr: | ||
| name: Retry teardown for PR ${{ matrix.number }} | ||
| needs: list_closed_previews | ||
| if: github.ref == 'refs/heads/main' && needs.list_closed_previews.outputs.numbers != '[]' | ||
| runs-on: blacksmith-8vcpu-ubuntu-2404 | ||
| timeout-minutes: 30 | ||
| strategy: | ||
| fail-fast: false | ||
| max-parallel: 4 | ||
| matrix: | ||
| number: ${{ fromJSON(needs.list_closed_previews.outputs.numbers) }} | ||
| # Same group as the deploy and teardown for this PR, so a reopened PR's | ||
| # deploy cannot run concurrently with a teardown of its previous stage. | ||
| concurrency: | ||
| group: relay-preview-${{ matrix.number }} | ||
| cancel-in-progress: false | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
| environment: | ||
| name: relay-preview-cleanup | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| STAGE: pr-${{ matrix.number }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | ||
| PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }} | ||
| AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }} | ||
| RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }} | ||
| RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }} | ||
| CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} | ||
| CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }} | ||
| APNS_ENABLED: "false" | ||
| ALCHEMY_TELEMETRY_DISABLED: "1" | ||
| steps: | ||
| # The lock above is taken before this runs, so the state read here is | ||
| # current. An open PR, including one reopened since the listing, is left | ||
| # alone. GitHub reports merged PRs as MERGED, so anything not OPEN counts. | ||
| - name: Confirm the PR is not open | ||
| id: eligible | ||
| run: | | ||
| state=$(gh pr view "${{ matrix.number }}" --repo "$GITHUB_REPOSITORY" \ | ||
| --json state --jq 'if .state == "OPEN" then "no" else "yes" end') \ | ||
| || { echo "Could not read PR ${{ matrix.number }}" >&2; exit 1; } | ||
| echo "eligible=$state" >> "$GITHUB_OUTPUT" | ||
| [ "$state" = yes ] || echo "PR ${{ matrix.number }} is open again; leaving $STAGE alone." | ||
|
|
||
| - name: Checkout default branch | ||
| if: steps.eligible.outputs.eligible == 'yes' | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ github.event.repository.default_branch }} | ||
| sparse-checkout: | | ||
|
macroscopeapp[bot] marked this conversation as resolved.
|
||
| /* | ||
| !/.repos/ | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Setup Vite+ | ||
| if: steps.eligible.outputs.eligible == 'yes' | ||
| uses: voidzero-dev/setup-vp@v1 | ||
| with: | ||
| node-version-file: package.json | ||
| cache: true | ||
| run-install: | | ||
| args: | ||
| - --filter=t3code-relay... | ||
|
|
||
| - name: Destroy preview stage | ||
| if: steps.eligible.outputs.eligible == 'yes' | ||
| run: vp run --filter t3code-relay destroy --stage "$STAGE" --yes --no-input | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }} | ||
| PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }} | ||
| AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} | ||
| CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} | ||
|
|
||
| - name: Clear the teardown marker | ||
| if: steps.eligible.outputs.eligible == 'yes' | ||
| run: gh pr edit "${{ matrix.number }}" --repo "$GITHUB_REPOSITORY" --remove-label relay:preview | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| name: Deploy relay preview | ||
|
|
||
| # Labeling a same-repository PR `relay:preview` deploys Alchemy stage `pr-<n>`. | ||
| # This job runs the PR's own code with account credentials, so it waits for an | ||
| # approval in the `relay-preview` environment on every run. Teardown is in | ||
| # relay-preview-cleanup.yml and never runs PR code. | ||
| on: | ||
| pull_request: | ||
| types: [labeled, synchronize, reopened] | ||
|
Comment on lines
+7
to
+9
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🔎 Supported by static analysis🏁 Script executed: set -eu
printf '%s\n' '--- workflow ---'
nl -ba .github/workflows/relay-preview.yml
printf '%s\n' '--- relevant workflow references ---'
rg -n -F --glob '.github/workflows/*.yml' -e 'environment:' -e 'secrets.' -e 'cloudflare' -e 'planetscale' .github/workflows || test "$?" -eq 1
printf '%s\n' '--- changed workflow diff against supplied merge base ---'
git diff --no-ext-diff --unified=40 52c2b38ea41ee2271f5aca527009d36d25432689 -- .github/workflows/relay-preview.ymlRepository: pingdotgg/t3code Length of output: 18224 🏁 Script executed: set -eu
printf '%s\n' '--- exact deployment-secret references and setup text ---'
rg -n -F --hidden --glob '!.git/**' \
-e 'CLOUDFLARE_API_TOKEN' \
-e 'PLANETSCALE_API_TOKEN_ID' \
-e 'PLANETSCALE_API_TOKEN' \
-e 'AXIOM_TOKEN' \
-e 'CLERK_SECRET_KEY' \
-e 'relay-preview' \
-e 'environment secrets' \
-e 'repository secrets' \
. || test "$?" -eq 1
printf '%s\n' '--- workflow-related repository files ---'
find .github -maxdepth 3 -type f -print | sortRepository: pingdotgg/t3code Length of output: 7773 🌐 Web query:
💡 Result: 🏁 Script executed: set -eu
printf '%s\n' '--- relay deployment secret contract ---'
nl -ba docs/operations/release.md | sed -n '110,175p'
printf '%s\n' '--- relay preview setup documentation ---'
nl -ba infra/relay/README.md | sed -n '120,170p'Repository: pingdotgg/t3code Length of output: 5798 Keep PR-editable workflows away from repository deployment secrets. This workflow checks out and runs the pull request's code. If a same-repository contributor can edit the workflow, they can remove Move the credentials used by this job to 🤖 Prompt for AI AgentsSource: Learnings |
||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
|
|
||
| concurrency: | ||
| group: relay-preview-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| deploy_preview: | ||
| name: Deploy relay preview | ||
| if: >- | ||
| github.event.pull_request.head.repo.full_name == github.repository && | ||
| contains(github.event.pull_request.labels.*.name, 'relay:preview') | ||
| runs-on: blacksmith-8vcpu-ubuntu-2404 | ||
| timeout-minutes: 20 | ||
| environment: | ||
| name: relay-preview | ||
| env: | ||
| STAGE: pr-${{ github.event.pull_request.number }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | ||
| PLANETSCALE_ORGANIZATION: ${{ vars.PLANETSCALE_ORGANIZATION }} | ||
| AXIOM_ORG_ID: ${{ vars.AXIOM_ORG_ID }} | ||
| RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }} | ||
| RELAY_TUNNEL_ZONE_NAME: ${{ vars.RELAY_TUNNEL_ZONE_NAME }} | ||
| CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} | ||
| CLERK_JWT_AUDIENCE: ${{ vars.CLERK_JWT_AUDIENCE }} | ||
| # Previews take no alert notifier, no tunnel cleanup, and no custom | ||
| # domain, so they stay silent and reap nothing outside their own stage. | ||
| APNS_ENABLED: "false" | ||
| ALCHEMY_TELEMETRY_DISABLED: "1" | ||
| steps: | ||
| - name: Confirm the PR is still open and labeled | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| run: | | ||
| state=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json state,labels \ | ||
| --jq 'if .state == "OPEN" and any(.labels[]; .name == "relay:preview") then "ok" else "stale" end') | ||
| if [ "$state" != "ok" ]; then | ||
| echo "PR #$PR_NUMBER is no longer open with relay:preview; not deploying." >&2 | ||
| exit 1 | ||
| fi | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| ref: ${{ github.event.pull_request.head.sha }} | ||
| sparse-checkout: | | ||
| /* | ||
| !/.repos/ | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Setup Vite+ | ||
| uses: voidzero-dev/setup-vp@v1 | ||
| with: | ||
| node-version-file: package.json | ||
| cache: true | ||
| run-install: | | ||
| args: | ||
| - --filter=t3code-relay... | ||
| - name: Deploy preview stage | ||
| run: vp run --filter t3code-relay deploy --stage "$STAGE" --yes --no-input | ||
| env: | ||
| T3CODE_RELAY_CLIENT_CONFIG_ENV: ${{ runner.temp }}/relay-client-config.env | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| PLANETSCALE_API_TOKEN_ID: ${{ secrets.PLANETSCALE_API_TOKEN_ID }} | ||
| PLANETSCALE_API_TOKEN: ${{ secrets.PLANETSCALE_API_TOKEN }} | ||
| AXIOM_TOKEN: ${{ secrets.AXIOM_TOKEN }} | ||
| CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} | ||
Uh oh!
There was an error while loading. Please reload this page.