From 2d16004c7800380eb461dcd1e31e28b815764854 Mon Sep 17 00:00:00 2001 From: zknpr <96851588+zknpr@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:09:46 +0200 Subject: [PATCH 1/3] release: 0.3.0 Version bump for the first public release. Changes since the 0.2.0 private beta: the navigation pin and CSP base-uri/form-action/frame-ancestors, the hardened-runtime ad-hoc macOS signature, notices bundled in every package, a .deb-only Linux release with a glibc 2.39 floor, and the release workflow. --- README.md | 2 +- docs/release-readiness.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 38144a8..18f101d 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Linux and Windows. It shares its viewer with the [SQLite Explorer VS Code extension](https://github.com/zknpr/SQLite-Explorer) and runs in a Tauri desktop shell. -The current desktop version is **0.2.0**, integrating the extension's **1.8.1** +The current desktop version is **0.3.0**, integrating the extension's **1.8.1** release and desktop-specific fixes. Packages for macOS, Linux and Windows are attached to each [release](https://github.com/zknpr/SQLite-Explorer-App/releases); see [Install](#install). See [release readiness](docs/release-readiness.md) for diff --git a/docs/release-readiness.md b/docs/release-readiness.md index 0c42544..550c2c3 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -1,6 +1,6 @@ # Release readiness -Status checked October 8, 2026, for desktop 0.2.0 and the extension 1.8.1 +Status checked October 8, 2026, for desktop 0.3.0 and the extension 1.8.1 integration. The source is public under MIT. Release packages are built by the [release workflow](../.github/workflows/release.yml); the gates below separate source publication from binary distribution. diff --git a/package-lock.json b/package-lock.json index 0771a1e..81ffd7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sqlite-explorer-desktop", - "version": "0.2.0", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sqlite-explorer-desktop", - "version": "0.2.0", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@tauri-apps/cli": "^2.11.4", diff --git a/package.json b/package.json index 2bb812d..a59a34c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqlite-explorer-desktop", - "version": "0.2.0", + "version": "0.3.0", "description": "SQLite Explorer desktop shell (Tauri v2) around the extension's desktop viewer artifacts", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2348e8d..e4b7653 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3154,7 +3154,7 @@ dependencies = [ [[package]] name = "sqlite-explorer-desktop" -version = "0.2.0" +version = "0.3.0" dependencies = [ "libc", "objc2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a806a38..4c57664 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlite-explorer-desktop" -version = "0.2.0" +version = "0.3.0" description = "SQLite Explorer desktop shell" edition = "2021" license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index beb28a6..7550237 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "SQLite Explorer", - "version": "0.2.0", + "version": "0.3.0", "identifier": "xyz.zknpr.sqlite-explorer", "build": { "frontendDist": "../viewer-dist" From 0fc3e9b2bde45d2c58e4b3fd127b3ddb213acdd5 Mon Sep 17 00:00:00 2001 From: zknpr <96851588+zknpr@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:21:33 +0200 Subject: [PATCH 2/3] fix(release): address the Codex review findings on #1 and #2 - THIRD_PARTY_NOTICES.txt still read "0.2.0 private beta" and sent MPL source requests to releases/tag/v0.2.0-beta.1. That tag never existed in this repository, so every package would ship a broken source pointer. The header is now version-neutral and points at the release page, which carries mpl-dependency-sources.tar.gz. - A prerelease tag drafted an ordinary release, which publishing could make Latest. The manifest now records prerelease, and the workflow adds --prerelease from it. - The DMG was described as ad-hoc signed. Only the app inside is (Tauri skips the disk image for identity "-"; codesign reports the DMG "not signed at all"). The docs and the manifest now say so. - sha256sum does not exist on Windows. The notes now give shasum for macOS, sha256sum for Linux and a PowerShell Get-FileHash check for Windows. That command comes from one exported function, and the Windows build job runs that exact string against its own installer: True for the real hash, False for a wrong one. Regression tests for the notices, prerelease, DMG wording and checksum commands failed before this change and pass after it. --- .github/workflows/release.yml | 18 ++++++++++++++++-- README.md | 15 ++++++++++++--- THIRD_PARTY_NOTICES.txt | 11 ++++++----- docs/code-signing-policy.md | 7 +++++-- docs/release-readiness.md | 4 ++-- scripts/release/assemble.mjs | 32 +++++++++++++++++++++++++++----- scripts/release/release.test.mjs | 29 +++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f515d86..896b996 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -171,7 +171,16 @@ jobs: grep -q 'THIRD_PARTY_NOTICES.txt' <<<"$listing" grep -q 'tjs.exe' <<<"$listing" mkdir dist - cp "${exes[0]}" "dist/$(node scripts/release/assemble.mjs asset-name windows)" + name=$(node scripts/release/assemble.mjs asset-name windows) + cp "${exes[0]}" "dist/$name" + # Run the exact checksum command the release notes publish for + # Windows, once against the real hash and once against a wrong one. + check="$RUNNER_TEMP/checksum-check" + mkdir "$check" + cp "dist/$name" "$check/" + command=$(node scripts/release/assemble.mjs windows-checksum-command) + (cd "$check" && sha256sum "$name" > SHA256SUMS && [ "$(pwsh -NoProfile -Command "$command" | tr -d '\r')" = True ]) + (cd "$check" && printf '%064d %s\n' 0 "$name" > SHA256SUMS && [ "$(pwsh -NoProfile -Command "$command" | tr -d '\r')" = False ]) - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: @@ -228,5 +237,10 @@ jobs: GH_TOKEN: ${{ github.token }} REF_NAME: ${{ github.ref_name }} run: | - gh release create "$REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" --draft --verify-tag \ + flags=(--draft --verify-tag) + # A prerelease version must not become Latest when the draft is published. + if [ "$(node -p "require('./dist/release-manifest.json').prerelease")" = true ]; then + flags+=(--prerelease) + fi + gh release create "$REF_NAME" dist/* --repo "$GITHUB_REPOSITORY" "${flags[@]}" \ --title "SQLite Explorer ${REF_NAME#v}" --notes-file "$RUNNER_TEMP/notes.md" diff --git a/README.md b/README.md index 18f101d..1beec35 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,22 @@ packages are not signed with a publisher identity yet; see the [code signing policy](docs/code-signing-policy.md). Verify a download before installing it: +Download `SHA256SUMS` next to the package and check it with the tool your +system has: + ```sh -sha256sum --check --ignore-missing SHA256SUMS -gh attestation verify --repo zknpr/SQLite-Explorer-App +shasum -a 256 --check --ignore-missing SHA256SUMS # macOS +sha256sum --check --ignore-missing SHA256SUMS # Linux ``` +On Windows, the release notes give a PowerShell `Get-FileHash` command that +prints `True` for a matching installer. With the GitHub CLI on any platform, +`gh attestation verify --repo zknpr/SQLite-Explorer-App` +confirms the file was built by this repository's release workflow. + **macOS (Apple Silicon).** Open the `.dmg` and drag SQLite Explorer to -Applications. The app has an ad-hoc signature and is not notarized, so macOS +Applications. The disk image is unsigned; the app inside has an ad-hoc +signature and is not notarized, so macOS blocks the first launch. Open **System Settings → Privacy & Security** and choose **Open Anyway** for SQLite Explorer. To uninstall, move the app to the Trash; settings live in `~/Library/Application Support/xyz.zknpr.sqlite-explorer`. diff --git a/THIRD_PARTY_NOTICES.txt b/THIRD_PARTY_NOTICES.txt index 8bfda55..36ee712 100644 --- a/THIRD_PARTY_NOTICES.txt +++ b/THIRD_PARTY_NOTICES.txt @@ -1,4 +1,4 @@ -SQLite Explorer desktop 0.2.0 private beta — third-party notices +SQLite Explorer desktop — third-party notices The application source is licensed under MIT (see LICENSE.md). Components listed here retain their own licenses. Inclusion in this inventory does not @@ -19,14 +19,15 @@ bytes are unchanged. Supporting Codicons code is MIT. Both texts follow. MPL 2.0 source availability: unmodified source packages for cssparser 0.36.0, cssparser-macros 0.6.1, dtoa-short 0.3.5, option-ext 0.2.0 and selectors 0.36.1 -are supplied alongside the installers in mpl-dependency-sources.tar.gz at -https://github.com/zknpr/SQLite-Explorer-App/releases/tag/v0.2.0-beta.1. +are attached to every binary release as mpl-dependency-sources.tar.gz, on the +release page that carries the installer: +https://github.com/zknpr/SQLite-Explorer-App/releases Each package is also available from the versioned crates.io URL in the inventory. These covered source files remain under MPL 2.0. third-party/inventory.json records package identities, source URLs and notice -hashes. This beta's AppImage is not distributed: the additional bundled Linux -system libraries still require their own distribution inventory. +hashes. No AppImage is distributed: the additional bundled Linux system +libraries still require their own distribution inventory. ============================================================================== diff --git a/docs/code-signing-policy.md b/docs/code-signing-policy.md index 2c74fbb..2ffede4 100644 --- a/docs/code-signing-policy.md +++ b/docs/code-signing-policy.md @@ -11,7 +11,7 @@ published by a maintainer. | Package | Signature | | --- | --- | -| macOS `.dmg` | Ad-hoc signature with the hardened runtime. Not signed with an Apple Developer ID and not notarized. | +| macOS `.dmg` | The disk image is unsigned. The app inside has an ad-hoc signature with the hardened runtime. Neither is signed with an Apple Developer ID, and nothing is notarized. | | Windows installer | Unsigned. Signing through the SignPath Foundation is planned. | | Linux `.deb` | Unsigned, as is usual for packages installed directly rather than from an apt repository. | @@ -20,10 +20,13 @@ Unsigned and ad-hoc packages are still verifiable. Each release lists ties it to the workflow run and commit that produced it: ```sh -sha256sum --check --ignore-missing SHA256SUMS +shasum -a 256 --check --ignore-missing SHA256SUMS # macOS +sha256sum --check --ignore-missing SHA256SUMS # Linux gh attestation verify --repo zknpr/SQLite-Explorer-App ``` +On Windows, use the PowerShell `Get-FileHash` command from the release notes. + ### Windows: what signing will and will not cover Once signing is approved, the application executable and the installer will be diff --git a/docs/release-readiness.md b/docs/release-readiness.md index 550c2c3..3a4d747 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -38,7 +38,7 @@ downloaded package or close the macOS interaction gates. | Security policy | [SECURITY.md](../SECURITY.md), private vulnerability reporting | | Continuous integration | Apple Silicon macOS: manifest verification, script tests, shell unit tests, clippy. No Linux or Windows lane | | Navigation pin and CSP `base-uri`/`form-action`/`frame-ancestors` | Live-verified in an Apple Silicon macOS QA build against a pre-change control build. Linux and Windows runtime behaviour unverified | -| Release pipeline | Builds macOS, Linux and Windows packages on their own OS from the viewer pin, with no cache, and drafts a release for a `v*` tag. Each asset gets SHA256SUMS and a build-provenance attestation. Publishing is manual | +| Release pipeline | Builds macOS, Linux and Windows packages on their own OS from the viewer pin, with no cache, and drafts a release for a `v*` tag; a prerelease version drafts a prerelease. Each asset gets SHA256SUMS and a build-provenance attestation. Publishing is manual | | Third-party notices | Bundled inside every package; the release also attaches them with the MPL Cargo sources, which are checksum-verified against Cargo.lock | | AppImage distribution | Not distributed: its extra bundled Linux system libraries have no notice/source inventory. Linux ships as `.deb` | | Linux glibc floor | The native engine needs glibc 2.38 and the app built on Ubuntu 24.04 needs 2.39; the `.deb` declares `libc6 (>= 2.39)` and the workflow fails if any packaged binary needs more | @@ -46,7 +46,7 @@ downloaded package or close the macOS interaction gates. | Linux/Windows installed-package checks | Passed for the packages and environments in [Linux/Windows QA](linux-windows-release-qa.md), including real shutdown/restart. Those packages predate the navigation pin and CSP change; rerun the affected checks on new packages | | Other Linux desktops, Wayland and physical keyboards | Unverified; limit platform claims to observed coverage | | Windows distribution | Unsigned. SignPath Foundation signing requires a prior public release, so the first release ships unsigned, then apply. Signing will cover the app and installer, not the upstream `tjs.exe`. Smart App Control blocks the unsigned installer; whether it blocks `tjs.exe` under a signed app (the app would fall back to WASM) is untested. SmartScreen on a clean machine unverified | -| macOS distribution | Ad-hoc signature with the hardened runtime, built by the workflow; no Developer ID signing or notarization by choice. Gatekeeper's Open Anyway flow on a downloaded package unverified | +| macOS distribution | The app has an ad-hoc signature with the hardened runtime, built by the workflow; the DMG is unsigned. No Developer ID signing or notarization, by choice. Gatekeeper's Open Anyway flow on a downloaded package unverified | The source MIT license does not replace third-party licenses. The [notice inventory](../third-party/inventory.json) records identities, source diff --git a/scripts/release/assemble.mjs b/scripts/release/assemble.mjs index bd19f54..daed43a 100644 --- a/scripts/release/assemble.mjs +++ b/scripts/release/assemble.mjs @@ -53,12 +53,14 @@ export function buildManifest({ version, tag, commit, runUrl, viewerRef, assets product: 'SQLite Explorer', version, tag, + // A semver prerelease (0.3.0-beta.1) must never be published as Latest. + prerelease: version.includes('-'), repository: REPOSITORY, commit, buildRun: runUrl, viewerSource: { repository: VIEWER_REPOSITORY, ref: viewerRef }, signing: { - macos: 'ad-hoc signature; not Developer ID signed or notarized', + macos: 'the app has an ad-hoc signature with the hardened runtime; the disk image is unsigned; not Developer ID signed or notarized', windows: 'unsigned', linux: 'unsigned package; verify SHA256SUMS and the build attestation' }, @@ -72,6 +74,17 @@ export function checksumsText(entries) { .map(({ name, sha256 }) => `${sha256} ${name}`).join('\n') + '\n'; } +/** + * The PowerShell check the notes publish for Windows, which has no sha256sum. + * Exported so the Windows build job runs this exact string against its own + * installer: the documented command is the tested one. Get-FileHash defaults to + * SHA256 and -eq compares strings case-insensitively, so its upper-case hex + * matches the lower-case SHA256SUMS line. + */ +export function windowsChecksumCommand(asset) { + return `(Get-FileHash .\\${asset}).Hash -eq ((Select-String -Path SHA256SUMS -SimpleMatch '${asset}').Line -split ' ')[0]`; +} + export function releaseNotes({ version, commit, runUrl, viewerRef }) { const rows = Object.values(PLATFORMS).map((p) => `| ${p.label} | \`${p.asset(version)}\` |`); return `SQLite Explorer ${version}: a desktop SQLite browser and editor built on the SQLite Explorer viewer. @@ -92,10 +105,13 @@ See the [installation notes](https://github.com/${REPOSITORY}#install) for unins ## Verify the download -\`\`\`sh -sha256sum --check --ignore-missing SHA256SUMS -gh attestation verify ${PLATFORMS.linux.asset(version)} --repo ${REPOSITORY} -\`\`\` +Download \`SHA256SUMS\` next to the package, then check it: + +- **macOS:** \`shasum -a 256 --check --ignore-missing SHA256SUMS\` +- **Linux:** \`sha256sum --check --ignore-missing SHA256SUMS\` +- **Windows (PowerShell):** \`${windowsChecksumCommand(PLATFORMS.windows.asset(version))}\` prints \`True\` when the file matches. + +With the GitHub CLI on any platform, \`gh attestation verify --repo ${REPOSITORY}\` confirms the file was built by this repository's release workflow. ## Licenses @@ -111,6 +127,12 @@ function arg(argv, flag) { function main(argv) { const [command, ...rest] = argv; + if (command === 'windows-checksum-command') { + const version = resolveVersion(readVersions()); + if (version instanceof Error) throw version; + console.log(windowsChecksumCommand(PLATFORMS.windows.asset(version))); + return; + } if (command === 'asset-name') { const platform = PLATFORMS[rest[0]]; if (!platform) throw new Error(`asset-name needs one of ${Object.keys(PLATFORMS).join(', ')}`); diff --git a/scripts/release/release.test.mjs b/scripts/release/release.test.mjs index 5800a7a..ba655a8 100644 --- a/scripts/release/release.test.mjs +++ b/scripts/release/release.test.mjs @@ -152,3 +152,32 @@ test('assemble writes a manifest and checksums that verify, and refuses a pollut fs.rmSync(`${dist}-notes.md`, { force: true }); } }); + +test('the bundled notices name no specific release and point at every release for MPL sources', () => { + // The same file ships inside every package of every version, so its header + // must not pin one release: a stale tag URL sends MPL source requests to a + // release that does not exist. + const notices = fs.readFileSync(new URL('../../THIRD_PARTY_NOTICES.txt', import.meta.url), 'utf8'); + const header = notices.slice(0, notices.indexOf('\n=====')); + assert.doesNotMatch(header, /\/releases\/tag\//, 'no pinned release URL'); + assert.doesNotMatch(header, /\bbeta\b/i, 'no beta wording'); + assert.doesNotMatch(header.split('\n')[0], /\d+\.\d+\.\d+/, 'no version in the title'); + assert.match(header, /mpl-dependency-sources\.tar\.gz/); + assert.match(header, /https:\/\/github\.com\/zknpr\/SQLite-Explorer-App\/releases(?!\/tag)/); +}); + +test('a prerelease version drafts a prerelease, and the manifest states the DMG is unsigned', async () => { + const { buildManifest } = await import('./assemble.mjs'); + const base = { tag: 'v', commit: 'c'.repeat(40), runUrl: 'u', viewerRef: 'r', assets: [] }; + assert.equal(buildManifest({ ...base, version: '0.3.0-beta.1' }).prerelease, true); + assert.equal(buildManifest({ ...base, version: '0.3.0' }).prerelease, false); + assert.match(buildManifest({ ...base, version: '0.3.0' }).signing.macos, /disk image is unsigned/); +}); + +test('the release notes give a checksum command that exists on each platform', async () => { + const { releaseNotes } = await import('./assemble.mjs'); + const notes = releaseNotes({ version: '0.3.0', commit: 'c'.repeat(40), runUrl: 'u', viewerRef: 'r' }); + assert.match(notes, /shasum -a 256 --check --ignore-missing SHA256SUMS/, 'macOS'); + assert.match(notes, /Get-FileHash/, 'Windows'); + assert.match(notes, /sha256sum --check --ignore-missing SHA256SUMS/, 'Linux'); +}); From 70c10ee739515c62ed8b8133abfc9f4f0c83a1dd Mon Sep 17 00:00:00 2001 From: zknpr <96851588+zknpr@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:26:07 +0200 Subject: [PATCH 3/3] ci(release): fail when the third-party notices drift from the release graph THIRD_PARTY_NOTICES.txt and third-party/inventory.json were generated once and nothing regenerated or checked them. A dependency bump would have shipped incomplete notices in every package without anything failing. scripts/release/notices.mjs checks that: - the inventory's Cargo components are exactly the packages a release resolves. This uses `cargo metadata` with default features. The raw lock would wrongly demand the 26 qa-webdriver-only crates, which no release builds; - the inventory was built for viewer-dist's pin; - every inventoried component has a section in the notices. It runs in CI, so a dependency-bump PR goes red, and in every release build. Today it passes: the release resolves 442 packages and the inventory lists exactly those 442. With one shipped crate removed from the inventory it fails and names the crate. ci.yml also moves to the Node-24 action SHAs release.yml already uses. --- .github/workflows/ci.yml | 8 +++- .github/workflows/release.yml | 5 +++ scripts/release/notices.mjs | 75 ++++++++++++++++++++++++++++++++ scripts/release/release.test.mjs | 28 ++++++++++++ third-party/README.md | 6 +++ 5 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 scripts/release/notices.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9ce0b2..e8ea399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,11 +21,11 @@ jobs: name: Verify artifacts, test & lint runs-on: macos-latest steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 cache: npm @@ -46,6 +46,10 @@ jobs: rustup toolchain install 1.93.0 --profile minimal --component clippy rustup default 1.93.0 + # A dependency bump fails here until the inventory and notices cover it. + - name: Third-party notices match the release dependency graph + run: node scripts/release/notices.mjs + - name: Shell unit tests run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 896b996..11bb540 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,6 +119,11 @@ jobs: npm test cargo test --locked --manifest-path src-tauri/Cargo.toml --lib + # Every package bundles THIRD_PARTY_NOTICES.txt, so no release may ship + # notices that miss a dependency or describe another viewer pin. + - name: Third-party notices match the release dependency graph + run: node scripts/release/notices.mjs + - name: Build run: npm run tauri build -- --bundles "$BUNDLES" diff --git a/scripts/release/notices.mjs b/scripts/release/notices.mjs new file mode 100644 index 0000000..9ea60dd --- /dev/null +++ b/scripts/release/notices.mjs @@ -0,0 +1,75 @@ +#!/usr/bin/env node +/** + * Fail when THIRD_PARTY_NOTICES.txt and third-party/inventory.json no longer + * describe what a release build ships. Both were generated once, and nothing + * regenerates them, so a dependency bump would otherwise ship stale notices + * silently. + * + * node scripts/release/notices.mjs + * + * Checks: + * - the inventory's Cargo components are exactly the packages a release build + * resolves (`cargo metadata` with default features, all targets). The test-only + * `qa-webdriver` feature is not in a release, so it is not resolved; + * - the inventory was built for the viewer pin that viewer-dist/ ships (the + * viewer's JS graph and the native runtime both come from that pin); + * - every inventoried component appears in THIRD_PARTY_NOTICES.txt. + * + * On failure, regenerate the inventory and notices for the new graph (see + * third-party/README.md) rather than editing this check. + */ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); + +/** `name version` of every non-workspace package in a release resolve. */ +export function releaseCargoPackages(metadata) { + const byId = new Map(metadata.packages.map((pkg) => [pkg.id, pkg])); + return new Set(metadata.resolve.nodes + .map((node) => byId.get(node.id)) + .filter((pkg) => pkg && pkg.source) + .map((pkg) => `${pkg.name} ${pkg.version}`)); +} + +export function checkNotices({ releasePackages, inventory, noticesText, viewerRef }) { + const problems = []; + const cargo = new Set(inventory.components.filter((c) => c.ecosystem === 'Cargo').map((c) => c.component)); + for (const pkg of [...releasePackages].sort()) { + if (!cargo.has(pkg)) problems.push(`release ships ${pkg}, which the inventory and notices omit`); + } + for (const pkg of [...cargo].sort()) { + if (!releasePackages.has(pkg)) problems.push(`inventory lists ${pkg}, which a release no longer ships`); + } + if (inventory.viewerSource !== viewerRef) { + problems.push(`inventory was built for viewer ${inventory.viewerSource}, but viewer-dist is pinned to ${viewerRef}`); + } + for (const { component } of inventory.components) { + if (!noticesText.includes(component)) problems.push(`THIRD_PARTY_NOTICES.txt has no section for ${component}`); + } + return problems; +} + +function main() { + const metadata = JSON.parse(execFileSync('cargo', ['metadata', '--locked', '--format-version', '1', + '--manifest-path', path.join(repoRoot, 'src-tauri', 'Cargo.toml')], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 })); + const read = (file) => fs.readFileSync(path.join(repoRoot, file), 'utf8'); + const problems = checkNotices({ + releasePackages: releaseCargoPackages(metadata), + inventory: JSON.parse(read('third-party/inventory.json')), + noticesText: read('THIRD_PARTY_NOTICES.txt'), + viewerRef: JSON.parse(read('viewer-dist/manifest.json')).ref + }); + if (problems.length > 0) { + for (const problem of problems) console.error(`notices: ${problem}`); + console.error(`notices: ${problems.length} problem(s); regenerate third-party/inventory.json and THIRD_PARTY_NOTICES.txt`); + process.exit(1); + } + console.log('notices: inventory and notices match the release dependency graph and viewer pin'); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main(); +} diff --git a/scripts/release/release.test.mjs b/scripts/release/release.test.mjs index ba655a8..0d9f8c1 100644 --- a/scripts/release/release.test.mjs +++ b/scripts/release/release.test.mjs @@ -181,3 +181,31 @@ test('the release notes give a checksum command that exists on each platform', a assert.match(notes, /Get-FileHash/, 'Windows'); assert.match(notes, /sha256sum --check --ignore-missing SHA256SUMS/, 'Linux'); }); + +test('notice drift is caught in every direction', async () => { + const { checkNotices, releaseCargoPackages } = await import('./notices.mjs'); + const metadata = { + packages: [ + { id: 'app', name: 'sqlite-explorer-desktop', version: '0.3.0', source: null }, + { id: 'a', name: 'serde', version: '1.0.0', source: 'registry+x' }, + { id: 'q', name: 'axum', version: '0.8.9', source: 'registry+x' } + ], + // `axum` is in the lock (a qa-webdriver dependency) but not resolved for a release. + resolve: { nodes: [{ id: 'app' }, { id: 'a' }] } + }; + const releasePackages = releaseCargoPackages(metadata); + assert.deepEqual([...releasePackages], ['serde 1.0.0'], 'the workspace crate and unresolved packages are excluded'); + const inventory = { viewerSource: 'pin', components: [{ component: 'serde 1.0.0', ecosystem: 'Cargo' }, { component: 'sql.js 1.14.1' }] }; + const noticesText = 'serde 1.0.0\n...\nsql.js 1.14.1\n'; + assert.deepEqual(checkNotices({ releasePackages, inventory, noticesText, viewerRef: 'pin' }), []); + + const bumped = new Set(['serde 1.0.1']); + assert.deepEqual(checkNotices({ releasePackages: bumped, inventory, noticesText, viewerRef: 'pin' }), [ + 'release ships serde 1.0.1, which the inventory and notices omit', + 'inventory lists serde 1.0.0, which a release no longer ships' + ]); + assert.deepEqual(checkNotices({ releasePackages, inventory, noticesText, viewerRef: 'newpin' }), + ['inventory was built for viewer pin, but viewer-dist is pinned to newpin']); + assert.deepEqual(checkNotices({ releasePackages, inventory, noticesText: 'serde 1.0.0\n', viewerRef: 'pin' }), + ['THIRD_PARTY_NOTICES.txt has no section for sql.js 1.14.1']); +}); diff --git a/third-party/README.md b/third-party/README.md index 41a5e81..bfd9c3b 100644 --- a/third-party/README.md +++ b/third-party/README.md @@ -30,3 +30,9 @@ inventoried. Regenerate and review the inventory when the source pins, dependency locks or packaging change. Preserve upstream copyright and attribution notices. +`scripts/release/notices.mjs` runs in CI and in every release build. It fails +when the inventory's Cargo components differ from the packages a release +resolves, when the inventory was built for another viewer pin, or when an +inventoried component has no section in the notices. No generator is checked in +yet: on a failure, add or remove the affected components and their notice texts +by hand, then rerun the check.