-
Notifications
You must be signed in to change notification settings - Fork 78
191 lines (179 loc) · 9.41 KB
/
Copy pathweb-deploy.yml
File metadata and controls
191 lines (179 loc) · 9.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
name: Web Deploy (Cloudflare Pages)
# Deploys the web app (web/, a Vite SPA) to Cloudflare Pages — but only AFTER
# the backend it talks to has actually been released.
#
# Why the chain: the SPA used to deploy on every push to main while the Fly
# release ("Deploy to Fly") is gated on Docker + the reliability Ring 1 suite
# ("User Journeys") being green for the same commit. Any Ring 1 red therefore
# parked the backend while the frontend kept shipping, and a frontend built
# against newer node metadata crashed on boot against the old API (17 h of
# version skew on 2026-08-11). So this workflow now workflow_run-triggers on
# "Deploy to Fly" completing, and the `gate` job releases the SPA only when
# that run's "Deploy server" job really ran and succeeded — not when it was
# skipped for a superseded commit (the newer commit's own chain ships both).
# The remaining skew window is the few minutes where the backend is newer than
# the SPA, which is the benign direction: the crash was a NEW frontend
# hydrating OLD metadata, never the reverse.
#
# workflow_run has no path filtering, so the old paths-ignore list is gone:
# every released backend commit also redeploys the SPA. Docker builds on every
# push to main, so web-only changes still flow through; a docs-only commit now
# costs one redundant, idempotent Pages upload.
#
# The web bundle inlines @nodetool-ai/* package SOURCE (the `nodetool-dev` Vite
# resolve condition) and reads generated pricing JSONs produced by
# `build:packages`, so package changes affect the bundle.
#
# Required GitHub config (Settings → Secrets and variables → Actions):
# Secrets:
# CLOUDFLARE_API_TOKEN — token with "Cloudflare Pages: Edit" on the account
# CLOUDFLARE_ACCOUNT_ID — account that owns the Pages project
# Variables:
# CLOUDFLARE_WEB_PAGES_PROJECT — the Cloudflare Pages project name to deploy to
# VITE_API_URL — API base URL (e.g. https://api.nodetool.ai)
# VITE_SUPABASE_URL — Supabase project URL
# VITE_SUPABASE_ANON_KEY — Supabase anon (publishable) key baked into the
# build. It's a Variable, not a Secret: the key is
# public by design (RLS-protected) and is shipped in
# the client bundle anyway, so a Secret would only
# hide it from this workflow, not from users.
# VITE_AUTH_REDIRECT_URL — optional OAuth redirect URL
#
# The deploy job runs in the `web-production` environment, so the secrets/variables
# above must be visible to it: set them either at the repository level or on the
# `web-production` environment itself (Settings → Environments → web-production).
# An env-scoped secret defined only on a DIFFERENT environment (e.g. the
# marketing deploy's `marketing-production`) is NOT available here — it expands to
# an empty string. For the Cloudflare creds, wrangler then fails with error 9106
# ("Authentication error" / missing Authorization header); for the Supabase build
# vars, the bundle silently falls back to localhost placeholders and breaks auth
# in production. The preflight step below catches both before the deploy runs.
#
# Disconnect the repo from the Cloudflare Pages git integration so the site is
# only deployed from here.
on:
workflow_run:
workflows: ["Deploy to Fly"]
types: [completed]
branches: [main]
# Manual deploy of the current ref, bypassing the backend-release gate —
# for emergencies and web-only hotfixes.
workflow_dispatch:
# The build only reads the repo; the Pages deploy authenticates with its own
# Cloudflare API token. `actions: read` lets the gate inspect the triggering
# Fly run's jobs.
permissions:
contents: read
actions: read
# One deploy at a time; let an in-flight upload finish rather than cancelling it.
concurrency:
group: web-deploy
cancel-in-progress: false
jobs:
# Confirms the triggering "Deploy to Fly" run actually released the backend:
# its "Deploy server" job must have concluded `success`. A `skipped`
# conclusion means Fly's gate declined the release (commit superseded on
# main, or Ring 1/Docker red) — shipping the SPA for that commit would
# recreate exactly the frontend-ahead-of-backend skew this chain exists to
# prevent, so we skip too and let the newer commit's chain deploy both.
gate:
name: Wait for backend release
runs-on: ubuntu-latest
if: github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success'
timeout-minutes: 5
outputs:
sha: ${{ github.event.workflow_run.head_sha }}
deploy: ${{ steps.check.outputs.deploy }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check the Fly run's deploy job conclusion
id: check
shell: bash
run: |
set -euo pipefail
repo="${{ github.repository }}"
run_id="${{ github.event.workflow_run.id }}"
sha="${{ github.event.workflow_run.head_sha }}"
conclusion="$(gh api "repos/${repo}/actions/runs/${run_id}/jobs?per_page=100" \
--jq '[.jobs[] | select(.name == "Deploy server")][0].conclusion // "missing"')"
echo "Fly run ${run_id} (commit ${sha}): 'Deploy server' concluded '${conclusion}'."
if [ "${conclusion}" = "success" ]; then
echo "deploy=true" >> "$GITHUB_OUTPUT"
else
echo "::notice::Backend was not released for ${sha} ('Deploy server' = '${conclusion}'); skipping the SPA deploy to keep frontend and backend in step."
echo "deploy=false" >> "$GITHUB_OUTPUT"
fi
deploy:
name: Build & deploy to Cloudflare Pages
needs: [gate]
# workflow_dispatch always runs (manual deploy of the current ref); on
# workflow_run the gate must have confirmed the backend release.
if: >
always() &&
(github.event_name == 'workflow_dispatch' ||
(needs.gate.result == 'success' && needs.gate.outputs.deploy == 'true'))
runs-on: ubuntu-latest
timeout-minutes: 30
environment:
name: web-production
steps:
- name: Checkout code
uses: actions/checkout@v7
with:
# Build the SPA at the exact commit whose backend just went live
# (falls back to the dispatch ref for manual runs).
ref: ${{ needs.gate.outputs.sha || github.sha }}
fetch-depth: 1
# Fail fast with an actionable message if the Cloudflare credentials, the
# Pages project name, or the Supabase build vars are missing, rather than
# letting wrangler fail late with a cryptic "Must specify a project name" or
# 9106 auth error — or, worse, shipping a bundle that silently falls back to
# the `http://localhost` / `public-anon-key` Supabase placeholders in
# web/src/lib/supabaseClient.ts and breaks auth in production with no error.
# Secrets/vars must be visible to the `web-production` environment (see the
# header note); they expand to empty strings when they are not.
- name: Check Cloudflare deploy config
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_WEB_PAGES_PROJECT: ${{ vars.CLOUDFLARE_WEB_PAGES_PROJECT }}
VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ vars.VITE_SUPABASE_ANON_KEY }}
run: |
missing=()
[ -n "$CLOUDFLARE_API_TOKEN" ] || missing+=("secret CLOUDFLARE_API_TOKEN")
[ -n "$CLOUDFLARE_ACCOUNT_ID" ] || missing+=("secret CLOUDFLARE_ACCOUNT_ID")
[ -n "$CLOUDFLARE_WEB_PAGES_PROJECT" ] || missing+=("variable CLOUDFLARE_WEB_PAGES_PROJECT")
[ -n "$VITE_SUPABASE_URL" ] || missing+=("variable VITE_SUPABASE_URL")
[ -n "$VITE_SUPABASE_ANON_KEY" ] || missing+=("variable VITE_SUPABASE_ANON_KEY")
if [ ${#missing[@]} -ne 0 ]; then
echo "::error::Missing required deploy config: ${missing[*]}"
echo "Set these at the repository level (Settings → Secrets and variables → Actions)"
echo "or on the 'web-production' environment (Settings → Environments → web-production)."
echo "An env-scoped secret on a different environment is NOT visible to this job."
exit 1
fi
echo "Deploy config present (project: $CLOUDFLARE_WEB_PAGES_PROJECT, Supabase URL: $VITE_SUPABASE_URL)."
# Installs the workspace and builds the backend packages, which generates
# the fal/kie pricing JSONs the Vite build aliases resolve.
- name: Set up Node, install deps, build packages
uses: ./.github/actions/setup-build
with:
build-packages: "true"
- name: Build (Vite)
working-directory: web
run: npx vite build
env:
VITE_API_URL: ${{ vars.VITE_API_URL }}
VITE_SUPABASE_URL: ${{ vars.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY: ${{ vars.VITE_SUPABASE_ANON_KEY }}
VITE_AUTH_REDIRECT_URL: ${{ vars.VITE_AUTH_REDIRECT_URL }}
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v4
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
wranglerVersion: "4.85.0"
workingDirectory: web
command: pages deploy dist --project-name=${{ vars.CLOUDFLARE_WEB_PAGES_PROJECT }} --branch=main