|
| 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)" |
0 commit comments