Repository navigation
Deploy PR previews and staging to the allthings Cloudflare account #72
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
503652f
Declare the CI deploy token in infra/stacks/github.ts
esthor 5cc973f
Explain that rerunning updates the token and destroying rotates it
esthor b6804ef
Use Alchemy's exact provider names in the admin profile commands
esthor dfc49ff
Keep the admin profile connected through destroy when rotating
esthor ae21b25
Merge remote-tracking branch 'origin/main' into esthor/ci-deploy-token
esthor ceb5d2a
Deploy PR previews and staging to the allthings Cloudflare account
esthor ca3947c
Read the preview URL from the stack's webUrl output; say where to run…
esthor File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| name: Deploy | ||
|
|
||
| # Deploys the Worker replacing the app to the allthings Cloudflare account: | ||
| # a pr-<number> stage per pull request (removed when it closes) and a staging | ||
| # stage for main. Prod stays a maintainer deploy until it moves accounts. | ||
| on: | ||
| push: | ||
| branches: ["main"] | ||
| pull_request: | ||
| types: [opened, synchronize, reopened, closed] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| concurrency: | ||
| group: deploy-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| STAGE: ${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'staging' }} | ||
|
|
||
| jobs: | ||
| deploy: | ||
| name: 🚀 Deploy | ||
| # Pull requests from forks don't get the deploy secrets. | ||
| if: >- | ||
| github.event.action != 'closed' && | ||
| (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) | ||
| runs-on: ubuntu-latest | ||
| environment: | ||
| name: ${{ github.event_name == 'pull_request' && 'preview' || 'staging' }} | ||
| url: ${{ steps.deploy.outputs.url }} | ||
| steps: | ||
| - name: ⬇️ Checkout repo | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: 🍞 Setup Bun | ||
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | ||
| with: | ||
| bun-version: 1.3.14 | ||
|
|
||
| - name: 📥 Download deps | ||
| run: | | ||
| bun install --frozen-lockfile | ||
| (cd infra && bun install --frozen-lockfile) | ||
|
|
||
| - name: 🧱 Build web | ||
| working-directory: web | ||
| run: bun run build | ||
|
|
||
| - name: 🚀 Deploy | ||
| id: deploy | ||
| working-directory: infra | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| run: | | ||
| bun alchemy deploy --stage "$STAGE" --yes --no-input | tee deploy.log | ||
| # The stack returns the Web Worker's URL as its webUrl output. | ||
| url=$(sed -nE "s/^ *webUrl: '(https:[^']+)'.*/\1/p" deploy.log | tail -1) | ||
| test -n "$url" | ||
| echo "url=$url" >> "$GITHUB_OUTPUT" | ||
| echo "Deployed \`$STAGE\`: $url" >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| destroy: | ||
| name: 🧹 Destroy preview | ||
| if: >- | ||
| github.event.action == 'closed' && | ||
| github.event.pull_request.head.repo.full_name == github.repository | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: ⬇️ Checkout repo | ||
| uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: 🍞 Setup Bun | ||
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | ||
| with: | ||
| bun-version: 1.3.14 | ||
|
|
||
| - name: 📥 Download deps | ||
| run: | | ||
| bun install --frozen-lockfile | ||
| (cd infra && bun install --frozen-lockfile) | ||
|
|
||
| - name: 🧹 Destroy | ||
| working-directory: infra | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| run: | | ||
| case "$STAGE" in pr-[0-9]*) ;; *) echo "refusing to destroy $STAGE" >&2; exit 1 ;; esac | ||
| bun alchemy destroy --stage "$STAGE" --yes --no-input |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "$comment": "The Cloudflare token GitHub Actions deploys with. scripts/ci-token.sh applies this file to the allthings account. Grant only what alchemy.run.ts declares, and widen it in the PR that declares more.", | ||
| "name": "allthings deploy (GitHub Actions)", | ||
| "account": "af627f300cd00c4dca56aacf05bea050", | ||
| "permissionGroups": [ | ||
| "Workers Scripts Write", | ||
| "Account Settings Read", | ||
| "Secrets Store Write" | ||
| ] | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| #!/usr/bin/env bash | ||
| # Applies infra/ci-token.json: creates or updates the account-owned Cloudflare | ||
| # token GitHub Actions deploys with, and keeps the repository's | ||
| # CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID secrets in step. The token's | ||
| # value goes straight from Cloudflare into the secret and is never printed. | ||
| # | ||
| # It runs on a maintainer's machine with the Cloudflare CLI (cf) signed in to | ||
| # the allthings account; that sign-in may create account tokens, so no Global | ||
| # API Key or token-minting token exists anywhere. Requires gh, jq and bun. | ||
| # | ||
| # bash infra/scripts/ci-token.sh # create, or update the policies in place | ||
| # bash infra/scripts/ci-token.sh --rotate # also roll the value and update the secret | ||
| # | ||
| # CF_PROFILE names the cf profile to use (default: allthings). | ||
| set -euo pipefail | ||
|
|
||
| here=$(cd "$(dirname "$0")" && pwd) | ||
| spec="$here/../ci-token.json" | ||
| repo=allthingsweb-dev/allthingsweb | ||
| profile=${CF_PROFILE:-allthings} | ||
| rotate=false | ||
| [[ ${1:-} == --rotate ]] && rotate=true | ||
|
|
||
| name=$(jq -er .name "$spec") | ||
| account=$(jq -er .account "$spec") | ||
| export CLOUDFLARE_ACCOUNT_ID=$account | ||
| # cf 1.0.0-beta.12 fails to reach Cloudflare over IPv6 on some networks. | ||
| export NODE_OPTIONS=--dns-result-order=ipv4first | ||
|
|
||
| # cf prints guidance for agents on stderr; keep stdout as the JSON result and | ||
| # show stderr only when a call fails. | ||
| cf() { | ||
| local err status=0 | ||
| err=$(mktemp) | ||
| bunx cf@1.0.0-beta.12 --profile "$profile" "$@" 2>"$err" || status=$? | ||
| if ((status != 0)); then | ||
| grep -vE "AGENT|=== |cli search|Keep cf|Never include|It returns|Run \`|For API|first port" "$err" >&2 | ||
| fi | ||
| rm -f "$err" | ||
| return "$status" | ||
| } | ||
| result() { jq 'if type == "object" and has("result") then .result else . end'; } | ||
|
|
||
| echo "→ resolving permission groups for $name" | ||
| groups=$(cf accounts tokens permission-groups list | result) | ||
| policies=$(jq -c --argjson groups "$groups" --arg resource "com.cloudflare.api.account.$account" ' | ||
| .permissionGroups as $wanted | ||
| | [$wanted[] as $n | ($groups | map(select(.name == $n)) | first) // error("unknown permission group: \($n)")] | ||
| | [{effect: "allow", permission_groups: map({id}), resources: {($resource): "*"}}] | ||
| ' "$spec") | ||
|
|
||
| id=$(cf accounts tokens list | result | jq -r --arg name "$name" 'map(select(.name == $name)) | first | .id // empty') | ||
|
|
||
| set_secret() { | ||
| jq -er '.value // empty' | gh secret set CLOUDFLARE_API_TOKEN --repo "$repo" | ||
| gh secret set CLOUDFLARE_ACCOUNT_ID --repo "$repo" --body "$account" | ||
| } | ||
|
|
||
| if [[ -z $id ]]; then | ||
| echo "→ creating the token" | ||
| cf accounts tokens create --name "$name" --policies "$policies" | result | set_secret | ||
| echo "✓ created \"$name\" and set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID on $repo" | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "→ updating the token's policies (its value stays the same)" | ||
| cf accounts tokens update "$id" --name "$name" --policies "$policies" | result | jq -e '.id' >/dev/null | ||
| if $rotate; then | ||
| echo "→ rolling the token's value" | ||
| cf accounts tokens roll "$id" | result | jq 'if type == "string" then {value: .} else . end' | set_secret | ||
| echo "✓ rotated \"$name\" and updated CLOUDFLARE_API_TOKEN on $repo" | ||
| else | ||
| echo "✓ \"$name\" now has the policies in ci-token.json" | ||
| fi |
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.