Skip to content
This repository was archived by the owner on Sep 8, 2026. It is now read-only.
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions .github/workflows/teardown.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
name: Teardown Squawk

# Squawk is retired. tektum/verity-images now re-evaluates every published
# digest from its attested per-platform SPDX SBOMs inside GitHub Actions and
# reports findings as code scanning alerts (verity-images#1086), so this
# service owns no monitoring responsibility. This workflow deletes the
# Cloudflare footprint one environment at a time and retires the GitHub App
# installation. Nothing here is recoverable: the D1 database is destroyed
# without an export.
on:
workflow_dispatch:
inputs:
environment:
description: Cloudflare environment to destroy.
required: true
default: none
type: choice
options: [none, staging, production]
uninstall_app:
description: Also retire the Squawk GitHub App installation.
required: true
default: false
type: boolean
confirm:
description: Type "destroy squawk" to confirm.
required: true
type: string

permissions: {}

concurrency:
group: teardown-${{ inputs.environment }}
cancel-in-progress: false

jobs:
infrastructure:
if: inputs.environment != 'none'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Confirm intent
env:
CONFIRM: ${{ inputs.confirm }}
run: |
set -euo pipefail
if [[ "$CONFIRM" != "destroy squawk" ]]; then
printf 'confirmation phrase did not match\n' >&2
exit 1
fi

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: jetify-com/devbox-install-action@8c6a66ed6273138b1915457069de78cb52fe3bd7 # v0.15.0
with:
devbox-version: "0.17.5"
enable-cache: "true"

- run: devbox run install

# Destroy reads the recorded worker version from state, never from disk,
# so a placeholder module satisfies the declared variable without
# rebuilding the gitignored generated matcher and admin sources.
- name: Stage a placeholder Worker bundle
run: |
set -euo pipefail
mkdir -p .tmp/worker-build
printf 'export default { fetch: () => new Response(null, { status: 410 }) };\n' \
> .tmp/worker-build/index.js

- name: Destroy Cloudflare resources
env:
AWS_ACCESS_KEY_ID: ${{ secrets.TF_STATE_ACCESS_KEY_ID }}
AWS_ENDPOINT_URL_S3: https://${{ vars.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
AWS_SECRET_ACCESS_KEY: ${{ secrets.TF_STATE_SECRET_ACCESS_KEY }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
ENVIRONMENT: ${{ inputs.environment }}
TF_VAR_cloudflare_account_id: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
TF_VAR_environment: ${{ inputs.environment }}
TF_VAR_worker_bundle_path: ../.tmp/worker-build/index.js
run: |
set -euo pipefail
devbox run -- tofu -chdir=infra init -input=false \
-backend-config="key=squawk/${ENVIRONMENT}.tfstate"
devbox run -- tofu -chdir=infra destroy -auto-approve

- name: Verify the footprint is gone
env:
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
ENVIRONMENT: ${{ inputs.environment }}
run: |
set -euo pipefail
account="https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}"
# WORKER is exported so the jq filters can read it without embedding
# a shell-looking variable in a single-quoted program.
export WORKER="squawk-${ENVIRONMENT}"
# Cloudflare list endpoints paginate, so a resource on a later page
# must not read as a successful teardown.
count_matching() {
local path=$1
local filter=$2
local page=1
local pages=1
local matches=0
local body
while [[ "$page" -le "$pages" ]]; do
body=$(curl --fail --silent --show-error \
--header "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
"${account}/${path}?page=${page}&per_page=50")
matches=$((matches + $(jq \
"[.result[]? | select(${filter})] | length" <<<"$body")))
pages=$(jq -r '.result_info.total_pages // 1' <<<"$body")
page=$((page + 1))
done
printf '%s' "$matches"
}
workers=$(count_matching workers/scripts '.id == env.WORKER')
queues=$(count_matching queues '(.queue_name | startswith(env.WORKER))')
databases=$(count_matching d1/database '.name == env.WORKER')
printf 'remaining workers=%s queues=%s databases=%s\n' "$workers" "$queues" "$databases"
if [[ "$workers" -ne 0 || "$queues" -ne 0 || "$databases" -ne 0 ]]; then
printf '%s still has Cloudflare resources\n' "$WORKER" >&2
exit 1
fi

github-app:
# The installation is shared by both environments, so it is retired only
# with the final production teardown.
if: inputs.uninstall_app && inputs.environment == 'production'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions: {}
steps:
- name: Confirm intent
env:
CONFIRM: ${{ inputs.confirm }}
run: |
set -euo pipefail
if [[ "$CONFIRM" != "destroy squawk" ]]; then
printf 'confirmation phrase did not match\n' >&2
exit 1
fi

# Only the App itself may delete its own installation, so the request is
# authenticated with a short-lived App assertion rather than a token.
- name: Retire the installation
env:
APP_ID: ${{ secrets.SQUAWK_APP_ID }}
APP_KEY: ${{ secrets.SQUAWK_APP_PEM }}
INSTALLATION_ID: ${{ vars.GH_APP_INSTALLATION_ID }}
run: |
set -euo pipefail
cat > assertion.mjs <<'NODE'
import { createSign } from "node:crypto";

const encode = (value) =>
Buffer.from(JSON.stringify(value)).toString("base64url");
const issued = Math.floor(Date.now() / 1000) - 30;
const unsigned = [
encode({ alg: "RS256", typ: "JWT" }),
encode({ iat: issued, exp: issued + 300, iss: process.env.APP_ID }),
].join(".");
const signer = createSign("RSA-SHA256");
signer.update(unsigned);
const signature = signer.sign(process.env.APP_KEY, "base64url");
process.stdout.write(`${unsigned}.${signature}`);
NODE
assertion=$(node assertion.mjs)
rm assertion.mjs
status=$(curl --silent --show-error --request DELETE \
--header "Authorization: Bearer ${assertion}" \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
--output response.json --write-out '%{http_code}' \
"https://api.github.com/app/installations/${INSTALLATION_ID}")
printf 'delete installation %s returned %s\n' "$INSTALLATION_ID" "$status"
if [[ "$status" != 204 ]]; then
cat response.json >&2
exit 1
fi