-
Notifications
You must be signed in to change notification settings - Fork 0
244 lines (225 loc) · 11.4 KB
/
Copy pathdeploy-site.yml
File metadata and controls
244 lines (225 loc) · 11.4 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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
name: Deploy site
# The whole public site, published as ONE GitHub Pages artifact on every push to
# `develop`. Four surfaces, three builders, one upload:
#
# / landing page + the live device demo trunk (obc-web-demo → wasm)
# /docs/ /blog/ docs site + expedition log build_docs.py (trunk pre_build hook)
# /builder/ the static map builder (#894 phase C) vite --mode web
#
# Because the demo is the firmware's own render path compiled to wasm, every push
# rebuilds it — the site can never show a stale screenshot. And everything here is
# static files: there is no backend on this tier at all. Route conversion runs as wasm
# in the tab (#896), maps are cell artifacts listed in a catalog manifest, and the only server involved
# is whoever serves the bytes.
#
# ── What GitHub Pages can and cannot do (C6 #905; measured 2026-07-26) ──────────────
#
# Pages serves every object with a fixed header set and offers **no** configuration
# surface for response headers — no `_headers`, no `.htaccess`, no per-path rules:
#
# $ curl -sSI https://openbikecomputer.com/
# cache-control: max-age=600
# access-control-allow-origin: *
# (no Cross-Origin-Opener-Policy, no Cross-Origin-Embedder-Policy)
#
# Two consequences this deploy is designed around:
#
# 1. **Cache-Control is not ours to set.** Hashed assets cannot be marked `immutable`
# — they are served `max-age=600` like everything else. That is harmless: the
# filenames are content-hashed, so a new build is a new URL and an old max-age can
# never serve stale code. What it does mean is that "the catalog manifest must be
# short-lived" is unsatisfiable for anything served from here — and the manifest
# must not live here anyway, because a manifest published beside the site could
# only change by redeploying the site, which is exactly what #905 says a fresh bake
# must not require. So the manifest lives **with the artifacts**, on whatever object
# storage the bakery (B1 #898) publishes to, where its cache policy is that host's
# to set (OBCC_Spec.md §11 states the ≤ 60 s expectation). This workflow carries only
# its URL — see OBC_CATALOG_URL below.
#
# 2. **No cross-origin isolation, ever, on this host.** Setting
# `Cross-Origin-Opener-Policy: same-origin` + `Cross-Origin-Embedder-Policy:
# require-corp` is the only way to reach `crossOriginIsolated === true`, and with it
# `SharedArrayBuffer` — which is what wasm *threads* need (wasm-bindgen-rayon,
# Emscripten pthreads). Pages cannot set them, so anything needing threads (the
# hypothetical in-browser packer) forces a move to
# a host with header control. The service-worker shim that fakes isolation on static
# hosts is not a way out here: under `require-corp` every cross-origin subresource
# needs CORP/CORS headers, and this tier's whole point is fetching catalog cells
# from another origin. Nothing shipped today wants isolation — obc-web-convert and
# obc-web-demo are both single-threaded — so this is a recorded constraint, not a
# present problem.
#
# ── Portability across a domain move ───────────────────────────────────────────────
#
# The site is expected to move to its own domain, so nothing published here may assume
# the project-Pages `/<repo>/` sub-path: trunk gets `--public-url ./` (which emits
# `<base href="./">`) and Vite is configured `base: "./"` (#895), so the artifact is
# mountable at any prefix. The "mountable at any prefix" step below proves that on
# every deploy by serving the artifact from a deep sub-path and fetching each page's
# own assets through it.
#
# ── Which branch the world sees ────────────────────────────────────────────────────
#
# `develop`, per #905's acceptance criterion — the integration branch is what the
# public site tracks. (It was `main`, which had drifted 365 commits behind.) One
# trigger only: two branches deploying into one Pages environment would fight.
on:
push:
branches: [develop]
paths:
- "docs/**"
- "firmware/**"
- "builder/**"
- "apps/obc-web-assemble/**"
- "apps/obc-web-convert/**"
- "apps/obc-skin-preview/**"
- "apps/obc-web-demo/**"
- "host/obcm-assemble/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/deploy-site.yml"
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
# One deployment at a time; let an in-progress run finish (don't cancel a live deploy).
concurrency:
group: pages
cancel-in-progress: false
env:
# Where the builder reads the OBCC catalog manifest (#897). A repository variable
# rather than a code constant, because the manifest lives with the artifacts and B1
# (#898) owns where that is — the frontend already treats it as configuration
# (platform/web.ts). Unset until something is baked: the builder is still published,
# says it has no catalog, and nothing on the site links to it (see OBC_BUILDER_PATH).
OBC_CATALOG_URL: ${{ vars.OBC_CATALOG_URL }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (wasm target)
uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-unknown-unknown
- name: Cache cargo + build artifacts
uses: Swatinem/rust-cache@v2
with:
workspaces: .
# Pinned to the versions ci.yml builds these with, so the deploy cannot differ
# from the tools that gated the PR.
- name: Install Trunk + wasm-pack
uses: taiki-e/install-action@v2
with:
tool: trunk@0.21.14,wasm-pack@0.15.0
# --public-url ./ rather than /<repo>/: relative asset URLs work under the
# project-Pages sub-path AND at a domain root, so the move costs a DNS record
# instead of a rebuild. --release turns on wasm-opt (see data-wasm-opt in
# index.html). The pre_build hook renders docs/ + blog/ into dist as it goes.
- name: Build the landing page, the demo, and the docs
env:
# The docs/blog header links the builder only when the builder has something
# to offer. A nav link to a map builder that opens on "couldn't load the map
# catalog" is a broken front door, and the front door is the one place never
# worth being brave about.
OBC_BUILDER_PATH: ${{ vars.OBC_CATALOG_URL != '' && 'builder/' || '' }}
run: trunk build --release --config docs/Trunk.toml --public-url "./"
# The builder imports the shared obc-route GPX↔OBCR path (#896), the cell
# assembler (#1034), and the production-rendered skin preview (#1045). The shared
# script is also used by CI so the lists cannot drift.
# None is optional: the frontend imports the generated bindings, so a missing
# pkg/ is a build failure, not a degraded site.
- name: Build the wasm bridges
run: bash builder/build-wasm-bridges.sh
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: builder/app/package-lock.json
# VITE_SITE_BASE makes the builder's links to the docs and the landing relative:
# they are siblings inside this artifact, so naming them by absolute URL would be
# the one place a domain move still bit us. Left unset everywhere else (dev
# server, desktop app), where the site genuinely is somewhere else.
- name: Build the static map builder
working-directory: builder/app
env:
VITE_SITE_BASE: "../"
VITE_CATALOG_URL: ${{ env.OBC_CATALOG_URL }}
run: |
npm ci
npm run build:web
- name: Assemble the site
run: |
mkdir -p docs/dist/builder
cp -R builder/app/dist/web/. docs/dist/builder/
du -sh docs/dist/* | sort -h
- name: Generate sitemap
run: python3 docs/generate_sitemap.py docs/dist
# Two ways a deploy ships something broken without anyone noticing. Both checks
# are cheap; both catch a class of failure that is invisible until a user hits it.
# Path-absolute references only: `href="/OpenBikeComputer/…"`, not
# `https://openbikecomputer.com/…`. The landing page's
# og:image/twitter:image MUST be absolute — scrapers don't resolve relative URLs
# — so those two tags are the one place the current origin is legitimately baked
# in, and they are the one thing to hand-edit on the day the domain changes.
- name: No baked-in site path
run: |
if grep -rIlE "[\"'(=]/OpenBikeComputer/" docs/dist/builder docs/dist/index.html; then
echo "::error::the bundle hard-codes the project-Pages sub-path — it would 404 on a custom domain"
exit 1
fi
echo "no absolute site paths in the published bundle"
# Mount the artifact at a deep prefix, then fetch each page AND the assets it
# names, resolved relative to that page. A page that loads while its script 404s
# would pass a naive existence check; this does not.
- name: Mountable at any prefix
run: |
set -euo pipefail
root="$RUNNER_TEMP/mount"
rm -rf "$root"; mkdir -p "$root/some/deep/prefix"
cp -R docs/dist/. "$root/some/deep/prefix/"
python3 -m http.server 8123 --directory "$root" >/dev/null 2>&1 &
server=$!
trap 'kill $server' EXIT
for _ in $(seq 1 30); do curl -sf -o /dev/null "http://127.0.0.1:8123/" && break; sleep 0.2; done
base="http://127.0.0.1:8123/some/deep/prefix"
fail=0
check() { # check <page-url> — fetch the page, then every asset it references
local page="$1" html ref n=0
html=$(curl -sf "$page") || { echo " x $page"; fail=1; return; }
echo " ok $page"
for ref in $(printf '%s' "$html" | grep -oE '(src|href)="[^"]+\.(js|css|wasm)"' | cut -d'"' -f2); do
case "$ref" in
*:*|//*) continue ;; # external, not ours
/*) echo " x $ref (rooted at /)"; fail=1; continue ;;
./*) ref="${ref#./}" ;;
esac
n=$((n + 1))
# Relative, so it resolves against the page's own directory — which is
# exactly the property under test.
if curl -sf -o /dev/null "${page%/}/$ref"; then echo " ok $ref"
else echo " x $ref"; fail=1; fi
done
# A page that referenced nothing would pass every assertion above without
# testing anything, so say so instead.
[ "$n" -gt 0 ] || { echo " x no local assets referenced — check is vacuous"; fail=1; }
}
check "$base/"
check "$base/builder/"
check "$base/docs/"
[ "$fail" = 0 ] || { echo "::error::the artifact is not mountable at a sub-path"; exit 1; }
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/dist
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4