From 381ea0f427b0a0d7875852922998370a29206857 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sat, 3 Oct 2026 07:32:47 +1000 Subject: [PATCH] Verify every pack through mdbase-ts 0.3.0-rc.8 as well as the Rust CLI Pin CI's mdbase-ts checkout to tag v0.3.0-rc.8, which supports seed upgrade_from, and run npm test once per engine in a matrix. Route all pack installs (verify, People and TaskNotes tests) through one engine selector so the TaskNotes upgrade and plugin-collection tests no longer skip without the CLI. Use rc.8's v0.3 update member patch in the People pack test. --- .github/workflows/ci.yml | 33 ++++++++++++------- README.md | 21 ++++++++---- scripts/cli-pack-engine.mjs | 2 +- scripts/pack-engine.mjs | 32 ++++++++++++++++++ scripts/person-pack.test.mjs | 29 +++++----------- scripts/tasknotes-plugin-collections.test.mjs | 7 ++-- scripts/tasknotes-upgrade.test.mjs | 7 ++-- scripts/verify.mjs | 22 +++---------- sources.json | 4 +-- 9 files changed, 91 insertions(+), 66 deletions(-) create mode 100644 scripts/pack-engine.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88ce565..5ec43e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,14 @@ permissions: jobs: verify: + name: verify (${{ matrix.engine }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # Every pack is installed, upgraded, and tested through both engines. + # mdbase-ts also reopens installed collections in both legs. + engine: [mdbase-ts, mdbase-cli] steps: - name: Check out contracts uses: actions/checkout@v7 @@ -21,10 +28,10 @@ jobs: run: | echo "repository=$(jq -r '.typescript_implementation.repository' sources.json)" >> "$GITHUB_OUTPUT" echo "ref=$(jq -r '.typescript_implementation.ref' sources.json)" >> "$GITHUB_OUTPUT" - echo "cli_repository=$(jq -r '.seed_upgrade_cli.repository' sources.json)" >> "$GITHUB_OUTPUT" - echo "cli_ref=$(jq -r '.seed_upgrade_cli.ref' sources.json)" >> "$GITHUB_OUTPUT" - echo "engine_repository=$(jq -r '.seed_upgrade_cli.engine_repository' sources.json)" >> "$GITHUB_OUTPUT" - echo "engine_ref=$(jq -r '.seed_upgrade_cli.engine_ref' sources.json)" >> "$GITHUB_OUTPUT" + echo "cli_repository=$(jq -r '.rust_cli.repository' sources.json)" >> "$GITHUB_OUTPUT" + echo "cli_ref=$(jq -r '.rust_cli.ref' sources.json)" >> "$GITHUB_OUTPUT" + echo "engine_repository=$(jq -r '.rust_cli.engine_repository' sources.json)" >> "$GITHUB_OUTPUT" + echo "engine_ref=$(jq -r '.rust_cli.engine_ref' sources.json)" >> "$GITHUB_OUTPUT" - name: Check out TypeScript implementation uses: actions/checkout@v7 @@ -33,9 +40,8 @@ jobs: ref: ${{ steps.sources.outputs.ref }} path: .sources/mdbase - # mdbase-ts cannot execute seed-type upgrades, so installs are verified - # through an mdbase CLI built on the Rust engine that can. - name: Check out mdbase CLI + if: matrix.engine == 'mdbase-cli' uses: actions/checkout@v7 with: repository: ${{ steps.sources.outputs.cli_repository }} @@ -43,6 +49,7 @@ jobs: path: .sources/cli/mdbase-connect - name: Check out its Rust engine + if: matrix.engine == 'mdbase-cli' uses: actions/checkout@v7 with: repository: ${{ steps.sources.outputs.engine_repository }} @@ -50,12 +57,16 @@ jobs: path: .sources/cli/mdbase-rs - uses: Swatinem/rust-cache@v2 + if: matrix.engine == 'mdbase-cli' with: workspaces: .sources/cli/mdbase-connect - name: Build mdbase CLI + if: matrix.engine == 'mdbase-cli' working-directory: .sources/cli/mdbase-connect - run: cargo build --locked -p mdbase-cli --bin mdbase + run: | + cargo build --locked -p mdbase-cli --bin mdbase + echo "MDBASE_VERIFY_CLI=$PWD/target/debug/mdbase" >> "$GITHUB_ENV" - uses: actions/setup-node@v7 with: @@ -67,14 +78,14 @@ jobs: - name: Build TypeScript implementation working-directory: .sources/mdbase - run: npm ci && npm run build + run: | + npm ci && npm run build + echo "mdbase-ts $(jq -r .version package.json) at $(git rev-parse HEAD)" - - name: Build and verify catalog + - name: Build and verify catalog with ${{ matrix.engine }} run: npm test env: MDBASE_TS_DIR: .sources/mdbase - MDBASE_VERIFY_CLI: ${{ github.workspace }}/.sources/cli/mdbase-connect/target/debug/mdbase - name: Check deterministic publication output run: git diff --exit-code -- dist - diff --git a/README.md b/README.md index 803bfa0..918cce3 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,11 @@ MDBASE_SPEC_DIR=../mdbase-spec npm run sync:runtime ## Build and verify Requires Node.js 22+ and a built checkout of -[`@callumalpass/mdbase`](https://github.com/callumalpass/mdbase). A sibling -`../mdbase` checkout is used by default; set `MDBASE_TS_DIR` to override it. +[`@callumalpass/mdbase`](https://github.com/callumalpass/mdbase) (mdbase-ts) +0.3.0-rc.8 or later. A sibling `../mdbase` checkout is used by default; set +`MDBASE_TS_DIR` to override it. CI checks out and builds the tag pinned in +`sources.json` (`typescript_implementation.ref`), which is the single source +of truth for the mdbase-ts version the catalog is verified against. ```sh npm install @@ -111,16 +114,20 @@ and rc.13 bytes remain unchanged. The rc.15 candidate upgrades the same rc.12 starter to contract rc.5, where `assignees` are links to `mdbase.person` 2.0.0 records declared in `collection.links`; Person 2.0.0 drops the separate `id`. The People pack 1.2.0 keeps shipping `mdbase.person` 1.0.0 so Person types -customised under 1.1.0 continue to validate. To verify with the updated Rust -engine: +customised under 1.1.0 continue to validate. + +Packs are installed through one of two engines. By default every check +(catalog verification, the People pack tests, and the TaskNotes upgrade and +plugin-collection tests) installs through mdbase-ts. To run the same checks +through a Rust-engine mdbase CLI instead: ```sh MDBASE_VERIFY_CLI=/absolute/path/to/mdbase npm test ``` -This runs dry-run/install/idempotency checks through that local CLI against -throwaway collections, then reopens the installed definitions with mdbase-ts. -It does not claim that older mdbase-ts versions can execute seed upgrades. +Installed collections are reopened with mdbase-ts either way. CI runs +`npm test` once per engine, with the CLI built from the commits pinned in +`sources.json` (`rust_cli`). ## TaskNotes assignments diff --git a/scripts/cli-pack-engine.mjs b/scripts/cli-pack-engine.mjs index 2daee56..2adcf5a 100644 --- a/scripts/cli-pack-engine.mjs +++ b/scripts/cli-pack-engine.mjs @@ -7,7 +7,7 @@ function adoptionArgs(adoptions) { return Object.entries(adoptions).flatMap(([target, digest]) => ["--adopt", `${target}=${digest}`]); } -/** Explicit CLI backend for engine features not yet available in mdbase-ts. */ +/** Installs packs through the Rust-engine mdbase CLI (`mdbase packs assess|apply`). */ export async function cliPackEngine(command, collectionRoot, provision, installedBy) { const bundle = await mkdtemp(join(tmpdir(), "mdbase-catalog-pack-")); const close = () => rm(bundle, { recursive: true, force: true }); diff --git a/scripts/pack-engine.mjs b/scripts/pack-engine.mjs new file mode 100644 index 0000000..8e88dd6 --- /dev/null +++ b/scripts/pack-engine.mjs @@ -0,0 +1,32 @@ +import { join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { cliPackEngine } from "./cli-pack-engine.mjs"; + +const root = fileURLToPath(new URL("../", import.meta.url)); + +/** The mdbase-ts build the checks import; CI pins it in sources.json. */ +export const mdbase = await import( + pathToFileURL(join(resolve(process.env.MDBASE_TS_DIR ?? join(root, "..", "mdbase")), "dist/index.js")).href +); + +/** Which engine installs packs: the Rust mdbase CLI when MDBASE_VERIFY_CLI is set, otherwise mdbase-ts. */ +export const packEngineName = process.env.MDBASE_VERIFY_CLI ? "mdbase CLI" : "mdbase-ts"; + +/** + * A pack installer with one interface for both engines: assess(adoptions) + * and apply(digest, adoptions) return v0.3 operation results. + */ +export async function packEngine(collectionRoot, provision, installedBy) { + if (process.env.MDBASE_VERIFY_CLI) { + return cliPackEngine(process.env.MDBASE_VERIFY_CLI, collectionRoot, provision, installedBy); + } + return { + assess: async (adoptions = {}) => mdbase.assessTypePack(collectionRoot, provision, { + installedBy, adoptResources: adoptions, + }), + apply: async (digest, adoptions = {}) => mdbase.applyTypePack(collectionRoot, provision, { + installedBy, adoptResources: adoptions, expectedAssessmentDigest: digest, + }), + close: async () => {}, + }; +} diff --git a/scripts/person-pack.test.mjs b/scripts/person-pack.test.mjs index b7d4571..9da92a8 100644 --- a/scripts/person-pack.test.mjs +++ b/scripts/person-pack.test.mjs @@ -1,17 +1,14 @@ import assert from "node:assert/strict"; import { createHash } from "node:crypto"; -import { cliPackEngine } from "./cli-pack-engine.mjs"; +import { mdbase, packEngine } from "./pack-engine.mjs"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; -import { join, resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import test from "node:test"; const root = fileURLToPath(new URL("../", import.meta.url)); -const mdbaseDir = resolve(process.env.MDBASE_TS_DIR ?? join(root, "..", "mdbase")); -const { Collection, applyTypePack, assessTypePack } = await import( - pathToFileURL(join(mdbaseDir, "dist/index.js")).href -); +const { Collection } = mdbase; test("the catalog offers only the single-type People starter", async () => { const catalog = JSON.parse(await readFile(join(root, "dist/catalog.json"), "utf8")); @@ -54,17 +51,9 @@ async function install(collectionRoot, version) { })), }, }; - const options = { installedBy: "dev.mdbase.first-party" }; - if (process.env.MDBASE_VERIFY_CLI) { - const engine = await cliPackEngine(process.env.MDBASE_VERIFY_CLI, collectionRoot, input, options.installedBy); - try { const assessment = valid(await engine.assess()); valid(await engine.apply(assessment.assessment_digest)); } - finally { await engine.close(); } - } else { - const assessment = valid(await assessTypePack(collectionRoot, input, options)); - valid(await applyTypePack(collectionRoot, input, { - ...options, expectedAssessmentDigest: assessment.assessment_digest, - })); - } + const engine = await packEngine(collectionRoot, input, "dev.mdbase.first-party"); + try { const assessment = valid(await engine.assess()); valid(await engine.apply(assessment.assessment_digest)); } + finally { await engine.close(); } } // 1.2.0 and 1.3.0 offer only the Person starter; 1.3.0 upgrades an unmodified v2 to v3. @@ -139,14 +128,14 @@ for (const versions of [["1.1.0"], ["1.0.0", "1.1.0"], ["1.2.0"], ["1.0.0", "1.2 })); const converted = valid(await operations.update({ path: "contacts/existing.md", if_revision: String(contact.revision), - fields: { type: "person", identities: [{ issuer: "https://connect.example", subject: "existing_account" }] }, + patch: { type: "person", identities: [{ issuer: "https://connect.example", subject: "existing_account" }] }, })); assert.equal(converted.frontmatter.id, "contact_existing"); assert.equal(converted.frontmatter.name, "Existing contact"); assert.equal(converted.frontmatter.email, "local@example.com"); assert.equal(converted.frontmatter.private_notes, "Collection-owned data"); assert.match(await readFile(join(collectionRoot, "contacts/existing.md"), "utf8"), /Keep this \*\*Markdown\*\* body\./); - const stale = await operations.update({ path: "contacts/existing.md", if_revision: String(contact.revision), fields: { name: "Stale overwrite" } }); + const stale = await operations.update({ path: "contacts/existing.md", if_revision: String(contact.revision), patch: { name: "Stale overwrite" } }); assert.equal(stale.valid, false, "conversion must not bypass revision checks"); } } finally { diff --git a/scripts/tasknotes-plugin-collections.test.mjs b/scripts/tasknotes-plugin-collections.test.mjs index f023657..933f4ee 100644 --- a/scripts/tasknotes-plugin-collections.test.mjs +++ b/scripts/tasknotes-plugin-collections.test.mjs @@ -3,14 +3,13 @@ import { test } from "node:test"; import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { tmpdir } from "node:os"; -import { cliPackEngine } from "./cli-pack-engine.mjs"; +import { packEngine } from "./pack-engine.mjs"; // Collections exactly as the TaskNotes 5.0 plugin writes them (upgraded from // TaskNotes 4.13.6 and the 5.0 betas, with default and customized settings). // Every TaskNotes pack must install over them, as TaskNotes App does through // Connect, without conflicts and without dropping statuses or touching records. const LATEST = "0.3.0-rc.17"; -const command = process.env.MDBASE_VERIFY_CLI; const fixtures = new URL("./fixtures/tasknotes-5.0/", import.meta.url); async function reviewedInstall(engine) { @@ -27,7 +26,7 @@ async function reviewedInstall(engine) { } for (const name of (await readdir(fixtures)).filter((file) => file.endsWith(".json")).sort()) { - test(`TaskNotes ${LATEST} installs over a TaskNotes 5.0 plugin collection: ${name}`, { skip: !command }, async () => { + test(`TaskNotes ${LATEST} installs over a TaskNotes 5.0 plugin collection: ${name}`, async () => { const { files } = JSON.parse(await readFile(new URL(name, fixtures), "utf8")); const root = await mkdtemp(join(tmpdir(), "tasknotes-plugin-collection-")); let engine; @@ -37,7 +36,7 @@ for (const name of (await readdir(fixtures)).filter((file) => file.endsWith(".js await writeFile(join(root, path), document); } const provision = JSON.parse(await readFile(new URL(`../dist/packs/tasknotes.task/${LATEST}.json`, import.meta.url), "utf8")); - engine = await cliPackEngine(command, root, provision, "dev.mdbase.tests"); + engine = await packEngine(root, provision, "dev.mdbase.tests"); const result = await reviewedInstall(engine); assert.equal(result.valid, true, JSON.stringify(result)); const type = await readFile(join(root, "_types/task.md"), "utf8"); diff --git a/scripts/tasknotes-upgrade.test.mjs b/scripts/tasknotes-upgrade.test.mjs index 88d8f62..5b9eeb0 100644 --- a/scripts/tasknotes-upgrade.test.mjs +++ b/scripts/tasknotes-upgrade.test.mjs @@ -4,9 +4,8 @@ import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { parse, stringify } from "yaml"; -import { cliPackEngine } from "./cli-pack-engine.mjs"; +import { packEngine } from "./pack-engine.mjs"; -const command = process.env.MDBASE_VERIFY_CLI; const load = async (version) => JSON.parse(await readFile(new URL(`../dist/packs/tasknotes.task/${version}.json`, import.meta.url), "utf8")); async function reviewedInstall(engine) { let assessment = await engine.assess(); @@ -46,14 +45,14 @@ function withoutAssignees(document) { for (const target of targets) for (const scenario of ["managed", "customized", "generator-defaults", "custom-mapping", "unmanaged", "conflict", "other-reference"]) { if (scenario === "generator-defaults" && !target.minimal) continue; if (scenario === "custom-mapping" && !target.customMapping) continue; - test(`TaskNotes rc.12 to ${target.pack} upgrade: ${scenario}`, { skip: !command }, async () => { + test(`TaskNotes rc.12 to ${target.pack} upgrade: ${scenario}`, async () => { const root = await mkdtemp(join(tmpdir(), "tasknotes-upgrade-test-")); const engines = []; try { await writeFile(join(root, "mdbase.yaml"), "spec_version: 0.3.0\nsettings:\n validation: error\n"); const old = await load("0.3.0-rc.12"); async function engine(provision) { - const value = await cliPackEngine(command, root, provision, "dev.mdbase.tests"); + const value = await packEngine(root, provision, "dev.mdbase.tests"); engines.push(value); return value; } diff --git a/scripts/verify.mjs b/scripts/verify.mjs index 729c666..c7528ee 100644 --- a/scripts/verify.mjs +++ b/scripts/verify.mjs @@ -2,10 +2,10 @@ import { createHash } from "node:crypto"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; -import { pathToFileURL, fileURLToPath } from "node:url"; +import { fileURLToPath } from "node:url"; import Ajv2020 from "ajv/dist/2020.js"; import addFormats from "ajv-formats"; -import { cliPackEngine } from "./cli-pack-engine.mjs"; +import { mdbase, packEngine, packEngineName } from "./pack-engine.mjs"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const dist = join(root, "dist"); @@ -20,11 +20,7 @@ if (!ajv.validate(catalogSchema, catalog)) { fail(`Catalog is invalid:\n${ajv.errorsText(ajv.errors, { separator: "\n" })}`); } -const mdbaseDir = resolve(process.env.MDBASE_TS_DIR ?? join(root, "..", "mdbase")); -const mdbaseEntry = join(mdbaseDir, "dist", "index.js"); -const { Collection, applyTypePack, assessTypePack } = await import( - pathToFileURL(mdbaseEntry).href -); +const { Collection } = mdbase; for (const contract of catalog.contracts) { const artifact = await readCatalogArtifact(contract.artifact); @@ -60,15 +56,7 @@ for (const pack of catalog.packs) { join(collectionRoot, "mdbase.yaml"), "spec_version: 0.3.0\nsettings:\n validation: error\n", ); - const assessmentOptions = { installedBy: catalog.id }; - engine = process.env.MDBASE_VERIFY_CLI - ? await cliPackEngine(process.env.MDBASE_VERIFY_CLI, collectionRoot, installProvision, catalog.id) - : { - assess: () => assessTypePack(collectionRoot, installProvision, assessmentOptions), - apply: (digest) => applyTypePack(collectionRoot, installProvision, { - ...assessmentOptions, expectedAssessmentDigest: digest, - }), - }; + engine = await packEngine(collectionRoot, installProvision, catalog.id); const dryRun = await engine.assess(); assertValid(dryRun, `${pack.id} dry run`); if (dryRun.result.resources.some(({ action }) => action !== "create")) { @@ -111,7 +99,7 @@ for (const pack of catalog.packs) { } } -console.log(`Verified ${catalog.contracts.length} contract and ${catalog.packs.length} pack.`); +console.log(`Verified ${catalog.contracts.length} contract and ${catalog.packs.length} pack installs with ${packEngineName}.`); async function readCatalogArtifact(relativeUrl) { if (typeof relativeUrl !== "string" || !relativeUrl.startsWith("./")) { diff --git a/sources.json b/sources.json index 8926b8c..2de0a6c 100644 --- a/sources.json +++ b/sources.json @@ -5,9 +5,9 @@ }, "typescript_implementation": { "repository": "callumalpass/mdbase", - "ref": "3c9c89b8000b925d6e584cce4e06c610ce42fd5d" + "ref": "v0.3.0-rc.8" }, - "seed_upgrade_cli": { + "rust_cli": { "repository": "mdbase-dev/mdbase-connect", "ref": "62242c21e754d27fee068e32ace02408bb572906", "engine_repository": "callumalpass/mdbase-rs",