Skip to content

Commit 2fbf03e

Browse files
authored
ci(create-objectstack): scaffold-E2E gate + registry canary + release-time template dep sync (#2912)
Close the remaining template-staleness gaps behind #2907 (fixes #2908): a scaffold-local PR job that scaffolds with the repo-built dist and runs the generated project end-to-end (install → validate → build → boot → health probes → docker build/run of examples/docker), a nightly registry canary running npx create-objectstack@latest across every template as a real first-run user, and scripts/sync-template-versions.mjs chained into the root version script so a release can never ship the blank template pinning a stale @objectstack/* range.
1 parent 10e8983 commit 2fbf03e

3 files changed

Lines changed: 234 additions & 1 deletion

File tree

.github/workflows/scaffold-e2e.yml

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Scaffold E2E — the first-run experience as a regression gate (#2908).
2+
#
3+
# Two failure classes broke `npm create objectstack` in the past and neither
4+
# was visible to unit tests:
5+
# 1. The bundled template drifted from the published framework (pinned
6+
# ^6.0.0 while the registry was at 14.x; used APIs removed in 14.x).
7+
# 2. Nothing ever exercised the published package end-to-end, so registry
8+
# breakage would only be found by a real new user.
9+
#
10+
# Job 1 (PRs touching the scaffolder / docker example) scaffolds with the
11+
# repo-built scaffolder and runs the generated project against the registry:
12+
# install → validate → build → boot → health probes → docker build/run.
13+
# Job 2 (nightly) does the same via the *published* `create-objectstack@latest`
14+
# across every template in the registry — the new-user canary.
15+
16+
name: Scaffold E2E
17+
18+
on:
19+
pull_request:
20+
paths:
21+
- 'packages/create-objectstack/**'
22+
- 'examples/docker/**'
23+
- '.github/workflows/scaffold-e2e.yml'
24+
schedule:
25+
- cron: '23 3 * * *'
26+
workflow_dispatch:
27+
28+
permissions:
29+
contents: read
30+
31+
jobs:
32+
scaffold-local:
33+
name: Scaffold with repo dist
34+
if: github.event_name != 'schedule'
35+
runs-on: ubuntu-latest
36+
timeout-minutes: 20
37+
steps:
38+
- name: Checkout repository
39+
uses: actions/checkout@v7
40+
41+
- name: Setup Node.js
42+
uses: actions/setup-node@v6
43+
with:
44+
node-version: '22'
45+
46+
- name: Enable Corepack
47+
run: corepack enable
48+
49+
- name: Get pnpm store directory
50+
shell: bash
51+
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
52+
53+
- name: Setup pnpm cache
54+
uses: actions/cache@v6
55+
with:
56+
path: ${{ env.STORE_PATH }}
57+
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
58+
restore-keys: |
59+
${{ runner.os }}-pnpm-store-v3-
60+
61+
- name: Install dependencies
62+
run: pnpm install --frozen-lockfile
63+
64+
- name: Build the scaffolder
65+
run: pnpm --filter create-objectstack build
66+
67+
- name: Scaffold a project
68+
run: |
69+
cd "$RUNNER_TEMP"
70+
node "$GITHUB_WORKSPACE/packages/create-objectstack/bin/create-objectstack.js" e2e-app --skip-install --skip-skills
71+
72+
- name: Install generated project (registry deps)
73+
run: |
74+
cd "$RUNNER_TEMP/e2e-app"
75+
# The scaffolder pins ^<repo version>. Right after a version bump
76+
# lands but before the release publishes, that version is not on the
77+
# registry yet — fall back to the latest published release so the
78+
# template is still exercised against the real registry.
79+
if ! npm install --no-fund --no-audit; then
80+
echo "::warning::repo version not published yet — falling back to latest"
81+
node -e '
82+
const fs = require("fs");
83+
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
84+
for (const deps of [pkg.dependencies, pkg.devDependencies]) {
85+
if (!deps) continue;
86+
for (const d of Object.keys(deps)) {
87+
if (d.startsWith("@objectstack/")) deps[d] = "latest";
88+
}
89+
}
90+
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
91+
'
92+
npm install --no-fund --no-audit
93+
fi
94+
95+
- name: Validate and build the generated project
96+
run: |
97+
cd "$RUNNER_TEMP/e2e-app"
98+
npm run validate
99+
npm run build
100+
101+
- name: Boot from the artifact and probe health
102+
run: |
103+
cd "$RUNNER_TEMP/e2e-app"
104+
npx os start --artifact ./dist/objectstack.json --port 8080 > server.log 2>&1 &
105+
SERVER_PID=$!
106+
ok=""
107+
for i in $(seq 1 30); do
108+
if curl -fsS http://localhost:8080/api/v1/health > /dev/null 2>&1; then ok=1; break; fi
109+
sleep 2
110+
done
111+
if [ -z "$ok" ]; then
112+
echo "::error::server never became healthy"; cat server.log; exit 1
113+
fi
114+
curl -fsS http://localhost:8080/api/v1/ready
115+
kill "$SERVER_PID"
116+
117+
- name: Docker build and run (examples/docker)
118+
run: |
119+
cd "$RUNNER_TEMP/e2e-app"
120+
cp "$GITHUB_WORKSPACE"/examples/docker/Dockerfile \
121+
"$GITHUB_WORKSPACE"/examples/docker/docker-compose.yml \
122+
"$GITHUB_WORKSPACE"/examples/docker/.dockerignore .
123+
docker build -t e2e-app .
124+
docker run -d --name e2e -p 18080:8080 \
125+
-e OS_SECRET_KEY="$(openssl rand -hex 32)" \
126+
-e OS_AUTH_SECRET="$(openssl rand -hex 32)" \
127+
e2e-app
128+
ok=""
129+
for i in $(seq 1 30); do
130+
if curl -fsS http://localhost:18080/api/v1/health > /dev/null 2>&1; then ok=1; break; fi
131+
sleep 2
132+
done
133+
if [ -z "$ok" ]; then
134+
echo "::error::container never became healthy"; docker logs e2e; exit 1
135+
fi
136+
docker rm -f e2e
137+
138+
registry-canary:
139+
name: 'Registry canary: ${{ matrix.template }}'
140+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
141+
runs-on: ubuntu-latest
142+
timeout-minutes: 15
143+
strategy:
144+
fail-fast: false
145+
matrix:
146+
template: [blank, todo, compliance, content, contracts, procurement]
147+
steps:
148+
- name: Setup Node.js
149+
uses: actions/setup-node@v6
150+
with:
151+
node-version: '22'
152+
153+
- name: Scaffold with published create-objectstack@latest
154+
run: |
155+
cd "$RUNNER_TEMP"
156+
npx -y create-objectstack@latest canary-app -t ${{ matrix.template }} --skip-skills
157+
158+
# Remote templates ship `build` (same gates) but not always `validate`.
159+
- name: Gate the generated project
160+
run: |
161+
cd "$RUNNER_TEMP/canary-app"
162+
if node -e "process.exit(JSON.parse(require('fs').readFileSync('package.json','utf8')).scripts?.validate ? 0 : 1)"; then
163+
npm run validate
164+
fi
165+
npm run build
166+
167+
- name: Boot and probe health (blank only)
168+
if: matrix.template == 'blank'
169+
run: |
170+
cd "$RUNNER_TEMP/canary-app"
171+
npx os start --artifact ./dist/objectstack.json --port 8080 > server.log 2>&1 &
172+
SERVER_PID=$!
173+
ok=""
174+
for i in $(seq 1 30); do
175+
if curl -fsS http://localhost:8080/api/v1/health > /dev/null 2>&1; then ok=1; break; fi
176+
sleep 2
177+
done
178+
if [ -z "$ok" ]; then
179+
echo "::error::server never became healthy"; cat server.log; exit 1
180+
fi
181+
curl -fsS http://localhost:8080/api/v1/ready
182+
kill "$SERVER_PID"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"test:e2e": "turbo run test:e2e",
1515
"clean": "turbo run clean && rm -rf dist",
1616
"setup": "pnpm install && pnpm --filter @objectstack/spec build",
17-
"version": "changeset version && node scripts/sync-protocol-version.mjs",
17+
"version": "changeset version && node scripts/sync-protocol-version.mjs && node scripts/sync-template-versions.mjs",
1818
"release": "pnpm run build && bash scripts/build-console.sh && bash scripts/release-publish.sh",
1919
"docs:dev": "pnpm --filter @objectstack/docs dev",
2020
"docs:build": "pnpm --filter @objectstack/docs build",

scripts/sync-template-versions.mjs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2026 ObjectStack contributors. Apache-2.0 license.
2+
//
3+
// Re-sync the create-objectstack blank template's @objectstack/* dependency
4+
// ranges with the scaffolder's own package version. Runs as part of the root
5+
// `version` script (changesets/action calls `pnpm run version` when preparing
6+
// the release PR), so a version bump can never ship with the template pinning
7+
// a stale range — the drift class behind #2907: the template froze at ^6.0.0
8+
// while the registry published 14.x, and every fresh `npm create objectstack`
9+
// project landed eight majors behind the docs. The scaffold-time dep rewrite
10+
// (pkg-utils.ts) and the ratchet test (template-consistency.test.ts) both
11+
// guard this too, but release PRs opened by changesets/action with the default
12+
// GITHUB_TOKEN do not trigger CI, so fixing the file at version time is the
13+
// only spot that cannot be skipped.
14+
15+
import { readFileSync, writeFileSync } from 'node:fs';
16+
import { fileURLToPath } from 'node:url';
17+
import { dirname, join } from 'node:path';
18+
19+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
20+
const scaffolderPkgPath = join(root, 'packages/create-objectstack/package.json');
21+
const templatePkgPath = join(
22+
root,
23+
'packages/create-objectstack/src/templates/blank/package.json',
24+
);
25+
26+
const version = JSON.parse(readFileSync(scaffolderPkgPath, 'utf8')).version;
27+
if (!/^\d+\.\d+\.\d+/.test(String(version))) {
28+
console.error(`✗ sync-template-versions: cannot parse create-objectstack version '${version}'`);
29+
process.exit(1);
30+
}
31+
const range = `^${String(version).split('.')[0]}.0.0`;
32+
33+
const templatePkg = JSON.parse(readFileSync(templatePkgPath, 'utf8'));
34+
let changed = 0;
35+
for (const deps of [templatePkg.dependencies, templatePkg.devDependencies]) {
36+
if (!deps) continue;
37+
for (const dep of Object.keys(deps)) {
38+
if (dep.startsWith('@objectstack/') && deps[dep] !== range) {
39+
console.log(` ${dep}: ${deps[dep]}${range}`);
40+
deps[dep] = range;
41+
changed++;
42+
}
43+
}
44+
}
45+
46+
if (changed === 0) {
47+
console.log(`✓ blank template already pins ${range} — in lockstep with create-objectstack@${version}`);
48+
} else {
49+
writeFileSync(templatePkgPath, JSON.stringify(templatePkg, null, 2) + '\n');
50+
console.log(`✓ blank template: ${changed} @objectstack/* range(s) → ${range} (lockstep with create-objectstack@${version})`);
51+
}

0 commit comments

Comments
 (0)