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 1 commit
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,141 @@ | ||
| 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 first, so the Worker that creates tunnels is gone before the | ||
| # sweep runs. If destroy fails the sweep is skipped, and the nightly | ||
| # sweep retries both. | ||
| - 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 }} | ||
|
|
||
| - name: Sweep preview tunnels and DNS | ||
| run: vp run --filter t3code-relay sweep:preview | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| PREVIEW_STAGE: ${{ env.STAGE }} | ||
| APPLY: "true" | ||
|
|
||
| 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 and sweep stages of closed labeled PRs | ||
| env: | ||
| APPLY: "true" | ||
| 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: | | ||
| failed=0 | ||
| for number in $(gh pr list --repo "$GITHUB_REPOSITORY" --state closed \ | ||
| --label "relay:preview" --limit 200 --json number --jq '.[].number'); do | ||
|
macroscopeapp[bot] marked this conversation as resolved.
Outdated
macroscopeapp[bot] marked this conversation as resolved.
Outdated
coderabbitai[bot] marked this conversation as resolved.
Outdated
macroscopeapp[bot] marked this conversation as resolved.
Outdated
|
||
| 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
|
||
| && PREVIEW_STAGE="$stage" vp run --filter t3code-relay sweep:preview \ | ||
| || { 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,134 @@ | ||
| import * as NodeRuntime from "@effect/platform-node/NodeRuntime"; | ||
| import * as Config from "effect/Config"; | ||
| import * as Console from "effect/Console"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Redacted from "effect/Redacted"; | ||
| import * as Schema from "effect/Schema"; | ||
| import * as FetchHttpClient from "effect/http/FetchHttpClient"; | ||
| import * as HttpClient from "effect/http/HttpClient"; | ||
| import * as HttpClientRequest from "effect/http/HttpClientRequest"; | ||
| import * as HttpClientResponse from "effect/http/HttpClientResponse"; | ||
|
|
||
| import { managedEndpointTunnelNamePrefix } from "../src/deploymentConfig.ts"; | ||
| import { | ||
| isPreviewStage, | ||
| isPreviewTunnelName, | ||
| isPreviewTunnelRecordName, | ||
| tunnelCnameContent, | ||
| } from "../src/previewStages.ts"; | ||
|
|
||
| // `alchemy destroy` removes the Worker and its custom domain. Managed endpoint | ||
| // tunnels and their CNAMEs are created at runtime, so Alchemy does not track | ||
| // them and they must be removed here. Dry run unless APPLY=true. | ||
|
|
||
| const API = "https://api.cloudflare.com/client/v4"; | ||
| const PAGE_SIZE = 100; | ||
|
|
||
| const TunnelPage = Schema.Struct({ | ||
| result: Schema.Array( | ||
| Schema.Struct({ id: Schema.String, name: Schema.optional(Schema.NullOr(Schema.String)) }), | ||
| ), | ||
| }); | ||
| const ZonePage = Schema.Struct({ result: Schema.Array(Schema.Struct({ id: Schema.String })) }); | ||
| const DnsRecordPage = Schema.Struct({ | ||
| result: Schema.Array( | ||
| Schema.Struct({ | ||
| id: Schema.String, | ||
| name: Schema.String, | ||
| content: Schema.optional(Schema.NullOr(Schema.String)), | ||
| }), | ||
| ), | ||
| }); | ||
|
|
||
| const collectPages = <T, E, R>( | ||
| fetchPage: (page: number) => Effect.Effect<ReadonlyArray<T>, E, R>, | ||
| ) => | ||
| Effect.gen(function* () { | ||
| const items: Array<T> = []; | ||
| for (let page = 1; ; page += 1) { | ||
| const batch = yield* fetchPage(page); | ||
| items.push(...batch); | ||
| if (batch.length < PAGE_SIZE) return items; | ||
| } | ||
| }); | ||
|
|
||
| const main = Effect.gen(function* () { | ||
| const stage = yield* Config.String("PREVIEW_STAGE"); | ||
| if (!isPreviewStage(stage)) { | ||
| return yield* Effect.die(`Refusing to sweep non-preview stage '${stage}'`); | ||
| } | ||
| const apply = yield* Config.Boolean("APPLY").pipe(Config.withDefault(false)); | ||
| const accountId = yield* Config.String("CLOUDFLARE_ACCOUNT_ID"); | ||
| const token = yield* Config.Redacted("CLOUDFLARE_API_TOKEN"); | ||
| const tunnelZoneName = yield* Config.String("RELAY_TUNNEL_ZONE_NAME"); | ||
| const client = yield* HttpClient.HttpClient; | ||
|
|
||
| const send = (request: HttpClientRequest.HttpClientRequest) => | ||
| request.pipe( | ||
| HttpClientRequest.bearerToken(Redacted.value(token)), | ||
| client.execute, | ||
| Effect.flatMap(HttpClientResponse.filterStatusOk), | ||
| ); | ||
| const getJson = <A, I>(url: string, params: Record<string, string>, schema: Schema.Codec<A, I>) => | ||
| send(HttpClientRequest.get(url, { urlParams: params })).pipe( | ||
| Effect.flatMap(HttpClientResponse.schemaBodyJson(schema)), | ||
| ); | ||
|
|
||
| const tunnelPrefix = managedEndpointTunnelNamePrefix(stage); | ||
| const tunnels = (yield* collectPages((page) => | ||
| getJson( | ||
| `${API}/accounts/${accountId}/cfd_tunnel`, | ||
| { | ||
| is_deleted: "false", | ||
| include_prefix: tunnelPrefix, | ||
| per_page: String(PAGE_SIZE), | ||
| page: String(page), | ||
| }, | ||
| TunnelPage, | ||
| ).pipe(Effect.map((body) => body.result)), | ||
| )).filter((tunnel) => tunnel.name != null && isPreviewTunnelName(tunnel.name, stage)); | ||
| const tunnelIds = new Set(tunnels.map((tunnel) => tunnel.id)); | ||
|
|
||
| const zone = yield* getJson(`${API}/zones`, { name: tunnelZoneName }, ZonePage); | ||
| const zoneId = zone.result[0]?.id; | ||
| if (zoneId === undefined) { | ||
| return yield* Effect.die(`No Cloudflare zone named ${tunnelZoneName}`); | ||
| } | ||
| const records = (yield* collectPages((page) => | ||
| getJson( | ||
| `${API}/zones/${zoneId}/dns_records`, | ||
| { type: "CNAME", per_page: String(PAGE_SIZE), page: String(page) }, | ||
| DnsRecordPage, | ||
| ).pipe(Effect.map((body) => body.result)), | ||
| )).filter( | ||
| (record) => | ||
| isPreviewTunnelRecordName(record.name, stage, tunnelZoneName) && | ||
| [...tunnelIds].some((id) => record.content === tunnelCnameContent(id)), | ||
|
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. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '55,134p' infra/relay/scripts/sweep-preview-tunnels.ts
rg -n 'Tunnel|CNAME|cfargotunnel' infra/relay --glob '!node_modules' -lRepository: pingdotgg/t3code Length of output: 4509 🏁 Script executed: set -o pipefail
printf '%s\n' '--- PR diff files and relevant hunks ---'
git diff --stat 52c2b38ea41ee2271f5aca527009d36d25432689..ea5dea29556a9d8a66f02d8da33002f57ff87a1d
git diff --unified=25 52c2b38ea41ee2271f5aca527009d36d25432689..ea5dea29556a9d8a66f02d8da33002f57ff87a1d -- 'infra/relay' '.github/workflows' | rg -n -C 8 -e 'destroy|CNAME|cfd_tunnel|tunnel|sweep-preview|preview' || true
printf '%s\n' '--- Alchemy and tunnel resource references ---'
rg -n -F --glob '*.ts' --glob '*.tsx' --glob '*.json' --glob '*.yml' --glob '*.yaml' -e 'alchemy' -e 'CNAME' -e 'cfd_tunnel' -e 'cfargotunnel.com' -e 'destroy' -e 'deleteTunnel' -e 'tunnelId' infra .github || test "$?" -eq 1
printf '%s\n' '--- relevant file inventory ---'
rg --files infra/relay .github/workflows | sort | sed -n '1,240p'Repository: pingdotgg/t3code Length of output: 42101 🏁 Script executed: set -o pipefail
printf '%s\n' '--- alchemy entrypoint and resource declarations ---'
sed -n '1,260p' infra/relay/alchemy.run.ts
printf '%s\n' '--- managed endpoint deletion/release block ---'
sed -n '840,1170p' infra/relay/src/environments/ManagedEndpointProvider.ts
printf '%s\n' '--- managed endpoint tunnel/CNAME creation block ---'
sed -n '1240,1540p' infra/relay/src/environments/ManagedEndpointProvider.ts
printf '%s\n' '--- exact CNAME and tunnel delete call sites ---'
rg -n -C 10 -F --glob '*.ts' -e 'dns_records' -e '.cfargotunnel.com' -e 'tunnels.delete(' -e 'release:' -e 'destroy:' infra/relay/src infra/relay/alchemy.run.tsRepository: pingdotgg/t3code Length of output: 44780 🏁 Script executed: set -o pipefail
printf '%s\n' '--- non-test release callers ---'
rg -n -C 12 -F --glob '*.ts' --glob '!*.test.ts' -e 'ManagedEndpointProvider.release' -e '.release({' -e 'provider.release' infra/relay/src || test "$?" -eq 1
printf '%s\n' '--- preview naming helpers ---'
sed -n '1,90p' infra/relay/src/previewStages.ts
printf '%s\n' '--- provider DNS deletion helper and allocation record shape ---'
sed -n '220,330p' infra/relay/src/environments/ManagedEndpointProvider.ts
sed -n '730,850p' infra/relay/src/environments/ManagedEndpointProvider.tsRepository: pingdotgg/t3code Length of output: 17859 Delete stage-owned CNAMEs after their tunnel is gone.
Keep the exact stage-specific record-name check. Replace the live-tunnel-ID requirement with validation that the CNAME target is a valid 🤖 Prompt for AI Agents |
||
| ); | ||
|
|
||
| yield* Console.log( | ||
| `${apply ? "Deleting" : "Would delete"} ${tunnels.length} tunnel(s) and ${records.length} CNAME(s) for ${stage}`, | ||
| ); | ||
| if (!apply) return; | ||
|
|
||
| // Records first, so no hostname points at a tunnel that is about to vanish. | ||
| for (const record of records) { | ||
| yield* send( | ||
| HttpClientRequest.make("DELETE")(`${API}/zones/${zoneId}/dns_records/${record.id}`), | ||
| ); | ||
| } | ||
| // Cloudflare refuses to delete a tunnel that still has connectors attached. | ||
| for (const tunnel of tunnels) { | ||
| yield* send( | ||
| HttpClientRequest.make("DELETE")( | ||
| `${API}/accounts/${accountId}/cfd_tunnel/${tunnel.id}/connections`, | ||
| ), | ||
| ); | ||
| yield* send( | ||
| HttpClientRequest.make("DELETE")(`${API}/accounts/${accountId}/cfd_tunnel/${tunnel.id}`), | ||
| ); | ||
| } | ||
| yield* Console.log(`Swept ${stage}`); | ||
| }).pipe(Effect.provide(FetchHttpClient.layer)); | ||
|
|
||
| NodeRuntime.runMain(main); | ||
Uh oh!
There was an error while loading. Please reload this page.