Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 22 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -33,29 +40,33 @@ 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 }}
ref: ${{ steps.sources.outputs.cli_ref }}
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 }}
ref: ${{ steps.sources.outputs.engine_ref }}
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:
Expand All @@ -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

21 changes: 14 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion scripts/cli-pack-engine.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
32 changes: 32 additions & 0 deletions scripts/pack-engine.mjs
Original file line number Diff line number Diff line change
@@ -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 () => {},
};
}
29 changes: 9 additions & 20 deletions scripts/person-pack.test.mjs
Original file line number Diff line number Diff line change
@@ -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"));
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 3 additions & 4 deletions scripts/tasknotes-plugin-collections.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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");
Expand Down
7 changes: 3 additions & 4 deletions scripts/tasknotes-upgrade.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down
22 changes: 5 additions & 17 deletions scripts/verify.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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);
Expand Down Expand Up @@ -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")) {
Expand Down Expand Up @@ -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("./")) {
Expand Down
4 changes: 2 additions & 2 deletions sources.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading