Skip to content

Commit 090bf64

Browse files
pmaxhoganclaude
andcommitted
ci(landing): whole-site no-wipe deploy integration + independent deploy-landing (M12)
The driven-updates CF Pages project serves a whole-site snapshot, so every `pages deploy site` must carry BOTH the landing AND both channels' live updates/ manifests or whatever is missing gets wiped. - release.yml + dev-channel.yml: add an assemble-landing.sh step AFTER the updates/ tree + other-channel overlay and BEFORE `pages deploy site`, so a release/dev deploy includes the root landing without wiping it. Additive to site/ root; site/updates and the existing manifest generate/overlay/deploy/ smoke are unchanged. - New deploy-landing.yml: on push to main touching site-landing/** (or the assemble/fetch scripts or this workflow) + workflow_dispatch. Reassembles the FULL site (landing + fetch-live-channel.sh for BOTH stable and dev) and deploys, then a root-200 + tagline-marker smoke and a manifest no-wipe re-check, both via the same bounded curl -f --retry pattern as the manifest smokes (no sleep/poll loop). actionlint 0 findings on all three workflows. Updates CODEX_NOTES.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CyiRqk2DVwmJjEu5gcD1m
1 parent 0809cdc commit 090bf64

4 files changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: deploy-landing
2+
3+
# Independent deploy of the driven.maxhogan.dev ROOT landing page (M12).
4+
#
5+
# `wrangler pages deploy site` publishes a WHOLE-SITE snapshot to the
6+
# driven-updates CF Pages project (which serves driven.maxhogan.dev). So the
7+
# landing page and the updater manifests share ONE site. The release.yml /
8+
# dev-channel.yml pipelines already redeploy the whole site (and now copy the
9+
# landing into it), but those only run on a tag / dev build. This workflow lets
10+
# the MARKETING page ship on its own - whenever site-landing/ (or the assemble
11+
# script / this workflow) changes on main - WITHOUT cutting a release.
12+
#
13+
# Critical no-wipe invariant: because the deploy is a whole-site snapshot, this
14+
# workflow must reassemble the FULL site every time:
15+
# site/ <- landing (index.html, styles.css, icon.svg, 404.html)
16+
# site/updates/stable/ <- the currently-live stable manifests (overlaid)
17+
# site/updates/dev/ <- the currently-live dev manifests (overlaid)
18+
# It does this by copying the landing in AND pulling BOTH channels' live
19+
# manifests via scripts/fetch-live-channel.sh (which tolerates a first-publish
20+
# 404 and otherwise fails closed). If it deployed the landing alone it would wipe
21+
# every updater manifest; this assembly preserves them.
22+
23+
on:
24+
push:
25+
branches: [main]
26+
paths:
27+
- "site-landing/**"
28+
- "scripts/assemble-landing.sh"
29+
- "scripts/fetch-live-channel.sh"
30+
- ".github/workflows/deploy-landing.yml"
31+
workflow_dispatch:
32+
33+
permissions:
34+
contents: read
35+
deployments: write
36+
37+
concurrency:
38+
# Serialize with itself; do not cancel an in-flight deploy mid-snapshot.
39+
group: deploy-landing
40+
cancel-in-progress: false
41+
42+
jobs:
43+
deploy:
44+
name: assemble + deploy landing (whole-site, no-wipe)
45+
runs-on: ubuntu-latest
46+
env:
47+
# The live updates site whose BOTH channels must be preserved across the
48+
# whole-site `pages deploy` snapshot.
49+
UPDATES_BASE: https://driven.maxhogan.dev/updates
50+
SITE_URL: https://driven.maxhogan.dev/
51+
# The tagline marker that must appear in the deployed landing body - this is
52+
# the README one-liner, also the #tagline element in index.html.
53+
TAGLINE_MARKER: "One-way, encrypted backup of your local folders to your own Google Drive."
54+
steps:
55+
- uses: actions/checkout@v4
56+
57+
# 1. Copy the committed landing page to the site root.
58+
- name: Assemble landing into site/
59+
run: bash scripts/assemble-landing.sh site site-landing
60+
61+
# 2. Overlay BOTH live channels' updater manifests so the whole-site deploy
62+
# does NOT wipe them. fetch-live-channel.sh writes
63+
# site/updates/<channel>/<plat>/update.json, tolerating a genuine
64+
# first-publish 404 and failing closed on any other fetch error.
65+
- name: Overlay live stable manifests (do not wipe stable)
66+
run: bash scripts/fetch-live-channel.sh stable site/updates "$UPDATES_BASE"
67+
68+
- name: Overlay live dev manifests (do not wipe dev)
69+
run: bash scripts/fetch-live-channel.sh dev site/updates "$UPDATES_BASE"
70+
71+
- name: Show assembled site tree
72+
run: |
73+
set -euo pipefail
74+
echo "site/ root:"; ls -la site
75+
echo "site/updates tree:"; find site/updates -type f 2>/dev/null | sort || true
76+
77+
# 3. Deploy the whole site (landing + both channels' updates) to CF Pages.
78+
- name: Deploy site to Cloudflare Pages
79+
uses: cloudflare/wrangler-action@v3
80+
with:
81+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
82+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
83+
command: pages deploy site --project-name=driven-updates --branch=main
84+
85+
# 4. Post-deploy smoke: the root must serve 200 AND its body must contain
86+
# the tagline marker. Cloudflare Pages propagation can lag the deploy and
87+
# is not atomic across edges, so a fresh root can 404 briefly. -f makes a
88+
# 404 a retryable error and curl's own bounded retry (8 attempts, 5s
89+
# apart) rides out propagation - the SAME pattern the manifest smokes use,
90+
# no bash sleep/poll loop.
91+
- name: Smoke test deployed landing root
92+
run: |
93+
set -euo pipefail
94+
body="$(mktemp)"
95+
code="$(curl -fsSL --retry 8 --retry-delay 5 --retry-all-errors \
96+
--connect-timeout 15 --max-time 120 \
97+
-o "$body" -w '%{http_code}' "$SITE_URL" 2>/dev/null || true)"
98+
code="${code:-000}"
99+
if [ "$code" != "200" ]; then
100+
echo "::error::landing smoke: ${SITE_URL} returned HTTP ${code} (expected 200)"
101+
exit 1
102+
fi
103+
if ! grep -qF "$TAGLINE_MARKER" "$body"; then
104+
echo "::error::landing smoke: ${SITE_URL} returned 200 but body is missing the tagline marker"
105+
exit 1
106+
fi
107+
echo "landing smoke OK: ${SITE_URL} serves 200 and contains the tagline marker"
108+
rm -f "$body"
109+
110+
# 5. Verify the deploy did NOT wipe the updater manifests: at least one
111+
# channel must still serve a valid manifest. A first-ever deploy may have
112+
# no live manifests to preserve (both 404), which is acceptable - the
113+
# release/dev pipelines publish them. So this only FAILS if a manifest was
114+
# present in the assembled tree but is unreachable after deploy.
115+
- name: Verify updater manifests survived the deploy
116+
run: |
117+
set -euo pipefail
118+
checked=0
119+
fail=0
120+
for chan in stable dev; do
121+
for t in windows/x86_64 darwin/x86_64 darwin/aarch64 linux/x86_64; do
122+
local_manifest="site/updates/${chan}/${t}/update.json"
123+
[ -f "$local_manifest" ] || continue
124+
checked=$((checked + 1))
125+
url="${UPDATES_BASE}/${chan}/${t}/update.json"
126+
code="$(curl -fsSL --retry 8 --retry-delay 5 --retry-all-errors \
127+
--connect-timeout 15 --max-time 120 \
128+
-o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
129+
code="${code:-000}"
130+
if [ "$code" != "200" ]; then
131+
echo "::error::no-wipe check: ${url} returned HTTP ${code} (expected 200) - the landing deploy may have wiped a live manifest"
132+
fail=1
133+
else
134+
echo "no-wipe OK: ${url} still serves 200"
135+
fi
136+
done
137+
done
138+
if [ "$fail" -ne 0 ]; then
139+
echo "::error::landing deploy appears to have wiped one or more live updater manifests"
140+
exit 1
141+
fi
142+
echo "no-wipe verified: ${checked} assembled manifest(s) still reachable (0 = first-ever deploy, nothing to preserve)"

.github/workflows/dev-channel.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,16 @@ jobs:
405405
- name: Overlay live stable manifests (do not wipe the other channel)
406406
run: bash scripts/fetch-live-channel.sh stable site/updates "$UPDATES_BASE"
407407

408+
# M12: copy the root landing page into the site/ deploy root BEFORE the
409+
# whole-site `pages deploy`. The deploy is a whole-site snapshot, so without
410+
# this a dev-channel deploy would WIPE the live landing at
411+
# driven.maxhogan.dev root. The copy is ADDITIVE - it only writes
412+
# site/{index.html,styles.css,icon.svg,404.html} and never touches
413+
# site/updates, so the freshly generated dev + overlaid live stable manifests
414+
# are unaffected.
415+
- name: Assemble landing into site/ (do not wipe the root page)
416+
run: bash scripts/assemble-landing.sh site site-landing
417+
408418
# R7-P1-1: deploy the `site/` PARENT (which contains `updates/`), NOT the
409419
# bare `updates/` dir, so the served path keeps the `/updates/` prefix and
410420
# matches the app's `updates/dev/<os>/<arch>/update.json` fetch endpoint.

.github/workflows/release.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,15 @@ jobs:
273273
- name: Overlay live dev manifests (do not wipe the other channel)
274274
run: bash scripts/fetch-live-channel.sh dev site/updates "$UPDATES_BASE"
275275

276+
# M12: copy the root landing page into the site/ deploy root BEFORE the
277+
# whole-site `pages deploy`. The deploy is a whole-site snapshot, so without
278+
# this the release deploy would WIPE the live landing at driven.maxhogan.dev
279+
# root. The copy is ADDITIVE - it only writes site/{index.html,styles.css,
280+
# icon.svg,404.html} and never touches site/updates, so the freshly generated
281+
# stable + overlaid live dev manifests are unaffected.
282+
- name: Assemble landing into site/ (do not wipe the root page)
283+
run: bash scripts/assemble-landing.sh site site-landing
284+
276285
# Publish the merged channel/target tree to Cloudflare Pages so
277286
# driven.maxhogan.dev/updates/stable/<os>/<arch>/update.json serves it
278287
# (SPEC s15.3 V1).

design/CODEX_NOTES.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3128,3 +3128,55 @@ restriction. release.yml's bundle set is otherwise UNCHANGED.
31283128
Gates: workflow-only change. actionlint clean on dev-channel.yml + release.yml. git diff --check clean.
31293129
ASCII + LF. No Rust/ui touched. NO tag, NO workflow_dispatch, NO release (the orchestrator re-triggers
31303130
the dev build after this push).
3131+
3132+
## M12 landing page
3133+
3134+
Built the root landing page for driven.maxhogan.dev. Before M12 the root 404'd: the
3135+
`driven-updates` CF Pages project only served `/updates/<channel>/...` manifests (assembled at
3136+
deploy time), with nothing at the site root. M12 adds an on-brand marketing page at the root WITHOUT
3137+
breaking the updater-manifest deploy/serve path.
3138+
3139+
What shipped:
3140+
- `site-landing/` (committed source): `index.html` + `styles.css` + `icon.svg` (a copy of the C2
3141+
road-to-cloud master `src-tauri/icons/icon.svg`) + `404.html`. Plain HTML/CSS, NO framework/build
3142+
step, NO external CDN/JS/trackers (privacy-respecting static page). Deep teal brand (#0F766E - the
3143+
icon background), the README tagline ("One-way, encrypted backup of your local folders to your own
3144+
Google Drive."), a feature list mirroring the README, a per-platform "Get it" section (Win
3145+
MSI/NSIS, macOS DMG universal, Linux AppImage/.deb) linking Releases, a bring-your-own-OAuth/Drive
3146+
trust blurb, and an MIT-OR-Apache-2.0 footer. Primary Download CTA ->
3147+
github.com/pmaxhogan/driven/releases. Responsive (CSS grid auto-fit, clamp() type), accessible
3148+
(semantic landmarks, skip link, alt text, focus-visible outlines, dark-mode aware with sufficient
3149+
contrast), ASCII-only, LF.
3150+
- `scripts/assemble-landing.sh <site-dir> <landing-dir>` (defaults `site` / `site-landing`): copies
3151+
the known landing files (index.html, styles.css, icon.svg, 404.html) to the site/ deploy root.
3152+
Idempotent, ADDITIVE (never touches site/updates), fail-closed if a required file is missing.
3153+
3154+
Whole-site no-wipe integration (the critical bit). `wrangler pages deploy site` publishes a
3155+
WHOLE-SITE snapshot to `driven-updates`, so the landing and the updater manifests share ONE site:
3156+
every `pages deploy site` must carry BOTH the landing AND both channels' live updates/ or whatever
3157+
is absent gets wiped. So:
3158+
- `release.yml` (stable) and `dev-channel.yml` (dev) now run `assemble-landing.sh` as a new step
3159+
placed AFTER their existing updates/ tree generation + `fetch-live-channel.sh` other-channel
3160+
overlay and BEFORE the `pages deploy site` step. The copy is purely additive to site/ root and
3161+
leaves site/updates untouched, so the 4 stable + 4 dev manifests still generate, overlay, deploy,
3162+
and pass their existing smokes exactly as before - the only delta is the root page now rides along.
3163+
- NEW `.github/workflows/deploy-landing.yml`: lets the marketing page ship independently of a
3164+
release. Triggers on push to main touching `site-landing/**`, `scripts/assemble-landing.sh`,
3165+
`scripts/fetch-live-channel.sh`, or the workflow itself, plus workflow_dispatch. It reassembles the
3166+
FULL site each run: assemble-landing into site/, then `fetch-live-channel.sh stable site/updates`
3167+
AND `fetch-live-channel.sh dev site/updates` to overlay BOTH live channels' manifests (tolerating a
3168+
first-publish 404, failing closed on any other fetch error), then `pages deploy site
3169+
--project-name=driven-updates --branch=main`. So a landing-only change never wipes the updater
3170+
manifests. Uses the CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID secrets.
3171+
- Post-deploy smokes in deploy-landing.yml use the SAME bounded curl retry as the manifest smokes
3172+
(`curl -f --retry 8 --retry-delay 5 --retry-all-errors`, no bash sleep/poll loop) to ride CF Pages
3173+
propagation: (1) the root `https://driven.maxhogan.dev/` must return 200 AND the body must contain
3174+
the tagline marker; (2) a no-wipe check re-fetches every manifest that was present in the assembled
3175+
tree and fails if any is no longer 200 (a first-ever deploy with no live manifests to preserve is
3176+
acceptable).
3177+
3178+
Gates: actionlint 0 findings on deploy-landing.yml + release.yml + dev-channel.yml; HTML tag-balance
3179+
validated; assemble-landing.sh `bash -n` + live-run verified; `git diff --check` clean; ASCII + LF
3180+
throughout; rendered the page in a headless browser to confirm it is on-brand. No app code
3181+
(src-tauri/ui/crates) touched; no release/tag triggered. The push to main fires deploy-landing.yml,
3182+
which deploys the landing live (intended).

0 commit comments

Comments
 (0)