Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,21 @@ jobs:
- name: Install locked frontend dependencies
run: npm ci --no-audit --no-fund
- name: Cache public school data and rendered markup
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule')
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: .cache/school-pages
# PRs can only write caches for their own ref. Production restores
# the separate main prefix; all entries are validated by the renderer.
# Only production builds use this cache; PR previews skip rendering.
# All entries are validated by the renderer.
# A unique save key persists data refreshed during each successful run.
key: school-pages-v1-${{ runner.os }}-node24-${{ github.event_name == 'pull_request' && 'pr' || 'main' }}-${{ github.run_id }}-${{ github.run_attempt }}
key: school-pages-v1-${{ runner.os }}-node24-main-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
school-pages-v1-${{ runner.os }}-node24-${{ github.event_name == 'pull_request' && 'pr' || 'main' }}-
- name: Build and render public HTML
school-pages-v1-${{ runner.os }}-node24-main-
- name: Build frontend
run: npm run build
env:
NODE_ENV: production
SKIP_PRERENDER: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'schedule') && '0' || '1' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the required status page in preview artifacts

On every pull_request run this sets SKIP_PRERENDER=1, so scripts/prerender.mjs returns before creating dist/status/index.html. The subsequent workflow_run deployment still executes scripts/cloudflare/deploy.cjs, which unconditionally requires status/index.html before invoking Wrangler, causing every PR preview deployment produced by this workflow to fail with Missing status/index.html rather than publishing the intended client-rendered preview.

Useful? React with 👍 / 👎.

VITE_API_URL: https://lanis-backend.joancode.dev
- name: Check deployment security rules
run: node --test scripts/cloudflare/*.test.cjs
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ browser. The client mounts normally, preserving stored themes and backend
selection. This is build-time generation, not a server needed at runtime.

Rendering failures fail the build. `SKIP_PRERENDER=1` explicitly skips generation
for local troubleshooting; CI requires the generated public pages for deployment.
for local troubleshooting and PR previews. CI prerenders only production builds
from pushes and scheduled runs on `main`; PRs still run TypeScript and Vite and
deploy a client-rendered preview without fetching or caching school data.
`PRERENDER_STRICT` is no longer needed or used.

Host the contents of **`dist/`** as a static site over HTTPS. Configure the host to serve existing assets and prerendered pages first, then fall back to **`/index.html`** for client-side routes such as `/login`, `/dashboard`, and `/demo/messages`. The app uses root-relative routes and assets, so deploy it at the domain root unless you also adapt those paths.
Expand Down Expand Up @@ -183,8 +185,8 @@ from lanis_api (`GET /schools`, `GET /schools/<id>/data`); image traffic uses it
- A daily Actions run at **03:17 UTC** refreshes and deploys public data even
without a code change. The deployment validator accepts only a successful
scheduled run for the current main commit and the trusted repository.
- PR and main cache prefixes are separate; untrusted PRs cannot write the
default branch's cache. Missing/corrupt cache entries are rebuilt.
- Only production builds on `main` restore and save the school-page cache;
PR previews skip it. Missing/corrupt cache entries are rebuilt.

No runtime rendering service, headless browser, or school-by-school image
download is needed for generation. Source code is in
Expand Down
8 changes: 6 additions & 2 deletions scripts/cloudflare/deploy.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ function validateFiles(directory) {
}
}
validateFiles(assets);
for (const file of ['index.html', 'status/index.html', 'sw.js']) {
if (!fs.statSync(path.join(assets, file)).isFile()) throw new Error(`Missing ${file}`);
// PR previews use the SPA fallback; only production requires prerendered HTML.
const requiredFiles = ['index.html', 'sw.js'];
if (kind === 'production') requiredFiles.push('status/index.html');
for (const file of requiredFiles) {
const target = path.join(assets, file);
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error(`Missing ${file}`);
}
// Preserve previews from branches predating school pages. For school builds,
// ensure every URL advertised in the sitemap has a static page before publishing.
Expand Down
29 changes: 29 additions & 0 deletions scripts/cloudflare/deploy.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,35 @@ test('artifact scripts, dotenv and configuration cannot change the deploy comman
assert.match(fs.readFileSync(path.join(assets, '_headers'), 'utf8'), /noindex/);
});

test('client-rendered previews deploy without prerendered status HTML', t => {
const {root, assets, invoke} = fixture(t);
fs.rmSync(path.join(assets, 'status'), {recursive: true});
const result = invoke();
assert.equal(result.status, 0, result.stderr);
const invocation = JSON.parse(fs.readFileSync(path.join(root, 'invocation.json')));
assert.equal(invocation.config.name, 'lanis-ui-preview');
assert.equal(invocation.config.assets.not_found_handling, 'single-page-application');
assert.match(fs.readFileSync(path.join(assets, '_headers'), 'utf8'), /noindex/);
});

test('production still requires prerendered status HTML', t => {
const {root, assets, invoke} = fixture(t);
fs.rmSync(path.join(assets, 'status'), {recursive: true});
const result = invoke({DEPLOY_KIND: 'production'});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /Missing status\/index.html/);
assert.equal(fs.existsSync(path.join(root, 'invocation.json')), false);
});

test('production with prerendered status HTML still deploys', t => {
const {root, invoke} = fixture(t);
const result = invoke({DEPLOY_KIND: 'production'});
assert.equal(result.status, 0, result.stderr);
const invocation = JSON.parse(fs.readFileSync(path.join(root, 'invocation.json')));
assert.equal(invocation.config.name, 'lanis-ui');
assert.equal(invocation.args[0], 'deploy');
});

test('symlinked artifacts fail before invoking Wrangler', t => {
const {root, assets, invoke} = fixture(t);
fs.symlinkSync(__filename, path.join(assets, 'linked-file'));
Expand Down
Loading