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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
cache: npm

- run: npm ci
- run: node --test scripts/check-version.test.mjs
- run: npm run check:version
- run: npm run lint:obsidian-warnings
- run: npx tsc -noEmit -skipLibCheck
Expand Down
19 changes: 15 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,23 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Test version validation
run: node --test scripts/check-version.test.mjs

- name: Verify version metadata
env:
RELEASE_TAG: ${{ github.ref_name }}
run: npm run check:version

- name: Lint
run: npm run lint:obsidian-warnings

- name: Build
run: npm run build

- name: Test
run: npm test

- name: Attest build provenance
uses: actions/attest-build-provenance@v4
with:
Expand All @@ -47,7 +56,9 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--generate-notes \
main.js manifest.json styles.css
notes="docs/releases/$tag/release-notes.md"
if [ -f "$notes" ]; then
gh release create "$tag" --title="$tag" --notes-file "$notes" main.js manifest.json styles.css
else
gh release create "$tag" --title="$tag" --generate-notes main.js manifest.json styles.css
fi
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ src/

- CI has an automatic release workflow triggered by tags — do NOT manually run `gh release create` after pushing a tag, it will conflict
- Release steps: bump version in `manifest.json` + `versions.json` → PR → merge → `git tag -a X.Y.Z` → `git push origin X.Y.Z` → CI creates the release with `main.js`, `manifest.json`, `styles.css`
- Version consistency across `package.json`, `package-lock.json`, `manifest.json` and the `versions.json` → `minAppVersion` mapping is enforced by `npm run check:version`; its regression tests run via `node --test scripts/check-version.test.mjs` (CI and release workflow run both)
- Release notes: when `docs/releases/<tag>/release-notes.md` exists, CI publishes it verbatim; otherwise GitHub generates notes. Write the reviewed file for planned releases

## Key References

Expand Down
22 changes: 11 additions & 11 deletions docs/releases/1.0.0/readiness.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# 1.0.0 Readiness Record

Release decision: NOT READY
Source commit: 2915687ebd656444054387fa474e0393bf420d7b (branch `fix/1.0-export-integrity`, local `main` clean before branching)
Runtime: Node v24.16.0, npm 11.13.0, macOS 27.0 (26A428, arm64); Obsidian version not yet measured — native acceptance pending (T6)
Source commit: branch `fix/1.0-export-integrity` (implementation commits e2c9dd3, 23e670a, 3098745, 064f518 on top of merged main 207a4d3)
Runtime: Node v24.16.0, npm 11.13.0, macOS 27.0 (arm64); Obsidian 1.11.5 installed; QA workspace: /var/folders/cg/8_2x8c9s5xx3dl1trdcs3ndh0000gn/T/document-exporter-1.0-qa.UgHjVD (temporary, retained until release verification)

| Gate | Status | Source commit / artifact SHA-256 | Evidence | Remaining action |
|---|---|---|---|---|
| Baseline | PASS | 2915687ebd656444054387fa474e0393bf420d7b | 2026-09-15 local run: `npm run check:version` ("Version metadata is consistent: 0.7.4"), `npm run lint:obsidian-warnings`, `npm run build`, `npm test` (20 files, 360 tests passed) — all exit 0. Remote read-only: 0 open issues, 0 open PRs; release 0.7.4 (published 2026-08-26) with `main.js`, `manifest.json`, `styles.css`; latest `verify` runs green including HEAD 2915687. No vault plugin symlink present, so builds cannot update a live plugin. | T0 complete |
| Output integrity | PASS | this branch's `fix: preserve existing export documents and assets` commit | T1 reproduced the overwrite corruption as required (expected `[1]`, received `[2]` in `src/export/ExportIntegrity.test.ts`). T2 added directory isolation, exclusive writes (`wx` external / create-only vault), report-name protection and a 10-case regression matrix. 2026-09-16: five export suites 145/145, full suite 378/378, lint and build exit 0. | T6 reruns the two-run case through the native export dialog |
| Outcomes | NOT RUN | Unmeasured | No run recorded | Execute T3-T4 |
| Headless artifacts | NOT RUN | Unmeasured | No run recorded | Execute T5 |
| Native artifacts | NOT RUN | Unmeasured | No run recorded | Execute T6 |
| Platforms | NOT RUN | Unmeasured | No run recorded | Execute T6 |
| Documentation | NOT RUN | Unmeasured | No review recorded | Execute T7 |
| Release gate | NOT RUN | Unmeasured | No run recorded | Execute T8 |
| Upgrade / candidate | NOT RUN | Unmeasured | No run recorded | Execute T9 |
| Published assets | NOT RUN | Unmeasured | Not published | Execute T10 |
| Output integrity | PASS | merged main 207a4d3 (PR #84, commit b44207c) | T1 reproduced the overwrite corruption as required (expected `[1]`, received `[2]` in `src/export/ExportIntegrity.test.ts`). T2 added directory isolation, exclusive writes (`wx` external / create-only vault), report-name protection and a 10-case regression matrix. Five export suites 145/145, full suite green, lint and build exit 0. | T6 reruns the two-run case through the native export dialog |
| Outcomes | PASS | e2c9dd3 (T3), 23e670a (T4) | `resolveExportStatus` state table (8 rows) plus 9 integration scenarios: early cancel, mid-batch cancel, cancel after render, second-source read failure (partial), PDF renderer rejection (failed), shared-attachment retry, missing input keeps original total, report-write failure stays completed-with-warning, unresolved-link-only stays completed. Runner finalizes through one path; `onFileComplete` reports completed count minus one. `exportResultMessage` distinguishes completed/partial/cancelled/failed for the UI. Full suite 411/411, lint and build exit 0. | Native dialog feedback wording checked in T6 (A08) |
| Headless artifacts | PASS | 3098745 | Fixture generator: 520 synthetic files, all manifest SHA-256 verified; second run refused non-empty destination; PNGs decode via macOS `sips` with matching dimensions (640x240, 160x100). Contract suite 10/10: Markdown/HTML-fallback/DOCX/EPUB content cases (markers, tables, code, XML validity, relationships, spine resolution, image byte equality, no `app://`), folder batch (nested primaries, relative links, shared attachment bytes), collision A→B, export-report name protection, missing-attachment and cancellation injections. `RELEASE_ARTIFACT_DIR` persistence verified with SHA-256 index (no PDF claims). Independent `unzip -t` passed on generated DOCX and EPUB. | Native artifact acceptance in T6 |
| Native artifacts | BLOCKED | Unmeasured | Environment probed 2026-09-16: available — macOS 27.0, Obsidian 1.11.5, `/Users/Roger/my-vault` with the plugin installed as an independent directory (not a symlink). Missing — an interactive acceptance session against a fixed candidate build (A01-A12 drive real dialogs and real output inspection); Java runtime for EPUBCheck; a real DOCX reader (no Word/LibreOffice in /Applications) and a real EPUB reader. No native case has been executed; no PASS is claimed. | Run A01-A12 per protocol T6.2/T6.3 against the final candidate (install or record EPUBCheck/Java and reader gaps explicitly) |
| Platforms | BLOCKED | Unmeasured | Only the macOS row of the T6.4 matrix is executable here. Missing — Windows, Linux, iOS, Android environments and an isolated desktop Obsidian 1.4.0 install. None tested; no platform claim made. | Execute the bounded platform matrix on the required devices |
| Documentation | NOT RUN | Unmeasured | No review recorded | Execute T7 after T6 evidence (capability table and privacy wording are evidence-bound) |
| Release gate | PASS (local) | 064f518 | `scripts/check-version.mjs` extended to lockfile root and `versions.json`↔`minAppVersion` mapping; `node --test scripts/check-version.test.mjs` 8/8 (drift, missing entry, mapping mismatch, lockfile cases, wrong tag); `npm run check:version` OK; CI adds the script test before `check:version`; release workflow runs script test, version gate, lint, build, test before attestation and prefers `docs/releases/<tag>/release-notes.md`; both workflow files parse as valid YAML; CLAUDE.md release guidance updated. | CI must verify the workflow changes on the PR |
| Upgrade / candidate | NOT RUN | Unmeasured | Blocked by T6: the plan requires T1-T8 including initial native artifact/platform rows before the version bump | Complete T6, then execute T9 (bump, final gates, install/upgrade smoke) |
| Published assets | NOT RUN | Unmeasured | Not published | Execute T10 after authorization |
8 changes: 4 additions & 4 deletions docs/superpowers/plans/2026-09-12-1.0.0-release-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ Alternatives intentionally rejected: pre-scanning every renderer to predict exac
- [x] T0 — Refresh baseline and create evidence record.
- [x] T1 — Add persistent in-memory vault fixture and reproduce output corruption.
- [x] T2 — Implement directory isolation, exclusive writes and report-name protection.
- [ ] T3 — Define structured outcomes and preserve partial results.
- [ ] T4 — Present accurate completion/cancellation/failure messages.
- [ ] T5 — Add reproducible artifact fixtures and automated contract checks.
- [x] T3 — Define structured outcomes and preserve partial results.
- [x] T4 — Present accurate completion/cancellation/failure messages.
- [x] T5 — Add reproducible artifact fixtures and automated contract checks.
- [ ] T6 — Execute native artifact and compatibility acceptance.
- [ ] T7 — Align docs, settings and metadata with verified behavior.
- [ ] T8 — Strengthen version checks and tag release verification.
- [x] T8 — Strengthen version checks and tag release verification.
- [ ] T9 — Validate upgrade and the final 1.0.0 candidate.
- [ ] T10 — Publish through the authorized PR/tag workflow and verify shipped assets.

Expand Down
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"url": "https://github.com/rogerdigital/document-exporter"
},
"devDependencies": {
"@types/jsdom": "^30.0.0",
"@types/node": "^20.11.0",
"esbuild": "^0.20.0",
"eslint-plugin-obsidianmd": "^0.4.1",
Expand Down
8 changes: 8 additions & 0 deletions scripts/check-version.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from "node:fs";
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
const manifest = JSON.parse(fs.readFileSync("manifest.json", "utf8"));
const versions = JSON.parse(fs.readFileSync("versions.json", "utf8"));
const lock = JSON.parse(fs.readFileSync("package-lock.json", "utf8"));

const expected = packageJson.version;
const errors = [];
Expand All @@ -19,6 +20,13 @@ if (tag && tag !== expected) {
errors.push(`release tag=${tag}, package.json=${expected}`);
}

if (lock.version !== expected || lock.packages?.[""]?.version !== expected) {
errors.push(`package-lock.json root versions must equal ${expected}`);
}
if (versions[expected] !== manifest.minAppVersion) {
errors.push(`versions.json[${expected}] must equal manifest.minAppVersion`);
}

if (errors.length > 0) {
process.stderr.write(`${errors.join("\n")}\n`);
process.exit(1);
Expand Down
111 changes: 111 additions & 0 deletions scripts/check-version.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";

const script = path.resolve(import.meta.dirname, "check-version.mjs");

function writeFixture(overrides = {}) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "check-version-"));
const version = overrides.version ?? "1.2.3";
const minApp = overrides.minApp ?? "1.4.0";
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "t", version }));
fs.writeFileSync(path.join(dir, "manifest.json"), JSON.stringify({
id: "t",
version: overrides.manifestVersion ?? version,
minAppVersion: minApp,
}));
fs.writeFileSync(path.join(dir, "versions.json"), JSON.stringify(
overrides.versions ?? { [version]: minApp },
));
fs.writeFileSync(path.join(dir, "package-lock.json"), JSON.stringify({
name: "t",
version: overrides.lockVersion ?? version,
lockfileVersion: 3,
packages: overrides.omitLockPackageRoot
? {}
: { "": { name: "t", version: overrides.lockPackageVersion ?? version } },
}));
return dir;
}

function run(dir, releaseTag) {
const env = { ...process.env };
delete env.RELEASE_TAG;
if (releaseTag) env.RELEASE_TAG = releaseTag;
return spawnSync(process.execPath, [script], { cwd: dir, env, encoding: "utf8" });
}

function withFixture(overrides, fn) {
const dir = writeFixture(overrides);
try {
return fn(dir);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}

test("accepts fully consistent metadata", () => {
withFixture({}, (dir) => {
const result = run(dir);
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /Version metadata is consistent: 1\.2\.3/);
});
});

test("rejects package/manifest version drift", () => {
withFixture({ manifestVersion: "1.2.2" }, (dir) => {
const result = run(dir);
assert.equal(result.status, 1);
assert.match(result.stderr, /manifest\.json=1\.2\.2, package\.json=1\.2\.3/);
});
});

test("rejects a missing versions.json entry", () => {
withFixture({ versions: { "1.0.0": "1.4.0" } }, (dir) => {
const result = run(dir);
assert.equal(result.status, 1);
assert.match(result.stderr, /versions\.json is missing 1\.2\.3/);
});
});

test("rejects a minimum-version mapping mismatch", () => {
withFixture({ versions: { "1.2.3": "1.5.0" } }, (dir) => {
const result = run(dir);
assert.equal(result.status, 1);
assert.match(result.stderr, /versions\.json\[1\.2\.3\] must equal manifest\.minAppVersion/);
});
});

test("rejects lockfile root version drift", () => {
withFixture({ lockVersion: "1.2.4" }, (dir) => {
const result = run(dir);
assert.equal(result.status, 1);
assert.match(result.stderr, /package-lock\.json root versions must equal 1\.2\.3/);
});
});

test("rejects a missing lockfile package root version", () => {
withFixture({ omitLockPackageRoot: true }, (dir) => {
const result = run(dir);
assert.equal(result.status, 1);
assert.match(result.stderr, /package-lock\.json root versions must equal 1\.2\.3/);
});
});

test("rejects a mismatched release tag", () => {
withFixture({}, (dir) => {
const result = run(dir, "wrong-tag");
assert.equal(result.status, 1);
assert.match(result.stderr, /release tag=wrong-tag, package\.json=1\.2\.3/);
});
});

test("accepts a matching release tag", () => {
withFixture({}, (dir) => {
const result = run(dir, "1.2.3");
assert.equal(result.status, 0, result.stderr);
});
});
Loading