Skip to content

Commit b64c76f

Browse files
NateIsernclaude
andcommitted
ci: deploy the web build to wallet.fairco.in on Cloudflare Pages
Follows CI rather than the push, so nothing reaches production that has not typechecked, linted and passed the suite — and only a green push to this repository's own main, since `workflow_run` fires on every conclusion including fork pull requests. `public/_redirects` is what makes `web.output: "single"` work on a static host: without it every deep link and every reload away from `/` 404s. The smoke test asserts the entry document carries the bundle THIS build emitted (an older deployment answering 200 is not a deploy) and that a route with no file behind it returns 200 (proves the rewrite is in force). Needs the repository secrets CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 42b445c commit b64c76f

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

.github/workflows/deploy-web.yml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Deploy Web
2+
3+
# The browser build of the wallet (wallet.fairco.in), on Cloudflare Pages —
4+
# the same host every Oxy frontend uses. It follows CI rather than the push, so
5+
# nothing reaches production that has not typechecked, linted and passed the
6+
# suite first.
7+
on:
8+
workflow_run:
9+
workflows: [CI]
10+
types: [completed]
11+
branches: [main]
12+
workflow_dispatch:
13+
14+
permissions:
15+
contents: read
16+
17+
concurrency:
18+
group: deploy-fairwallet-web
19+
cancel-in-progress: false
20+
21+
env:
22+
# `workflow_run` reports the commit CI actually verified; a manual dispatch
23+
# runs against the ref it was launched from.
24+
DEPLOY_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
25+
26+
jobs:
27+
deploy:
28+
# `workflow_run` fires on EVERY conclusion of CI, failures and fork pull
29+
# requests included. Only a green push to this repository's own main may
30+
# reach production.
31+
if: >-
32+
github.event_name == 'workflow_dispatch' ||
33+
(github.event.workflow_run.conclusion == 'success' &&
34+
github.event.workflow_run.event == 'push' &&
35+
github.event.workflow_run.head_repository.full_name == github.repository &&
36+
github.event.workflow_run.head_branch == 'main')
37+
runs-on: ubuntu-latest
38+
timeout-minutes: 30
39+
40+
steps:
41+
- name: Checkout the verified commit
42+
uses: actions/checkout@v4
43+
with:
44+
ref: ${{ env.DEPLOY_SHA }}
45+
46+
- name: Install Bun
47+
uses: oven-sh/setup-bun@v2
48+
with:
49+
bun-version: latest
50+
51+
- name: Install dependencies
52+
run: bun install --frozen-lockfile
53+
54+
- name: Build the web bundle
55+
run: bun run export:web
56+
57+
- name: Validate the static hosting contract
58+
# `_redirects` and `_headers` are copied out of `public/` by the export.
59+
# If either goes missing the deploy is still green — and every deep link
60+
# 404s, or every visitor keeps a stale bundle. Fail here instead.
61+
run: |
62+
set -euo pipefail
63+
for file in dist/index.html dist/_redirects dist/_headers; do
64+
if [[ ! -s "$file" ]]; then
65+
echo "::error::$file is missing from the export"
66+
exit 1
67+
fi
68+
done
69+
70+
- name: Deploy to Cloudflare Pages
71+
uses: cloudflare/wrangler-action@v4
72+
with:
73+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
74+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
75+
packageManager: bun
76+
command: pages deploy dist --project-name=fairwallet --branch=main
77+
78+
- name: Smoke test wallet.fairco.in
79+
# Two assertions, because they fail independently: the entry document
80+
# must carry the bundle this build just emitted (proves the promotion
81+
# landed, not that some older deployment answers), and a route with no
82+
# file behind it must return 200 (proves `_redirects` is in force).
83+
run: |
84+
set -euo pipefail
85+
bundle="$(basename dist/_expo/static/js/web/entry-*.js)"
86+
for attempt in $(seq 1 20); do
87+
body="$(curl --fail --silent --show-error https://wallet.fairco.in/ || true)"
88+
deep="$(curl --output /dev/null --silent --write-out '%{http_code}' https://wallet.fairco.in/pockets || true)"
89+
if [[ "$body" == *"$bundle"* && "$deep" == "200" ]]; then
90+
echo "wallet.fairco.in serves $bundle and rewrites deep links."
91+
exit 0
92+
fi
93+
if [[ "$attempt" -eq 20 ]]; then
94+
echo "::error::wallet.fairco.in did not converge on $bundle (deep link: $deep)."
95+
exit 1
96+
fi
97+
sleep 15
98+
done

public/_headers

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Metro fingerprints every bundle and asset it emits under /_expo/static, so
2+
# those URLs are immutable: a new build is a new path and can never be served
3+
# from a stale cache entry.
4+
/_expo/static/*
5+
Cache-Control: public, max-age=31536000, immutable
6+
7+
# The entry document is the one file whose URL never changes, so it must be
8+
# revalidated on every load — otherwise a browser keeps pointing at the bundle
9+
# hashes of a previous deploy.
10+
/index.html
11+
Cache-Control: public, max-age=0, must-revalidate

public/_redirects

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Expo Router exports this app with `web.output: "single"` — one index.html and
2+
# client-side routing for every path. Cloudflare Pages serves static files and
3+
# 404s anything without one, so a deep link (/pockets, /transaction/<txid>) or a
4+
# reload away from "/" would 404 without this rewrite. 200, not 301: the URL the
5+
# router reads must stay the one the user asked for.
6+
/* /index.html 200

0 commit comments

Comments
 (0)