From fa751c909d8ec44695de6eb3e0e8750d8f7f80fc Mon Sep 17 00:00:00 2001 From: Steve Juma Date: Tue, 13 Oct 2026 19:31:45 +0100 Subject: [PATCH 1/4] Require complete Web and native packaged CI acceptance Build a test-signed registry fixture from the immutable Plugins candidate and run the complete Web suite against it before aggregate validation. Require independent native macOS and Linux package builds, cold readiness and URL handoff smoke tests, and retained candidate artifacts. Consume Terminal Host through its public npm Deno and native metadata exports so clean checkouts need no sibling source. Validation: root check 18/18, unit 18/18 and build 10/10 tasks; desktop 218 passes with one existing skip; fresh macOS package and cold smoke; complete Web 10/10; release-tool 67/67 with the pinned CI environment; spec, ownership, metadata, release intent, formatting and actionlint pass. Production publication, signing and deployment stay separately gated. --- .github/workflows/lapis-ci.yml | 115 +++++++++++++++ .gitignore | 1 + deno.lock | 1 + package.json | 4 +- packages/desktop-deno/deno.json | 6 +- .../scripts/distribution.test.mjs | 4 +- packages/desktop-deno/scripts/package-app.mjs | 8 +- scripts/check-release-workflow.spec.mjs | 73 ++++++++++ scripts/run-web-registry-acceptance.mjs | 131 ++++++++++++++++++ scripts/web-registry-acceptance.test.mjs | 48 +++++++ spec-validator.config.mjs | 2 +- .../records/static-registry-github-ci-plan.md | 89 +++++++++--- spec/src/architecture/build-and-validation.md | 4 + .../packaging-and-acceptance.md | 4 + spec/src/packages/releases-and-exports.md | 20 ++- spec/src/spec-governance/validation-and-ci.md | 5 + spec/src/verification/pkg-02.md | 5 +- 17 files changed, 488 insertions(+), 32 deletions(-) create mode 100644 scripts/run-web-registry-acceptance.mjs create mode 100644 scripts/web-registry-acceptance.test.mjs diff --git a/.github/workflows/lapis-ci.yml b/.github/workflows/lapis-ci.yml index 8901a345..9d133b47 100644 --- a/.github/workflows/lapis-ci.yml +++ b/.github/workflows/lapis-ci.yml @@ -25,6 +25,7 @@ defaults: shell: bash env: + LAPIS_PLUGIN_CANDIDATE_COMMIT: "61bf175f21d91f17e06a071bdaadbaf5d87645a1" TURBO_API: ${{ vars.TURBO_API }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} @@ -158,6 +159,111 @@ jobs: - name: Report Storybook cache result run: node scripts/report-turbo-cache.mjs --github-summary + registry-fixture: + runs-on: ubuntu-latest + timeout-minutes: 30 + container: + image: ghcr.io/lapismd/lapis-ci@sha256:8fb0ecf5e978c9b44acd8394d78f689d93404f30f8014861a10a7eb2b90b96b8 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v7 + - name: Checkout the exact Plugins validation candidate + uses: actions/checkout@v7 + with: + repository: lapismd/lapis-plugins + ref: ${{ env.LAPIS_PLUGIN_CANDIDATE_COMMIT }} + path: .ci-candidates/plugins + - name: Build test-signed registry candidate + working-directory: .ci-candidates/plugins + env: + GITHUB_SHA: ${{ env.LAPIS_PLUGIN_CANDIDATE_COMMIT }} + run: | + pnpm install --frozen-lockfile + node scripts/check-framework-publication.mjs + pnpm build + pnpm packages:pack -- --already-built + pnpm plugin:build + node scripts/create-registry-acceptance-fixture.mjs + - uses: actions/upload-artifact@v7 + with: + name: signed-registry-acceptance-fixture + path: .ci-candidates/plugins/.release/registry-acceptance + if-no-files-found: error + retention-days: 14 + + web-acceptance: + needs: [build-cache, registry-fixture] + runs-on: ubuntu-latest + timeout-minutes: 30 + container: + image: ghcr.io/lapismd/lapis-ci@sha256:8fb0ecf5e978c9b44acd8394d78f689d93404f30f8014861a10a7eb2b90b96b8 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/ci-setup + - name: Restore package builds + run: node scripts/run-turbo.mjs build --summarize + - uses: actions/download-artifact@v8 + with: + name: signed-registry-acceptance-fixture + path: .ci-candidates/registry-fixture + - run: pnpm test:web:e2e + env: + LAPIS_WEB_REGISTRY_FIXTURE: .ci-candidates/registry-fixture + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: web-acceptance-diagnostics + path: packages/web/test-results + retention-days: 14 + + desktop-packaged: + needs: build-cache + strategy: + fail-fast: false + matrix: + os: [macos-15, ubuntu-24.04] + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + with: + version: 10.34.5 + - uses: actions/setup-node@v7 + with: + node-version: "24.15.0" + - uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 + with: + deno-version: v2.9.7 + - name: Install Linux native acceptance dependencies + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-0 libgtk-3-0t64 libayatana-appindicator3-1 libfuse2t64 xvfb dbus-x11 + - run: pnpm install --frozen-lockfile + - name: Build validation-only native package + run: pnpm package:desktop + - name: Cold packaged acceptance on macOS + if: runner.os == 'macOS' + run: pnpm test:desktop:packaged + - name: Cold packaged acceptance on Linux + if: runner.os == 'Linux' + run: xvfb-run -a dbus-run-session -- pnpm test:desktop:packaged + env: + APPIMAGE_EXTRACT_AND_RUN: "1" + - uses: actions/upload-artifact@v7 + with: + name: desktop-validation-${{ matrix.os }} + path: | + packages/desktop-deno/release/*.zip + packages/desktop-deno/release/*.AppImage + packages/desktop-deno/release/*.tar.gz + if-no-files-found: error + retention-days: 14 + release-artifacts: needs: build-cache runs-on: ubuntu-latest @@ -201,6 +307,9 @@ jobs: - unit - storybook-static - storybook-interaction + - web-acceptance + - registry-fixture + - desktop-packaged - release-artifacts runs-on: ubuntu-latest steps: @@ -212,6 +321,9 @@ jobs: UNIT: ${{ needs.unit.result }} STORYBOOK_STATIC: ${{ needs.storybook-static.result }} STORYBOOK_INTERACTION: ${{ needs.storybook-interaction.result }} + WEB_ACCEPTANCE: ${{ needs.web-acceptance.result }} + REGISTRY_FIXTURE: ${{ needs.registry-fixture.result }} + DESKTOP_PACKAGED: ${{ needs.desktop-packaged.result }} RELEASE_ARTIFACTS: ${{ needs.release-artifacts.result }} run: | set -euo pipefail @@ -222,6 +334,9 @@ jobs: "$UNIT" "$STORYBOOK_STATIC" "$STORYBOOK_INTERACTION" + "$WEB_ACCEPTANCE" + "$REGISTRY_FIXTURE" + "$DESKTOP_PACKAGED" "$RELEASE_ARTIFACTS" ) for result in "${results[@]}"; do diff --git a/.gitignore b/.gitignore index a64b32e4..edf0cff8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist-server .svelte-kit storybook-static .release/ +.ci-candidates/ .ci/dependency-context/ spec/book .qmd/index.sqlite* diff --git a/deno.lock b/deno.lock index 6dd1164f..96fb2f62 100644 --- a/deno.lock +++ b/deno.lock @@ -8644,6 +8644,7 @@ "dependencies": [ "npm:@codemirror/language@6.11.3", "npm:@lapismd/ai-controller@0.1", + "npm:@lapismd/terminal-host@0.1", "npm:@lezer/common@1.2.3", "npm:@lezer/highlight@1.2.1", "npm:@lezer/lr@1.4.2", diff --git a/package.json b/package.json index 3e4fa48d..3e5f0388 100644 --- a/package.json +++ b/package.json @@ -97,10 +97,10 @@ "test:visual:run": "node ./scripts/test-visual.mjs", "test:visual:update:run": "node ./scripts/update-visual-baselines.mjs", "test:spec-validation:run": "node --test scripts/spec-validation/*.test.mjs", - "release:test:run": "node --test ./scripts/storybook-turbo-cache.test.mjs ./scripts/ci-images.test.mjs ./scripts/ci-container.test.mjs ./scripts/check-release-intent.spec.mjs ./scripts/check-release-workflow.spec.mjs ./scripts/create-github-releases.spec.mjs ./scripts/publish-release.spec.mjs ./scripts/release-manifest.spec.mjs ./scripts/verify-release.spec.mjs ./scripts/release-plan.spec.mjs", + "release:test:run": "node --test ./scripts/web-registry-acceptance.test.mjs ./scripts/storybook-turbo-cache.test.mjs ./scripts/ci-images.test.mjs ./scripts/ci-container.test.mjs ./scripts/check-release-intent.spec.mjs ./scripts/check-release-workflow.spec.mjs ./scripts/create-github-releases.spec.mjs ./scripts/publish-release.spec.mjs ./scripts/release-manifest.spec.mjs ./scripts/verify-release.spec.mjs ./scripts/release-plan.spec.mjs", "test:desktop:packaged:run": "pnpm --filter @lapis-notes/desktop-deno test:packaged", "test:local-plugin-consumer:run": "node --test scripts/local-plugin-pnpmfile.test.mjs scripts/lib/local-plugin-consumer.test.mjs", - "test:web:e2e:run": "pnpm --filter @lapis-notes/web test:e2e", + "test:web:e2e:run": "node scripts/run-web-registry-acceptance.mjs", "test:specification:dev:run": "playwright test -c playwright.specification.config.ts", "test:specification:static:run": "SPECIFICATION_STATIC=1 playwright test -c playwright.specification.config.ts", "build-storybook:run": "node -e \"require('node:fs').mkdirSync('.visual-delta/cache',{recursive:true})\" && NODE_OPTIONS=\"${NODE_OPTIONS:---max-old-space-size=8192}\" storybook build --stats-json .visual-delta/cache && pnpm check:storybook-index", diff --git a/packages/desktop-deno/deno.json b/packages/desktop-deno/deno.json index 7f8906b7..02e239a9 100644 --- a/packages/desktop-deno/deno.json +++ b/packages/desktop-deno/deno.json @@ -19,7 +19,7 @@ "@lapis-notes/deno-host-services/vault-fs": "../deno-host-services/src/vault-fs.ts", "@lapis-notes/deno-host-services/vault-resources": "../deno-host-services/src/vault-resources.ts", "@lapis-notes/language-service/markdownlint/runtime": "../language-service/src/markdownlint/runtime.ts", - "@lapismd/terminal-host/deno": "../../../terminal-host/src/deno.ts", + "@lapismd/terminal-host/deno": "npm:@lapismd/terminal-host@^0.1.0/deno", "@lezer/common": "npm:@lezer/common@1.2.3", "@lezer/highlight": "npm:@lezer/highlight@1.2.1", "@lezer/lr": "npm:@lezer/lr@1.4.2", @@ -43,8 +43,6 @@ }, "compilerOptions": { "strict": true, - "lib": [ - "deno.window" - ] + "lib": ["deno.window"] } } diff --git a/packages/desktop-deno/scripts/distribution.test.mjs b/packages/desktop-deno/scripts/distribution.test.mjs index dbf2c5e0..43de5e6f 100644 --- a/packages/desktop-deno/scripts/distribution.test.mjs +++ b/packages/desktop-deno/scripts/distribution.test.mjs @@ -91,9 +91,9 @@ describe("Deno desktop distribution", () => { "utf8", ); expect(source).toContain( - 'path.resolve(packageDir, "../../../terminal-host")', + 'import.meta.resolve("@lapismd/terminal-host/native-artifacts.json")', ); - expect(source).toContain('"native-artifacts.json"'); + expect(source).not.toContain("../../../terminal-host"); expect(source).toContain('"--include"'); expect(source).toContain('"native"'); expect(source).toContain('"build/icon-light.png"'); diff --git a/packages/desktop-deno/scripts/package-app.mjs b/packages/desktop-deno/scripts/package-app.mjs index 3515b201..50cf4df7 100644 --- a/packages/desktop-deno/scripts/package-app.mjs +++ b/packages/desktop-deno/scripts/package-app.mjs @@ -35,9 +35,13 @@ const releaseDir = path.join(packageDir, "release"); const packageMetadata = JSON.parse( await readFile(path.join(packageDir, "package.json"), "utf8"), ); -const terminalHostDir = path.resolve(packageDir, "../../../terminal-host"); const terminalArtifacts = JSON.parse( - await readFile(path.join(terminalHostDir, "native-artifacts.json"), "utf8"), + await readFile( + new URL( + import.meta.resolve("@lapismd/terminal-host/native-artifacts.json"), + ), + "utf8", + ), ); function resolveDenoExecutable() { diff --git a/scripts/check-release-workflow.spec.mjs b/scripts/check-release-workflow.spec.mjs index a260855e..2847c31a 100644 --- a/scripts/check-release-workflow.spec.mjs +++ b/scripts/check-release-workflow.spec.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { readFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; import { parse } from "yaml"; import { validateReleaseWorkflows } from "./check-release-workflow.mjs"; import "./ci-setup.test.mjs"; @@ -178,6 +179,78 @@ test("generated Changesets PR retains remaining validation lanes", () => { assert.equal(workflow.jobs.validate.if, "always()"); }); +test("CI runs complete web and native packaged acceptance before aggregation", () => { + assert.equal( + workflow.jobs["web-acceptance"].steps.some( + (step) => step.run === "pnpm test:web:e2e", + ), + true, + ); + const native = workflow.jobs["desktop-packaged"]; + assert.deepEqual(native.strategy.matrix.os, ["macos-15", "ubuntu-24.04"]); + assert.equal(native.strategy["fail-fast"], false); + assert.ok(native.steps.some((step) => step.run === "pnpm package:desktop")); + assert.ok( + native.steps.some((step) => step.run === "pnpm test:desktop:packaged"), + ); + assert.ok( + native.steps.some((step) => + step.run?.includes( + "xvfb-run -a dbus-run-session -- pnpm test:desktop:packaged", + ), + ), + ); + assert.ok( + native.steps.some((step) => + step.uses?.startsWith("actions/upload-artifact@"), + ), + ); + for (const lane of ["web-acceptance", "desktop-packaged"]) + assert.ok(workflow.jobs.validate.needs.includes(lane)); +}); + +test("Web fixtures come from an immutable Plugins candidate", () => { + assert.match(workflow.env.LAPIS_PLUGIN_CANDIDATE_COMMIT, /^[a-f0-9]{40}$/); + const fixture = workflow.jobs["registry-fixture"]; + const checkout = fixture.steps.find( + (step) => step.with?.repository === "lapismd/lapis-plugins", + ); + assert.equal(checkout.with.ref, "${{ env.LAPIS_PLUGIN_CANDIDATE_COMMIT }}"); + const build = fixture.steps.find( + (step) => step.name === "Build test-signed registry candidate", + ); + assert.equal( + build.env.GITHUB_SHA, + "${{ env.LAPIS_PLUGIN_CANDIDATE_COMMIT }}", + ); + assert.match(build.run, /pnpm install --frozen-lockfile/); + assert.match( + build.run, + /node scripts\/create-registry-acceptance-fixture\.mjs/, + ); + assert.ok(workflow.jobs["web-acceptance"].needs.includes("registry-fixture")); +}); + +test("the actual Notes aggregate rejects missing acceptance in every lane", () => { + const step = workflow.jobs.validate.steps[0]; + const success = Object.fromEntries( + Object.keys(step.env).map((name) => [name, "success"]), + ); + const run = (env) => + spawnSync("bash", ["-c", step.run], { + env: { ...process.env, ...env }, + encoding: "utf8", + }); + assert.equal(run(success).status, 0); + for (const name of Object.keys(success)) + for (const result of ["skipped", "failure", "cancelled"]) + assert.notEqual( + run({ ...success, [name]: result }).status, + 0, + `${name}: ${result}`, + ); +}); + test("Pages restores workspace builds before loading Storybook configuration", () => { const pagesWorkflow = parse( readFileSync( diff --git a/scripts/run-web-registry-acceptance.mjs b/scripts/run-web-registry-acceptance.mjs new file mode 100644 index 00000000..922a5327 --- /dev/null +++ b/scripts/run-web-registry-acceptance.mjs @@ -0,0 +1,131 @@ +import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; +import { createServer } from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +export async function startRegistryFixture(directory) { + const root = path.resolve(directory); + const metadata = JSON.parse( + await readFile(path.join(root, "fixture.json"), "utf8"), + ); + if ( + metadata.schemaVersion !== 1 || + !/^[a-f0-9]{40}$/.test(metadata.sourceCommit) || + metadata.pluginCount < 1 + ) + throw new Error("Invalid registry acceptance artifact metadata"); + if ( + process.env.LAPIS_PLUGIN_CANDIDATE_COMMIT && + metadata.sourceCommit !== process.env.LAPIS_PLUGIN_CANDIDATE_COMMIT + ) + throw new Error( + "Registry fixture does not match the pinned plugin candidate", + ); + const server = createServer(async (request, response) => { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); + try { + const pathname = decodeURIComponent( + new URL(request.url, "http://localhost").pathname, + ); + const target = path.resolve(root, `.${pathname}`); + if (!pathname.startsWith("/v2/") || !target.startsWith(root + path.sep)) { + response.writeHead(404).end(); + return; + } + const bytes = await readFile(target); + response.setHeader( + "Content-Type", + target.endsWith(".json") + ? "application/json" + : "application/octet-stream", + ); + response.end(bytes); + } catch { + response.writeHead(404).end(); + } + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const url = `http://127.0.0.1:${server.address().port}`; + return { + environment: { + LAPIS_WEB_REGISTRY_ACCEPTANCE: "true", + VITE_LAPIS_LOCAL_REGISTRY_URL: `${url}/v2/index.json`, + VITE_LAPIS_LOCAL_REGISTRY_ARTIFACT_ORIGIN: metadata.artifactOrigin, + VITE_LAPIS_LOCAL_REGISTRY_TRUSTED_KEYS: JSON.stringify( + metadata.trustedKeys, + ), + }, + close: () => + new Promise((resolve, reject) => { + server.closeAllConnections(); + server.close((error) => (error ? reject(error) : resolve())); + }), + }; +} + +async function run(args, env) { + const root = fileURLToPath(new URL("../", import.meta.url)); + await new Promise((resolve, reject) => { + const child = spawn("pnpm", args, { + cwd: root, + env: { ...process.env, ...env }, + stdio: "inherit", + }); + child.once("error", reject); + child.once("exit", (code, signal) => + code === 0 + ? resolve() + : reject( + new Error( + `Web acceptance command failed (${ + signal ?? code + }): pnpm ${args.join(" ")}`, + ), + ), + ); + }); +} + +async function main() { + if (!process.env.LAPIS_WEB_REGISTRY_FIXTURE) + return run([ + "--filter", + "@lapis-notes/web", + "test:e2e", + ...process.argv.slice(2), + ]); + const fixture = await startRegistryFixture( + process.env.LAPIS_WEB_REGISTRY_FIXTURE, + ); + try { + await run(["build:web"]); + await run( + [ + "--filter", + "@lapis-notes/web", + "exec", + "vite", + "build", + "--mode", + "registry-acceptance", + "--outDir", + "dist-registry-acceptance", + ], + fixture.environment, + ); + await run( + ["--filter", "@lapis-notes/web", "test:e2e", ...process.argv.slice(2)], + fixture.environment, + ); + } finally { + await fixture.close(); + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) + await main(); diff --git a/scripts/web-registry-acceptance.test.mjs b/scripts/web-registry-acceptance.test.mjs new file mode 100644 index 00000000..36438689 --- /dev/null +++ b/scripts/web-registry-acceptance.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { startRegistryFixture } from "./run-web-registry-acceptance.mjs"; + +test("fixture transport serves only V2 data and closes its listener", async (t) => { + const previousCandidate = process.env.LAPIS_PLUGIN_CANDIDATE_COMMIT; + process.env.LAPIS_PLUGIN_CANDIDATE_COMMIT = "a".repeat(40); + t.after(() => { + if (previousCandidate === undefined) + delete process.env.LAPIS_PLUGIN_CANDIDATE_COMMIT; + else process.env.LAPIS_PLUGIN_CANDIDATE_COMMIT = previousCandidate; + }); + const root = await mkdtemp(path.join(tmpdir(), "lapis-registry-transport-")); + t.after(() => rm(root, { recursive: true, force: true })); + await mkdir(path.join(root, "v2")); + await writeFile( + path.join(root, "fixture.json"), + JSON.stringify({ + schemaVersion: 1, + sourceCommit: "a".repeat(40), + pluginCount: 12, + artifactOrigin: "https://registry.local.lapis.md", + trustedKeys: [], + }), + ); + await writeFile(path.join(root, "v2/index.json"), "{}"); + const fixture = await startRegistryFixture(root); + t.after(() => fixture.close()); + const url = fixture.environment.VITE_LAPIS_LOCAL_REGISTRY_URL; + const response = await fetch(url); + assert.equal(response.headers.get("content-type"), "application/json"); + assert.equal(response.headers.get("access-control-allow-origin"), "*"); + assert.deepEqual(await response.json(), {}); + for (const pathname of [ + "/fixture.json", + "/v2/%2e%2e/fixture.json", + "/v2/missing.json", + ]) + assert.equal((await fetch(new URL(pathname, url))).status, 404); + process.env.LAPIS_PLUGIN_CANDIDATE_COMMIT = "b".repeat(40); + await assert.rejects( + startRegistryFixture(root), + /does not match the pinned plugin candidate/, + ); +}); diff --git a/spec-validator.config.mjs b/spec-validator.config.mjs index 981690b9..a31551a7 100644 --- a/spec-validator.config.mjs +++ b/spec-validator.config.mjs @@ -123,7 +123,7 @@ export default defineConfig(headingRequirements(), { rules: [ { pattern: - "^(?:\\.ci/(?:dependency-image\\.Dockerfile|images\\.json)|\\.env\\.example|\\.github/actions/ci-setup/action\\.yml|\\.github/workflows/(?:dependency-image|lapis-ci|publish-storybook-pages|release)\\.yml|\\.changeset/(?:config\\.json|README\\.md)|RELEASING\\.md|scripts/(?:public-packages|release-plan|release-manifest|prepare-release|publish-release|verify-release|create-github-releases|check-package-boundaries|check-package-tarballs|check-release-config|check-release-workflow|check-release-intent|prepare-local-plugin-consumer|pack-packages|prepare-ci-dependency-context|update-ci-image-manifest|run-ci-container|run-turbo|report-turbo-cache|lib/(?:local-plugin-consumer|concurrency|turbo-invocation|ci-images|ci-container))\\.mjs|scripts/local-plugin-pnpmfile(?:\\.test)?\\.(?:cjs|mjs))$", + "^(?:\\.ci/(?:dependency-image\\.Dockerfile|images\\.json)|\\.env\\.example|\\.github/actions/ci-setup/action\\.yml|\\.github/workflows/(?:dependency-image|lapis-ci|publish-storybook-pages|release)\\.yml|\\.changeset/(?:config\\.json|README\\.md)|RELEASING\\.md|scripts/(?:run-web-registry-acceptance|public-packages|release-plan|release-manifest|prepare-release|publish-release|verify-release|create-github-releases|check-package-boundaries|check-package-tarballs|check-release-config|check-release-workflow|check-release-intent|prepare-local-plugin-consumer|pack-packages|prepare-ci-dependency-context|update-ci-image-manifest|run-ci-container|run-turbo|report-turbo-cache|lib/(?:local-plugin-consumer|concurrency|turbo-invocation|ci-images|ci-container))\\.mjs|scripts/local-plugin-pnpmfile(?:\\.test)?\\.(?:cjs|mjs))$", chapterPatterns: [ "^spec/src/architecture/(?!(?:index|context(?:-\\d+)?|context\\-01|context\\-02|context\\-03)\\.md$)[^/]+\\.md$", "^spec/src/packages/(?!(?:index|context(?:-\\d+)?|context\\-01|context\\-02|context\\-03)\\.md$)[^/]+\\.md$", diff --git a/spec/records/static-registry-github-ci-plan.md b/spec/records/static-registry-github-ci-plan.md index 4895b92d..720c1618 100644 --- a/spec/records/static-registry-github-ci-plan.md +++ b/spec/records/static-registry-github-ci-plan.md @@ -1,9 +1,62 @@ # Static registry integration and GitHub CI plan -The user paused final local verification on 2026-10-12. The task changes are -committed and integrated into the default workspaces. No GitHub push, package -publication, signing workflow, registry dispatch, deployment, or visual baseline -update is authorized by this handoff. +The user resumed this plan on 2026-10-13 after the Lapis Notes framework release. +This continuation prepares source-review PRs and blocking GitHub CI qualification. +Production plugin publication, protected registry signing, registry dispatch and +deployment remain separate release gates. + +## Resumed qualification — 2026-10-13 + +- Design Core 0.3.2 is published. Source PR 17 and version PR 18 are merged; + [CI run 38317471796](https://github.com/lapismd/design-core/actions/runs/38317471796) + executed all 11 jobs successfully, including cold catalog and browser lanes. +- Notes source PR 4 and the framework version PR are merged. npm exposes API, + UI and File Explorer 0.1.2 and Workspace 0.2.0 under `next`; `latest` still + names the older releases. Explicit portable ranges resolve the new versions. +- Plugins now qualifies API/UI/File Explorer 0.1.2, Workspace 0.2.0, + Language Service 0.1.1 and AI Controller 0.1.0. CI no longer looks up the + nonexistent API 0.1.1 or grants success when functional jobs are skipped. + The isolated `lapis-plugins-static-ci` workspace excludes the newer Sync + implementation from this source candidate. Notes pins the pushed candidate + `61bf175f21d91f17e06a071bdaadbaf5d87645a1` for validation-only artifact builds. + [Plugins PR 6](https://github.com/lapismd/lapis-plugins/pull/6) is open; its + source-bound 12-plugin archives and signed acceptance fixture verify locally. +- Notes has validation-only complete Web and native macOS/Linux packaged jobs. + Its Web lane consumes a retained, test-signed fixture built from an exact + frozen Plugins source candidate. Fixture keys are ephemeral and production + builds ignore fixture trust. Desktop packaging uses Terminal Host's public + npm Deno and native-artifact exports instead of a sibling source checkout. +- [Registry draft PR 12](https://github.com/lapismd/plugin-registry/pull/12) + contains the V2 source and a portable API 0.1.2 lock. Strict remote validation + verifies all 11 existing published entries. GitHub CI reaches the unchanged + production signature gate and fails because `generated/v2/index.sig` is absent. + The protected signing workflow can target this review branch and open a + generated-only PR against it before the final main merge. + +Current local evidence: Notes root check 18/18 tasks, root tests 18/18 tasks, +root build 10/10 tasks, desktop tests 218 passes plus one existing skip, and a +fresh ad-hoc-signed macOS packaged smoke pass. Complete Web acceptance changes +from four registry failures and six passes against the uncut production endpoint +to 10/10 passing against the verified 12-plugin fixture rebuilt from the pushed +Plugins commit. Notes release-tool tests pass 67/67 under the CI candidate-pin +environment. Its fixture transport regression changed from one failure to one +pass under that environment and verifies rejection of mismatched source metadata. +Registry records +8/8 focused schema/source tests, 77/77 script tests, 17/17 browser cases, +Astro check, statistics validation and formatting. Production V2 signature +verification and site packaging remain blocked on approved metadata signatures. + +The complete Plugins lane exposed a story setup that toggled an already-open +preference menu closed. Its corrected setup observes the public expanded state, +retains hover and catalog assertions, and passes the exact 62-story shard and +all 177 interaction/accessibility scenarios. Source also records 36 package +tasks, 105 release-tool tests and the static catalog build. Linux native +qualification must execute on GitHub; local macOS evidence is not Linux evidence. + +## Historical integration handoff + +The 2026-10-12 handoff paused final local verification after integrating the +following changes. Its local evidence is retained below for chronology. ## Integrated repositories @@ -20,7 +73,7 @@ Nostr/relay discovery remain deferred; Community presentation references do not reactivate its runtime. Older unrelated release, Sync and Community workspaces are preserved. -## Push sequence after approval +## Source-review sequence 1. Fetch the GitHub remote through Jujutsu and inspect new upstream commits, branch protection, package availability and the CI prerequisites. Reconcile @@ -40,23 +93,21 @@ are preserved. accepted. Package publication and deployment require a separate release approval, dependency order, and production credentials. -## Current CI coverage and work still needed +## Current CI coverage and remaining gates -| Repository | Existing PR checks | Remaining coverage or prerequisite | -| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Design Core | `ci.yml`: governance, quality, units, static catalog, Storybook interactions, Workspace/Shell/Shadcn/AI browser suites, full/production audits and aggregate validation | Verify a fresh catalog using the final `.js` helper imports. The full aggregate was not rerun after the final infrastructure/spec updates. See `registry-ui-ci-handoff.md`. | -| Notes | `lapis-ci.yml`: governance/release intent, quality/ownership, units, static catalog, full interactions, package/artifact planning and aggregate validation | Add validation-only full web acceptance and native packaged jobs; these are absent from the current PR workflow. macOS needs a cold first launch without refresh, URL handoff, controller/tool/service checks and clean close. Add Linux packaged acceptance with its native dependencies. | -| Plugins | `ci.yml`: build cache, audit/package/spec checks, units/release tests, static catalog, full interactions, pack/archive verification and release planning | Jobs are gated by `FRAMEWORK_PACKAGES_PUBLISHED` plus npm availability of API `0.1.1` and AI Controller. The aggregate intentionally succeeds when all functional jobs are deferred. This is not acceptance. | -| Registry | `checks.yml`: frozen install, strict remote artifact validation, generation/signatures, statistics, Astro check/build, unit/browser tests, formatting and generated-tree consistency | API `^0.1.1` must expose the new registry subpaths. Strict remote checks require published signed bundle origins; local review fixtures cannot prove production origins. | +| Repository | Existing PR checks | Remaining coverage or prerequisite | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| Design Core | `ci.yml`: governance, quality, units, static catalog, Storybook interactions, Workspace/Shell/Shadcn/AI browser suites, full/production audits and aggregate validation | Completed: all 11 jobs executed successfully on the released source. | +| Notes | `lapis-ci.yml`: governance/release intent, quality/ownership, units, static catalog, full interactions, complete Web, signed fixture, native macOS/Linux packages, package/artifact planning and aggregate validation | Source review and clean GitHub execution of every lane remain required; Linux evidence must come from its native runner. | +| Plugins | `ci.yml`: published-framework prerequisite, build cache, audit/package/spec checks, units/release tests, static catalog, full interactions, pack/archive verification and test-signed artifact planning | PR 6 must execute every lane successfully. Production publication and signing remain separate gates. | +| Registry | `checks.yml`: frozen install, strict remote artifact validation, generation/signatures, statistics, Astro check/build, unit/browser tests, formatting and generated-tree consistency | API `^0.1.2` resolves publicly. Production V2 metadata signatures are absent; approved signing must precede final CI and deployment. | Clean CI cannot consume ignored checkout-specific hooks or local tarballs. -The Design Core source changes and new API contracts must be validated before -consumer release qualification. To run cross-repository CI before publication, -first add an explicitly documented validation-only artifact lane that builds and -installs exact source candidates without modifying committed npm ranges or -lockfiles. Otherwise consumer qualification must wait for separately approved -upstream releases and dependency refreshes. Do not disable audits, ownership, -signature, strict remote, or prerequisite checks to obtain a green run. +The framework prerequisites are now published. Notes' documented validation-only +fixture lane builds exact Plugins candidates while retaining registry semver +manifests and lockfiles. Its ephemeral keys establish acceptance evidence only. +Do not disable audits, ownership, signature, strict remote, or prerequisite checks +to obtain a green run. ## Local evidence and review status diff --git a/spec/src/architecture/build-and-validation.md b/spec/src/architecture/build-and-validation.md index 7f737288..a4d42116 100644 --- a/spec/src/architecture/build-and-validation.md +++ b/spec/src/architecture/build-and-validation.md @@ -23,3 +23,7 @@ **Requirement.** API MUST own explicit sync enrollment, multi-epoch key custody, owner membership authority, bounded pairing state including validation of the user-confirmed SAS, and opaque recovery bundles. Protocol plugins MUST remain responsible for transporting returned artifacts, choosing endpoints, validating revision ancestry, and presenting pairing or recovery policy. + +The CI acceptance graph uses exact frozen source candidates for temporary signed +registry fixtures before V2 production cutover. Public package ranges stay +portable; full Web and native packaged results remain blocking under LN-PKG-142. diff --git a/spec/src/desktop-deno-host/packaging-and-acceptance.md b/spec/src/desktop-deno-host/packaging-and-acceptance.md index 4cdf3078..1a9b99ba 100644 --- a/spec/src/desktop-deno-host/packaging-and-acceptance.md +++ b/spec/src/desktop-deno-host/packaging-and-acceptance.md @@ -1,5 +1,9 @@ # Deno Desktop Host / Packaging and acceptance +Desktop distribution consumes the published Terminal Host Deno and native-artifact +exports. Clean CI builds on native macOS and Linux runners and executes the +packaged first-launch, handoff, service and close contract under LN-PKG-162. + ## LN-DENO-038 — Before mounting application UI, the desktop renderer: mark the document engine from the native diff --git a/spec/src/packages/releases-and-exports.md b/spec/src/packages/releases-and-exports.md index 99b43a87..5bb24c1c 100644 --- a/spec/src/packages/releases-and-exports.md +++ b/spec/src/packages/releases-and-exports.md @@ -241,7 +241,7 @@ Registry-consumer toolchain and build-policy acceptance is recorded in -**Requirement.** CI MUST populate package build outputs once, then run governance, quality, unit, static Storybook, interaction/accessibility, and release-artifact jobs concurrently before one stable `validate` aggregation job. Functional, interaction, and accessibility failures MUST block; visual comparison MUST remain non-blocking. +**Requirement.** CI MUST populate package build outputs once, then run governance, quality, unit, static Storybook, interaction/accessibility, web acceptance, native packaged acceptance and release-artifact jobs concurrently before one stable `validate` aggregation job. Every required job MUST succeed; functional, interaction, and accessibility failures MUST block. Visual comparison MUST remain non-blocking. ## LN-PKG-143 — Release candidates: reuse the validated Turbo build graph @@ -291,6 +291,24 @@ Registry-consumer toolchain and build-policy acceptance is recorded in **Requirement.** Public `@lapismd/terminal-host` MUST live in the `terminal-host` repository, version independently, and expose build, check, test, Deno embed, and CLI entries. Lapis hosts MUST consume it through published npm semver ranges. Deno desktop MUST pass the vault path, optional `cwd`, optional absolute `shell`, and a packaged verified native-library path. +## LN-PKG-161 — Complete web acceptance in CI + + + +**Requirement.** Pull-request and reusable CI MUST run the complete Web end-to-end suite against freshly built output after a frozen registry install. Failures MUST retain browser diagnostics and block the aggregate. Before production V2 cutover, CI MUST use a retained fixture from an exact frozen Plugins source candidate with ephemeral signing keys; production builds MUST ignore fixture trust. + +## LN-PKG-162 — Native packaged acceptance in CI + + + +**Requirement.** Pull-request and reusable CI MUST build validation-only packages on native macOS and Linux runners and execute cold packaged acceptance for first launch, URL handoff, controller, tools, services and clean close. Both platforms MUST complete independently and retain candidate artifacts. This lane MUST NOT publish, use production signing credentials or notarize. + +## LN-PKG-163 — Portable terminal dependency during desktop packaging + + + +**Requirement.** Desktop packaging MUST read native artifact metadata from the public npm Terminal Host export and resolve its Deno runtime through the public npm Deno entry. A clean checkout MUST NOT require a sibling Terminal Host checkout. Native downloads MUST retain checksum verification. + ## Current CI dependency image diff --git a/spec/src/spec-governance/validation-and-ci.md b/spec/src/spec-governance/validation-and-ci.md index ce1f6844..e86629b6 100644 --- a/spec/src/spec-governance/validation-and-ci.md +++ b/spec/src/spec-governance/validation-and-ci.md @@ -29,3 +29,8 @@ **Requirement.** Agent guidance MUST require canonical movable-panel registrations to declare their opening commands together, while file-backed editor views use editor associations and compatibility aliases reuse canonical commands. + +Changes to the shared validation workflow and Web fixture runner map to the +package acceptance requirements LN-PKG-161 and LN-PKG-162. Fixture signing uses +ephemeral keys and exact source provenance; it cannot authorize a release or +replace the production signing gate. diff --git a/spec/src/verification/pkg-02.md b/spec/src/verification/pkg-02.md index 348a024a..e0519b41 100644 --- a/spec/src/verification/pkg-02.md +++ b/spec/src/verification/pkg-02.md @@ -25,7 +25,7 @@ | LN-PKG-139 | [packages, spec-governance](../packages/releases-and-exports.md#ln-pkg-139) | Implemented | Release workflow validation requires `include-hidden-files: true`; the verified `.release` manifest, plan, and tarballs are retained as one GitHub artifact before publication. | | LN-PKG-140 | [packages, spec-governance](../packages/releases-and-exports.md#ln-pkg-140) | Implemented | Release workflow validation rejects publication jobs that are not gated by an explicit manual dispatch with the `publish` input enabled; ordinary pushes prepare the candidate artifact only. | | LN-PKG-141 | [packages, spec-governance](../packages/releases-and-exports.md#ln-pkg-141) | Implemented | Published-package dependency mapping and its partial-manifest regression install the selected manifest version plus unchanged UI, API, File Explorer, and Workspace registry versions before the import/resolve smoke check. | -| LN-PKG-142 | [packages, spec-governance](../packages/releases-and-exports.md#ln-pkg-142) | Implemented | CI workflow defines build-cache fan-out, five independent blocking validation lanes, and one stable aggregate result while excluding visual comparison from the blocking graph.; shared CI setup provisions and verifies Deno 2.9.7 before package tasks, with missing/wrong/correct-version regressions; normal release-test lane includes image-context and container regressions in CI unit execution | +| LN-PKG-142 | [packages, spec-governance](../packages/releases-and-exports.md#ln-pkg-142) | Implemented | CI workflow defines build-cache fan-out, independent blocking source, complete Web and native macOS/Linux packaged validation lanes, and one stable aggregate result while excluding visual comparison from the blocking graph.; shared CI setup provisions and verifies Deno 2.9.7 before package tasks, with missing/wrong/correct-version regressions; normal release-test lane includes image-context and container regressions in CI unit execution | | LN-PKG-143 | [packages, spec-governance](../packages/releases-and-exports.md#ln-pkg-143) | Implemented | Package packing exposes an already-built path; release workflow preserves candidate upload, manual dispatch, environment, OIDC, bootstrap, verification, and notes gates. | | LN-PKG-124 | [packages](../packages/releases-and-exports.md#ln-pkg-124) | Implemented | Desktop-package recovery tests verify private session ownership over API Safe Mode state without public exports or canonical vault configuration changes. | | LN-PKG-125 | [packages](../packages/core-contract.md#ln-pkg-125) | Implemented | AI owns conversation checkpoints; `@lapismd/ai-controller` owns later-task start, prompt, and journal delivery; desktop exposes only an authenticated controller connection | @@ -45,3 +45,6 @@ | LN-PKG-158 | [packages](../packages/releases-and-exports.md#ln-pkg-158) | Implemented | Generated-consumer regression copies only approved builds and rejects malformed policies; the same published-npm consumer passes without repository hooks, patches, or overrides; [release verification record](../../records/2026-10-13-registry-consumer-toolchain.md). | | LN-PKG-159 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-159) | Implemented | Public-entry inventory and real pnpm/Turbo fixture prove cache hits, source/argument invalidation, repeated failures, and forced reruns. The actual 37-test spec suite reuses its unchanged result; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | | LN-PKG-160 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-160) | Implemented | External-state tasks are explicitly uncached; argument/policy and real fixture regressions prove live acceptance disables cache reads/writes and executes twice; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | +| LN-PKG-161 | [packages](../packages/releases-and-exports.md#ln-pkg-161) | In progress | Workflow regression requires the complete Web suite and blocking aggregation; the native CI run supplies clean registry/browser evidence. | +| LN-PKG-162 | [packages](../packages/releases-and-exports.md#ln-pkg-162) | In progress | Workflow regression requires independent macOS/Linux packaging, cold smoke and candidate retention; native runner execution remains required. | +| LN-PKG-163 | [packages](../packages/releases-and-exports.md#ln-pkg-163) | In progress | Distribution regression requires public npm metadata instead of a sibling path; fresh packaged smoke verifies the public Deno runtime and checksum-verified native library. | From 5bc7165fb1d9aad118fa8410c498f61e0bf042d8 Mon Sep 17 00:00:00 2001 From: Steve Juma Date: Tue, 13 Oct 2026 20:39:08 +0100 Subject: [PATCH 2/4] Repair registry provenance and native package qualification boundaries Verify the exact Plugins checkout and export its commit inside the fixture build shell, since GitHub retains its default workflow SHA over step-level environment declarations. Correct macOS Deno output naming to avoid .app.app bundles and select the configured or PATH runtime consistently. Preserve error messages alongside WebKit stacks and include the failed startup phase in packaged smoke output; retain native candidates on failure. Validation: three focused regressions change from one failure each to one pass; root check 18/18, unit 18/18 and build 10/10 tasks; desktop 219 passes with one existing skip; release-tool 68/68; spec, ownership, metadata, release intent, formatting and actionlint pass. A fresh Deno 2.9.7 macOS package passes cold smoke. Linux startup remains under CI diagnosis; production gates remain closed. --- .github/workflows/lapis-ci.yml | 5 ++- .../desktop-deno/scripts/distribution.mjs | 1 + .../scripts/distribution.test.mjs | 1 + packages/desktop-deno/scripts/package-app.mjs | 7 ++- .../desktop-deno/scripts/smoke-packaged.mjs | 2 +- .../desktop-deno/src/desktop-recovery.test.ts | 10 +++++ packages/desktop-deno/src/desktop-recovery.ts | 8 +++- scripts/check-release-workflow.spec.mjs | 38 +++++++++++++--- .../records/static-registry-github-ci-plan.md | 43 +++++++++++++++++++ spec/src/architecture/build-and-validation.md | 2 + .../native-vault-and-storage-02.md | 2 +- .../packaging-and-acceptance.md | 3 ++ spec/src/packages/releases-and-exports.md | 3 ++ spec/src/spec-governance/validation-and-ci.md | 2 + spec/src/verification/deno.md | 2 +- spec/src/verification/pkg-02.md | 2 +- 16 files changed, 113 insertions(+), 18 deletions(-) diff --git a/.github/workflows/lapis-ci.yml b/.github/workflows/lapis-ci.yml index 9d133b47..462f1f0e 100644 --- a/.github/workflows/lapis-ci.yml +++ b/.github/workflows/lapis-ci.yml @@ -177,9 +177,9 @@ jobs: path: .ci-candidates/plugins - name: Build test-signed registry candidate working-directory: .ci-candidates/plugins - env: - GITHUB_SHA: ${{ env.LAPIS_PLUGIN_CANDIDATE_COMMIT }} run: | + test "$(git rev-parse HEAD)" = "$LAPIS_PLUGIN_CANDIDATE_COMMIT" + export GITHUB_SHA="$LAPIS_PLUGIN_CANDIDATE_COMMIT" pnpm install --frozen-lockfile node scripts/check-framework-publication.mjs pnpm build @@ -255,6 +255,7 @@ jobs: env: APPIMAGE_EXTRACT_AND_RUN: "1" - uses: actions/upload-artifact@v7 + if: always() with: name: desktop-validation-${{ matrix.os }} path: | diff --git a/packages/desktop-deno/scripts/distribution.mjs b/packages/desktop-deno/scripts/distribution.mjs index fffb86a3..b4ec03c0 100644 --- a/packages/desktop-deno/scripts/distribution.mjs +++ b/packages/desktop-deno/scripts/distribution.mjs @@ -55,6 +55,7 @@ export function createDistributionPlan({ target, baseName, icon: path.join(sharedBuildDir, "icon.icns"), + desktopOutput: path.join(releaseDir, baseName), appBundle: path.join(releaseDir, `${baseName}.app`), archive: path.join(releaseDir, `${baseName}.zip`), }; diff --git a/packages/desktop-deno/scripts/distribution.test.mjs b/packages/desktop-deno/scripts/distribution.test.mjs index 43de5e6f..7c504954 100644 --- a/packages/desktop-deno/scripts/distribution.test.mjs +++ b/packages/desktop-deno/scripts/distribution.test.mjs @@ -52,6 +52,7 @@ describe("Deno desktop distribution", () => { ...common, target: "aarch64-apple-darwin", }); + expect(mac.desktopOutput).toBe(path.join(common.releaseDir, mac.baseName)); expect( mac.appBundle.endsWith( path.join("release", "Lapis-Notes-2026.31.5-macos-arm64.app"), diff --git a/packages/desktop-deno/scripts/package-app.mjs b/packages/desktop-deno/scripts/package-app.mjs index 50cf4df7..26cafae0 100644 --- a/packages/desktop-deno/scripts/package-app.mjs +++ b/packages/desktop-deno/scripts/package-app.mjs @@ -46,9 +46,7 @@ const terminalArtifacts = JSON.parse( function resolveDenoExecutable() { const configured = process.env.DENO_BIN?.trim(); - if (configured) return configured; - const userInstall = path.join(os.homedir(), ".deno", "bin", "deno"); - return fs.existsSync(userInstall) ? userInstall : "deno"; + return configured || "deno"; } async function run(command, args) { @@ -217,7 +215,8 @@ async function notarizeMacApplication(appBundle) { } async function packageMac(plan) { - await buildDesktopOutput(plan, plan.appBundle); + await rm(plan.appBundle, { force: true, recursive: true }); + await buildDesktopOutput(plan, plan.desktopOutput); await declareMacApplicationMetadata(plan.appBundle); await writeFile( path.join( diff --git a/packages/desktop-deno/scripts/smoke-packaged.mjs b/packages/desktop-deno/scripts/smoke-packaged.mjs index b97a86f0..22b9fdab 100644 --- a/packages/desktop-deno/scripts/smoke-packaged.mjs +++ b/packages/desktop-deno/scripts/smoke-packaged.mjs @@ -253,7 +253,7 @@ try { await waitForCleanExit(secondary, secondaryDiagnostics); const report = await waitForReport(reportPath, child, diagnostics); - assert.equal(report.ok, true, report.detail); + assert.equal(report.ok, true, JSON.stringify(report, null, 2)); assert.equal(report.runtime, "deno-desktop"); assert.equal(report.vault, "vault"); assert.deepEqual(report.plugins, expectedPlugins); diff --git a/packages/desktop-deno/src/desktop-recovery.test.ts b/packages/desktop-deno/src/desktop-recovery.test.ts index ea22c72c..1b7ca92b 100644 --- a/packages/desktop-deno/src/desktop-recovery.test.ts +++ b/packages/desktop-deno/src/desktop-recovery.test.ts @@ -84,6 +84,16 @@ describe("Deno desktop startup recovery", () => { expect(diagnostic.detail).toContain("/Users/example/Desktop/notes"); }); + it("preserves the error message when WebKit stacks contain only locations", () => { + const error = new Error("Native database could not open"); + error.stack = + "@user-script:1:58:33\nglobal code@http://127.0.0.1:1234/:1:23"; + const failure = createDesktopStartupFailure("vault", error); + expect(failure.detail).toContain("Native database could not open"); + expect(failure.detail).toContain(error.stack); + expect(failure.message).toBe(error.message); + }); + it("describes layout-only recovery without unavailable policies", () => { const storage = memoryStorage(); const state = updateDesktopSafeModeState( diff --git a/packages/desktop-deno/src/desktop-recovery.ts b/packages/desktop-deno/src/desktop-recovery.ts index 554942bd..f1b92e32 100644 --- a/packages/desktop-deno/src/desktop-recovery.ts +++ b/packages/desktop-deno/src/desktop-recovery.ts @@ -103,10 +103,16 @@ export function createDesktopStartupFailure( error: unknown, ): AppSafeModeFailure { const message = error instanceof Error ? error.message : String(error); + let detail = message; + if (error instanceof Error && error.stack) { + detail = error.stack; + if (!detail.includes(message)) + detail = `${error.name}: ${message}\n${detail}`; + } return { task, message, - detail: error instanceof Error ? (error.stack ?? error.message) : message, + detail, pluginId: null, }; } diff --git a/scripts/check-release-workflow.spec.mjs b/scripts/check-release-workflow.spec.mjs index 2847c31a..cd259575 100644 --- a/scripts/check-release-workflow.spec.mjs +++ b/scripts/check-release-workflow.spec.mjs @@ -200,10 +200,11 @@ test("CI runs complete web and native packaged acceptance before aggregation", ( ), ), ); - assert.ok( - native.steps.some((step) => + assert.equal( + native.steps.find((step) => step.uses?.startsWith("actions/upload-artifact@"), - ), + ).if, + "always()", ); for (const lane of ["web-acceptance", "desktop-packaged"]) assert.ok(workflow.jobs.validate.needs.includes(lane)); @@ -219,10 +220,6 @@ test("Web fixtures come from an immutable Plugins candidate", () => { const build = fixture.steps.find( (step) => step.name === "Build test-signed registry candidate", ); - assert.equal( - build.env.GITHUB_SHA, - "${{ env.LAPIS_PLUGIN_CANDIDATE_COMMIT }}", - ); assert.match(build.run, /pnpm install --frozen-lockfile/); assert.match( build.run, @@ -231,6 +228,33 @@ test("Web fixtures come from an immutable Plugins candidate", () => { assert.ok(workflow.jobs["web-acceptance"].needs.includes("registry-fixture")); }); +test("fixture build uses the checked-out Plugins source instead of the host workflow SHA", () => { + const build = workflow.jobs["registry-fixture"].steps.find( + (step) => step.name === "Build test-signed registry candidate", + ); + const candidate = workflow.env.LAPIS_PLUGIN_CANDIDATE_COMMIT; + const script = ` + pnpm() { :; } + git() { printf '%s\\n' "$TEST_CHECKOUT_SHA"; } + node() { printf '%s\\n' "$GITHUB_SHA"; } + ${build.run} + `; + const run = (checkout) => + spawnSync("bash", ["-e", "-o", "pipefail", "-c", script], { + env: { + ...process.env, + GITHUB_SHA: "b".repeat(40), + LAPIS_PLUGIN_CANDIDATE_COMMIT: candidate, + TEST_CHECKOUT_SHA: checkout, + }, + encoding: "utf8", + }); + const matched = run(candidate); + assert.equal(matched.status, 0, matched.stderr); + assert.equal(matched.stdout.trim(), `${candidate}\n${candidate}`); + assert.notEqual(run("c".repeat(40)).status, 0); +}); + test("the actual Notes aggregate rejects missing acceptance in every lane", () => { const step = workflow.jobs.validate.steps[0]; const success = Object.fromEntries( diff --git a/spec/records/static-registry-github-ci-plan.md b/spec/records/static-registry-github-ci-plan.md index 720c1618..5ae04cd1 100644 --- a/spec/records/static-registry-github-ci-plan.md +++ b/spec/records/static-registry-github-ci-plan.md @@ -53,6 +53,49 @@ all 177 interaction/accessibility scenarios. Source also records 36 package tasks, 105 release-tool tests and the static catalog build. Linux native qualification must execute on GitHub; local macOS evidence is not Linux evidence. +### GitHub execution and repaired boundaries + +- [Plugins CI 38454548155](https://github.com/lapismd/lapis-plugins/actions/runs/38454548155) + executed all nine jobs successfully, including 177 interaction/accessibility + scenarios, 105 release-tool tests and retained test-signed candidate artifacts. +- [Notes PR 13](https://github.com/lapismd/lapis-notes/pull/13) adds the complete + acceptance graph. Its first run passed governance, quality, units, static + catalog, all 243 interaction/accessibility scenarios, fixture generation and + artifact planning. Web rejected incorrectly + labelled provenance: GitHub retained Notes' workflow SHA despite a step-level + environment override. The fixture build now verifies its Plugins checkout + and exports provenance inside the executing shell. Its exact regression + changes from one failure to one pass and rejects a mismatched checkout. +- The first macOS job exposed Deno 2.9.7 appending `.app` to an output name that + already ended in `.app`. Packaging now supplies an extension-free name and + selects `DENO_BIN` or `PATH`. Earlier local packaging had selected the separate + Deno 2.9.5 user installation and did not establish 2.9.7 qualification. + The corrected fresh Deno 2.9.7 package passes local cold smoke, including + first launch, URL handoff, controller/tool/service readiness and clean close. +- The first Linux package built but startup failed. WebKit's location-only + stack concealed the actual message. Desktop diagnostics now retain both; + packaged smoke reports include the failed phase, and failed native jobs retain + their archives. The message-preservation regression changes from one failure + to one pass. Linux startup qualification remains required. +- The repaired Notes candidate passes root check 18/18 tasks, root unit 18/18 + tasks, build 10/10 tasks, desktop 219 tests plus one existing skip, 68 release + tests, specification/App-ownership/metadata gates, actionlint and formatting. +- Registry Astro generation previously modified a tracked cache file outside + `generated/`, which would reject a signing candidate. Generated Astro files + are now untracked and ignored; cold type generation and all 77 script tests + pass, and repeated source checking leaves the committed tree clean. + +### Next protected release gate + +Once Notes' required GitHub jobs pass, the next action is protected production +metadata signing for registry PR 12's `review/static-registry-v2` branch. Run the +review branch's signing workflow with `source_ref=review/static-registry-v2` only +after explicit approval; it creates a generated-only PR against that branch. +Review and integrate those signatures, then require registry CI before the final +source merge. npm plugin publication, registry release dispatch and Cloudflare +deployment each remain separately approved operations. No production operation +has been performed during this continuation. + ## Historical integration handoff The 2026-10-12 handoff paused final local verification after integrating the diff --git a/spec/src/architecture/build-and-validation.md b/spec/src/architecture/build-and-validation.md index a4d42116..aa9eebc3 100644 --- a/spec/src/architecture/build-and-validation.md +++ b/spec/src/architecture/build-and-validation.md @@ -27,3 +27,5 @@ The CI acceptance graph uses exact frozen source candidates for temporary signed registry fixtures before V2 production cutover. Public package ranges stay portable; full Web and native packaged results remain blocking under LN-PKG-142. +Fixture builds verify the checked-out Plugins commit before setting source +provenance inside the executing shell, independently of the host workflow SHA. diff --git a/spec/src/desktop-deno-host/native-vault-and-storage-02.md b/spec/src/desktop-deno-host/native-vault-and-storage-02.md index 86a37eb8..4af4228b 100644 --- a/spec/src/desktop-deno-host/native-vault-and-storage-02.md +++ b/spec/src/desktop-deno-host/native-vault-and-storage-02.md @@ -58,7 +58,7 @@ Native About-window acceptance verifies: -**Requirement.** A failed saved-vault restore MUST retain the profile and recent-vault entry, show the actual failure, and allow another vault to be selected. Session startup failures MUST keep the failed phase visible and expose the complete error stack plus a copyable diagnostic; they MUST NOT return to an indefinite loading surface. +**Requirement.** A failed saved-vault restore MUST retain the profile and recent-vault entry, show the actual failure, and allow another vault to be selected. Session startup failures MUST keep the failed phase visible and expose the error message, complete stack and copyable diagnostic, including WebKit stacks that omit their message. They MUST NOT return to an indefinite loading surface. ## LN-DENO-053 — Desktop startup recovery: offer retry, Manage Vaults, optional-core-plugin disablement, layout-skip diff --git a/spec/src/desktop-deno-host/packaging-and-acceptance.md b/spec/src/desktop-deno-host/packaging-and-acceptance.md index 1a9b99ba..12dbe6f2 100644 --- a/spec/src/desktop-deno-host/packaging-and-acceptance.md +++ b/spec/src/desktop-deno-host/packaging-and-acceptance.md @@ -3,6 +3,9 @@ Desktop distribution consumes the published Terminal Host Deno and native-artifact exports. Clean CI builds on native macOS and Linux runners and executes the packaged first-launch, handoff, service and close contract under LN-PKG-162. +macOS packaging passes an extension-free output name to Deno, which adds `.app`. +Packaging selects `DENO_BIN` when configured and otherwise uses the runtime on +`PATH`, matching the pinned CI toolchain instead of a separate user installation. ## LN-DENO-038 — Before mounting application UI, the desktop renderer: mark the document engine from the native diff --git a/spec/src/packages/releases-and-exports.md b/spec/src/packages/releases-and-exports.md index 5bb24c1c..f0142d2e 100644 --- a/spec/src/packages/releases-and-exports.md +++ b/spec/src/packages/releases-and-exports.md @@ -297,6 +297,9 @@ Registry-consumer toolchain and build-policy acceptance is recorded in **Requirement.** Pull-request and reusable CI MUST run the complete Web end-to-end suite against freshly built output after a frozen registry install. Failures MUST retain browser diagnostics and block the aggregate. Before production V2 cutover, CI MUST use a retained fixture from an exact frozen Plugins source candidate with ephemeral signing keys; production builds MUST ignore fixture trust. +Fixture builds verify their checkout and export source provenance in the executing +shell so the host workflow SHA cannot label another repository's artifacts. + ## LN-PKG-162 — Native packaged acceptance in CI diff --git a/spec/src/spec-governance/validation-and-ci.md b/spec/src/spec-governance/validation-and-ci.md index e86629b6..e31d13fd 100644 --- a/spec/src/spec-governance/validation-and-ci.md +++ b/spec/src/spec-governance/validation-and-ci.md @@ -34,3 +34,5 @@ Changes to the shared validation workflow and Web fixture runner map to the package acceptance requirements LN-PKG-161 and LN-PKG-162. Fixture signing uses ephemeral keys and exact source provenance; it cannot authorize a release or replace the production signing gate. +Workflow-shell regressions execute checkout and provenance handling against a +different host SHA and reject a mismatched checkout. diff --git a/spec/src/verification/deno.md b/spec/src/verification/deno.md index e7bdfca2..06f1fb78 100644 --- a/spec/src/verification/deno.md +++ b/spec/src/verification/deno.md @@ -54,7 +54,7 @@ | LN-DENO-049 | [desktop-deno-host](../desktop-deno-host/window-and-shell.md#ln-deno-049) | Implemented | Development-command tests cover the root Deno workspace and cache-only npm resolution, deterministic WebView/CEF host layouts, and pinned Deno packaging arguments; the launcher patches and reuses only an ignored package-local bundle through `LAUFEY_DEV_DIR`; menu-model tests preserve About and Quit; live system-WebView acceptance verifies the first macOS menu-bar item no longer reports the shared Laufey `Deno` identity. | | LN-DENO-050 | [desktop-deno-host](../desktop-deno-host/startup-and-lifecycle.md#ln-deno-050) | Implemented | Development-command tests distinguish identity, signature freshness, and a valid unchanged runtime; the macOS launcher repairs current cached hosts, signs after plist identity patching and Deno runtime insertion, and accepts a reused runtime only after deep strict verification. | | LN-DENO-051 | [desktop-deno-host](../desktop-deno-host/native-vault-and-storage-02.md#ln-deno-051) | Implemented | Native filesystem tests cover portable classification of missing, permission, wrong-type, busy, and existence failures; existence and stat suppress only missing paths. | -| LN-DENO-052 | [desktop-deno-host](../desktop-deno-host/native-vault-and-storage-02.md#ln-deno-052) | Implemented | Desktop recovery tests preserve failed saved profiles, full stack details, absolute target paths, copyable diagnostics, and another-vault access instead of silently clearing restore state. | +| LN-DENO-052 | [desktop-deno-host](../desktop-deno-host/native-vault-and-storage-02.md#ln-deno-052) | Implemented | Desktop recovery tests preserve failed saved profiles, error messages even when WebKit stacks omit them, full stack details, absolute target paths, copyable diagnostics, and another-vault access instead of silently clearing restore state. | | LN-DENO-053 | [desktop-deno-host](../desktop-deno-host/native-vault-and-storage-02.md#ln-deno-053) | Implemented | Session recovery tests cover normalized Safe Mode persistence, optional-plugin disablement, layout skip/reset, retry, and Manage Vaults actions while Design Core interaction coverage keeps failure controls active inside drag regions. | | LN-DENO-054 | [desktop-deno-host](../desktop-deno-host/startup-and-lifecycle.md#ln-deno-054) | Implemented | Desktop recovery and session-source tests cover visible Safe Mode reasons, metadata/Search rebuilds, normal restart, and omission of unavailable community-plugin and notebook controls. | | LN-DENO-055 | [desktop-deno-host, ai-plugin](../desktop-deno-host/startup-and-lifecycle.md#ln-deno-055) | Implemented | Desktop advertises only an authenticated lapis-ai-controller connection; leftover ACP embed helpers may still defer start. Renderer no longer listens for retired desktop*agent*\* ACP event channels. | diff --git a/spec/src/verification/pkg-02.md b/spec/src/verification/pkg-02.md index e0519b41..6a7c0af4 100644 --- a/spec/src/verification/pkg-02.md +++ b/spec/src/verification/pkg-02.md @@ -45,6 +45,6 @@ | LN-PKG-158 | [packages](../packages/releases-and-exports.md#ln-pkg-158) | Implemented | Generated-consumer regression copies only approved builds and rejects malformed policies; the same published-npm consumer passes without repository hooks, patches, or overrides; [release verification record](../../records/2026-10-13-registry-consumer-toolchain.md). | | LN-PKG-159 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-159) | Implemented | Public-entry inventory and real pnpm/Turbo fixture prove cache hits, source/argument invalidation, repeated failures, and forced reruns. The actual 37-test spec suite reuses its unchanged result; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | | LN-PKG-160 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-160) | Implemented | External-state tasks are explicitly uncached; argument/policy and real fixture regressions prove live acceptance disables cache reads/writes and executes twice; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | -| LN-PKG-161 | [packages](../packages/releases-and-exports.md#ln-pkg-161) | In progress | Workflow regression requires the complete Web suite and blocking aggregation; the native CI run supplies clean registry/browser evidence. | +| LN-PKG-161 | [packages](../packages/releases-and-exports.md#ln-pkg-161) | In progress | Workflow regression requires the complete Web suite, blocking aggregation, and an executed fixture build that verifies its checkout and overrides the host workflow SHA in the shell; clean GitHub browser execution remains required. | | LN-PKG-162 | [packages](../packages/releases-and-exports.md#ln-pkg-162) | In progress | Workflow regression requires independent macOS/Linux packaging, cold smoke and candidate retention; native runner execution remains required. | | LN-PKG-163 | [packages](../packages/releases-and-exports.md#ln-pkg-163) | In progress | Distribution regression requires public npm metadata instead of a sibling path; fresh packaged smoke verifies the public Deno runtime and checksum-verified native library. | From 13fbed20149826b41c1b0925efa1dd9d978d2fc4 Mon Sep 17 00:00:00 2001 From: Steve Juma Date: Tue, 13 Oct 2026 21:02:48 +0100 Subject: [PATCH 3/4] Provision real controller and Docker prerequisites for full acceptance Native runners build and install the separately distributed controller CLI from exact public release source 0082a115, pass its isolated executable path to packaged smoke, and retain its manifest. The SDK package alone does not install that service executable. Run complete Web acceptance on native Ubuntu with Chromium and Docker, building the candidate server image before the suite. Exclude candidate areas from the server image context. Validation: two missing-prerequisite regressions change from failures to passes; 70 release-tool and 23 workflow/transport tests; spec, ownership, metadata, release intent, formatting and actionlint. The released controller source passes 194 Node tests and seven runtime-install tests; Notes macOS cold smoke passes with its isolated CLI. A fresh server image passes the exact server-host test and all ten Web cases. Native GitHub qualification remains required. --- .dockerignore | 1 + .github/workflows/lapis-ci.yml | 38 ++++++++++++++---- scripts/check-release-workflow.spec.mjs | 39 +++++++++++++++++++ .../records/static-registry-github-ci-plan.md | 19 +++++++++ spec/src/architecture/build-and-validation.md | 3 ++ spec/src/packages/releases-and-exports.md | 7 ++++ spec/src/spec-governance/validation-and-ci.md | 2 + spec/src/verification/pkg-02.md | 2 +- 8 files changed, 103 insertions(+), 8 deletions(-) diff --git a/.dockerignore b/.dockerignore index b90c1ba3..00e74cff 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ .env.* .pnpmfile.cjs .release +.ci-candidates .turbo .visual-delta coverage diff --git a/.github/workflows/lapis-ci.yml b/.github/workflows/lapis-ci.yml index 462f1f0e..6b212c69 100644 --- a/.github/workflows/lapis-ci.yml +++ b/.github/workflows/lapis-ci.yml @@ -26,6 +26,7 @@ defaults: env: LAPIS_PLUGIN_CANDIDATE_COMMIT: "61bf175f21d91f17e06a071bdaadbaf5d87645a1" + LAPIS_CONTROLLER_CANDIDATE_COMMIT: "0082a1151ad2da20d0288384636b25bec0778482" TURBO_API: ${{ vars.TURBO_API }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} @@ -195,18 +196,25 @@ jobs: web-acceptance: needs: [build-cache, registry-fixture] - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 timeout-minutes: 30 - container: - image: ghcr.io/lapismd/lapis-ci@sha256:8fb0ecf5e978c9b44acd8394d78f689d93404f30f8014861a10a7eb2b90b96b8 - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v7 - - uses: ./.github/actions/ci-setup + - uses: pnpm/action-setup@v6 + with: + version: 10.34.5 + - uses: actions/setup-node@v7 + with: + node-version: "24.15.0" + - uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 + with: + deno-version: v2.9.7 + - run: pnpm install --frozen-lockfile + - run: pnpm exec playwright install --with-deps chromium - name: Restore package builds run: node scripts/run-turbo.mjs build --summarize + - name: Build the server-host acceptance image + run: docker build -f packages/server-deno/Dockerfile -t lapis-server:e2e . - uses: actions/download-artifact@v8 with: name: signed-registry-acceptance-fixture @@ -231,6 +239,12 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v7 + - name: Checkout the released controller source + uses: actions/checkout@v7 + with: + repository: lapismd/ai-controller + ref: ${{ env.LAPIS_CONTROLLER_CANDIDATE_COMMIT }} + path: .ci-candidates/controller - uses: pnpm/action-setup@v6 with: version: 10.34.5 @@ -244,6 +258,14 @@ jobs: if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-0 libgtk-3-0t64 libayatana-appindicator3-1 libfuse2t64 xvfb dbus-x11 - run: pnpm install --frozen-lockfile + - name: Provision the released controller CLI + working-directory: .ci-candidates/controller + run: | + test "$(git rev-parse HEAD)" = "$LAPIS_CONTROLLER_CANDIDATE_COMMIT" + pnpm install --frozen-lockfile + pnpm build:cli + node scripts/install-cli.mjs --home "$GITHUB_WORKSPACE/.ci-candidates/controller-home" --install-path "$GITHUB_WORKSPACE/.ci-candidates/bin/lapis-ai-controller" + printf 'LAPIS_AI_CONTROLLER_BIN=%s\n' "$GITHUB_WORKSPACE/.ci-candidates/bin/lapis-ai-controller" >> "$GITHUB_ENV" - name: Build validation-only native package run: pnpm package:desktop - name: Cold packaged acceptance on macOS @@ -258,10 +280,12 @@ jobs: if: always() with: name: desktop-validation-${{ matrix.os }} + include-hidden-files: true path: | packages/desktop-deno/release/*.zip packages/desktop-deno/release/*.AppImage packages/desktop-deno/release/*.tar.gz + .ci-candidates/controller/artifacts/runtime/runtime-manifest.json if-no-files-found: error retention-days: 14 diff --git a/scripts/check-release-workflow.spec.mjs b/scripts/check-release-workflow.spec.mjs index cd259575..7f7a62a9 100644 --- a/scripts/check-release-workflow.spec.mjs +++ b/scripts/check-release-workflow.spec.mjs @@ -228,6 +228,45 @@ test("Web fixtures come from an immutable Plugins candidate", () => { assert.ok(workflow.jobs["web-acceptance"].needs.includes("registry-fixture")); }); +test("native acceptance provisions the released standalone controller CLI", () => { + const native = workflow.jobs["desktop-packaged"]; + assert.match( + workflow.env.LAPIS_CONTROLLER_CANDIDATE_COMMIT, + /^[a-f0-9]{40}$/, + ); + const checkout = native.steps.find( + (step) => step.with?.repository === "lapismd/ai-controller", + ); + assert.equal( + checkout.with.ref, + "${{ env.LAPIS_CONTROLLER_CANDIDATE_COMMIT }}", + ); + const install = native.steps.find( + (step) => step.name === "Provision the released controller CLI", + ); + assert.match(install.run, /git rev-parse HEAD/); + assert.match(install.run, /pnpm install --frozen-lockfile/); + assert.match(install.run, /pnpm build:cli/); + assert.match(install.run, /node scripts\/install-cli\.mjs/); + assert.match(install.run, /LAPIS_AI_CONTROLLER_BIN=/); + assert.match(install.run, /GITHUB_ENV/); +}); + +test("Web acceptance provisions Docker and builds the server candidate", () => { + const web = workflow.jobs["web-acceptance"]; + assert.equal(web.container, undefined); + const server = web.steps.findIndex((step) => + step.run?.includes("docker build -f packages/server-deno/Dockerfile"), + ); + const suite = web.steps.findIndex((step) => step.run === "pnpm test:web:e2e"); + assert.ok(server >= 0 && suite > server); + assert.ok( + web.steps.some((step) => + step.run?.includes("playwright install --with-deps chromium"), + ), + ); +}); + test("fixture build uses the checked-out Plugins source instead of the host workflow SHA", () => { const build = workflow.jobs["registry-fixture"].steps.find( (step) => step.name === "Build test-signed registry candidate", diff --git a/spec/records/static-registry-github-ci-plan.md b/spec/records/static-registry-github-ci-plan.md index 5ae04cd1..b5b6d7c0 100644 --- a/spec/records/static-registry-github-ci-plan.md +++ b/spec/records/static-registry-github-ci-plan.md @@ -77,6 +77,25 @@ qualification must execute on GitHub; local macOS evidence is not Linux evidence packaged smoke reports include the failed phase, and failed native jobs retain their archives. The message-preservation regression changes from one failure to one pass. Linux startup qualification remains required. +- The second Notes run verifies the correct Plugins source provenance and the + macOS bundle name. Both native runners then report the missing separately + installed controller executable. The npm package supplies the SDK, not that + CLI. CI now provisions the CLI from public release source + `0082a1151ad2da20d0288384636b25bec0778482` using the owning build and verified + installer, passes its isolated path explicitly, and retains its manifest. + That source passes 194 Node tests, seven runtime-install tests, specification, + architecture, types and boundaries; Notes' local cold smoke passes with this + exact released CLI instead of the developer's installed service snapshot. +- Complete Web execution on the second run records nine passes and one failure: + the server-host test requires Docker, absent from the job container. The Web + job now uses native Ubuntu with Docker and Chromium, builds the server image + from the candidate, and preserves all ten tests. Candidate areas are excluded + from the server Docker context. Its prerequisite regression changes from one + failure to one pass; the controller prerequisite regression does the same. + A fresh local server image passes the exact server-host test (1/1), followed + by the complete signed-fixture Web suite (10/10). The prerequisite slice also + passes 70 release-tool tests, 23 workflow/transport tests, specification gates + and actionlint; clean GitHub execution remains required. - The repaired Notes candidate passes root check 18/18 tasks, root unit 18/18 tasks, build 10/10 tasks, desktop 219 tests plus one existing skip, 68 release tests, specification/App-ownership/metadata gates, actionlint and formatting. diff --git a/spec/src/architecture/build-and-validation.md b/spec/src/architecture/build-and-validation.md index aa9eebc3..f3d64905 100644 --- a/spec/src/architecture/build-and-validation.md +++ b/spec/src/architecture/build-and-validation.md @@ -29,3 +29,6 @@ registry fixtures before V2 production cutover. Public package ranges stay portable; full Web and native packaged results remain blocking under LN-PKG-142. Fixture builds verify the checked-out Plugins commit before setting source provenance inside the executing shell, independently of the host workflow SHA. +Native acceptance provisions the separately installed controller CLI from its +exact public release source. Complete Web acceptance uses a native runner with +Docker and builds the server image from the checked-out Notes candidate. diff --git a/spec/src/packages/releases-and-exports.md b/spec/src/packages/releases-and-exports.md index f0142d2e..df106736 100644 --- a/spec/src/packages/releases-and-exports.md +++ b/spec/src/packages/releases-and-exports.md @@ -299,6 +299,8 @@ Registry-consumer toolchain and build-policy acceptance is recorded in Fixture builds verify their checkout and export source provenance in the executing shell so the host workflow SHA cannot label another repository's artifacts. +The full suite includes server-host Docker acceptance, so the runner provisions +Docker and builds the candidate server image before browser execution. ## LN-PKG-162 — Native packaged acceptance in CI @@ -306,6 +308,11 @@ shell so the host workflow SHA cannot label another repository's artifacts. **Requirement.** Pull-request and reusable CI MUST build validation-only packages on native macOS and Linux runners and execute cold packaged acceptance for first launch, URL handoff, controller, tools, services and clean close. Both platforms MUST complete independently and retain candidate artifacts. This lane MUST NOT publish, use production signing credentials or notarize. +The separately installed controller CLI is provisioned from exact released source +through its owning build and installer, in an isolated candidate area. Its npm SDK +does not substitute for that executable. Acceptance passes the explicit CLI path +and retains its runtime manifest alongside native package candidates. + ## LN-PKG-163 — Portable terminal dependency during desktop packaging diff --git a/spec/src/spec-governance/validation-and-ci.md b/spec/src/spec-governance/validation-and-ci.md index e31d13fd..32fae061 100644 --- a/spec/src/spec-governance/validation-and-ci.md +++ b/spec/src/spec-governance/validation-and-ci.md @@ -36,3 +36,5 @@ ephemeral keys and exact source provenance; it cannot authorize a release or replace the production signing gate. Workflow-shell regressions execute checkout and provenance handling against a different host SHA and reject a mismatched checkout. +CI runners declare the external controller and Docker prerequisites needed to +execute the complete acceptance graph; prerequisite absence remains a failure. diff --git a/spec/src/verification/pkg-02.md b/spec/src/verification/pkg-02.md index 6a7c0af4..69b81f30 100644 --- a/spec/src/verification/pkg-02.md +++ b/spec/src/verification/pkg-02.md @@ -46,5 +46,5 @@ | LN-PKG-159 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-159) | Implemented | Public-entry inventory and real pnpm/Turbo fixture prove cache hits, source/argument invalidation, repeated failures, and forced reruns. The actual 37-test spec suite reuses its unchanged result; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | | LN-PKG-160 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-160) | Implemented | External-state tasks are explicitly uncached; argument/policy and real fixture regressions prove live acceptance disables cache reads/writes and executes twice; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | | LN-PKG-161 | [packages](../packages/releases-and-exports.md#ln-pkg-161) | In progress | Workflow regression requires the complete Web suite, blocking aggregation, and an executed fixture build that verifies its checkout and overrides the host workflow SHA in the shell; clean GitHub browser execution remains required. | -| LN-PKG-162 | [packages](../packages/releases-and-exports.md#ln-pkg-162) | In progress | Workflow regression requires independent macOS/Linux packaging, cold smoke and candidate retention; native runner execution remains required. | +| LN-PKG-162 | [packages](../packages/releases-and-exports.md#ln-pkg-162) | In progress | Workflow regression requires independent macOS/Linux packaging, exact released controller CLI provisioning, cold smoke and candidate retention; native runner execution remains required. | | LN-PKG-163 | [packages](../packages/releases-and-exports.md#ln-pkg-163) | In progress | Distribution regression requires public npm metadata instead of a sibling path; fresh packaged smoke verifies the public Deno runtime and checksum-verified native library. | From 96fdc2a78700b802616a90ed6b348c5609844438 Mon Sep 17 00:00:00 2001 From: Steve Juma Date: Tue, 13 Oct 2026 21:40:19 +0100 Subject: [PATCH 4/4] Qualify portable controller staging and private server bootstrap in CI Pin native acceptance to the source-owned controller repair so Linux can materialize pnpm-linked dependencies without GNU tar hard-link failures. Seed the server browser fixture through stdin into a mode-0600 Docker volume owned by the service user, preserving browser assertions and startup deadlines. Record the canonical contracts and source qualification evidence. Validation: exact server acceptance 1/1, complete Web 10/10, fresh macOS packaged smoke with controller 11c70f, release tools 70/70, workflow/fixture 23/23, Web check, specification/ownership/metadata gates, release intent, formatting and actionlint. Controller source CI and Linux install proof pass. --- .github/workflows/lapis-ci.yml | 6 +- packages/web/e2e/server-host.spec.ts | 56 ++++++++++++++----- scripts/check-release-workflow.spec.mjs | 2 +- .../records/static-registry-github-ci-plan.md | 27 +++++++++ spec/src/architecture/build-and-validation.md | 4 +- spec/src/packages/releases-and-exports.md | 4 ++ .../distribution-and-registry-02.md | 4 ++ spec/src/server-deno-host/core-contract.md | 4 ++ spec/src/spec-governance/validation-and-ci.md | 2 + spec/src/verification/pkg-02.md | 4 +- spec/src/web-host/core-contract.md | 4 ++ 11 files changed, 97 insertions(+), 20 deletions(-) diff --git a/.github/workflows/lapis-ci.yml b/.github/workflows/lapis-ci.yml index 6b212c69..dc7d803e 100644 --- a/.github/workflows/lapis-ci.yml +++ b/.github/workflows/lapis-ci.yml @@ -26,7 +26,7 @@ defaults: env: LAPIS_PLUGIN_CANDIDATE_COMMIT: "61bf175f21d91f17e06a071bdaadbaf5d87645a1" - LAPIS_CONTROLLER_CANDIDATE_COMMIT: "0082a1151ad2da20d0288384636b25bec0778482" + LAPIS_CONTROLLER_CANDIDATE_COMMIT: "11c70f4390190b8f9e1bc0d06ed349df5c142754" TURBO_API: ${{ vars.TURBO_API }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} @@ -239,7 +239,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v7 - - name: Checkout the released controller source + - name: Checkout the exact controller validation source uses: actions/checkout@v7 with: repository: lapismd/ai-controller @@ -258,7 +258,7 @@ jobs: if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-0 libgtk-3-0t64 libayatana-appindicator3-1 libfuse2t64 xvfb dbus-x11 - run: pnpm install --frozen-lockfile - - name: Provision the released controller CLI + - name: Provision the controller CLI candidate working-directory: .ci-candidates/controller run: | test "$(git rev-parse HEAD)" = "$LAPIS_CONTROLLER_CANDIDATE_COMMIT" diff --git a/packages/web/e2e/server-host.spec.ts b/packages/web/e2e/server-host.spec.ts index 3a733086..61914ef2 100644 --- a/packages/web/e2e/server-host.spec.ts +++ b/packages/web/e2e/server-host.spec.ts @@ -13,18 +13,20 @@ const image = process.env.LAPIS_SERVER_E2E_IMAGE ?? "lapis-server:e2e"; const suffix = `${process.pid}-${Date.now().toString(36)}`; const containerName = `lapis-server-e2e-${suffix}`; const volumeName = `lapis-server-e2e-data-${suffix}`; +const secretVolumeName = `lapis-server-e2e-secret-${suffix}`; let baseUrl = ""; let password = ""; -let passwordFile = ""; let importArchive = ""; let temporaryRoot = ""; let containerCreated = false; let volumeCreated = false; +let secretVolumeCreated = false; +let serverReady = false; -function docker(args: string[]): Promise { +function docker(args: string[], input?: string): Promise { return new Promise((resolvePromise, reject) => { - execFile( + const child = execFile( "docker", args, { cwd: repoRoot, maxBuffer: 8 * 1024 * 1024 }, @@ -40,6 +42,7 @@ function docker(args: string[]): Promise { resolvePromise(stdout.trim()); }, ); + child.stdin?.end(input); }); } @@ -69,7 +72,10 @@ async function waitUntilReady(): Promise { while (Date.now() < deadline) { try { const response = await fetch(`${baseUrl}/readyz`); - if (response.ok) return; + if (response.ok) { + serverReady = true; + return; + } } catch { // The container may still be starting or recreating its native database. } @@ -84,6 +90,7 @@ async function waitUntilReady(): Promise { } async function startServer(): Promise { + serverReady = false; await docker([ "run", "--detach", @@ -94,7 +101,7 @@ async function startServer(): Promise { "--mount", `type=volume,source=${volumeName},target=/data`, "--mount", - `type=bind,source=${passwordFile},target=/run/secrets/lapis_bootstrap_password,readonly`, + `type=volume,source=${secretVolumeName},target=/run/secrets,readonly`, "--env", `LAPIS_SERVER_ORIGIN=${baseUrl}`, "--env", @@ -206,9 +213,7 @@ async function manageVaults(page: Page): Promise { test.beforeAll(async () => { temporaryRoot = await mkdtemp(join(tmpdir(), "lapis-server-browser-")); password = randomBytes(32).toString("base64url"); - passwordFile = join(temporaryRoot, "bootstrap-password"); importArchive = join(temporaryRoot, "Imported Vault.zip"); - await writeFile(passwordFile, `${password}\n`, { mode: 0o600 }); await writeFile( importArchive, storedZip("Imported.md", "# Imported through the server\n"), @@ -217,10 +222,33 @@ test.beforeAll(async () => { baseUrl = `http://localhost:${port}`; await docker(["volume", "create", volumeName]); volumeCreated = true; + await docker(["volume", "create", secretVolumeName]); + secretVolumeCreated = true; + await docker( + [ + "run", + "--rm", + "--interactive", + "--user", + "root", + "--entrypoint", + "sh", + "--mount", + `type=volume,source=${secretVolumeName},target=/run/secrets`, + image, + "-c", + "umask 077; cat > /run/secrets/lapis_bootstrap_password; chown deno:deno /run/secrets/lapis_bootstrap_password", + ], + `${password}\n`, + ); await startServer(); }); test.afterAll(async () => { + if (containerCreated && !serverReady) + console.error( + await docker(["logs", "--tail", "100", containerName]).catch(String), + ); await stopServer(false).catch(() => undefined); if (containerCreated) { await docker(["rm", "--force", containerName]).catch(() => undefined); @@ -230,6 +258,10 @@ test.afterAll(async () => { await docker(["volume", "rm", volumeName]).catch(() => undefined); volumeCreated = false; } + if (secretVolumeCreated) { + await docker(["volume", "rm", secretVolumeName]).catch(() => undefined); + secretVolumeCreated = false; + } if (temporaryRoot) await rm(temporaryRoot, { recursive: true, force: true }); }); @@ -388,9 +420,7 @@ test("server-hosted vault survives editing, sharing events, and restart", async }); await manageVaults(page); await expect(page.getByRole("button", { name: "View all" })).toHaveCount(0); - await page - .getByRole("button", { name: "Return to Online Vault" }) - .click(); + await page.getByRole("button", { name: "Return to Online Vault" }).click(); await expect( page.getByRole("button", { name: /Current workspace:/ }), ).toContainText("Online Vault"); @@ -444,9 +474,9 @@ test("server-hosted vault survives editing, sharing events, and restart", async await managedVaultDialog .getByRole("combobox", { name: "Search managed vaults" }) .fill("Browser Folder"); - await expect( - managedVaultDialog.locator('[data-ui-part="item"]'), - ).toHaveCount(1); + await expect(managedVaultDialog.locator('[data-ui-part="item"]')).toHaveCount( + 1, + ); await managedVaultDialog .locator('[data-ui-part="item"]') .filter({ hasText: "Browser Folder Vault" }) diff --git a/scripts/check-release-workflow.spec.mjs b/scripts/check-release-workflow.spec.mjs index 7f7a62a9..3880b3df 100644 --- a/scripts/check-release-workflow.spec.mjs +++ b/scripts/check-release-workflow.spec.mjs @@ -242,7 +242,7 @@ test("native acceptance provisions the released standalone controller CLI", () = "${{ env.LAPIS_CONTROLLER_CANDIDATE_COMMIT }}", ); const install = native.steps.find( - (step) => step.name === "Provision the released controller CLI", + (step) => step.name === "Provision the controller CLI candidate", ); assert.match(install.run, /git rev-parse HEAD/); assert.match(install.run, /pnpm install --frozen-lockfile/); diff --git a/spec/records/static-registry-github-ci-plan.md b/spec/records/static-registry-github-ci-plan.md index b5b6d7c0..72b7ac42 100644 --- a/spec/records/static-registry-github-ci-plan.md +++ b/spec/records/static-registry-github-ci-plan.md @@ -103,6 +103,33 @@ qualification must execute on GitHub; local macOS evidence is not Linux evidence `generated/`, which would reject a signing candidate. Generated Astro files are now untracked and ignored; cold type generation and all 77 script tests pass, and repeated source checking leaves the committed tree clean. +- [Notes CI 38460618229](https://github.com/lapismd/lapis-notes/actions/runs/38460618229) + executed all required jobs. macOS packaged smoke, governance, quality, units, + static catalog, all 243 interaction/accessibility scenarios, fixture generation + and artifact planning passed. Linux controller provisioning failed on GNU tar + extraction of pnpm dependency hard links; complete Web execution recorded nine + passes and one server startup failure. The aggregate correctly failed. +- [Controller PR 1](https://github.com/lapismd/ai-controller/pull/1) repairs + dependency staging in its owning package. It materializes the npm dry-run file + inventory as independent files, preserving bytes and executable flags within + the verified package root. The exact hard-link regression changes from one + failure to one pass. Its 195 Node tests, seven runtime-install tests, spec, + architecture, type, boundary, CLI build and publint checks pass; a fresh Linux + Docker environment also builds and verifies the installed CLI. Source CI + [38463053946](https://github.com/lapismd/ai-controller/actions/runs/38463053946) + and the specification workflow pass at + `11c70f4390190b8f9e1bc0d06ed349df5c142754`. Notes pins that exact validation + candidate; the default Controller workspace's newer unpublished work remains + separate and npm SDK ranges stay portable. +- The Web server failure came from a mode-0600 bootstrap file owned by the host + runner rather than the container's `deno` user. The fixture now seeds the + credential through stdin into a private service-user-owned Docker volume, + mounts it read-only and removes it after acceptance. Assertions and deadlines + are unchanged. The exact server-host test passes 1/1, followed by complete Web + acceptance 10/10. Fresh macOS packaged smoke passes with the committed repaired + Controller candidate. The final repair also passes 70 release-tool tests, Web + package check, specification/App-ownership/metadata gates and actionlint; + fresh complete GitHub execution remains the source qualification gate. ### Next protected release gate diff --git a/spec/src/architecture/build-and-validation.md b/spec/src/architecture/build-and-validation.md index f3d64905..5812de67 100644 --- a/spec/src/architecture/build-and-validation.md +++ b/spec/src/architecture/build-and-validation.md @@ -30,5 +30,7 @@ portable; full Web and native packaged results remain blocking under LN-PKG-142. Fixture builds verify the checked-out Plugins commit before setting source provenance inside the executing shell, independently of the host workflow SHA. Native acceptance provisions the separately installed controller CLI from its -exact public release source. Complete Web acceptance uses a native runner with +exact validation source based on the public release. Complete Web acceptance uses a native runner with Docker and builds the server image from the checked-out Notes candidate. +The server fixture seeds its private bootstrap secret into a Docker volume owned +by the image's service user, so host UID differences cannot prevent startup. diff --git a/spec/src/packages/releases-and-exports.md b/spec/src/packages/releases-and-exports.md index df106736..a4ff7832 100644 --- a/spec/src/packages/releases-and-exports.md +++ b/spec/src/packages/releases-and-exports.md @@ -301,6 +301,8 @@ Fixture builds verify their checkout and export source provenance in the executi shell so the host workflow SHA cannot label another repository's artifacts. The full suite includes server-host Docker acceptance, so the runner provisions Docker and builds the candidate server image before browser execution. +Server acceptance seeds bootstrap credentials through stdin into a private +service-user-owned Docker volume and mounts it read-only into the candidate. ## LN-PKG-162 — Native packaged acceptance in CI @@ -312,6 +314,8 @@ The separately installed controller CLI is provisioned from exact released sourc through its owning build and installer, in an isolated candidate area. Its npm SDK does not substitute for that executable. Acceptance passes the explicit CLI path and retains its runtime manifest alongside native package candidates. +The validation pin includes the source-owned portable dependency staging repair +on top of released 0.1.0 controller source; npm SDK ranges remain unchanged. ## LN-PKG-163 — Portable terminal dependency during desktop packaging diff --git a/spec/src/plugin-model/distribution-and-registry-02.md b/spec/src/plugin-model/distribution-and-registry-02.md index 1a700da5..6a1ac588 100644 --- a/spec/src/plugin-model/distribution-and-registry-02.md +++ b/spec/src/plugin-model/distribution-and-registry-02.md @@ -6,6 +6,10 @@ **Requirement.** App Profile MAY support explicit local signed-registry configuration only in development or `registry-acceptance` mode. It MUST require a loopback catalog, public signing keys, and an exact HTTPS artifact origin. It MUST preserve signature, hash, compatibility, and loader checks, honor explicit host options, and ignore fixture trust in production/server builds. +LN-PKG-161 runs the complete Web suite with the exact signed fixture, including +server-managed vault acceptance. The server image remains a normal server-mode +build; its private bootstrap credential fixture does not extend registry trust. + ## LN-PLUG-070 — Approximate monthly plugin downloads diff --git a/spec/src/server-deno-host/core-contract.md b/spec/src/server-deno-host/core-contract.md index fde8454f..a0332dfc 100644 --- a/spec/src/server-deno-host/core-contract.md +++ b/spec/src/server-deno-host/core-contract.md @@ -24,6 +24,10 @@ **Requirement.** The first user MUST be bootstrapped only when no user exists, using a username and password-file environment variable. Bootstrap MUST be idempotent and MUST NOT log or persist the plaintext password. +Browser acceptance supplies this input through stdin to a private Docker volume. +The fixture file stays mode 0600, belongs to the image's service user and is +mounted read-only, avoiding a dependency on the host runner's UID. + ## LN-SRV-005 — Passwords: use bounded scrypt parameters and random salts diff --git a/spec/src/spec-governance/validation-and-ci.md b/spec/src/spec-governance/validation-and-ci.md index 32fae061..fa90be68 100644 --- a/spec/src/spec-governance/validation-and-ci.md +++ b/spec/src/spec-governance/validation-and-ci.md @@ -38,3 +38,5 @@ Workflow-shell regressions execute checkout and provenance handling against a different host SHA and reject a mismatched checkout. CI runners declare the external controller and Docker prerequisites needed to execute the complete acceptance graph; prerequisite absence remains a failure. +Linux qualification includes controller dependency materialization and a private +server secret readable by the container service user without relaxing assertions. diff --git a/spec/src/verification/pkg-02.md b/spec/src/verification/pkg-02.md index 69b81f30..8b12f929 100644 --- a/spec/src/verification/pkg-02.md +++ b/spec/src/verification/pkg-02.md @@ -45,6 +45,6 @@ | LN-PKG-158 | [packages](../packages/releases-and-exports.md#ln-pkg-158) | Implemented | Generated-consumer regression copies only approved builds and rejects malformed policies; the same published-npm consumer passes without repository hooks, patches, or overrides; [release verification record](../../records/2026-10-13-registry-consumer-toolchain.md). | | LN-PKG-159 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-159) | Implemented | Public-entry inventory and real pnpm/Turbo fixture prove cache hits, source/argument invalidation, repeated failures, and forced reruns. The actual 37-test spec suite reuses its unchanged result; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | | LN-PKG-160 | [packages, architecture](../packages/vault-and-storage.md#ln-pkg-160) | Implemented | External-state tasks are explicitly uncached; argument/policy and real fixture regressions prove live acceptance disables cache reads/writes and executes twice; [local cache acceptance](../../records/2026-10-13-local-test-caching.md). | -| LN-PKG-161 | [packages](../packages/releases-and-exports.md#ln-pkg-161) | In progress | Workflow regression requires the complete Web suite, blocking aggregation, and an executed fixture build that verifies its checkout and overrides the host workflow SHA in the shell; clean GitHub browser execution remains required. | -| LN-PKG-162 | [packages](../packages/releases-and-exports.md#ln-pkg-162) | In progress | Workflow regression requires independent macOS/Linux packaging, exact released controller CLI provisioning, cold smoke and candidate retention; native runner execution remains required. | +| LN-PKG-161 | [packages](../packages/releases-and-exports.md#ln-pkg-161) | In progress | Workflow regression requires the complete Web suite, blocking aggregation, and an executed fixture build that verifies its checkout and overrides the host workflow SHA in the shell; server acceptance seeds its private secret for the image service user; clean GitHub browser execution remains required. | +| LN-PKG-162 | [packages](../packages/releases-and-exports.md#ln-pkg-162) | In progress | Workflow regression requires independent macOS/Linux packaging, exact controller CLI candidate provisioning, cold smoke and candidate retention; native runner execution remains required. | | LN-PKG-163 | [packages](../packages/releases-and-exports.md#ln-pkg-163) | In progress | Distribution regression requires public npm metadata instead of a sibling path; fresh packaged smoke verifies the public Deno runtime and checksum-verified native library. | diff --git a/spec/src/web-host/core-contract.md b/spec/src/web-host/core-contract.md index dba83ced..b38106e7 100644 --- a/spec/src/web-host/core-contract.md +++ b/spec/src/web-host/core-contract.md @@ -1,5 +1,9 @@ # Web Host / Browser integration +Complete CI browser acceptance under LN-PKG-161 includes the real server-host +editing, sharing and restart scenario. Its private bootstrap fixture is seeded +for the container user and retains the original browser assertions and deadlines. + ## LN-WEB-032 — Web host: persist terminal-host URL and token as web.terminalHost.url