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
8 changes: 6 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
23 changes: 21 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -171,7 +176,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:
Expand Down Expand Up @@ -228,5 +242,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"
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <downloaded file> --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 <downloaded file> --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`.
Expand Down
11 changes: 6 additions & 5 deletions THIRD_PARTY_NOTICES.txt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.


==============================================================================
Expand Down
7 changes: 5 additions & 2 deletions docs/code-signing-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |

Expand All @@ -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 <downloaded file> --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
Expand Down
6 changes: 3 additions & 3 deletions docs/release-readiness.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -38,15 +38,15 @@ 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 |
| Final macOS installed-package checks | Open cases and older tested source are recorded in [macOS QA](macos-release-qa.md); rerun against the final candidate |
| 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
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
32 changes: 27 additions & 5 deletions scripts/release/assemble.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
Expand All @@ -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.
Expand All @@ -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 <file> --repo ${REPOSITORY}\` confirms the file was built by this repository's release workflow.

## Licenses

Expand All @@ -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(', ')}`);
Expand Down
75 changes: 75 additions & 0 deletions scripts/release/notices.mjs
Original file line number Diff line number Diff line change
@@ -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();
}
Loading
Loading