Skip to content

Commit 818f2ee

Browse files
MajorTalclaude
andcommitted
fix(ci): unconditional SSR adapter + adapter-aware custom-page materialize
Fixes the two pre-existing CI failures that predate the auth migration: 1. portal-first-byte-build test threw NoAdapterInstalled. Root cause: the SSR adapter was gated on useRun402Integration (needs KYCHON_PROJECT + RUN402_PROJECT_ID), but calendar/search/ssr-probe export prerender=false, so any adapter-less `astro build` fails. Fix: register createRun402Adapter() unconditionally — it only emits dist/run402/{client,server,adapter.json} locally and has no auth/ credential requirements. The image-upload integration stays gated on useRun402Integration so non-deploy builds don't reach for gateway creds. 2. deploy-ci.ts patchDeploy threw "dist/page.html does not exist". With the adapter active, [customPage].astro's getStaticPaths writes per-slug HTML under dist/run402/client/ and dist/page.html never exists. patchDeploy + generate-static-page-aliases.ts now mirror runDeploy's adapterActive detection (existsSync dist/run402/adapter.json) and synthesize the slug list from the seed instead of calling materializeCustomPageStaticFiles. Build test reconciled with the SSR-guard revert (d0b69ae): admin*/profile are static again, so they're back in REPRESENTATIVE_PAGES with the ADMIN_PAGES assertions (static bake contains the "Checking access" placeholder, never data-admin-content). Output reads from dist/run402/ client/ since the adapter now always relocates prerendered HTML there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d0b69ae commit 818f2ee

4 files changed

Lines changed: 56 additions & 10 deletions

File tree

astro.config.mjs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -212,13 +212,17 @@ const useRun402Integration = Boolean(integrationAssetsDir && process.env.RUN402_
212212
// `PUBLIC_PATH_MODE_WIDENS_TO_IMPLICIT` warning — covered by
213213
// `RUN402_ALLOW_WARNINGS=true`.
214214
//
215-
// Skipped when `useRun402Integration` is false — pure-static builds
216-
// (integration tests, neutral fallback, local `astro dev` without env
217-
// vars) keep the pre-adapter pipeline + the original `dist/` layout
218-
// unchanged.
215+
// The adapter is unconditional: several routes (admin*, profile,
216+
// calendar, search, ssr-probe) export `prerender = false`, so any
217+
// `astro build` without an adapter throws NoAdapterInstalled. The
218+
// adapter itself has no auth requirements — it just emits
219+
// `dist/run402/{client,server,adapter.json}` locally — so it's safe
220+
// to register in test / neutral-fallback builds. The image-upload
221+
// integration stays gated on `useRun402Integration` so non-deploy
222+
// builds don't reach for Run402 gateway credentials.
219223
export default defineConfig({
220224
output: 'static',
221-
...(useRun402Integration ? { adapter: createRun402Adapter() } : {}),
225+
adapter: createRun402Adapter(),
222226
devToolbar: {
223227
enabled: false,
224228
},

scripts/_lib.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,11 +933,25 @@ export async function patchDeploy(
933933

934934
const project = await r.project(opts.projectId);
935935
const distDir = join(ROOT, "dist");
936+
// Mirror runDeploy's adapter detection: when @run402/astro@1.0.4+'s SSR
937+
// adapter is active, `[customPage].astro`'s `getStaticPaths` writes
938+
// per-slug HTML directly under `dist/run402/client/` and `dist/page.html`
939+
// no longer exists. Skip `materializeCustomPageStaticFiles` (which would
940+
// otherwise throw on the missing shell) and synthesize the same slug list
941+
// from the seed — downstream code only needs the `{slug, file}` shape.
942+
const adapterActive = existsSync(join(distDir, "run402", "adapter.json"));
936943

937944
injectEnvJs(distDir, opts.anonKey);
938945
generateAndValidateHeaders(distDir);
939946
const deploySeed = await resolveDeployOutputSeed(opts.chromeSnapshot);
940-
const materializedCustomPages = materializeCustomPageStaticFiles(distDir, deploySeed);
947+
const materializedCustomPages = adapterActive
948+
? safeCustomPageSlugs(deploySeed.pages)
949+
.map((slug) => {
950+
const file = customPageStaticFile(slug);
951+
return file ? { slug, file } : null;
952+
})
953+
.filter((entry): entry is MaterializedCustomPageFile => entry !== null)
954+
: materializeCustomPageStaticFiles(distDir, deploySeed);
941955

942956
const sql = readMigrations(ROOT, opts.seedFile);
943957
const migrationId = `kychon_${sha256Hex(sql).slice(0, 16)}`;

scripts/generate-static-page-aliases.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
1+
import { existsSync } from 'node:fs';
12
import { join } from 'node:path';
23

34
import { materializeCustomPageStaticFiles, ROOT } from './_lib.ts';
5+
import { customPageStaticFile, safeCustomPageSlugs } from '../src/lib/clean-routes.ts';
46
import { describeSeedSource, resolveActiveProjectSeed } from '../src/seeds/index.ts';
57

68
async function main(): Promise<void> {
79
const { seed, source } = await resolveActiveProjectSeed();
810
const distDir = join(ROOT, 'dist');
9-
const materialized = materializeCustomPageStaticFiles(distDir, seed);
11+
// Adapter-aware: when @run402/astro's SSR adapter ran, `[customPage].astro`'s
12+
// getStaticPaths already emitted per-slug HTML under `dist/run402/client/`,
13+
// and `dist/page.html` doesn't exist — so the legacy copy step would throw.
14+
// Mirror `runDeploy`'s detection (scripts/_lib.ts:634) and just enumerate
15+
// slugs for the log line.
16+
const adapterActive = existsSync(join(distDir, 'run402', 'adapter.json'));
17+
const materialized = adapterActive
18+
? safeCustomPageSlugs(seed.pages)
19+
.map((slug) => {
20+
const file = customPageStaticFile(slug);
21+
return file ? { slug, file } : null;
22+
})
23+
.filter((entry): entry is { slug: string; file: `${string}.html` } => entry !== null)
24+
: materializeCustomPageStaticFiles(distDir, seed);
1025
if (materialized.length === 0) {
1126
process.stdout.write(`No clean custom page aliases generated for ${describeSeedSource(source)}.\n`);
1227
return;

tests/integration/portal-first-byte-build.test.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@ const ROOT = process.cwd();
77
const SNAPSHOT = join(ROOT, 'fixtures/chrome/odbc.chrome-snapshot.json');
88
const BRAND = 'Old Dominion Boat Club';
99
const FORBIDDEN_BRAND = 'Kychon Community';
10+
// The @run402/astro adapter relocates prerendered HTML from `dist/` to
11+
// `dist/run402/client/`. The adapter is unconditional in astro.config.mjs
12+
// because calendar / search / ssr-probe export `prerender = false` — without
13+
// the adapter `astro build` throws NoAdapterInstalled. Static pages still
14+
// prerender, just under the client dir.
15+
const CLIENT_DIR = join(ROOT, 'dist', 'run402', 'client');
1016

17+
// Representative prerendered pages. admin* and profile are STATIC with
18+
// client-side access gating (the ADMIN_PAGES assertions below verify the
19+
// "Checking access" placeholder is baked but the real admin content is not).
20+
// calendar / search / ssr-probe are SSR-only (prerender = false), never
21+
// appear as static HTML, and are covered by the SSR entry's render path.
1122
const REPRESENTATIVE_PAGES = [
1223
'index.html',
1324
'page.html',
1425
'events.html',
15-
'calendar.html',
1626
'directory.html',
1727
'committees.html',
1828
'forum.html',
@@ -40,7 +50,7 @@ function buildPortal(): void {
4050
}
4151

4252
function readRepresentativeHtml(): Map<string, string> {
43-
return new Map(REPRESENTATIVE_PAGES.map((page) => [page, readFileSync(join(ROOT, 'dist', page), 'utf8')]));
53+
return new Map(REPRESENTATIVE_PAGES.map((page) => [page, readFileSync(join(CLIENT_DIR, page), 'utf8')]));
4454
}
4555

4656
function requireSnapshotHtml(snapshot: Map<string, string>, page: string): string {
@@ -72,6 +82,9 @@ describe('Portal first-byte build output', () => {
7282
expect(html, page).toContain('/js/env.js');
7383
expect(html, page).not.toContain('/js/env.js?');
7484

85+
// Static admin pages must bake only the client-side access-checking
86+
// placeholder, never the real admin UI — admin content is gated in the
87+
// React island at runtime, so it must not leak into public static HTML.
7588
if (ADMIN_PAGES.has(page)) {
7689
expect(html, page).toContain('data-admin-access-checking');
7790
expect(html, page).toContain('Checking access');
@@ -82,7 +95,7 @@ describe('Portal first-byte build output', () => {
8295
buildPortal();
8396

8497
for (const [page, html] of firstBuildHtml) {
85-
expect(normalizeAstroAssetUrls(readFileSync(join(ROOT, 'dist', page), 'utf8')), page).toBe(
98+
expect(normalizeAstroAssetUrls(readFileSync(join(CLIENT_DIR, page), 'utf8')), page).toBe(
8699
normalizeAstroAssetUrls(html),
87100
);
88101
}

0 commit comments

Comments
 (0)