Skip to main content
Suchi uses Conventional Commits, Keep a Changelog, and SemVer. Pre-1.0 releases may break documented interfaces when that produces a cleaner product; the changelog must still explain the impact.

Commits

Use this shape:
Common types are feat, fix, perf, refactor, docs, test, build, and chore. Scopes name the affected area, such as mail, ui, or api. Use ! and a BREAKING CHANGE: footer when a commit breaks a documented interface. Keep subjects brief and bodies focused.

Repository and tag authority

GitHub gh hosts the release workflow, artifacts, images, and release notes. A release is not ready to dispatch until the signed annotated tag resolves to the same commit locally and on GitHub. Push the release commit and that exact tag; never recreate a tag independently after pushing it.

Changelog and versions

User-visible changes update the [Unreleased] section of CHANGELOG.md in the same pull request. Write for operators and call out required migration steps directly. A published core migration is immutable. A release normally adds at most one numbered core migration; never rewrite or renumber one that shipped. Suchi follows SemVer:
  • MAJOR: incompatible stable-interface changes after 1.0
  • MINOR: features and potentially breaking pre-1.0 changes
  • PATCH: fixes and internal changes
Only the current pre-1.0 release is supported.

Stable-v1 database compatibility

This section is the release authority for accepted core database states. The signed pre-v0.1.0 SQL, tags and release artifacts remain byte-identical. v0.1.0 embeds frozen compatibility SQL in core/db/compatibility/0002–0005; its declarative baseline is active migration 0001_baseline.sql, user_version = 1, lineage stable-v1. v0.2.0 adds active 0002_metadata_cleanup.sql and 0003_automation_asks.sql and targets user_version = 3. v0.3.0 adds 0004_product_foundations.sql and targets user_version = 4. v0.4.0 adds 0005_product_refinements.sql and targets user_version = 5 without changing that lineage. The exact stable schema-1 fingerprint is 9d2a6320eae1582b0f294caa6ed518b5a73ab3dbe6597c9ca1a36cc563e03240. The development schema-2 fingerprint is 645e7798d663fff83d120182f661e7148fbd68d263517de9d44783d80aaebae3. The v0.2.0 schema-3 fingerprint is cfbdda9e3e80f35be33f763ad14c1c0d83a22ead62e91d04f6dc900331742a82. The v0.3.0 schema-4 fingerprint is 360611dcca51a4ce23d0114e58c82b5208ce72e7575afa25e0c350be7d3100b4. The current schema-5 fingerprint is 8f13f6a89fc56a04e027b0cdb19156a405445b8ee6bf6a80629f32e54c1e3d04. The one admitted pre-identity schema-4 boundary remains 5d6ac98308eb644b030092178792a46f36e6f8f5041411f020ed2dcf216f46c2. Startup admits only these exact states: “Canonical” means the complete normalized core sqlite_schema fingerprint, not only the integer version or a claimed lineage row. Intermediate, partial, spoofed, corrupt and future schemas are rejected before mutation and do not get an adoption snapshot. Extension objects created after _suchi_extension_migrations are outside the core fingerprint; core objects rebuilt during adoption remain part of it regardless of their replacement rowids. Frozen compatibility migration 0005 rebuilds users with nullable issuer/subject and rebuilds audit_events with retained-event metadata. It copies numeric user and audit IDs exactly, so foreign-key and logical owner references stay stable. Active migration 0002 adds version-family storage and indexes, validates the predecessor graph, assigns deterministic family keys only to existing multi-row histories, and leaves standalone documents without a family key. Active migration 0004 adds title ownership and its revision guard. Active migration 0005 adds sealed local-auth TOTP seeds and digested recovery codes. All accepted paths reach the same fresh schema-5 fingerprint. Every admitted pre-stable database passes integrity and foreign-key checks, gets an exclusive mode-0600 SQLite snapshot beside the live database, and applies the remaining compatibility steps, active migrations 0002–0005, stable lineage and schema version 5 in one immediate writer transaction. Startup then checks integrity, foreign keys and equality with the fresh schema-5 manifest. Failure rolls back the transaction and retains the snapshot. Fresh and already-current databases do not create adoption snapshots. Stable-lineage forward upgrades also do not create an adoption snapshot; keep the operator’s complete backup before upgrading. Before release, verify fresh initialization and restart, all five pre-stable paths, stable schema-1/2/3 forward upgrades, data/ID/family/title-ownership preservation, snapshot creation and failure, fault-injected rollback, foreign-key restoration, cycle rejection without a version bump, invalid-state rejection without writes, and final manifest equality. Keep the operator’s complete backup containing SQLite, blobs, the credential key and configuration; an adoption snapshot is database-only. See Storage and transaction invariants for runtime invariants and Backup and restore for operator procedure.

Major-version schema epochs

Suchi resets migration history at major releases instead of carrying every old upgrade path forever:
  1. The final release of the outgoing major is the bridge. It accepts the last canonical schema, applies one reviewed conversion, and writes an application-owned marker such as schema_state(epoch, version).
  2. The new major starts from one declarative baseline. Fresh installs create it directly; the normal runtime drops older migrations, fingerprints, and release-specific checksums. Git tags retain that history.
  3. Migrations within the new major use the application-owned epoch/version marker rather than PRAGMA user_version. Some explicit compatibility marker remains mandatory so partial or hand-edited databases fail closed.
  4. Older databases upgrade through the bridge release. If direct legacy conversion becomes necessary, it belongs in a separate offline upgrade command, not normal server startup.
  5. Repeat only at the next major boundary: v1 carries v1 migrations, v2 starts a new epoch, and so on.
The first cutover is v1.0. Until its bridge ships, the pre-1.0 compatibility table above remains authoritative.

Already-adopted pre-identity archives

An untagged archive may already carry user_version = 1 and stable-v1 while still matching the pre-identity fingerprint. Automatic startup deliberately rejects that state: it has crossed the old adoption boundary and does not have a new automatic pre-adoption snapshot. Use only the reviewed manual path:
  1. Preserve a live browser session before the window if an existing OIDC account must be bound; verified-email fallback is intentionally absent.
  2. Stop Suchi and every CLI/import writer. Require the exact pre-identity fingerprint, valid lineage/version, clean integrity and foreign keys, and the reviewed 0005 checksum before changing the database.
  3. Copy the complete quiescent data directory and effective configuration to verified storage outside DATA_DIR; keep the old binary/config. A normal live database copy is not rollback.
  4. Apply the exact 0005 SQL through the table-rebuild transaction, restore user_version = 1 and stable-v1, then require the exact stable schema-1 fingerprint, integrity, foreign keys, stable user IDs, and expected counts before starting any writer.
  5. Start the current binary. It admits that exact schema-1 state, applies active migrations 0002–0005, and requires the schema-5 manifest. From the preserved session, use Sync from identity provider and verify the same user ID gains issuer/subject, a retained user.oidc_bound row, a replacement cookie, and unchanged ownership.
When adopting a pre-v0.1.0 archive, keep both the automatic database snapshot and a complete operator backup. If no live session survives to bind an occupied unbound OIDC email, stop and restore with the old binary/config; do not delete, merge, or recreate the account to bypass the conflict.

Release checklist

Suchi Companion has an independent version and signed tag. Its release gates, artifact checks and coordinated store publication order live in the Companion release runbook; do not duplicate them here.
  1. Build and verify the web app from the release sources:
    core/ui/spa/dist remains ignored. CI builds it once from the immutable release revision, passes that artifact to every tagged binary build, and rebuilds it in the pinned Bun stage for container images.
  2. Move the [Unreleased] entries under the new version and date. Update developmentVersion in distro/cmd/suchi/main.go to the current source version line with a -dev suffix.
  3. Review every OCR, content and LLM pipeline revision changed since the previous release. distro/cmd/suchi/pipeline_proposals.go is the tagged automatic reminder policy:
    • Keep 0 when that pipeline has no recommended automatic upgrade.
    • Set a field to the current pipeline revision when existing successful results should receive a Processing update reminder.
    • Leave an earlier positive recommendation unchanged when a newer processing change is not worth prompting. This preserves the earlier recommendation for archives that skipped releases without opting in the newer revision.
    Each field is independent and cannot exceed its compiled pipeline revision. Commit this file with the release metadata; never inject or override the policy in the build workflow.
  4. Confirm the shipped Microsoft OAuth registration still permits personal and work/school device-code sign-in.
  5. Run the local verification suite:
  6. Manually exercise behavior that depends on external services or accounts, including Microsoft sign-in, configured email providers, and hosted LLMs affected by the release.
  7. Recheck public size, memory, startup, network, and feature claims against the accepted benchmark and the final build.
  8. Commit the release metadata on main, create a signed annotated tag, and push the exact commit and tag to GitHub:
  9. Dispatch the manual release workflow:
    This verifies that the local and GitHub tags resolve to the same commit. Use PUBLISH=false to validate the signed tag and release gates without packaging or publishing release artifacts. The checked-in proposal policy remains identical across every published binary and image. Zero thresholds do not cancel existing reminders, and explicit rescans remain available.
The workflow builds standard and full multi-architecture images, cross-platform binaries, checksums, SBOMs, signatures, and GitHub release notes derived from the changelog. Tag pushes alone do not publish anything. Release notes travel as a workflow artifact into the collector, rather than a multiline job output that GitHub’s secret detection may silently suppress. AnyDoc is compiled once per Linux architecture through the Dockerfile’s anydoc export target. Those binaries and licenses are packaged for standalone downloads. The image job downloads only that run’s AnyDoc artifacts, verifies their checksums, and supplies them as an anydoc named build context for both runtime variants. Docker selects the matching amd64 or arm64 directory; missing or damaged artifacts fail the build instead of triggering recompilation. Plain local Docker builds omit that context and still compile from pinned source. Release builds inject the version and source revision into the Go binary. Settings reads the identity from /api/whoami, showing only the version for tagged production releases. Unversioned builds use the source’s developmentVersion and append the short revision when available. suchi version likewise retains an injected release version and adds the revision for development builds. Docker has no .git directory, so manual builds must pass REVISION to retain source identification and VERSION for a tagged release. Local VCS builds without an injected version use the source version line, not Go’s tag-derived module pseudo-version. Missing development revisions are labeled unavailable, not guessed.

Publishing container images without Actions

GHCR images can be built and pushed directly when Actions is unavailable. Build from the release tag, never from a different branch with the release version injected: the image contents must agree with the Git tag. The Dockerfile pins base-image digests, the AnyDoc source commit and checked third-party archives. Go and Cargo dependency checksums verify downloaded source. Alpine and Debian runtime packages still resolve from their live repositories, so these controls provide traceable inputs, not byte-for-byte reproducibility from a source tag. The signed published image digest and its provenance/SBOM are the release artifacts to verify and deploy. Set the release coordinates on every build machine:
Check out that exact tag in a detached worktree or fresh clone:
Authenticate with a classic GitHub personal access token carrying write:packages. The login username is the personal account that owns the token, which may differ from the organization in IMAGE:

Build on native architecture hosts

Rust compilation for anydoc is very slow through QEMU. A cancelled cargo build step is not cached because its Docker layer never completes. For a faster multi-architecture release, build AMD64 on an x86-64 Linux host and ARM64 on an Apple Silicon or Linux ARM64 host, push temporary architecture tags, and combine them into the public manifest. On the x86-64 Linux host:
On the native ARM64 host, after checking out the same tag and logging into GHCR:
After both builds are present in GHCR, create only the versioned manifest:
Confirm that both platform manifests are available:
Build and verify the full image the same way under $IMAGE:$VERSION-full. Complete image signing, checksums, SBOMs, release artifacts, and the GitHub Release before changing a moving channel. Only then promote the stable versioned manifests:
Release candidates promote rc / rc-full. Never create a moving alias in the same command that first publishes the version manifest. The first successful native build seeds its architecture’s registry cache. Later local releases import those caches, while the release workflow imports both native caches for runtime layers and maintains its own GitHub Actions cache. Release images obtain AnyDoc from the standalone artifacts described above. For local source builds, the anydoc source commit and build instructions are pinned independently of the Suchi version, so the completed Rust layer is reused until that pin or recipe changes. Cargo registry and target cache mounts also retain incremental work in the local BuildKit builder when a compile is interrupted before its image layer can be exported. The full image uses --target full and temporary tags ending in -full-amd64 and -full-arm64. The first command-line push creates a private, unlinked GHCR package by default. In the package settings, make it public, connect it to the Suchi repository, and grant that repository Actions access so later workflow pushes can update it. Direct local publication does not reproduce the workflow’s keyless Cosign signatures or GitHub release artifacts; handle those separately before calling the release complete.

Automation

  • checks.yml runs the complete repository validation graph on pushes to main, pull requests and manual dispatch.
  • ci.yml runs formatting, tests, vet, lint, documentation release-metadata checks, UI checks and browser tests when called by it.
  • smoke.yml tests the built images and ingest paths when called by it. Both variants are rebuilt with the exported AnyDoc context and an unusable Rust base to catch accidental recompilation; binary and license bytes must match the export before the usual ingestion checks run.
  • restore-drill.yml validates backup restoration when called by it.
  • release.yml publishes only when manually dispatched from an existing signed annotated tag. Its preflight repeats the complete reusable validation graph, race tests, security scans, local smoke and benchmark gates, Docker checks and documentation validation. Only jobs that publish releases, packages or keyless signatures receive the corresponding write permissions.
The release workflow verifies the tag with the public key pinned at .github/release-signing-key.asc and the expected full fingerprint in hack/verify-release-tag.sh. It rejects missing, lightweight, unsigned, wrong-key, locally divergent and remotely divergent tags. Images wait for both binary matrices before publication and reuse the AnyDoc matrix’s verified binaries rather than rebuilding them. Release runs are serialized because moving image channels are shared mutable state. Validation-only tag runs execute the reusable checks and release preflight, then skip release notes, binary and image packaging, the artifact collector, GitHub Release creation, and channel promotion. Reusable checks may carry their short-lived SPA build between jobs; it is not a release artifact. For a security hotfix, branch from the affected tag, land the fix and changelog entry, publish the patch release, then merge the fix back into main.