chore: version packages #240
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Scaffold E2E — the first-run experience as a regression gate (#2908). | |
| # | |
| # Two failure classes broke `npm create objectstack` in the past and neither | |
| # was visible to unit tests: | |
| # 1. The bundled template drifted from the published framework (pinned | |
| # ^6.0.0 while the registry was at 14.x; used APIs removed in 14.x). | |
| # 2. Nothing ever exercised the published package end-to-end, so registry | |
| # breakage would only be found by a real new user. | |
| # | |
| # Job 1 (PRs touching the scaffolder / docker example) scaffolds with the | |
| # repo-built scaffolder and runs the generated project against the registry: | |
| # install → validate → build → boot → health probes → docker build/run. | |
| # Job 2 (nightly) does the same via the *published* `create-objectstack@latest` | |
| # across every template in the registry — the new-user canary. | |
| name: Scaffold E2E | |
| on: | |
| pull_request: | |
| paths: | |
| - 'packages/create-objectstack/**' | |
| - 'docker/**' | |
| - '.github/workflows/scaffold-e2e.yml' | |
| schedule: | |
| - cron: '23 3 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| scaffold-local: | |
| name: Scaffold with repo dist | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Enable Corepack | |
| run: corepack enable | |
| - name: Get pnpm store directory | |
| shell: bash | |
| run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV | |
| - name: Setup pnpm cache | |
| uses: actions/cache@v6 | |
| with: | |
| path: ${{ env.STORE_PATH }} | |
| key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm-store-v3- | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build the scaffolder | |
| run: pnpm --filter create-objectstack build | |
| - name: Scaffold a project | |
| run: | | |
| cd "$RUNNER_TEMP" | |
| node "$GITHUB_WORKSPACE/packages/create-objectstack/bin/create-objectstack.js" e2e-app --skip-install --skip-skills | |
| - name: Install generated project (registry deps) | |
| run: | | |
| cd "$RUNNER_TEMP/e2e-app" | |
| # The scaffolder pins ^<repo version>. Right after a version bump | |
| # lands but before the release publishes, that version is not on the | |
| # registry yet — fall back to the latest published release so the | |
| # template is still exercised against the real registry. | |
| if ! npm install --no-fund --no-audit; then | |
| echo "::warning::repo version not published yet — falling back to latest" | |
| node -e ' | |
| const fs = require("fs"); | |
| const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); | |
| for (const deps of [pkg.dependencies, pkg.devDependencies]) { | |
| if (!deps) continue; | |
| for (const d of Object.keys(deps)) { | |
| if (d.startsWith("@objectstack/")) deps[d] = "latest"; | |
| } | |
| } | |
| fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); | |
| ' | |
| npm install --no-fund --no-audit | |
| fi | |
| - name: Validate and build the generated project | |
| run: | | |
| cd "$RUNNER_TEMP/e2e-app" | |
| npm run validate | |
| npm run build | |
| - name: Boot from the artifact and probe health | |
| run: | | |
| cd "$RUNNER_TEMP/e2e-app" | |
| npx os start --artifact ./dist/objectstack.json --port 8080 > server.log 2>&1 & | |
| SERVER_PID=$! | |
| ok="" | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://localhost:8080/api/v1/health > /dev/null 2>&1; then ok=1; break; fi | |
| sleep 2 | |
| done | |
| if [ -z "$ok" ]; then | |
| echo "::error::server never became healthy"; cat server.log; exit 1 | |
| fi | |
| curl -fsS http://localhost:8080/api/v1/ready | |
| kill "$SERVER_PID" | |
| - name: 'Skills boundary: only the curated skills/ catalog installs' | |
| # 15.1 third-party eval: the repo-internal dogfood-verification skill | |
| # (.claude/skills/) leaked into scaffolded projects. Two leak paths, | |
| # two assertions against THIS checkout: | |
| # 1. The scaffolder/docs command is `skills add …/framework/skills | |
| # --all` — the /skills subpath is the hard boundary, because the | |
| # skills CLI's --all implies --skill '*' which INCLUDES | |
| # metadata.internal skills. Install from the subpath and assert | |
| # set-equality with the curated catalog. | |
| # 2. Interactive `skills add …/framework` (no --all) relies on | |
| # metadata.internal hiding — assert repo-root discovery surfaces | |
| # exactly the curated set and nothing internal. | |
| run: | | |
| cd "$RUNNER_TEMP" | |
| node "$GITHUB_WORKSPACE/packages/create-objectstack/bin/create-objectstack.js" skills-probe --skip-install --skip-skills | |
| cd skills-probe | |
| expected=$(cd "$GITHUB_WORKSPACE/skills" && ls -d objectstack-*/ | tr -d '/' | sort) | |
| expected_count=$(echo "$expected" | wc -l | tr -d ' ') | |
| npx -y skills add "$GITHUB_WORKSPACE/skills" --all --copy | |
| installed=$(find . -name SKILL.md -not -path '*/node_modules/*' -exec dirname {} \; | xargs -rn1 basename | sort -u) | |
| echo "installed skills:"; echo "$installed" | |
| if [ "$installed" != "$expected" ]; then | |
| echo "::error::scaffolded skill set != curated skills/ catalog (internal-skill leak or missing skill)" | |
| printf 'expected:\n%s\n' "$expected" | |
| exit 1 | |
| fi | |
| # Strip ANSI color codes — the skills CLI colors the count even | |
| # when piped ("Found \e[32m9\e[39m skills"), which broke the greps. | |
| listing=$(npx -y skills add "$GITHUB_WORKSPACE" --list 2>&1 | sed 's/\x1b\[[0-9;]*m//g') | |
| echo "$listing" | |
| if echo "$listing" | grep -q 'dogfood-verification'; then | |
| echo "::error::repo-root discovery surfaced an internal skill (metadata.internal marker missing?)" | |
| exit 1 | |
| fi | |
| if ! echo "$listing" | grep -q "Found $expected_count skill"; then | |
| echo "::error::repo-root discovery does not match the curated catalog (expected $expected_count skills) — a SKILL.md outside skills/ is missing metadata.internal" | |
| exit 1 | |
| fi | |
| - name: Build official runtime image from this checkout | |
| # The scaffolded app's Dockerfile builds FROM | |
| # ghcr.io/objectstack-ai/objectstack. Build that base HERE from | |
| # docker/Dockerfile instead of pulling, so | |
| # (a) PRs exercise the official runtime Dockerfile itself, and | |
| # (b) the e2e stays hermetic — no dependency on a prior release | |
| # having published the tag (chicken-and-egg on the very first one). | |
| # | |
| # Pin the runtime's CLI to the SAME version the generated project | |
| # actually resolved to — NOT a hardcoded `latest`. The scaffolded | |
| # artifact is built by that project's `@objectstack/cli` and carries | |
| # its `engines.protocol` major; `os start` in the runtime image rejects | |
| # any artifact from a different protocol major. During an RC window the | |
| # project pins `^<repo rc>` (e.g. 16.0.0-rc.1, protocol 16) while the | |
| # `latest` dist-tag still points at the last STABLE cli (protocol 15), | |
| # so a hardcoded `latest` skews the two and boot fails. Reading the | |
| # resolved version keeps the image in lockstep whether the project got | |
| # the repo RC, the `latest` fallback (see the install step), or a | |
| # published stable. | |
| run: | | |
| CLI_VERSION=$(node -p "require('$RUNNER_TEMP/e2e-app/node_modules/@objectstack/cli/package.json').version") | |
| echo "Runtime image will bundle @objectstack/cli@$CLI_VERSION (matches the scaffolded artifact's protocol)" | |
| docker build -t ghcr.io/objectstack-ai/objectstack:latest \ | |
| --build-arg OS_CLI_VERSION="$CLI_VERSION" \ | |
| "$GITHUB_WORKSPACE/docker" | |
| - name: Docker build and run (scaffolded Dockerfile) | |
| # The blank template ships its own Dockerfile / docker-compose.yml / | |
| # .dockerignore, so build the generated project as-is — this exercises | |
| # the exact Docker path a real `npm create objectstack` user ships (the | |
| # standalone examples/docker copy was removed in 6c28bac). | |
| run: | | |
| cd "$RUNNER_TEMP/e2e-app" | |
| docker build --pull=false -t e2e-app . | |
| docker run -d --name e2e -p 18080:8080 \ | |
| -e OS_SECRET_KEY="$(openssl rand -hex 32)" \ | |
| -e OS_AUTH_SECRET="$(openssl rand -hex 32)" \ | |
| e2e-app | |
| ok="" | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://localhost:18080/api/v1/health > /dev/null 2>&1; then ok=1; break; fi | |
| sleep 2 | |
| done | |
| if [ -z "$ok" ]; then | |
| echo "::error::container never became healthy"; docker logs e2e; exit 1 | |
| fi | |
| docker rm -f e2e | |
| registry-canary: | |
| name: 'Registry canary: ${{ matrix.template }}' | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| template: [blank, todo, compliance, content, contracts, procurement] | |
| steps: | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Scaffold with published create-objectstack@latest | |
| run: | | |
| cd "$RUNNER_TEMP" | |
| npx -y create-objectstack@latest canary-app -t ${{ matrix.template }} --skip-skills | |
| # Remote templates ship `build` (same gates) but not always `validate`. | |
| - name: Gate the generated project | |
| run: | | |
| cd "$RUNNER_TEMP/canary-app" | |
| if node -e "process.exit(JSON.parse(require('fs').readFileSync('package.json','utf8')).scripts?.validate ? 0 : 1)"; then | |
| npm run validate | |
| fi | |
| npm run build | |
| - name: Boot and probe health (blank only) | |
| if: matrix.template == 'blank' | |
| run: | | |
| cd "$RUNNER_TEMP/canary-app" | |
| npx os start --artifact ./dist/objectstack.json --port 8080 > server.log 2>&1 & | |
| SERVER_PID=$! | |
| ok="" | |
| for i in $(seq 1 30); do | |
| if curl -fsS http://localhost:8080/api/v1/health > /dev/null 2>&1; then ok=1; break; fi | |
| sleep 2 | |
| done | |
| if [ -z "$ok" ]; then | |
| echo "::error::server never became healthy"; cat server.log; exit 1 | |
| fi | |
| curl -fsS http://localhost:8080/api/v1/ready | |
| kill "$SERVER_PID" |