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 4 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,139 @@ | ||
| name: Clean up relay preview | ||
|
|
||
| # Tears down stage `pr-<n>` when `relay:preview` is removed or the PR closes, | ||
| # and sweeps stages left behind by a failed teardown. | ||
| # | ||
| # 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 | ||
| pull-requests: read | ||
|
|
||
| concurrency: | ||
| group: relay-preview-${{ github.event.pull_request.number || 'sweep' }} | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| cleanup_pr: | ||
| name: Tear down relay preview | ||
| if: >- | ||
| 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')) | ||
| ) | ||
| runs-on: blacksmith-8vcpu-ubuntu-2404 | ||
| timeout-minutes: 30 | ||
| environment: | ||
| name: relay-preview-cleanup | ||
| 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 }} | ||
| 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 }} | ||
|
|
||
| sweep_orphans: | ||
| name: Sweep orphaned relay previews | ||
| if: github.event_name != 'pull_request_target' | ||
| runs-on: blacksmith-8vcpu-ubuntu-2404 | ||
| timeout-minutes: 60 | ||
| environment: | ||
| name: relay-preview-cleanup | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| 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: | ||
| sparse-checkout: | | ||
|
macroscopeapp[bot] marked this conversation as resolved.
|
||
| /* | ||
| !/.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: Destroy stages of closed labeled PRs | ||
| 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 }} | ||
| run: | | ||
| # A failed listing must fail the step, not look like an empty sweep. | ||
| limit=1000 | ||
| numbers=$(gh pr list --repo "$GITHUB_REPOSITORY" --state closed \ | ||
| --label "relay:preview" --limit "$limit" --json number --jq '.[].number') \ | ||
| || { echo "Could not list closed relay:preview PRs" >&2; exit 1; } | ||
| # gh silently truncates at --limit, which would skip the oldest stages. | ||
| if [ "$(printf '%s\n' "$numbers" | grep -c .)" -ge "$limit" ]; then | ||
| echo "Reached the $limit-PR listing limit; paginate before trusting this sweep" >&2 | ||
| exit 1 | ||
| fi | ||
| failed=0 | ||
| for number in $numbers; do | ||
| stage="pr-$number" | ||
| echo "::group::$stage" | ||
| vp run --filter t3code-relay destroy --stage "$stage" --yes --no-input \ | ||
|
macroscopeapp[bot] marked this conversation as resolved.
Outdated
|
||
| || { echo "Cleanup failed for $stage" >&2; failed=1; } | ||
| echo "::endgroup::" | ||
| done | ||
| exit "$failed" | ||
| 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 }} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { describe, expect, it } from "vite-plus/test"; | ||
|
|
||
| import { | ||
| isPreviewStage, | ||
| isPreviewTunnelName, | ||
| isPreviewTunnelRecordName, | ||
| previewSweepTargets, | ||
| tunnelCnameContent, | ||
| } from "./previewStages.ts"; | ||
|
|
||
| const KEY = "0123456789abcdef"; | ||
|
|
||
| describe("preview stages", () => { | ||
| it("accepts only pull-request stages", () => { | ||
| expect(isPreviewStage("pr-42")).toBe(true); | ||
| expect(isPreviewStage("prod")).toBe(false); | ||
| expect(isPreviewStage("dev_julius")).toBe(false); | ||
| expect(isPreviewStage("pr-0")).toBe(false); | ||
| expect(isPreviewStage("pr-42/../prod")).toBe(false); | ||
| }); | ||
|
|
||
| it("matches only tunnels of the exact stage", () => { | ||
| expect(isPreviewTunnelName(`t3coderelay-managedendpoint-pr-42-${KEY}`, "pr-42")).toBe(true); | ||
| expect(isPreviewTunnelName(`t3coderelay-managedendpoint-pr-42-${KEY}`, "pr-4")).toBe(false); | ||
| expect(isPreviewTunnelName(`t3coderelay-managedendpoint-prod-${KEY}`, "pr-42")).toBe(false); | ||
| expect(isPreviewTunnelName(`t3coderelay-managedendpoint-pr-42-xyz`, "pr-42")).toBe(false); | ||
| }); | ||
|
|
||
| it("matches only hostnames of the exact stage under the tunnel zone", () => { | ||
| const zone = "tunnels.example.com"; | ||
| expect(isPreviewTunnelRecordName(`pr-42-${KEY}.${zone}`, "pr-42", zone)).toBe(true); | ||
| expect(isPreviewTunnelRecordName(`PR-42-${KEY}.Tunnels.example.com`, "pr-42", zone)).toBe(true); | ||
| expect(isPreviewTunnelRecordName(`pr-4-${KEY}.${zone}`, "pr-42", zone)).toBe(false); | ||
| expect(isPreviewTunnelRecordName(`pr-42-${KEY}.other.com`, "pr-42", zone)).toBe(false); | ||
| expect(isPreviewTunnelRecordName(`relay.${zone}`, "pr-42", zone)).toBe(false); | ||
| }); | ||
|
|
||
| it("targets only this stage's tunnels and the records that point at them", () => { | ||
| const zone = "tunnels.example.com"; | ||
| const mine = `t3coderelay-managedendpoint-pr-42-${KEY}`; | ||
| const otherStage = `t3coderelay-managedendpoint-pr-7-${KEY}`; | ||
| const otherKey = "fedcba9876543210"; | ||
| const targets = previewSweepTargets({ | ||
| stage: "pr-42", | ||
| tunnelZoneName: zone, | ||
| tunnels: [ | ||
| { id: "t-mine", name: mine }, | ||
| { id: "t-other", name: otherStage }, | ||
| ], | ||
| records: [ | ||
| { id: "r-mine", name: `pr-42-${KEY}.${zone}`, content: tunnelCnameContent("t-mine") }, | ||
| { id: "r-other", name: `pr-7-${KEY}.${zone}`, content: tunnelCnameContent("t-other") }, | ||
| // Right label, but it targets a tunnel outside this sweep. | ||
| { id: "r-stranger", name: `pr-42-${otherKey}.${zone}`, content: "x.cfargotunnel.com" }, | ||
| ], | ||
| }); | ||
| expect(targets.tunnels.map((tunnel) => tunnel.id)).toEqual(["t-mine"]); | ||
| expect(targets.records.map((record) => record.id)).toEqual(["r-mine"]); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.