A release-readiness check engine. Before you spend hosted CI minutes on a release, it runs the real checks locally, prints one pass/fail report (text or JSON), and writes a receipt that your dispatch step can require. It also keeps a budget ledger of the hosted runs you have already paid for, so "don't re-run a failed run just to retry" is a mechanism and not a promise. It works on any project; it is not Kubernetes and not Docker.
Names. The project is Seaworthy; the command is seaworthy and the Python import name is seaworthy. On the Python
Package Index the distribution is named seaworthy-release, because the plain name seaworthy belongs to an unrelated
package. Nothing is published to a package index yet; the build here is the zip from scripts/build-package, named
seaworthy-release-<version>.zip.
It is built for teams that ship desktop apps (signed, notarized, several platforms) on a small hosted-minutes budget, but nothing in the engine is specific to one project: a project is a profile, a JSON file of ordered checks.
- Real checks, not proxies. Git state, tool versions, workflow files, build-host reachability and GitHub reads run
for real. A check that cannot be sure says
unknown; a check that did not run saysnot run. Neither is ever a pass. - Platform checks for signing and publishing. Apple notarization credential and agreement, Developer ID identity and
expiry, Windows signing role and module visibility, repository secrets (names only) and draft release state, all read-only,
through the vendors' own tools. A check whose tool is missing on this machine says
cannot check here, never a pass (docs/checks.md). - Cheapest first. Free checks, then cheap-network checks, then heavy local ones. One run lists every cheap problem; heavy work is skipped once a required check has failed.
- A dry run that cannot spend.
--dry-runnever leaves the machine: network answers come from a fixtures file or the check is reportednot run. A dry run never writes a receipt into the state directory; with--receipt-out FILEit can write one to the file you name, and the gate never accepts it. - Standard library only. Python 3.8 or newer, one command, no build step, no daemon, no telemetry, no model call.
There is nothing to install from a package index yet, and the tool has no dependencies. Get the source (a clone of this
repository, or the zip from scripts/build-package) and run it in place: python3 bin/seaworthy on macOS and Linux,
bin\seaworthy.cmd on Windows. You need Python 3.8 or newer and git on your PATH.
To get a seaworthy command on your PATH, install the checkout into a virtual environment:
python3 -m venv .venv
.venv/bin/python -m pip install .
.venv/bin/seaworthy --version
- On Debian and Ubuntu,
python3 -m venvfails with "ensurepip is not available" until you install the distribution's venv package (sudo apt install python3-venv, orpython3.N-venvfor a specific version). Without it, runpython3 bin/seaworthydirectly. - Installing with
pipneeds Python 3.9 or newer (the build needssetuptools>=77). On Python 3.8, runpython3 bin/seaworthyin place. - A symlink to
bin/seaworthyon your PATH also works, without a virtual environment. - To uninstall, delete the checkout (or the virtual environment, or the symlink), and delete the state directory
(
$SEAWORTHY_STATE, else~/.local/state/seaworthyon Linux,~/Library/Application Support/seaworthyon macOS,%LOCALAPPDATA%\seaworthyon Windows), which holds the receipts and the ledger.
Below, seaworthy means python3 bin/seaworthy on macOS and Linux and bin\seaworthy.cmd on Windows (or the command
from the virtual environment above).
-
Watch it work on a synthetic project (nothing leaves your machine):
seaworthy demoIt builds a throwaway git project, runs a good release, then breaks one thing per check and shows which check catches it, with the exit code. Then it walks the receipt and the dispatch gate: refuse with no receipt, allow with a fresh one, refuse an edited receipt, refuse a dry-run receipt, refuse a re-run of a failed SHA, allow a named override.
-
Read a profile.
seaworthy checkslists the checks;seaworthy explain git.source_frozenshows one with its parameters. Which example fits you:examples/profiles/minimal.json: three free checks (source frozen, clean checkout, hosted budget). Start here for any project; it passes when your clean checkout is at the fetched tip oforigin/main. Add checks as your release needs them.examples/profiles/python-library.json: a pure-Python library that publishes from a tag: annotated tag on the frozen source, clean checkout, version inpyproject.tomlandCHANGELOG.md, bounded publish workflow, budget, and the project's own tests. docs/ci-receipt.md walks it through a CI job.examples/profiles/electron-desktop.json: a commented example with every check type, for a signed desktop app on several platforms. Expect a NO-GO on a project that is not one; delete the checks that do not apply.examples/profiles/platform/seaworthy.json: every Apple, Windows and GitHub release check once, with stand-in tools.
-
Dry-run it on your project. Copy an example, set
params,hostsandbudget, then:seaworthy validate my-profile.json seaworthy run my-profile.json --repo /path/to/checkout --sha <frozen-sha> --dry-runChecks that need the network are reported
not rununtil you give them recorded answers (--fixtures, see docs/fixtures.md). -
Run it for real, the day of the release, on the release machine:
seaworthy run my-profile.json --repo /path/to/checkout --sha <frozen-sha> --set version=1.4.0A
PASSwrites a receipt into your per-user state directory. Exit codes:0pass,1engine error,2bad profile or arguments,3no-go,4pass with warnings.Not every check applies at every stage of a release.
--stage publish(or"stage": "publish"in the profile) says which stage this run is for: a check that does not apply there is shown as not applicable, with whatever evidence it has, instead of failing for something the stage does not need. A check that applies still fails honestly, and a receipt records its stage, so apublishreceipt does not open the gate for another stage. -
Gate the paid step. Put one line in front of whatever starts the hosted run:
seaworthy gate my-profile.json --sha <frozen-sha> --set version=1.4.0 --planned linux:60 --planned windows:180 && ./start-hosted-releasegateexits 0 only with a fresh, intact, real-run PASS receipt for that SHA and profile, enough budget left, and no earlier hosted failure at that SHA that a re-run could not change. After the run, record it withseaworthy ledger addso the next gate sees it. See docs/receipt.md and docs/budget.md.
Seaworthy 0.1.0 profile demo sha 6bab4f76a426 DRY RUN
started 2026-10-09T02:35:42Z on <host> scope all
FREE CHECKS (6 ms)
PASS source-frozen HEAD = expected = last-fetched origin/main (6bab4f76)
FAIL clean-checkout 1 changed or untracked path(s)
- ?? stray.txt
would change: fix what the evidence names and re-run; a fail is never waivable
PASS version-and-notes version 1.2.3 and notes agree
PASS workflow-hygiene 2 workflow file(s) bounded
PASS hosted-budget 1.56 of 5.00 USD committed, 0 of 2 runs left after the plan
CHEAP NETWORK CHECKS (0 ms)
UNKNOWN tool-pins-builder node did not answer in time
would change: answer it by running where the tool or host is available; this check is not waivable in this profile
FAIL builder-reachable answers: timed out after 5s
would change: fix what the evidence names and re-run; a fail is never waivable
PASS runner-image-migration no open announcement names a label in use
VERDICT: NO-GO 8 pass, 2 fail, 1 unknown, 1 skip
blocking: clean-checkout, tool-pins-builder, builder-reachable
to waive: seaworthy run PROFILE --sha SHA --waive CHECK_ID [--waive CHECK_ID ...] --by NAME --reason TEXT
a waiver names each check, covers only this SHA (one release), is written in the report and the receipt, and applies only to an 'unknown' result of a check the profile marks waivable; a fail is never waivable
waivable among the blocking checks now: none
dry run: 0 command(s) left this machine, 5 answered from fixtures; a dry run never writes a receipt
exit code 3 (0 pass, 1 error, 2 bad spec, 3 no-go, 4 pass with warnings)
The "never writes a receipt" in the dry-run line means no receipt reaches the state directory. With --receipt-out FILE
a passing dry run writes a receipt to that file, marked dry-run, and the gate never accepts it.
--json prints the same report as seaworthy.report 1.0 (every check with status, evidence, details, timing and
cost tier, plus a gate object {value, as_of, note} that a status board can show as a release gate).
The --json report is the integration point: anything that wants a verdict (a status line, a card on a dashboard) reads
seaworthy.report 1.0 from seaworthy run --json and needs nothing else from this repository.
A check that comes back unknown (the tool or host could not answer) blocks the release. A person who has the answer by
other means can waive that one result, and only that one:
seaworthy run my-profile.json --sha <frozen-sha> --waive CHECK_ID --by NAME --reason "what you checked and how"
A waiver is a named, release-scoped, recorded acknowledgement, never a silent pass. It names each check (--waive is
repeated per check), it covers the one SHA being released, and it is written in the report and in the receipt with who made it and
why. It applies only to an unknown result of a check the profile marks waivable; a fail is never waivable, and
the check still shows as unknown, counted as waived. A NO-GO report prints this syntax once and lists, for every check
that is not a pass, what would change the verdict.
A skip is not a pass. A check that is not applicable at the stage, or that was skipped because its credential variables
are not set in this shell (apple.notary_credentials does this: "not checked", nothing sent to Apple), proved nothing and
blocks nothing. The check that requires the configuration to exist is the separate apple.notarization_configured; keep it
enabled in any profile that notarizes. A skip that has a next step is listed in what would change the verdict.
- Start, re-run, cancel or publish anything hosted. The engine refuses commands such as
gh workflow run,gh run rerun,gh release create,gh apiwith a write method, andgit push, in any check, however the profile spells them. - Run a shell string. Commands are argv arrays; nothing goes through
sh -coreval. - Sign, notarize, submit, create or publish anything: the platform checks only list, read and ask for versions.
- Read or print a secret. Checks receive environment variable names; output is scrubbed of credential patterns before it is stored or shown; tokens are read from files or the environment, never from argv.
- Call the network from a dry run, or let a dry run authorise spend.
- Write outside its own state directory (and the report file you ask for). It never changes the repository it inspects.
- Report a pass it did not earn: unreachable services, missing tools and unreadable usage are
unknownornot run.
Every claim below can be checked in the file named beside it.
- Measured, derived, unknown. A check's status is measured when a command, file or service answered (
pass,fail,warn), and unknown when it could not (unknown,not run;src/seaworthy/report.py, per-check rules in docs/checks.md). The verdict, the counts, the budget figures and the releasegateobject are derived from those rows (src/seaworthy/engine.py). Minutes in agateledger row are an upper bound from the plan's worst-case timeouts, labelled as such (docs/receipt.md). Spend is estimated, not billed (docs/budget.md). - Unknown and stale are shown, never zero or healthy. An unreachable service, a missing tool or an unreadable
usage endpoint is
unknownorcannot check here, never a pass (docs/checks.md, docs/budget.md). The report'sgatevalue ispass,failorunknown, never a default pass, and carries anas_oftime (src/seaworthy/engine.py). A receipt older thanreceipt.max_age_hours(default 12) stops counting, and so does one for another profile or checks hash (src/seaworthy/receipt.py, docs/receipt.md). A gate row with no qualifying receipt saysreceipt_unknown, not an age of 0. - What it cannot know. Whether a hosted run was started outside the wrapper (the ledger only knows what you record, docs/budget.md); that the real vendor services accept a release, because the platform checks have only been run against stand-in tools (examples/profiles/platform/PROOF.md); that a profile is complete, since it checks only what the profile lists; or who is at the keyboard, because the receipt hash and HMAC catch accident, not the owner (docs/receipt.md).
- What it never claims. A pass it did not earn (docs/safety.md: an unknown, not-run or
failed required check is a no-go); that a dry run authorised anything (a dry-run receipt never opens the gate,
src/seaworthy/receipt.py); that a refusal "saved" money (it reports minutes not dispatched, as an upper bound, not a saving). It makes no model call and sends no telemetry (docs/safety.md). - Advice, not action. Its built-in checks start, re-run, cancel and publish nothing hosted, and sign and submit
nothing; the refusal list is a backstop, not a sandbox, so
externalchecks a profile defines are your responsibility (docs/safety.md). The gate decides and exits 0 (go) or 3 (refused), or 2 for a bad profile or arguments, and your wrapper decides what happens next (docs/safety.md, docs/receipt.md).
| Platform | Python | Status |
|---|---|---|
| Ubuntu / WSL2 | 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14 | run: demo, smoke, self-audit (the platform proof on 3.11 and 3.14) |
| macOS (Apple Silicon) | 3.13 | run on a hosted macOS 15 image (2026-10-16): smoke, demo, self-audit. Python 3.8 is not in the matrix (no Apple Silicon build). Not run on a developer's own Mac |
| Windows 11 (native; NT 10.0.26300, PowerShell 5.1, Python 3.12.10) | 3.8+ | run: install, seaworthy demo, the quickstart and gate. python scripts\smoke runs from a git clone, not from the zip (the zip has no scripts/) |
| Windows Server 2022 (hosted image) | 3.8, 3.13 | run on a hosted windows-2022 image (2026-10-16): smoke, demo, self-audit |
The platform checks are proven on Linux against stand-in xcrun, security, spctl, gh and ssh programs
(examples/profiles/platform/PROOF.md); they have not yet been run against Apple's,
Azure's or GitHub's real services, on a real Mac, or on a real Windows signing machine. The first real run of each is its check.
Python 3.8 is the floor: no later standard-library API is used. scripts/smoke, seaworthy demo and scripts/self-audit
pass on Linux on Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13 and 3.14. The table says what was actually run; it will be
updated as the other rows are.
What is open today. The Honesty section says what the tool cannot know; this list is what has not been done yet.
- Not published to a package index; install from a clone or the zip from
scripts/build-package(Install). - The platform checks have only been run against stand-in tools, not Apple's, Azure's or GitHub's real services, a real Mac, or a real Windows signing machine (Supported platforms, PROOF.md).
- The workflow snippet in docs/ci-receipt.md has not run on hosted CI; the commands in it were run locally.
release.version_notesreads a JSON package file only (package.jsonshape); other formats such aspyproject.tomlneed anexternalcheck (python-library.json).- Hosted CI is manual-dispatch only and runs Python 3.8 and 3.13; macOS with Python 3.8 is not in the matrix, and 3.9 to 3.12 and 3.14 have been run on Linux only (ci.yml).
- Installing with
pipneeds Python 3.9 or newer; on 3.8, runpython3 bin/seaworthyin place (Install). apple.notary_agreementcannot see a pending agreement by itself; it passes only with a dated human confirmation (docs/checks.md).- Three vendor details (notarytool's JSON output and
@env:password reference, and the shape of GitHub's immutable-releases answer) come from documentation as read and have not been confirmed with a real call (CHANGELOG).
| What | Budget | Measured |
|---|---|---|
| The free checks of a typical profile | under 10 s | 6 checks, about 15 ms (git plus file reads) |
| One whole invocation, including interpreter start | about 60 ms | |
| Memory | under 50 MB resident | about 22 MB |
| Idle cost | none: no daemon, nothing runs between invocations | |
The gate row (one ledger line per gate decision) |
no new process, no network | about 0.02 ms and one write of about 0.6 KB per decision; a whole gate call is the same within noise (about 43 ms either way) |
- docs/profiles.md: the profile format, every field, scopes, waivers, platform slots.
- docs/checks.md: every built-in check, its parameters, what it proves and what it cannot.
- docs/receipt.md: the receipt, the dispatch gate, the retry rule, overrides, signing.
- docs/ci-receipt.md: running the preflight in a CI job, uploading the receipt, reading it and the exit codes.
- docs/budget.md: the budget ledger, rates, windows, carried spend, the usage cross-check.
- docs/fixtures.md: dry runs, fixture files, writing a known-bad case.
- docs/safety.md: the safety model: what is refused, what is written, exit codes.
- docs/troubleshooting.md: real messages and what to do about them.
examples/: neutral example profiles (minimal,python-library,electron-desktop) andexamples/profiles/platform/(every platform check once, with its stand-in-tool proof driver).- In the source repository:
scripts/self-audit(scan for secrets and names before a release),scripts/release-scan(the self-audit plus a scan of the whole git history; see CONTRIBUTING),scripts/build-package(build the distributable zip; refuses on any audit finding),scripts/smoke(the three-OS smoke),scripts/merged-check(the checks on the result of a merge, run right before merging; see CONTRIBUTING).
CONTRIBUTING.md (including packaging), SUPPORT.md, SECURITY.md and CODE_OF_CONDUCT.md.
MIT licensed, see LICENSE. Copyright (c) 2026 Moshik21.