`gitzone services clean`, `gitzone services prune` and the lifecycle rollback remove data owned by a container's user from inside a container. When that service's image was missing, which is the case whenever an earlier gitzone release wrote the data, they refused and left the deletion half-finished, so `gitzone services start` refused until the image was pulled by hand. removeDataDirectory now pulls the image through DockerContainer.ensureImagePresent. The legacy MongoDB migration's private pull helper is folded into the same method, and a migration test asserts both images are present before the legacy container is stopped.
@git.zone/cli 🚀
@git.zone/cli is the development workflow CLI behind the gitzone and gzone commands. It helps TypeScript-heavy teams keep projects tidy, create semantic source commits, manage local Docker-backed services, scaffold new modules, and release software through explicit, target-based release configuration.
It is opinionated where that saves time: source commits and releases are separate, changelog entries flow through a standard Pending section, project config lives in .smartconfig.json, and release targets make side effects visible before they happen.
Issue Reporting and Security
For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.
Install
pnpm add -g @git.zone/cli
After installation, both binaries point to the same CLI:
gitzone --help
gzone --help
The Big Idea
gitzone commit handles source history.
gitzone release handles release transactions.
That split is intentional. A commit should not unexpectedly publish npm packages, push Docker images, or trigger remote release pipelines. A release should clearly show which targets it will publish to.
Quick Start
# Preview project standardization work
gitzone format
# Apply formatting changes
gitzone format --write
# Create a semantic source commit
gitzone commit
# Preview the configured release transaction
gitzone release --plan
# Release pending changelog entries to configured targets
gitzone release
Commands
| Command | Purpose |
|---|---|
commit |
Analyze changes and create one semantic source commit |
release |
Turn pending changelog entries into a versioned release and publish targets |
format |
Plan or apply project formatting and standardization |
config |
Inspect, update, and migrate .smartconfig.json |
services |
Manage local MongoDB, ObjectStorage, and Elasticsearch containers |
tools |
Manage the global @git.zone toolchain |
template |
Scaffold projects from built-in templates |
meta |
Manage multi-repository workspaces |
open |
Open repository assets like CI pages |
docker |
Report and reclaim Docker resources created by git.zone tooling |
deprecate |
Deprecate npm packages across registries |
start |
Prepare an existing project for local work |
helpers |
Run small helper utilities |
Global flags include --help, --json, --plain, --agent, --no-interactive, and --no-check-updates.
Toolchain Management
gitzone tools replaces the former gtools command from @git.zone/tools. It manages globally installed @git.zone development tools through pnpm.
# Check installed @git.zone tools and update outdated packages
gitzone tools update
# Update without prompts
gitzone tools update -y
# Install missing managed @git.zone tools
gitzone tools install
The managed tools include @git.zone/tsrepo and @git.zone/rsrepo, not the retired single tools they replace (tsbuild, tstest, tsbundle, tswatch, tsdoc, tspublish, tsrust), which gitzone tools neither installs nor updates; a global rsrepo is the engine a Rust project's release finds on the PATH when its package.json does not declare one.
gitzone tools update checks @git.zone/cli first. If the CLI itself needs an update, it updates @git.zone/cli and asks you to rerun the command before updating the rest of the toolchain.
Commit Workflow
gitzone commit creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images.
# Interactive semantic commit
gitzone commit
# Read-only AI recommendation
gitzone commit recommend --json
# Auto-accept safe recommendations
gitzone commit -y
# Auto-accept a breaking recommendation explicitly
gitzone commit -y --allow-breaking
# Auto-accept, test, build, and push
gitzone commit -ytbp
# Show the resolved workflow without mutating anything
gitzone commit --plan
# Supply a message and changelog entry without AI
gitzone commit -y --message 'fix(cache): expire stale entries' --changelog 'Expired entries are removed before lookup.'
# Preserve a multiline message and custom Pending Markdown
gitzone commit -y --message-file /tmp/commit.txt --changelog-file /tmp/pending.md
The commit flow:
- Analyze the working tree.
- Suggest commit type, scope, and message.
- Write a human-readable entry into
changelog.mdunder## Pending. - Stage and create one semantic source commit.
- Optionally run formatting, tests, build, and push based on flags or config.
Commit flags:
| Flag | Meaning |
|---|---|
-y, --yes |
Auto-accept safe recommendations |
--allow-breaking |
Allow -y/--yes to accept BREAKING CHANGE recommendations |
-t, --test |
Add test step |
-b, --build |
Add build step |
-p, --push |
Push after the source commit |
-f, --format |
Run gitzone format --write before commit |
--plan |
Show resolved workflow only |
-m, --message <text> |
Use a complete semantic commit message without AI |
--message-file <path> |
Read the complete message from a UTF-8 file |
--changelog <text> |
Supply custom Pending text with a manual message |
--changelog-file <path> |
Read custom Pending text from a UTF-8 file |
Manual messages start with type(scope): description; the scope is optional.
Conventional ! subjects and BREAKING CHANGE: footers are supported. The entire
message, including its body, is passed literally to Git. File inputs preserve
multiline text; CRLF becomes LF and terminal newlines are normalized. Each input
is limited to 128 KiB. Use --message or --message-file once, and at most one
changelog option. Changelog overrides require a manual message.
Without a changelog override, GitZone derives the Pending entry from the semantic subject and body. Custom text becomes an entry in the message's semantic bucket. For complete Markdown, supply a Pending fragment such as:
### Fixes
- Remove expired entries before lookup.
- Preserve entries whose validity has not expired.
Fragments may contain ### Breaking Changes, ### Features, ### Fixes,
### Documentation, and ### Maintenance. Omit the document title, ## Pending,
and version headings. GitZone merges entries into the existing Pending buckets
and preserves previous entries and release history. Pending headings continue to
determine the release bump; a breaking message requires a Breaking Changes entry,
and either a breaking message or new breaking changelog text requires the usual
interactive confirmation or -y --allow-breaking.
Manual mode replaces the AI analysis step with manual. It keeps the configured
format, test, build, staging and push behavior. --plan validates and displays the
message and changelog without mutation or AI access. A clean repository remains
unchanged. Input is validated before workflow steps run; it never falls back to AI
after an input error. Place input files outside the repository if they should not
be included by the existing stage-all workflow.
-r is intentionally not part of commit anymore. Use gitzone release.
Release Workflow
gitzone release releases from main. Running it on another branch fails before release metadata or ref mutation unless the bare --merge flag is explicitly supplied, or the branch carries a release.line configuration that names it (see Maintenance lines).
--merge is intentionally strict: the source and existing main worktree must be clean, the source must be linearly rebased onto current remote main, and the Git target must push both the branch and tag. GitZone pins the single configured push URL, compare-and-swap fast-forwards local main, updates its verified worktree, and lease-pushes that exact source commit before creating release metadata. It rejects diverged or merged history, local replacement refs or grafts, stale worktrees, incomplete shallow history, changed destinations, and remote races. If the pre-release push fails, GitZone restores local main only when doing so cannot overwrite concurrent work. This plumbing-level integration intentionally does not run merge hooks or write ORIG_HEAD.
gitzone release performs the release core once, then publishes to configured targets. When Docker is active, its immutable candidate must qualify before any Git, npm, or OCI destination publication. An active Git target must then succeed and be verified before npm or OCI publication can start.
Before interactive confirmation or source mutation, a Docker release verifies the project-local tSDocker capabilities and validates the deterministic request against the source commit. After the release commit and configured build, GitZone validates the final request against the release commit immediately before atomically installing the schema-2 journal.
The release core is not configurable plumbing. It always follows the same professional release transaction:
- Verify the release branch, clean state, and worktree ownership of
main(or of the maintenance branch a release line names). A project that configurespackageFilesmust carry exactly thefileslist it generates (steppreflight.packageFiles, see Package contents). A release that builds or packs a commit with apackage.jsonalso proves here that the source commit carries apnpm-lock.yamlits checkout can install with--frozen-lockfile. An npm release then reads every registry's dist-tags for every package it publishes:latestnever moves downwards, and a line takeslatestonly while every higher major is a prerelease (steppreflight.npmDistTags, see A line that keepslatest). - Read
changelog.md## Pendingentries and infer or accept a semver bump, raised torelease.versionFloorwhen that is higher. - Run configured tests.
- With
--merge, fast-forward and lease-pushmainbefore release metadata is created. - Update the version through the project's version source (
package.json, or the Cargo workspace of aprojectType: "rust"project, see Rust workspaces and the cargo target) and, in TypeScript projects, the baked commit info. - Move pending changelog entries into the new version section.
- Create the local release commit and tag on
main, and record the release intent, so any later failure is resumable (see Resuming a release that failed before its journal). - Create a disposable detached checkout of that release commit, install its locked dependencies when the commit carries a
package.json(and runcargo fetch --lockedin a Rust project), and run the configured release build there. An active Docker target additionally builds the release worktree its qualification reads. - When npm is selected, prepare every named tspublish module outside the repository, then pack each module and the public root package once into an exact tarball from that checkout. Every tarball must then carry each entry point its packed
package.jsonnames (core.packNpmArtifact.entryPoints), and its estimated publish body must fit each registry'smaxPublishBytes(core.packNpmArtifact.publishSize, planned only when a limit is configured); the packed size and the body per registry are printed either way. See Package contents. - Atomically install a durable journal before final publication. A release that publishes only the root package to npm uses journal schema 1, one that also or only publishes tspublish modules uses schema 3, a Docker release uses schema 2 with or without modules, and a Gitea asset release uses schema 4, all in the same storage tree.
- For schema 2, run the project-local tSDocker qualification build and configured image tests, then durably record the candidate digest graph and complete ordered promotion set.
- Publish and verify Git, then every npm package at every registry, modules first and the root package last, then each Docker destination. Remove only the exact qualified candidate after every destination is verified, or after a terminal qualification destination conflict before Git/npm publication.
- Remove the test artifacts tstest wrote, so a machine does not accumulate test output release after release. This runs only after a successful publication, leaving the logs in place when a release fails. It runs the project's
node_modules/.bin/tsrepo test cleanwhen the project installs@git.zone/tsrepo, otherwise itsnode_modules/.bin/tstest clean(tstest 6.1.0 or later). It is skipped when the project has neither, and a project opts out with{"@git.zone/tsrepo": {"test": {"cleanup": {"onRelease": false}}}}, or the legacy{"@git.zone/tstest": {"cleanup": {"onRelease": false}}}, in.smartconfig.json; both at once is an error. See the tstest readme for what it removes; snapshots are kept.
The release builds and packs the release commit
A release publishes what its release commit contains. After the commit and tag
exist, GitZone creates a disposable detached worktree of that exact commit under
the system temporary directory, runs pnpm install --frozen-lockfile in it when
that commit carries a package.json, and runs the configured release build and
pnpm pack there. Gitea assets are frozen from the same checkout. The checkout
must still be the exact commit with a clean tree before and after packing, and it
is removed on success and on failure. Recovery (gitzone release recover) has
always worked this way; every release now does.
Consequences worth knowing before upgrading:
- The release build no longer refreshes
dist_*in your working folder for npm-only, Git-only, and Gitea-asset releases. Runpnpm buildyourself when you want fresh local output. - A build that depends on untracked or ignored local files now fails instead of publishing output nobody else can reproduce.
- Untracked files that match the
filesglobs can no longer reach the tarball. - Unreadable paths anywhere in the working folder, such as container-owned
service data under
.nogit/, no longer break packing. pnpm walks the whole project tree before applyingfiles, and the checkout does not contain that data. - A release commit that carries a
package.jsonmust install from its own committed lockfile. Preflight proves that before anything is created: the source commit has to carry a trackedpnpm-lock.yaml, andpnpm install --frozen-lockfile --lockfile-only— pnpm's no-install, no-write form of the same check — has to accept it. A lockfile that is missing, ignored, or outdated stops the release there, with no release commit, no tag, and nothing pushed. Commit the lockfile, or runpnpm installand commit it, then release again. The version bump only changesversion, which never changes a dependency specifier, so a lockfile accepted in preflight is still accepted in the release commit. - Pnpm commands use the scope carried by the release commit. A repository that
tracks its own root
pnpm-workspace.yamlretains that workspace's settings and member validation. Otherwise, lockfile verification and checkout installation use--ignore-workspace. For a committed root workspace, configured test and build commands clear inherited workspace overrides and let each pnpm process discover the workspace from its own working directory. Independent fixture projects can therefore install their own dependencies, including their own workspace members. Without a committed root workspace, configured commands retain the repository directory as a forced pnpm scope; independent fixture installs are not supported in that mode. Both modes exclude an unrelated enclosing workspace from release lockfile validation and installation. - The release commit must carry a
package.json: it holds the version gitzone releases. A commit without one is refused before anything is created. - The checkout holds only committed files, so a gitignored project-level
.npmrcis not in it. A project that authenticates private dependencies through such a file now fails its checkout install after the local commit and tag exist. Authenticate at user level instead:pnpm npm login --registry=<registry>— whatgitzone config doctorsuggests as well — writes~/.npmrc, and the checkout reads that. - An install that fails in the checkout for an environmental reason — an
unreachable registry, a missing credential, an unusable store — stops the
release with the local release commit and tag created, nothing pushed, and no
release journal written. The release recorded its intent, so fix the
environment and run
gitzone release resume <version> -y: it prepares and publishes that exact tagged commit without moving any ref. A tag created by GitZone 7.2.6 or earlier has no recorded intent; a release with a Git target plus an npm and/or Docker target continues withgitzone release recover <version>, and other configurations of that age delete the tag and drop the release commit (git tag -d v<version>andgit reset --hard HEAD^onmain) and release again. Confirm first thatHEADis the release commit —git log -1 --format=%sprintsv<version>— so that a commit made on top of it is never dropped. - The
--planstep list names the isolated work:preflight.lockfilefor a project with apackage.json, thencore.releaseCheckout,core.releaseCheckout.build, thencore.packNpmArtifactand its checkscore.packNpmArtifact.entryPointsand, with a configured limit,core.packNpmArtifact.publishSize.
An active docker target additionally runs the build in the release worktree,
as before, and the plan shows that step as core.build. tSDocker binds
qualification to the project directory: the journaled request carries that cwd
and a project identity hash derived from it, so the image is still built from the
working folder and untracked files there can still reach an image. Moving Docker
qualification to the release commit belongs to @git.zone/tsdocker and is not
part of this release.
Targets decide what happens after that:
| Target | What it does |
|---|---|
git |
Atomically pushes the exact release commit to main (or to the maintenance branch of a release line) and the new tag, often triggering remote CI release builds |
npm |
Publishes the same journaled tarball to every configured registry; verifies its bytes anonymously, or its integrity record with the registry's configured authentication for private packages |
docker |
Qualifies immutable OCI digest graphs with project-local tSDocker, promotes each journaled destination under single-writer alias fencing, verifies exact digests, then cleans the candidate; a terminal qualification conflict permits cleanup only |
# Preview the resolved release plan
gitzone release --plan
# Build and pack HEAD like a release, without creating or publishing anything
gitzone release check
# Release to configured targets
gitzone release
# From a clean feature branch already rebased onto current main
gitzone release --merge
# Release only to npm
gitzone release --target npm
# Remove npm and Docker from the resolved targets; Git remains only if configured
gitzone release --no-publish
# Override inferred semver level
gitzone release --minor
# Run the release in its own session and return once it has started
gitzone release -y --detach
Release flags:
| Flag | Meaning |
|---|---|
-y, --yes |
Run without interactive confirmation |
-t, --test |
Enable preflight tests |
-b, --build |
Enable the release build after local release metadata is created |
-p, --push |
Explicitly select the git target; combine with other target flags as needed |
--target <csv> |
Select git, giteaAssets, npm, cargo, and/or docker; npm cannot omit a configured enabled Gitea asset target |
--npm |
Explicitly select the npm target; combine with other target flags as needed |
--docker |
Explicitly select digest-qualified Docker publication; requires project-local @git.zone/tsdocker >= 3.5.1 with protocol v1 |
--no-publish |
Remove Gitea assets, npm and Docker from the resolved target set without implicitly enabling Git |
--no-build |
Disable the separate post-metadata project build; Docker qualification still performs its required image build |
--merge |
Fast-forward and lease-push a cleanly rebased feature branch into main, then release from main; incompatible with Docker or Gitea assets |
--major, --minor, --patch |
Override inferred semver level; release.versionFloor still applies |
--plan |
Show the resolved workflow without fetching or mutating refs, files, the index, or worktrees |
check |
Build and pack HEAD of a clean tree in a disposable checkout, print the artifacts and every problem, and remove everything; see Checking a release without publishing |
inspect [version] |
Read one or all durable release journals; add --json for machine-readable output |
resume <version> |
Resume journaled Git, npm, qualification, promotion, or cleanup work; fresh-release overrides are rejected, and --json remains inspect-only |
--recover-attempt <id> |
Recover the exact 32-character lowercase hexadecimal attempt.id shown by inspect --json, after independently proving its publisher stopped |
--detach |
With -y, run a release or resume in its own session after the foreground checks; see Running a release detached |
Checking a release without publishing
gitzone release check runs the build and pack half of a release on HEAD and
creates nothing:
gitzone release check
gitzone release check --json
gitzone release check --target cargo --no-build
It needs a clean working tree, because a release packs only committed files,
and a version source the release accepts: a project that gitzone release --plan refuses for its version source (a Cargo.toml without
projectType: "rust", a Rust workspace outside the repository root,
independent crate versions) is reported with the plan's message before
anything is checked out. It checks out HEAD into a disposable detached worktree exactly as a release
checks out its release commit (pnpm install --frozen-lockfile when the commit
carries a package.json, cargo fetch --locked in a Rust project), runs the
configured release build there, and then, for the active targets:
- npm: packs every named tspublish module and the public root package with
the qualified pnpm, runs the release's tarball checks (entry points and the
configured
maxPublishBytes), and lists every packed file. - cargo: reads the workspace plan through
rsrepo engine plan, prints the same plan lines asgitzone release --planwith every problem a release would refuse, and packs the crates the release would publish throughrsrepo engine package, listing each crate's files and sha256. It probes no registry.
Docker and Gitea assets are named as not checked: tSDocker qualifies images
and the asset manifest is frozen only during a release. The checkout and every
artifact are removed afterwards, also after a failure. The check never changes
the version, commits, tags, writes a journal or publishes, and it takes no
--plan, --merge or --detach. Targets and the build resolve as for
gitzone release --plan, so --target, --cargo, --npm and --no-build
apply. It exits 1 when it finds a problem, such as a dirty tree, a failed
install or build, a tarball check, a blocking crate problem or a failed
package. --json prints the report (ok, sourceOid, targets, build,
npm.packages, cargo.order, cargo.crates, notChecked, problems).
Version floor
release.versionFloor sets a minimum for the next release version. Use it when a repository must join a common version line, for example when every serve.zone package moves to major 32:
{
"@git.zone/cli": {
"release": {
"versionFloor": "32.0.0"
}
}
}
GitZone first infers the version as usual. It starts from the current package.json version and applies the level from the Pending changelog or from --major, --minor or --patch. The release version is then the higher of that inferred version and the floor. The floor is committed configuration, so the plain gitzone release -y honours it without a flag, and nobody has to hand-edit package.json to a version that was never published.
gitzone release --plan states the outcome:
| Plan line | Meaning |
|---|---|
version floor: 32.0.0 applied (inferred 3.6.0 -> release 32.0.0) |
The floor is higher than the inferred version and becomes the release version |
version floor: 3.5.4 satisfied by inferred 3.6.0 |
The inferred version already meets the floor; nothing changes |
version floor: 32.0.0 inert (current 32.0.0 already meets it); remove release.versionFloor |
The project has reached the floor; delete the setting |
version notice: the floor raises the major version above inferred 3.6.0, and Pending ... |
The floor raised the major version above the inferred version and Pending has no ### Breaking Changes section; the line is informational and stays until such a section exists |
A floor does not require a ### Breaking Changes Pending section. --major never required one, and the floor is the same kind of explicit maintainer decision, reviewed in a commit. Aligning a version line is often not an API break, so requiring the section would force a false changelog entry. A major release is also the safe direction for consumers, because ^3 ranges never resolve to 32.0.0. The plan notice only makes that jump visible and never blocks the release. It appears only when the floor itself raises the major version; a major that --major produces prints no notice.
When the floor decides the version, the release journal records that decision with the release identity:
"release": {
"version": "32.0.0",
"tag": "v32.0.0",
"mainOid": "...",
"tagOid": "...",
"versionFloor": { "floor": "32.0.0", "inferredVersion": "3.6.0" }
}
gitzone release inspect and gitzone release resume print the same version floor: ... applied line. The record is part of the journal's immutable identity. resume always publishes the journaled version and never resolves a new one. This matters because after the release commit the floor is inert, and a fresh resolution would infer 32.0.1. recover takes its version from the existing tag and writes no floor record, because the failed run did not record its level override.
The floor must be a canonical x.y.z string whose components do not exceed 9007199254740991, except for a matching prerelease channel. Every rejection throws ReleaseVersionError, and its code names the rule that failed. malformed-version-floor and unsupported-prerelease-floor are configuration errors that --plan and gitzone config doctor report. --plan and the release raise noncanonical-version when the current package.json version is not canonical. current-version-changed is raised only while the release runs:
| Code | Cause |
|---|---|
malformed-version-floor |
Anything else, such as "32", "v32.0.0", null, a number, or a component above 9007199254740991 |
unsupported-prerelease-floor |
A prerelease or build suffix without a matching configured prerelease channel |
invalid-prerelease-config |
A malformed channel, mismatched or missing prerelease floor, forbidden dist-tag or version-level override |
noncanonical-version |
A floor is configured but the current package.json version is not a canonical x.y.z version to compare against |
current-version-changed |
The release checkout's package.json version differs from the version the release was resolved from |
gitzone config show lists the configured floor.
Prerelease channels on main
A committed prerelease channel publishes a new version, including a newer major,
from integrated main under a non-latest npm dist-tag. For example, to release
version 6 while stable consumers remain on version 5:
{
"@git.zone/cli": {
"release": {
"versionFloor": "6.0.0-beta.0",
"prerelease": { "identifier": "beta", "distTag": "beta" }
}
}
}
Use gitzone release --plan, then gitzone release -y. Starting from 5.1.2,
this releases 6.0.0-beta.0; subsequent releases become 6.0.0-beta.1,
6.0.0-beta.2, and so on, regardless of the Pending changelog's semver level.
--major, --minor, and --patch are rejected while the channel is configured.
Raising the floor selects a higher sequence or a newer version core; a floor
cannot lower the current core. Components and the sequence must be safe integers.
The plan and gitzone config doctor validate the channel and its floor.
Keep both settings throughout the beta. The required matching
x.y.z-identifier.N floor makes earlier GitZone releases, including 8.8.2,
reject the release before creating metadata instead of ignoring an unknown
channel and publishing latest. Identifiers and dist-tags use lowercase letters
and digits separated by hyphens and start with a letter. Dist-tags cannot be
latest or a semver range. Prerelease channels and maintenance lines are
mutually exclusive, and prereleases release main in place without --merge.
Every selected registry must already have a stable latest below the prerelease
core. The channel's tag must be absent or point to an equal or earlier prerelease
of the same identifier. Publication always passes --tag=beta in this example;
there is no flag that overrides the committed channel. Verification proves the
exact artifact and the channel tag (or a later sequence of the same core and
identifier), and rejects a prerelease taking latest. The journal records the
stable latest found before publication. A registry conflict stops the release;
GitZone never repairs dist-tags automatically.
The identifier, dist-tag, and configured floor are part of the immutable release
intent and journal. resume refuses changed channel configuration and resumes
the recorded version. Accepted uploads are verified without publishing again.
Use gitzone release resume 6.0.0-beta.0 -y after an interruption; unjournaled
prerelease recover is rejected because it cannot prove the original channel.
Git and npm targets are supported. Docker and Gitea asset targets are explicitly
rejected in --plan and before any release side effects; disable those targets
before configuring a prerelease channel.
To promote 6.0.0-beta.N to 6.0.0, remove both release.prerelease and its
prerelease release.versionFloor in a reviewed commit, add the Pending release
entry, and run the normal release flow. That explicit removal promotes the exact
version core, without a semver-level flag. Leaving the channel configured always
produces another prerelease.
Maintenance lines
A maintenance line releases an older major from its own branch under its own
npm dist-tag, so that latest never moves. While main has published the
next major only as a prerelease, a line may instead keep latest (see
A line that keeps latest). It is opted into
per branch: the maintenance branch commits a release.line block in its
.smartconfig.json, and that block makes that branch, and only that branch,
a release source. main never carries the block.
Worked example: @push.rocks/smartserve publishes 8.x as latest, and its 4.x
line lives on the branch v4-maintenance under the dist-tag v4-lts
(4.4.0). A security fix is released as 4.4.1 like this. On v4-maintenance,
.smartconfig.json carries:
{
"@git.zone/cli": {
"release": {
"line": { "branch": "v4-maintenance", "distTag": "v4-lts" },
"targets": {
"git": { "enabled": true, "remote": "origin", "pushBranch": true, "pushTags": true },
"npm": {
"enabled": true,
"registries": ["https://verdaccio.lossless.digital", "https://registry.npmjs.org"],
"accessLevel": "public"
}
}
}
}
}
Commit the fix with gitzone commit on v4-maintenance; the branch keeps its
own changelog.md, whose ## Pending section feeds the release exactly as on
main. Then, still on v4-maintenance:
gitzone release --plan
# source branch: v4-maintenance
# release line: v4-maintenance -> npm dist-tag v4-lts (major 4; latest is never moved)
# npm dist-tag: v4-lts
# npm dist-tags of @push.rocks/smartserve at https://registry.npmjs.org: v4-lts 4.4.0 -> 4.4.1, latest 8.2.0 unchanged
gitzone release -y
The release commits v4.4.1 on v4-maintenance, tags it, pushes exactly
refs/heads/v4-maintenance and refs/tags/v4.4.1 with a lease on
v4-maintenance, and publishes each package with pnpm publish <tarball> --tag=v4-lts. main is neither read nor written.
The rules:
release.linetakes exactlybranchanddistTag. The branch is a canonical branch name other thanmain. The dist-tag islatest(see A line that keepslatest) or a tag of the line's own: lowercase letters and digits joined by hyphens that start with a letter and are never a semver range such asv4orx, which npm refuses as a tag.- The release must run on the branch the line names. On any other branch,
mainincluded, the release stops before anything is created. A branch withoutrelease.linestill releases onlymain. - A line publishes to Git, npm and Docker. Docker images carry only tags bound
to the release version (see Docker images on a line).
Gitea marks its newest release as the latest one, so a line with an enabled
giteaAssetstarget is refused, and so are--mergeandgitzone release recover. A line release records its intent, sogitzone release resume <version> -ycontinues it. - A line keeps its major. A Pending
### Breaking Changessection,--majoror arelease.versionFloorthat changes the major stops the release. - Before anything is created, and again right before each upload, every
package must, at every registry, already have a canonical
latestwith a higher major than the release, and the line's own tag must be absent or below the release version. Otherwise the release stops; a refusal right before an upload is recorded as the terminaldist-tag-conflictand nothing is published. - After the upload the line's tag must name the version, or a later version
of the same major, and
latestmust still carry a higher major. A line whose version tooklatestis a terminaldist-tag-conflict. gitzone never writes dist-tags: the error names the exact command that restoreslatest,pnpm dist-tag add <package>@<previous latest> latest --registry=<registry>, where the previouslatestis the version the release recorded when it claimed the upload. release.targets.npm.alreadyPublishedapplies unchanged: identical bytes already published, with the line's tag on them andlateststill a higher major, verify without an upload undersuccessand stop undererror. Identical bytes without the line's tag stay unverified; gitzone does not add the tag.- A line whose
distTagislatestreplaces the three dist-tag rules above with its own, described in the next section. - Private packages follow the same rules. Their dist-tags are read with the
registry's authentication through
pnpm view <package> dist-tags --json, which every release path (fresh,resumeandrecover) qualifies frompnpm view --helpin both pnpm command dialects before it authenticates, reads or publishes anything.
A line that keeps latest
While main publishes the next major only as a prerelease (see
Prerelease channels on main), latest still
belongs to the current major, and fixes for it come from its maintenance
branch. Such a line names latest as its dist-tag. Continuing the prerelease
example: main has released 6.0.0-beta.0 under beta, latest is
5.1.2, and release/5.x branched from v5.1.2 carries:
{
"@git.zone/cli": {
"release": {
"line": { "branch": "release/5.x", "distTag": "latest" }
}
}
}
The version comes from the branch's own changelog.md Pending section, as
on every line; a ### Features entry releases 5.2.0:
gitzone release --plan
# source branch: release/5.x
# release line: release/5.x -> npm dist-tag latest (major 5; latest moves only while every higher major is a prerelease)
# npm dist-tag: latest
# npm dist-tags of @example/package at https://registry.npmjs.org: latest 5.1.2 -> 5.2.0, beta 6.0.0-beta.0 unchanged
gitzone release -y
Before anything is created, and again right before each upload, every package must, at every registry, show exactly that state:
- no dist-tag carries a stable version of a higher major;
latestis a canonical version of the line's major, not above the release;- some dist-tag carries a prerelease of a higher major.
Otherwise the release stops like any line: before anything is committed or
tagged, or right before an upload with the terminal dist-tag-conflict and
nothing published. Publication passes --tag=latest, and verification
treats latest as it does for main: it names the version or a later one.
The same holds for release.targets.npm.alreadyPublished: identical bytes
already published with latest on them verify without an upload.
The line records no previousLatest, because it moves latest itself. Every
other line rule applies unchanged: the branch, the major, Docker images only
under version-bound tags (the Docker latest tag never moves), no Gitea
assets, no --merge and no recover.
Once main promotes the higher major, for example to 6.0.0, the line
stops in preflight because latest carries 6.0.0. Commit a dist-tag of its
own on the branch, such as "distTag": "v5-lts", and the line continues
under the usual rules.
Docker images on a line
A line may enable the docker target, but its images never move a tag that
another release owns, such as latest, a literal like stable or a major tag
like 4. gitzone does not decide this itself: it asks the project-local
tSDocker for its release-version alias policy (tSDocker 3.11.0 or later,
capability release-version-alias-policy), which admits a tag only when its
template contains ##version##. Name the Dockerfiles Dockerfile_##version##
(or Dockerfile_##version##-alpine and the like) and keep every
@git.zone/tsdocker.additionalTags entry bound to ##version##, such as
v##version##.
gitzone release --plan asks tSDocker (release-validate, which builds
nothing and contacts no registry) for the exact tags each selected Dockerfile
will publish:
gitzone release --plan
# release line: maintenance/32.2 -> npm dist-tag corestore32 (major 32; latest is never moved)
# targets: git, npm, docker
# docker alias policy: release-version (every tag is bound to 32.2.4; floating tags such as latest are refused)
# docker image Dockerfile_##version##: 32.2.4
A Dockerfile or additional tag that would publish a floating tag stops the plan and the release before anything is committed or tagged, naming the Dockerfile or tag and the tag it would publish:
Release line corestore32 publishes Docker images only under tags bound to 32.2.4, and tSDocker refused the release (INVALID_RELEASE_CONFIG): Alias policy release-version refuses this release: Dockerfile publishes the tag "latest", which is not bound to the release version; name the Dockerfile with ##version##, such as Dockerfile_##version##.
The policy is part of the journaled qualification request, so tSDocker
enforces it again when it qualifies the built images, and resume continues
under the same policy. A project-local tSDocker without the capability stops
a line with Docker before anything is created; main releases keep every
configured tag, latest included, and do not need the capability.
latest only moves upwards
The same check guards every main release: when a registry's latest is a
canonical version above the release version, the release stops before
anything is committed, tagged or published, and names release.line as the
way to release an older major. A release that finds latest moved above it
right before an upload records a terminal dist-tag-conflict and publishes
nothing. A prerelease or non-semver latest and a package the registry does
not know yet do not stop a main release.
A registry whose dist-tags cannot be read (an outage, a timeout, an
unexpected answer) now stops a main release in preflight as well, before
the release commit. This is intended: an unknown latest could be above the
release, so publishing would risk a downgrade. Release again once the
registry answers.
Journal compatibility
A line release journals release.line (branch and distTag) in the
release identity and its intent, and its dist-tag as npm.tag. Each npm
destination it claims records previousLatest, the latest version found
when the upload was claimed. git.expectedRemoteMainOid keeps its name and
holds the leased commit of the release branch, main or the line's branch.
Journals of main releases are byte-identical to before. GitZone 8.3.x and
earlier refuse to read a line journal or intent, so they can neither resume a
line release onto main nor publish it under latest. A line release with
Docker writes a schema 2 journal whose Docker request carries
options.aliasPolicy: "release-version"; a schema 2 journal is accepted only
when that policy is present exactly for a line. GitZone 8.7.x and earlier
refuse to read such a journal or its intent. A line that keeps latest
journals distTag: "latest" and npm.tag: "latest" and never records
previousLatest. GitZone 8.9.x and earlier refuse to read its journal or
intent, and GitZone 8.4.0 to 8.9.x refuse its configuration as well.
Exact artifacts and release journals
Journal schema 1 supports Git and public or private npm publication. Schema 2 adds Docker qualification, promotion, and cleanup evidence without changing the storage path or schema-1 parsing and resume behavior. Fresh and resumed npm publication dynamically qualifies the active stable pnpm version; there is no version allowlist. GitZone reads minimum-release-age and minimum-release-age-exclude through that exact pnpm invocation in the release directory, so project configuration and global policy retain pnpm's normal precedence. It verifies the selected version's publication time through the configured registry unless the effective policy disables the age delay or excludes that version. Missing timestamps follow minimum-release-age-ignore-missing-time, including pnpm's default of true; malformed policy, metadata failures, immature versions, and prerelease versions stop qualification. GitZone also verifies the required pack/publish command contract and keeps every subsequent command bound to the qualified exact version. A new pnpm version with the same capabilities needs no GitZone update once it satisfies the policy. Registries must be unique canonical credential-free HTTP(S) URLs. release.targets.npm.accessLevel is public (the default) or private; see Private npm packages.
GitZone keeps the selected pnpm version for help checks, packing, and publication
through pnpm with <version>, including release recovery and resume. This avoids
mixing a launcher's help output with a project-selected package-manager version.
Use a standalone pnpm installation; Corepack launchers cannot run pnpm with.
The normal release-age policy also applies when pnpm provisions that version.
The pnpm release tests cover known command dialects and dynamic version qualification
and qualify actual packaging and script-free tarball publication with the active
version. Run testUnit with each maturity-eligible standalone version and a matching
project pin in an isolated checkout. For a focused matrix, invoke the project-local
tsrepo test on test/test.pnpmrelease.node.ts from a disposable directory whose
package.json pins that row. Do not wrap these invocations in another pnpm with:
pnpm rejects nested use. Keep the normal release-age policy for every row.
For a repository's first release, run from clean local main against an empty
remote. GitZone verifies that no remote refs exist, records Git's zero object ID
as the expected absent main, and atomically creates main and the release tag
with an exact absent-ref lease. Concurrent creation of main rejects the push.
A nonempty remote missing main, an unreachable remote, and first-release
--merge are rejected. The normal release journal retains this initial identity
for inspect/resume if publication is interrupted.
A selected Git target requires a canonical remote name and exactly one resolved push URL. Before any remote contact, including during --plan, GitZone rejects HTTP(S) usernames or passwords, embedded passwords, query strings, fragments, and unsupported protocols. Canonical local paths and SSH destinations may retain their required SSH username.
Every release that reaches final publication stores journal.json under the repository's Git common directory. A release that publishes the root package additionally runs pnpm pack once and stores the resulting package.tgz beside it; every tspublish module is stored as package.<sha256-of-package-name>.tgz. The v1 path names the storage format and contains all supported journal schemas:
<git-common-dir>/gitzone/releases/v1/v<version>/package.tgz
<git-common-dir>/gitzone/releases/v1/v<version>/journal.json
Frozen Gitea assets are stored beside them as asset.<sha256-of-asset-name>.bin.
Together, the tarballs and assets are a record's payload.
After a fresh release, recovery or resume completes, GitZone keeps the payload
of the newest three completed local version records by journal creation time
and deletes the payload of older completed records. Journals and every other
file of a record stay, so gitzone release inspect still lists every version.
Set @git.zone/cli.release.recordRetention in .smartconfig.json to a positive
integer to change this limit, for example "release": { "recordRetention": 5 }.
The current run's record is always retained, even when resuming an older
version. Incomplete records (including accepted uploads awaiting verification),
release intents, temporary preparation directories and records locked by an
active publication or resume are retained in addition to the configured count.
A malformed or missing journal stops cleanup before deletion; stale record locks
require the same manual owner check as other release locks. Retention affects
local records only; it leaves Git tags and published releases unchanged. A
pruned completed version has nothing left to resume: gitzone release resume
reports that its artifacts were removed.
Pruning on demand
A repository that is not released again keeps its records until the next
release completes. gitzone release prune applies the same retention at any
time, through the same implementation:
gitzone release prune # report only
gitzone release prune --keep 1 # report against another retention count
gitzone release prune --apply # delete the reported payloads
gitzone release prune --json # machine-readable report
It reads release.recordRetention (default 3) unless --keep <n> overrides it,
and deletes nothing without --apply. It refuses to run, with --apply or
without, while the release store shows a release at work: a journal, record,
creation or recovery-preparation lock (v<version>.lock,
v<version>.record.lock, v<version>.create.lock,
v<version>.prepare.lock), the detached-run lock (detached.lock) or a
temporary preparation directory (.v<version>.tmp-*). The error lists those
paths; remove a stale one only after checking that no release process is left.
An attached release has no such state before it starts preparing its
artifacts; prune never touches a record that release later creates.
--json prints one object, also on failure ({ "ok": false, "error": "..." }
with exit code 1):
{
"ok": true,
"dryRun": true,
"keep": 3,
"root": "/repo/.git/gitzone/releases/v1",
"records": [
{
"version": "35.1.9",
"state": "completed",
"createdAt": "2026-10-12T17:49:20.443Z",
"completedAt": "2026-10-12T18:32:01.907Z",
"bytes": 2125164346,
"payloadBytes": 2125156896,
"payloadFiles": 12,
"action": "kept",
"reason": "newest-completed"
}
],
"totals": {
"records": 47,
"kept": 4,
"pruned": 43,
"blocked": 0,
"bytes": 83701754868,
"wouldFreeBytes": 73850194634,
"freedBytes": 0
}
}
Records are ordered newest first by journal creation time. state is
completed, incomplete or invalid (the journal is missing or malformed;
createdAt and completedAt are then null). bytes counts every regular
file of the record directory, payloadBytes and payloadFiles its tarballs
and assets. action and reason are:
| action | reason | meaning |
|---|---|---|
kept |
newest-completed |
within the newest keep completed records |
kept |
incomplete |
not complete; resume needs its payload |
pruned |
older-completed |
payload removed by --apply; a dry run reports it as removable |
pruned |
payload-absent |
beyond retention with no payload left; nothing to free |
blocked |
intent-present |
a release intent for this version exists |
blocked |
record-in-use |
a publication or resume holds the record lock |
blocked |
invalid-journal |
detail names the error; --apply stops before any deletion |
totals.wouldFreeBytes sums the payloads of pruned records;
totals.freedBytes is the same number after --apply and 0 in a dry run.
The canonical journal binds the release commit, annotated tag object, hashed Git destination, npm registries, per-target attempt states, and, when npm is selected, the package identity, tarball size, SHA-1, SHA-256, and SHA-512 integrity. Schema 2 also binds the exact tSDocker request, candidate identity, qualification result, ordered destination promotions, probe or promotion evidence, and terminal cleanup result. Journal updates use revision compare-and-swap under an interprocess lock. Release identity and destinations cannot change after journal creation. Qualification and promotion evidence is append-only, destination conflicts are terminal, and completed Docker state cannot regress. Inspect and resume reject malformed, future, noncanonical, duplicate-key, or merely reformatted journal JSON rather than repairing or normalizing it.
Packing must leave the release tree clean. Each registry receives that exact stored tarball through pnpm publish <tarball> --tag=<dist-tag> --ignore-scripts, so publish lifecycle scripts do not run. For a public package, success is recorded only after anonymous no-redirect probes verify version metadata, SHA-1 and integrity fields, the downloaded tarball bytes, and the release's dist-tag: latest, or the tag of a maintenance line. Each metadata request is bounded at 10 s; the tarball download, sized by the packed artifact, runs as long as bytes keep arriving, aborting after 30 s without data or once 10 s plus the time its size needs at 256 KiB/s have passed, so large tarballs with native binaries verify while a stalled registry still ends the probe as retryable. The dist-tag is satisfied by the released version itself and by any later release that has since carried the tag on (for a line tag, a later version of the same major), so superseded releases stay verifiable; an older version, a prerelease, a non-semver value or an absent tag does not. A matching pre-existing version follows release.targets.npm.alreadyPublished; conflicting bytes always stop the release, while transient responses and dist-tag propagation remain retryable.
Private npm packages
With release.targets.npm.accessLevel: "private" the journal records
npm.access: "private", each registry receives the stored tarball through
pnpm publish <tarball> --access=restricted --tag=<dist-tag> --ignore-scripts, and verification
runs through the same qualified pnpm with the registry's configured
authentication instead of anonymously. GitZone never reads, parses, logs or
journals a credential: pnpm reads its own .npmrc for the destination, exactly
as it does when it publishes, so authenticate at user level with pnpm login --registry=<registry>.
- Before anything is committed, tagged or published — and again on
resumeandrecover—pnpm whoami --registry=<registry>must succeed for every private destination. Otherwise the release stops withNpmRegistryAuthenticationError, which names the registry, the login step and the command to run next, and quotes the redactedpnpm whoamifailure. - A destination is
verifiedwhenpnpm view <name>@<version> --jsonreports the packed artifact'sdist.integrityanddist.shasum, adist.tarballon the registry's own origin without credentials, query or fragment, and the same dist-tag rules as public releases. pnpm has no command that downloads a tarball with the registry's authentication, so the bytes are proven by the registry's integrity record rather than downloaded. - A not-found answer counts as absence only when
pnpm whoamiconfirms the authentication, because a registry answers an unauthenticated read of a private package with 401, 403 or 404 alike.whoamiproves you are logged in, not that your account may read that package: a registry that answers 404 to a logged-in account without read access still looks like absence, so the release credential must be able to read the packages it publishes. Recovery checks a private version unpublished the same way. - An authentication failure found by the checks before and after publishing
changes nothing in the journal: a pending target stays pending, and an
accepted upload stays accepted and is never sent again. A credential revoked
between that check and
pnpm publishmakes the publish command fail, and the target is recorded as failed like any rejected publish. Fix the credential and rungitzone release resume <version> -y. - A scope registry in an
.npmrc(@scope:registry=…) overrides--registryfor that scope's packages, so private publish and view commands also pass--config.@scope:registry=<registry>and always address the journaled destination.
Public releases are unchanged.
Accepted npm uploads
A registry accepts an upload before it serves it, and npmjs can need minutes
or more than an hour to make the version readable. GitZone records that state
as accepted: the upload was taken, its bytes are proven locally, and only an
exact probe (anonymous, or authenticated for a private package) promotes it to verified. accepted is not failed.
failed means the registry rejected the submission or the publish command
itself failed.
Journals written before the accepted state recorded the same upload as
failed with verification-inconclusive, because every earlier writer chose
that cause exactly when the publish command succeeded. Loading such a journal
adopts those registries as accepted, so their uploads are verified instead of
sent again; a registry that failed for any other cause, or that never claimed a
publication attempt, keeps its recorded failure. Loading leaves the stored bytes
untouched, and the next journal transaction writes the adopted state.
After submitting, GitZone waits release.targets.npm.verificationWindowMinutes
(default 15) for the version to become readable, probing with doubling backoff
from two seconds up to twenty. If the window ends without a readable version,
the run reports the accepted target, exits successfully, and leaves the journal
incomplete; the release did not fail. A later gitzone release resume <version> -y verifies the accepted target and never republishes it. An HTTP 409 during
any publish attempt, for a staged or an already published version, is read the
same way: the registry holds that version, so the target becomes accepted and
verification still has to prove the exact bytes.
Because Docker destinations follow verified npm publication, a Docker release
whose npm target is still accepted stops after npm and is resumed from the
release commit, because Docker promotion still has work to do. Only a resume
with no publication work left runs from a later main.
A resume whose remaining targets are only accepted or verified publishes
nothing, so it does not require the working tree to sit on the release commit.
It runs from any later main that still contains that commit and still carries
the release tag, both locally and on the unchanged remote; work can therefore
continue on main while a registry catches up. A resume that still has
publication work left continues to require the journaled release commit.
Publishing independently installable components
The npm target publishes what the project declares, without further configuration:
- every
ts*/tspublish.jsondescriptor that names a package, in tspublish's dependency andordersequence; - then the root package, unless
package.jsonsets"private": true. A private root manifest stays the shared version and dependency catalog, and only its modules publish.gitzone formatadds"private": trueto apackage.jsonwithoutprivateonly when npm is not a release target; with an npm target the choice stays yours, so formatting never stops the root package from publishing.
Descriptors without a name stay build-order-only folders for TsBuild. Every
named module must declare "registries": ["useBase"]: GitZone publishes it to
release.targets.npm.registries with the target's access level and
alreadyPublished policy, exactly like the root package. Explicit, empty, and
extendBase registry declarations are rejected before the release creates
anything, and so are a module named like a public root package, a non-boolean
"private", and an enabled npm target with nothing to publish. Each error names
the file and the change that resolves it.
{
"@git.zone/cli": {
"release": {
"targets": {
"npm": {
"enabled": true,
"registries": [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org"
],
"accessLevel": "public"
},
"docker": { "enabled": true, "engine": "tsdocker" }
}
}
}
}
gitzone release --plan reads and validates the package set and prints it in
publication order, for example:
plan: … core.releaseCheckout -> core.releaseCheckout.build -> core.prepareNpmModules -> core.packNpmArtifact -> core.writeReleaseJournal -> target.docker.qualify -> target.git -> target.npm -> target.docker.promote -> target.docker.cleanup
npm packages (publication order):
@example/api-client (ts_apiclient, ts_interfaces)
@example/service (root)
A private root prints root @example/service: private, not published instead
of its line. Modules publish before the root package because a module can only
depend on its siblings and on the root's dependencies, while the root package
may depend on its own modules.
The normal build must generate every module's compiled folders. In the release
checkout, GitZone re-reads the package set from the release commit and refuses
to publish one that differs from the plan. It then uses tspublish's prepare()
API to isolate the built modules beside the checkout, outside every repository,
packs each module and the root package once, and removes the preparation
directory.
The journal records the ordered module set and one target per package and
registry: the modules first, then the root package. Each module tarball uses the
canonical filename package.<sha256-of-package-name>.tgz beside journal.json;
the root package keeps package.tgz. Every package uses the release version,
and sibling dependencies use that exact version. Git is published before any npm
package. All stored hashes are checked before publication; completion requires
every package at every registry to verify. If publication fails partway
through, release resume <version> -y verifies the completed targets and
publishes only the missing ones from the retained tarballs, never a target that
already verified. Resume never reruns discovery, preparation, builds, or
packing.
npm can hold accepted uploads for publish-time scanning before they become installable. A release with modules submits every package in publication order, then waits for availability across the submitted set so scans can overlap. Command failures and artifact conflicts still stop submission immediately. Every package, tarball and dist-tag must verify before the release is complete, and Docker promotion waits until every package verified.
See npm's publish-time scanning announcement for the registry's availability behavior.
GitZone is the only publisher of these packages. A package.json script that
runs the standalone tspublish publisher (without the plan or prepare
subcommand) fails every release and resume that publishes npm packages, because
it would upload different bytes for the same version outside the journal.
Rust workspaces and the cargo target
A pure Rust repository sets "projectType": "rust" under @git.zone/cli in
.smartconfig.json. Its root Cargo workspace carries the version; the
repository never needs or gets a package.json, and its default test and build
commands are rsrepo test and rsrepo build. Only TypeScript projects get a
baked 00_commitinfo_data.ts.
Every release reads and writes the version through one version source:
| Version source | Project | What the release versions |
|---|---|---|
npm |
a TypeScript project | package.json |
npm+internal-rust |
a TypeScript project with an internal rust/ or ts_rust/ Cargo workspace |
package.json only; the internal crates keep their own versions |
cargo |
projectType: "rust" |
[workspace.package].version (or the single package's version) and every internal path dependency's version requirement, through rsrepo engine set-version, which also updates Cargo.lock offline |
A workspace whose crates carry independent versions is refused: gitzone
releases one shared version. Every Cargo operation runs through the rsrepo
release engine (protocol version 1). gitzone uses the project's
node_modules/.bin/rsrepo when package.json declares @git.zone/rsrepo
(a declared but missing install is an error), and otherwise rsrepo on PATH.
gitzone has no dependency on rsrepo; it checks the protocol version and the
engine commands before use.
The release checkout of a Rust project needs a committed Cargo.lock. It runs
cargo fetch --locked and installs npm dependencies only when the release
commit carries a package.json.
Crates publish only through the cargo target. Without one, a release tags
and pushes the workspace but publishes no crate:
{
"@git.zone/cli": {
"projectType": "rust",
"release": {
"targets": {
"git": { "enabled": true, "remote": "origin" },
"cargo": {
"registries": ["crates-io"],
"members": ["mycrate-core", "mycrate"],
"verificationWindowMinutes": 15
}
}
}
}
}
registriesnamescrates-ioor registries defined in cargo config. Credentials stay with cargo; gitzone and rsrepo never read them.membersis optional. Without it, the release publishes every member whose Cargopublishfield allows every listed registry.verificationWindowMinutes(1 to 720, default 15) is how long the release waits for an accepted crate to appear in the registry index.enabled: falseturns the target off;--cargoor--target cargoselects it for one release.
The cargo target needs projectType: "rust", and a Rust project refuses the
npm target, because it has no package.json version to publish. Docker and
Gitea assets cannot join a cargo release yet.
A cargo release then works like this:
- Before it creates anything, it reads the workspace plan from
rsrepo engine planand refuses the release on a missingCargo.lock, independent versions, an unknownmembersentry, no publishable member, a blocking problem on a published crate (such as a missing license or description, a path dependency without a version, a wildcard dependency, or a dependency that cannot be published), and a published crate whose dependency the release does not publish. Because the release moves every internal requirement to the new version, such a dependency would have no matching version in the registry. Each crate's new version must also be absent from every registry (steppreflight.cargoRegistries). - After the release commit, it packs the published crates in the release
checkout through
rsrepo engine packageand stores each one in the journal ascrate.<sha256(name)>.crate(stepcore.packageCrates). The cargo cells are an optionalcargosection of journal schema 1 or 3, one cell per registry and crate. - After Git is verified, it publishes the crates dependencies first through
rsrepo engine publish(cargo publish --locked --no-verify), from a fresh checkout of the release commit. A crate is published to a registry only after its dependencies are verified there, because crates.io refuses a crate whose dependency version it has not indexed yet. The release probes the sparse index from 2 s, doubling up to 20 s between probes, within the verification window. - A crate whose index entry carries the packed sha256 is
verified. An accepted crate is never uploaded again; a crate still unindexed after its window stops the run, andgitzone release resume <version> -yprobes it later, skips verified crates and publishes the rest. A registry that serves other bytes for the version becomes a terminalconflict, because a published crate version is immutable.
gitzone release --plan prints the registries, the publication order, the
members it does not publish and every problem the engine reports. When a
problem would make the release refuse (step 1 above), the plan ends with
The release would be refused: <n> blocking cargo problem(s), listed above.
and exits 1, as it does for its other refusals:
targets: git, cargo
version: 0.3.0 -> 0.4.0
cargo registries: crates-io
cargo workspace: /work/fx (workspace version 0.3.0, Cargo.lock tracked)
cargo publication order: fx-core -> fx-util -> fx-cli
cargo index wait: 15 min per accepted crate
cargo member fx-internal: not published (publish = false)
cargo member fx-internal: problem publish-disabled: fx-internal sets publish = false and is never published
gitzone release check packs the same crates without publishing anything (see
Checking a release without publishing).
gitzone template rust scaffolds such a repository: a root virtual workspace
whose [workspace.package] carries the version, edition (2024), license (MIT)
and repository, one library crate in crates/<name>/ that inherits them, the
Cargo.lock that cargo generate-lockfile writes, a .smartconfig.json with
projectType: "rust", the Git target and a disabled cargo target, Gitea
workflows that run rsrepo check, a readme and license.md. gitzone format
treats a Rust project as described in Formatting.
Limits of this version: one release cannot version both package.json and a
Cargo workspace, gitzone release recover refuses a cargo project (a cargo
release always records its intent, so gitzone release resume continues it),
and an older gitzone that does not know the cargo target ignores it.
Gitea release attachments before npm
Enable release.targets.giteaAssets when public source archives, native executables,
or other versioned artifacts must be available before npm packages. This works
with root packages and ordered tspublish modules. The Git branch and tag target
must be active; integrate main before releasing. Docker and --merge cannot be
combined with this target.
{
"@git.zone/cli": {
"release": {
"targets": {
"git": { "enabled": true, "remote": "origin", "pushBranch": true, "pushTags": true },
"giteaAssets": {
"enabled": true,
"apiOrigin": "https://code.example.org",
"owner": "example",
"repository": "native-cli",
"tokenEnv": "GITZONE_RELEASE_GITEA_TOKEN",
"manifestPath": "dist/release-assets.json"
},
"npm": { "enabled": true, "registries": ["https://registry.npmjs.org"], "accessLevel": "public" }
}
}
}
}
The configured HTTPS origin must match the resolved SSH/HTTPS Git host and exact
owner/repository path. SSH aliases cannot establish that identity. tokenEnv is
the environment variable's name; credentials are never stored in the journal.
Before Git publication, authenticated and anonymous repository checks require a
public repository, repository-admin authority, and a PAT with write:repository
scope (or an existing all scope). Code-push permission alone is insufficient
because Gitea grants code and release-unit permissions separately. Configure an
existing appropriate credential; the CLI does not acquire or broaden tokens.
The normal release build generates a strict manifest for the new release version:
{
"schemaVersion": 1,
"version": "1.2.3",
"assets": [
{ "name": "source.tar.gz", "path": "dist/source.tar.gz", "size": 12345, "sha256": "<64 lowercase hexadecimal characters>" },
{ "name": "native-linux-x64", "path": "dist/native-linux-x64", "size": 23456, "sha256": "<64 lowercase hexadecimal characters>" }
]
}
Replace the example sizes and hashes with the actual file identities. Names and
paths must be canonical and unique. Regular files and their ancestor directories
must not be symlinks; credential/runtime paths, traversal and globs are rejected.
The release is created as a draft and published only after every
attachment verified, so no attachment is ever public without the others —
native executables never appear without their corresponding source, relinking
materials and notices, whatever interrupts the release. Gitea shows a draft,
its attachment list and its attachment downloads only to accounts with write
access to the repository's releases; everyone else gets 404, also for the
attachment's UUID URL and for /releases/download/<tag>/<name>.
Attachments publish one at a time in manifest order by default: each uploads,
then is downloaded and hashed, then the next starts. Two transfers in the same
direction share the link, so each takes longer than it would alone; a large
upload that runs long enough outlasts the reverse proxy in front of Gitea, which
then answers HTTP 504 although the bytes kept moving (Spark 35.0.1: two
concurrent 385 MB uploads took over 11 minutes each, against about 3 minutes
each one after another, and one ended in a 504). Every upload must therefore
finish, including the time Gitea needs to store it, within that proxy's timeout,
and gitzone never runs two uploads, or two verification downloads, at the same
time. concurrency: 2 pipelines the attachments instead: the next one uploads
while the previous one is downloaded and hashed, largest first so the last
verification is a short one. That is the only overlap that does not share a
direction; on a link where the two directions are not independent, keep the
default.
Once an attachment publication fails, no further attachment starts; the one
already running finishes, because cutting a running transfer off would leave its
result inconclusive, and every failure is reported. The journal keeps each
attachment's own attempt, so gitzone release resume continues with exactly the
unfinished ones. A crash while pipelining can leave two attachments in
publishing; each needs its own --recover-attempt, one resume at a time.
An upload that ends in HTTP 504 (or any other unknown outcome) is inspected right away: an attachment Gitea stored anyway is downloaded and hashed and then counts as published, a missing one is recorded as failed and uploaded again on resume. If the proxy let Gitea finish the abandoned request after the resume uploaded the attachment again, the release lists the name twice; that is a destination conflict that stops the release, never a silent replacement.
The lifecycle of the Gitea release:
- After the Git push, wait until Gitea has processed the pushed tag, then
create the release with
draft: true(or adopt the draft or published release that already carries the tag) and read it back. Gitea processes a tag push in its push queue after the push returned, sometimes minutes later, and publishes (draft: false) any release it then finds for the tag, so a draft created earlier would turn public on its own with the attachments it has by then (Spark 35.0.1 with gitzone 8.5.0: the draft turned public while its two largest binaries were uploading). Processing the tag writes a tag-only release row, which the API hides and the public release page (/<owner>/<repo>/releases/tag/<tag>) shows; once that page answers 200, creating the release converts that row into the draft in place and Gitea never touches it again. The wait polls every 5 seconds for at most 15 minutes. A page request that times out, aborts or encounters a transient network failure counts as not yet processed and is polled again within that same deadline; each request is capped by the remaining wait. Definite HTTP statuses other than 200 or 404 still stop immediately. When the wait runs out, its error includes the last page-check error's name and safe message, nothing was created andgitzone release resumewaits again. A release that this run created must read back as a draft, otherwise the release stops before any upload. - Upload every attachment into it and download and hash each one. While the release is a draft, listing and downloads carry the token (sent only to the configured Gitea origin, never along a redirect to another host); a draft's attachment cannot be downloaded anonymously. The bytes hashed are the stored object the public URL serves later, so integrity is proven the same way.
- Verify the complete set: nothing missing, nothing extra, every frozen identity unchanged.
- Before publishing, every listed attachment must be exactly the one this run downloaded and hashed — same attachment id and UUID — so an attachment replaced after the final check, even by one of identical size, is never published: that is a destination conflict and the release stays a draft. A server that reports no UUIDs is held to the attachment id alone; there the final check downloads every attachment again, and Gitea never replaces an attachment's bytes under its id.
- Publish the release (
PATCH … {"draft": false}), then prove the public view: the release is anonymously visible with the same id, the anonymous attachment list carries exactly the verified attachment ids and UUIDs, and every attachment's public URL answers200with its frozenContent-Lengthand no content encoding; no name or id may appear twice. These public checks read headers only; the bytes are not downloaded again. - Record the journal's completion, then publish npm.
Setting draft: false changes nothing but the flag, is idempotent, and its
result is observable, so it has no journal attempt of its own: a resume after a
crash before or after it finds the draft or the published release, verifies it
and continues. A release that an older gitzone already published keeps the
previous semantics: its attachments are listed and downloaded anonymously, and
publishing only verifies it. Because every attachment of such a release is
public the moment it lands, its missing attachments upload one at a time in
manifest order, whatever concurrency says, so sources and notices still come
before the binaries that follow them in the manifest. The upload order and
concurrency follow the state Gitea reports, never the state gitzone intended,
and every upload first reads the release again: a draft that turned public
meanwhile stops the release before the next byte is sent
(GiteaReleaseStateChangedError), and so does a draft that turned public
before publication; resume then continues it as a published release.
Tag-triggered CI that reads the Gitea release sees the same thing anonymous
users see. The Git target pushes the tag before the Gitea target creates the
draft, so such a job starts while the release is still a draft or absent and
gets 404 for it until the release is published complete. A job that polls
until the release is complete (Spark's verify-node-release.ts treats a missing
release as pending and waits up to 40 minutes) keeps working, and it never sees
a partially uploaded release; a job that reads the release once, immediately,
fails, as it did before this change.
Publication runs build → freeze every artifact → write schema-4 journal → Git →
Gitea assets → npm. Stored assets use asset.<sha256-of-name>.bin filenames next
to the retained npm archives. The CLI verifies local hashes before Git, then
inspects or creates the exact non-prerelease tag release, as a draft until every
attachment verified (see below). Every upload has a revisioned journal attempt.
Each accepted attachment must download with the expected size and SHA256 —
with the token while the release is a draft, anonymously once it is published;
authenticated API redirects are rejected and the token never leaves the
configured Gitea origin. Transfers and hashing stream with bounded memory.
A Gitea request is bounded by its progress, not by a fixed duration, so a large binary on a slow server finishes as long as bytes keep moving:
Setting (release.targets.giteaAssets) |
Default | Bound |
|---|---|---|
responseTimeoutSeconds |
120 | Wait for response headers of a metadata request or download |
stallTimeoutSeconds |
60 | Longest gap without a byte sent or received while a transfer runs |
minimumKiBPerSecond |
256 | Rate floor; a request may take the two bounds above plus its size at this rate |
concurrency |
1 | Attachments of a draft in progress at the same time (1 or 2); 2 uploads one while the previous is verified |
The rate floor applies to every request on its own. Since uploads never
overlap, and neither do downloads, each direction of the link has to carry
minimumKiBPerSecond once, whatever concurrency says. gitzone config doctor prints that rate. These bounds are gitzone's own; a reverse proxy in
front of Gitea can end a request sooner (HTTP 504), which is why uploads never
share the link.
Gitea stores an upload before it answers, which takes minutes for a large
binary. After an upload's last byte the CLI therefore waits for the upload's
size at minimumKiBPerSecond (about 28 minutes for 440 MB at 256 KiB/s), or
responseTimeoutSeconds when that is longer, and adds that wait to the
upload's total bound.
Each bound fails with its own message: no response, upload not acknowledged
(every byte sent, no answer within its size allowance), stalled (no bytes for
the stall bound), or still progressing but below the rate floor. All of them
leave the target inconclusive for gitzone release resume. The settings tune transfers
only and may change between runs and resumes.
Any other request failure is inconclusive too, and says why without a URL,
header or token: the HTTP status it reached, each error of the cause chain by
name and code, a message only when it is plain words, and how far the transfer
got, for example Gitea public asset verification is inconclusive (HTTP 200; TypeError: terminated; SocketError [UND_ERR_SOCKET]: other side closed; 1048576 of 461373440 bytes received).
The complete remote attachment set must equal the manifest. Extra names, duplicates, changed release identity, or conflicting bytes stop publication; the CLI has no deletion or replacement path. It verifies the complete set again before it publishes the draft and before npm: the set must be complete with no extra attachment, and every attachment must still carry its frozen name, size and URL. An attachment this run uploaded, downloaded and hashed is not downloaded a second time when its attachment id and UUID are unchanged since that download. Gitea stores an attachment's bytes under its UUID and its API can only rename an attachment, so an unchanged id and UUID mean the bytes the run hashed. Every other attachment, including one whose id or UUID changed, or every attachment on a server that reports no UUIDs, is downloaded and hashed again.
A journal records its completion (completedAt) only after that final check
passed. A Gitea-asset release without npm used to be recorded as complete as
soon as its last attachment verified, before the final check ran; now a journal
whose targets all verified but whose final check has not passed reads
completed: no, and gitzone release resume <version> -y runs the check.
An inconclusive or rejected upload leaves npm unpublished and retains
the journal for gitzone release resume <version> -y. Resume uses the recorded
destination and retained bytes without rebuilding or repacking. It checks the
metadata of attachments the journal records as verified without downloading
them again, downloads and hashes only the unverified ones, and then verifies the
complete public set once more before npm, even for completed journals; a resume
runs in a new process, so that final check downloads every attachment. An unresolved active attempt requires
the exact --recover-attempt identifier after its publisher has stopped.
Gitea's general attachment-enabled switch is checked. Its issue-attachment size
setting is distinct from repository.release.FILE_MAX_SIZE; confirm the actual
release and proxy upload limits for the largest planned artifact. A size rejection
blocks npm and retains the original artifact; archives are never split or replaced
automatically. The manifest and journal provide exact sizes for this check.
Artifact immutability here means conflict refusal by the publisher. Administrators can still change server state. Distributors must retain the exact public corresponding-source and relinking artifacts while distributing the binaries and include those artifacts in their operational retention and backup policy. Independent CI publishers must not rebuild, upload, delete, or publish these same release assets or packages; keep tag jobs limited to verification.
Independent tag-triggered npm publishers are incompatible with this transaction. GitZone rejects a release while .gitea/workflows/default_tags.yaml or .gitlab-ci.yml still contains the legacy npmci npm publish command. Apply the v6 Gitea workflow template or remove the legacy GitLab job and commit that change first.
Inspect and resume without regenerating release identity:
# List journals or inspect one exact version
gitzone release inspect
gitzone release inspect 6.0.0 --json
# Reconcile remote state, then continue only unfinished targets
gitzone release resume 6.0.0 -y
# Only after proving the recorded publisher process has stopped
gitzone release resume 6.0.0 -y --recover-attempt="$ATTEMPT_ID"
Resume requires the annotated release tag and each journaled destination setting to remain exact; the local main commit must be exact for any remaining publication work and must merely contain the release commit for a verification-only resume. The Git remote must still match when Git is journaled; npm registry, access, and already-published settings must still match when npm is journaled. Before any recovery or publication work, a nonterminal schema-2 resume canonical-compares the current Docker request with the journal, verifies every required project-local tSDocker protocol capability, and validates the journaled request; additive future capabilities are accepted. A completed schema-2 journal trusts its terminal cleanup evidence and does not require candidate state or current Docker configuration. Non-journaled target configuration is ignored, so releases created with a target subset remain resumable. Fresh-release overrides such as targets, integration, build, test, and version flags are rejected even in negated forms such as --no-git, --no-publish, and --no-build; JSON output is available only through inspect --json, prune --json and check --json.
Resume probes remotely observable targets before acting. A published Git release is identified by its annotated tag and that tag's commit, so remote main carrying later commits is exact state rather than a conflict. Exact state is accepted without republishing; positive byte, metadata, ref, or digest conflicts fail closed, while inconclusive results do not overwrite a previously verified state. An npm target in accepted state is only ever verified: its bytes are never sent again, and a resume that still cannot read the version leaves it accepted and succeeds. A Git, npm, Docker qualification, promotion, or cleanup target left in publishing state retains authority until the exact recorded 32-character lowercase hexadecimal attempt.id is supplied after independently proving its publisher stopped. An interrupted tSDocker preparing record has no qualification evidence and cannot be promoted; when tSDocker reports RECOVERY_REQUIRED, the exact recovered qualification owner removes only that incomplete deterministic preparation before claiming a fresh qualification attempt. Promotion recovery first reclaims the retired owner's request files and probes the journaled destination; an exact qualified digest is accepted, pending state retires the old attempt and claims a fresh owner before retry, an inconclusive probe retains the old journal owner, and conflicts halt permanently. Final cleanup of a qualified candidate starts only after every promotion is verified and is itself idempotently recoverable. A terminal qualification destination conflict has no promotions, blocks Git/npm, and permits only exact qualified-candidate cleanup.
Schema 1 resumes final Git release-ref and npm publication after the journal is atomically installed. Schema 2 additionally resumes Docker qualification, ordered promotion, and cleanup. Schema 3 resumes the complete npm package set. --merge remains available only for non-Docker releases because its pre-release Git push would violate qualification-before-publication ordering.
Resuming a release that failed before its journal
Once the local release commit and annotated tag exist, a release records its intent before it builds, packs or checks the remote again:
<git-common-dir>/gitzone/releases/v1/v<version>.intent.json
The intent binds the release commit and tag object, the version floor record,
the targets and release-build choice the command line selected, and, for a Git
target, the remote name, the hashed push destination and the remote main the
push will lease. A failure between the tag and the installed journal — the
release build, packing, freezing Gitea assets, Docker validation, or a remote
that cannot be inspected — is then resumable with the command below. A failed
check of the packed tarballs (entry points, publish size, or a tarball too
large to inspect) is not: the release commit causes it, so it needs the
abandon steps further down.
gitzone release resume 6.0.0 -y
Without a journal, resume reads the intent, resolves the release from the
.smartconfig.json the release commit carries with the recorded targets and
build choice, and requires clean main at the release commit, the recorded tag
object, the recorded push destination and a remote main that still equals the
leased value. It then prepares the journal exactly as the release would have —
the Docker worktree build, the disposable checkout with its build, packing and
frozen assets — and continues as a normal resume from that journal. Tests are
not run again: preflight ran them on the source the release commit only adds
version metadata to. Installing the journal removes the intent, and from then on
the journal alone decides what resume does. A failed resume leaves the intent in
place for the next one. recover on a version with a recorded intent runs this
resume.
To abandon such a release instead of resuming it (nothing reached a remote, and
you delete the local tag and reset main to the commit before the release
commit), delete its intent file as well; otherwise a later tag of the same version
is handed to a resume that refuses it.
A resume packs the same commit with the configuration that commit carries, so a
failure that the commit itself causes — a build that always fails, a missing
entry point, a publish body over its limit — fails the resume again. Abandon
such a release as described above, fix the project, and release again. For a
failed tarball check the error spells these steps out with git -C on the
checkout that holds the release commit, which can differ from the folder the
release started in (--merge).
Recovering a tag without a recorded intent
For a Git release with npm and/or Docker targets whose local release commit and annotated tag were created by GitZone 7.2.6 or earlier, which recorded no intent, and whose build or pack failed before journal installation, use:
gitzone release recover 6.0.0 --plan
gitzone release recover 6.0.0 -y
Recovery supports configured Git with npm and/or Docker targets, including
tspublish modules. It requires clean main, the exact annotated tag at that
commit, matching package and completed changelog versions, and no pending
changes. The metadata commit must not change release configuration. It also
checks remote ancestry, requires the release tag to be absent remotely, and
requires an anonymous HTTP 404 for the version of every package it publishes at
every npm registry when npm is selected (for a private package, an
authenticated not-found answer; see Private npm packages). Published or uncertain Git/npm state
cannot be adopted without its original journal. Gitea asset preparation recovery
is not supported.
The first attempt records the exact refs, destination and resolved workflow under
the common Git directory in gitzone/releases/v1/v<version>.preparation.json.
For Docker, this binding also includes the exact canonical qualification request.
Retries must match that binding, and a binding written by another GitZone version
does not match, because the resolved workflow contains the release plan: when
Release recovery identity or configuration differs from its original preparation. appears on the first recovery after upgrading from 6.16.3 or
earlier, remove that <git-common-dir>/gitzone/releases/v1/v<version>.preparation.json
file and recover again. Recovery uses the same disposable checkout every
release uses: a detached worktree of the tagged commit, pnpm install --frozen-lockfile, then the configured tests and build before packing. Recovery
always installs, because it matches the release identity against the root
package.json and is therefore available only to projects that have one.
Recovery runs the configured tests there because no preflight ran ahead of it. Ignored
runtime data and local project secrets are not copied. A build that depends on
those files must be made reproducible through the project's normal configuration
before it can recover.
Prepared npm artifacts are installed through the normal journal API. Docker recovery verifies the project-local tSDocker capabilities and configuration before preparation and validates again before installing a schema-2 journal. Its complete qualification, including a fresh build and configured image tests, runs from the unchanged canonical checkout before Git or npm publication. Candidate state stays at that stable project identity for later resume; images from the disposable preflight build are not adopted. Docker destinations retain the normal qualified digest, observed alias predecessor, probe and conflict checks.
Recovery never creates another version or moves refs. Once the journal exists,
another recover call uses resume
without repeating preparation. Any unfinished Docker qualification remains part
of the normal resume workflow. Use resume --recover-attempt for an interrupted
publisher that still owns a journaled attempt, after proving that process stopped.
Concurrent preparations are excluded by v<version>.prepare.lock. A process crash
can leave that lock behind; inspect its owner and prove the process has stopped
before manually removing that specific lock. Failed build or pack attempts retain
their preparation binding and remove only their disposable worktree. --plan
validates and describes the local identity without installing dependencies,
building, fetching, writing a binding, or publishing; it does not certify registry
absence. When the current remote commit is unavailable locally, the plan reports
deferred ancestry; execution fetches and verifies that history before writing the
preparation binding. Earlier version, changelog, commit or tag failures still
require explicit operator reconciliation.
Running a release detached
A release runs its tests, build, push and every publication in one process,
which can take longer than the tool call or terminal that started it. With
--detach, gitzone release -y and gitzone release resume <version> -y
return once the release is running in a session of its own:
gitzone release -y --detach
gitzone release resume 6.0.0 -y --detach
In the foreground, --detach checks what --plan checks — arguments,
configuration, the release branch, a clean tree, the Git destination and the
version — plus pending changelog entries, or for resume the journal or
recorded intent of that version. It needs -y (or release.confirmation: "auto" for a fresh release), because nothing can answer a prompt later, and it
cannot be combined with --plan. Any failure there exits non-zero and starts
nothing. Everything else — the pnpm and tSDocker capability checks, tests,
build, commit, tag and publication — runs in the detached release and appears
in its log and recorded outcome.
A fresh release prints its release plan there, as --plan does. The launcher
then starts a supervisor with detached: true — a new session and process
group without a controlling terminal, stdin /dev/null — and the supervisor
starts the release as its child with the same arguments minus --detach. Once
the supervisor reports the release running, the launcher prints one line and
exits 0:
Release running detached: run <id>, process group <pid>, log <path>. Follow it with `tail -f <path>`; `gitzone release inspect` shows its outcome.
Phase lines in the log carry the time they were printed, for example
[2026-10-03T01:48:31.402Z] Running tests followed by Running tests passed in 7 min 8 s., so the log of a release shows where its time went: tests, the
release checkout and its build, the Git target, the Gitea release and every
attachment (uploading <bytes>, uploaded, downloaded and hashed in <time>),
the final attachment-set verification, the publication of the draft, each npm registry and its availability
wait, and Docker qualification, promotion and cleanup. A foreground release
prints the same lines.
When the supervisor refuses to start the release, the launcher prints why and
exits 1. When the supervisor does not report within 60 seconds, the launcher
terminates its whole process group (SIGTERM, and SIGKILL after another 10
seconds), then exits 1 and says whether a release had started before it was
stopped; gitzone release inspect shows the outcome of such a run.
Everything lives beside the release journals:
<git-common-dir>/gitzone/releases/v1/detached.lock held by the supervisor while the release runs
<git-common-dir>/gitzone/releases/v1/detached/<id>.json run record: command, version, pids, state, exit code or signal
<git-common-dir>/gitzone/releases/v1/detached/<id>.log stdout and stderr of the release
The supervisor records the run as running, and when the release ends, as
succeeded (exit code 0) or failed with its non-zero exit code or the signal
that killed it, and appends Detached release finished: … to the log. A crash
or kill of the release is recorded the same way. From the moment it holds the
lock, a SIGHUP, SIGINT or SIGTERM does not end the supervisor: before the
release has started, the signal cancels the launch, the lock is released and no
run is recorded; after that, the supervisor passes each signal on to the
release once and stays to record the outcome. kill -- -<process group> stops
the whole run: the release receives the signal directly as well, and the
supervisor passes nothing on to a release that has already ended.
gitzone release inspect lists the runs, latest first, before the journals,
and inspect --json adds them as runs. The run's state says how the process
ended; the journal of its version still says what was published, and a failed
run continues with gitzone release resume <version> -y, detached or not.
While the lock is held, every other release, resume or recover of the
project, detached or not, is refused; only the run's own release passes.
--plan runs and inspect stay allowed, since they change nothing. The
lock sits in the Git common directory, so all worktrees of a repository share
it. A release started without --detach does not take the lock, so a detached
launch cannot see it. When the supervisor itself is killed with SIGKILL, or
the machine stops, the run stays running and the lock stays held: prove the
recorded supervisor process no longer exists, then remove
<git-common-dir>/gitzone/releases/v1/detached.lock, as with every other
GitZone lock. Run records and logs are kept; remove old ones from
detached/ when they are no longer needed.
Standard Changelog
The changelog is convention-based and intentionally not configured.
gitzone commit appends entries to:
## Pending
gitzone release moves those pending entries into a dated version section:
## 2026-05-10 - 2.15.0
The standard buckets are Breaking Changes, Features, Fixes, Documentation, and Maintenance.
Configuration
CLI workflow config lives under @git.zone/cli in .smartconfig.json. Docker release selection lives under @git.zone/cli.release.targets.docker; canonical registries, repository mappings, and platforms remain owned by @git.zone/tsdocker.
{
"@git.zone/cli": {
"schemaVersion": 2,
"projectType": "npm",
"commit": {
"confirmation": "prompt",
"steps": ["analyze", "test", "build", "changelog", "commit", "push"]
},
"release": {
"confirmation": "prompt",
"preflight": {
"test": false,
"build": true
},
"targets": {
"git": {
"enabled": true,
"remote": "origin",
"pushBranch": true,
"pushTags": true,
"remoteTimeoutSeconds": 180
},
"npm": {
"enabled": true,
"registries": ["https://registry.npmjs.org"],
"accessLevel": "public",
"alreadyPublished": "success",
"verificationWindowMinutes": 15,
"maxPublishBytes": {
"https://registry.npmjs.org": 10485760
}
},
"docker": {
"enabled": false,
"engine": "tsdocker",
"registry": "registry.gitlab.com",
"buildRegistries": ["registry.gitlab.com"],
"test": true,
"patterns": [],
"cached": true,
"parallel": true
}
}
}
},
"@git.zone/tsdocker": {
"registries": ["registry.gitlab.com"],
"registryRepoMap": {
"registry.gitlab.com": "myorg/myproject"
},
"platforms": ["linux/amd64", "linux/arm64"]
}
}
NPM registries belong only here:
@git.zone/cli.release.targets.npm.registries
Canonical Docker destinations and repository mappings belong here. Registry
values should be hosts without http:// or https://:
@git.zone/tsdocker.registries
Docker release configuration uses the project's absolute project-local
node_modules/.bin/tsdocker. Install tSDocker 3.5.1 or newer in each
Docker-producing project; fresh release and nonterminal resume verify matching
package and binary versions plus the required protocol-v1 capabilities before
use. Additional future protocol capabilities are accepted:
pnpm add --save-dev @git.zone/tsdocker@3.5.2
release.targets.docker.registry, buildRegistries, test, patterns,
cached, parallel, and context become part of the immutable qualification
request. noBuild must remain false or absent because digest qualification
always builds. The separate release --no-build flag only disables the normal
project build step and does not bypass Docker qualification.
Useful config commands:
# Show current @git.zone/cli config
gitzone config show --json
# Configure project basics, CLI behavior, and release targets interactively
gitzone config project
gitzone config cli
gitzone config release
# Validate schema, legacy keys, release targets, registries, and npm auth
gitzone config doctor
# Use opencode to repair configuration issues found by doctor
gitzone config fix
# Read the npm release target registries
gitzone config get release.targets.npm.registries
# Add an npm release target registry
gitzone config add https://registry.npmjs.org
# Set npm target access level
gitzone config access public
# Run schema migration to v2
gitzone config migrate 2
@ship.zone/szci belongs to szci, which GitZone no longer uses or reads. gitzone config migrate and gitzone format remove only an empty npmGlobalTools list, szci's default, and delete a block left empty; every other key stays. szci still reads two keys, and no migration deletes or moves them. npmRegistryUrl is the registry szci npm prepare installs from, for example in a Dockerfile that installs private packages. It is not a publish registry, even when release.targets.npm.registries lists the same URL, so no migration, the one from schema 1 included, moves or copies it into the publish registries: a registry list alone enables the npm target. npmAccessLevel is the access level szci git mirror checks. GitZone publishes with release.targets.npm.accessLevel, so the migration copies npmAccessLevel there when that key is absent and keeps it in the block: the duplicate is deliberate. gitzone config doctor names the current home of each kept key GitZone once read, for example @git.zone/tsdocker.registryRepoMap for dockerRegistryRepoMap and @git.zone/tsdocker.buildArgEnvMap for dockerBuildargEnvMap, and reports npmRegistryUrl and npmAccessLevel as szci's own. A block that holds only these two keys needs no step once release.targets.npm.accessLevel is set.
gitzone config doctor also reports unknown release.targets.* keys, which a
release ignores, validates release.targets.cargo, and checks the sections of
the consolidated build tools:
@git.zone/tsrepotakes the subsectionsbuild,test,bundle,docs,watchandsea, each shaped like the section of the single tool it replaces (@git.zone/tstest,@git.zone/tsbundle,@git.zone/tsdoc,@git.zone/tswatch,@git.zone/tssea;buildhas none). A subsection beside its legacy section is an error: tsrepo never merges the two. A tsrepo section in a project whosepackage.jsondoes not declare@git.zone/tsrepois a warning, because the single tools do not read it.@git.zone/rsrepotakes the tsrust fields (targets,targetsByHost,locked,static,rustflags,targetDir,pruneAfterBuild,matrix,notices) plusfeatures,toolchainsandmembers. Keeping it beside the legacy@git.zone/tsrustsection is an error.
gitzone config fix moves the legacy tool sections into their
@git.zone/tsrepo subsections, but only when package.json declares
@git.zone/tsrepo (as a dependency or devDependency), because older tools do
not read the new key. A legacy section whose subsection already exists stays
where it is, and the fix names it so you can merge the two by hand.
Package contents
gitzone format writes the files list of package.json, and nothing else
should. By default it writes the list every gitzone project has always had. A
project that needs different contents describes them in .smartconfig.json:
{
"@git.zone/cli": {
"packageFiles": {
"sets": ["source", "dist", "assets", "config", "docs"],
"include": ["third-party-notices.md", "licenses/**/*"],
"exclude": ["dist_bundle/**/*", "html/**/*"]
}
}
}
sets picks named groups of entries; without packageFiles, or without
sets, every set applies:
| Set | Entries |
|---|---|
source |
ts/**/*, ts_web/**/*, excluding ts/tsconfig.json and ts_web/tsconfig.json |
dist |
dist/**/*, dist_*/**/*, dist_ts/**/*, dist_ts_web/**/* |
assets |
assets/**/* |
cli |
cli.js |
config |
.smartconfig.json |
docs |
readme.md |
binary |
With managed binaryCli assets: the installer (install.sh or assets.installer.file) and, with the npm wrapper, bin/ and its postinstall script; empty otherwise |
The list carries the entries of the selected sets in this order, then
include, then every exclusion: those of the sets first, then exclude, each
written with a leading !. Exclusions come last because pnpm 10 and 12 apply
an exclusion only after the entries that include the file. A repeated entry
keeps its first position. Write exclude entries without the !.
pnpm packs some files whatever files says: package.json, every root file
whose name starts with readme, license or licence (any case), the main
file and every bin file. An exclusion naming one of them would have no
effect, so gitzone format refuses it, as it refuses an unknown set name.
Nested files such as docs/readme.md are not packed implicitly.
A release checks the package it builds:
preflight.packageFiles, before anything is created and only whenpackageFilesis configured:package.jsonfilesmust equal the generated list. Otherwise rungitzone format, commit, and release again.core.packNpmArtifact.entryPoints, after packing and before Git is pushed: everymain,module,types/typings,binandexportstarget of the packedpackage.json(all conditions; a*pattern must match at least one file) must be in the tarball.mainalso resolves the way Node does (.js,.json,.node, or a folder'sindex).- The packed size and the estimated publish body per registry are printed.
The body is what
pnpm publishuploads: the tarball as base64 plus the packed manifest with its readme, which is about 4/3 of the tarball size. Withrelease.targets.npm.maxPublishBytes(registry URL to bytes; every key must be a configured registry) the release stops when a body exceeds its registry's limit (core.packNpmArtifact.publishSize). There is no default limit, because registries differ.
The two artifact checks run after the local release commit and tag exist and
before anything is pushed, so a failure leaves the release where a failed
build or pack leaves it: commit and tag local, intent recorded, no journal,
nothing pushed. Because the commit itself causes the failure, the error does
not suggest a resume; it names the steps to abandon the release (see Resuming a release that failed before its journal).
Then fix the package and release again. gitzone config doctor validates
packageFiles and maxPublishBytes and reports a files list that differs.
Managed Assets
Projects can opt into generated, updateable repository assets through @git.zone/cli.assets. The kind binaryCli manages installer scripts, npm binary wrappers, postinstall downloaders, and Gitea release workflows for CLI executables that tsrepo sea from @git.zone/tsrepo builds as Node.js single executable applications.
{
"@git.zone/cli": {
"schemaVersion": 2,
"assets": {
"schemaVersion": 1,
"kind": "binaryCli",
"cliName": "onebox",
"displayName": "Onebox",
"repository": {
"host": "code.foss.global",
"path": "serve.zone/onebox"
},
"installer": {
"enabled": true,
"installDir": "/opt/onebox",
"binDir": "/usr/local/bin",
"modes": {
"default": "binary",
"source": {
"enabled": true,
"commands": ["pnpm install --frozen-lockfile", "pnpm run build"],
"executable": "cli.js",
"executableFiles": ["cli.js", "cli.ts.js", "cli.child.js"],
"validate": "node cli.js --version"
}
},
"service": {
"detectNames": ["onebox"],
"refreshCommand": "onebox systemd enable",
"startHint": "onebox systemd start"
},
"ensureDirs": ["/var/lib/onebox", "/var/www/certbot"],
"preservePaths": ["/var/lib/onebox"]
},
"npmWrapper": {
"enabled": false
},
"releaseWorkflow": {
"compileCommand": "pnpm run build:binary",
"packNpmArtifact": true
}
}
},
"@git.zone/tssea": {
"nodeVersion": "26.11.1",
"bundle": "./dist_node/main.mjs",
"outDir": "./dist/binaries",
"targets": [
{ "target": "linux-x64", "name": "onebox-linux-x64" },
{ "target": "linux-arm64", "name": "onebox-linux-arm64" }
]
}
}
The executables come from the project's SEA configuration; GitZone keeps no target list of its own. The project declares @git.zone/tsrepo ^1.0.0 in dependencies or devDependencies and has it installed (pnpm install --save-dev @git.zone/tsrepo, then pnpm install); GitZone reads the configuration through @git.zone/tsrepo/sea/config from the project's own install, so the project's tsrepo version interprets its own configuration and GitZone has no runtime dependency on it. tsrepo's configuration rule applies: @git.zone/tsrepo.sea, else the legacy @git.zone/tssea section, and both at once is an error. A project still on the deprecated @git.zone/tssea (^1.1.0, without tsrepo) is read through @git.zone/tssea/config; it must move to tsrepo as the tsrepo readme's "Migrating from the single tools" describes, or set releaseWorkflow.compileCommand to its tssea build, because the default compileCommand runs tsrepo; gitzone format and gitzone config doctor say which step is missing, and they refuse an installed tssea outside ^1.1.0 and an outDir outside the project.
- Asset names: each target's
nameis the executable's file name and its release asset name, used exactly as given. A Windows name carries its.exe. The installer mapsunametolinuxormacosandx64orarm64; the npm wrapper mapsprocess.platform:process.arch; both downloadreleases/download/<version>/<name>. - Windows:
install.shinstalls on Linux and macOS only. Under Git Bash, MSYS2 or Cygwin it stops before any download, also with--dry-run, and names the release's Windows executables (releases/download/<version or latest>/<name>) andSHA256SUMS.txtto check them against: there it has no root user for itssudo bashusage, no systemd, and itsbinDirlies inside the Git installation, outside the WindowsPATH. The npm wrapper does install on Windows. - Runtime dependencies: the executables carry the bundled application, and the npm wrapper and postinstall use Node.js built-ins only, so neither needs the project's
dependencies. npm and pnpm still install every package listed there for each user of the npm package. Keep packages that are only bundled into the executable indevDependencies;releaseWorkflow.installCommandinstalls both, so the build is unaffected. GitZone does not move them for you. - Release workflow:
releaseWorkflow.installCommand(defaultpnpm install --frozen-lockfile --ignore-scripts) installs the dependencies andreleaseWorkflow.compileCommand(defaultpnpm exec tsrepo bundle && pnpm exec tsrepo sea build, the@git.zone/tsrepobundle and SEA build) builds; set either when the project needs another command, for examplepnpm exec tsbundle && pnpm exec tssea buildin a project not yet moved from tsbundle and tssea to tsrepo. The workflow sets up Node.js 22, the runtime of its tooling (tsrepo searuns on Node.js 22 or later and builds and smoke-runs every executable with thenodeVersionit pins, never with the Node.js that runs it; acompileCommandthat runs the project's own code runs it on Node.js 22), installs dependencies, checks that thepackage.jsonversion equals the tag, builds, and then checksums and uploads exactly the configured executables, the notice filestsrepo seawrites next to each, the staged installer (when the installer is enabled), the extra release assets andSHA256SUMS.txt. A missing file or a Gitea API error fails the job, and nothing else in the output directory is uploaded. A published asset is never replaced: when the release already has an asset of the same name, the job fails before uploading anything and names each such asset; delete them in Gitea before rerunning. An asset list that is not a JSON array of named assets fails the job as well, never counting as "no such asset". WithpackNpmArtifactit also packs and uploads the npm tarball. - Extra release assets:
releaseWorkflow.extraAssetslists further repository files the release publishes, such as an uninstaller or the changelog: exact/-separated paths whose segments consist of letters, digits,.,_and-and start with a letter or digit (no globs, no.., no leading/or./, no backslash). Each file is published under its last path segment, which must differ from every other release asset (the executables, their notice files, the name of an enabled installer,SHA256SUMS.txt, and withpackNpmArtifactany.tgzname); a path inside the tssea output directory is refused, since extras are repository files, not build outputs. After the build and before the release is created, the workflow checks that every extra is a regular file whose real path lies inside the checkout, then stages them; any failure ends the job with no release and no upload. Extras are listed inSHA256SUMS.txtand uploaded like every other asset.installer.extraFilesnames the extras (by asset name) thatinstall.shinstalls intoinstallDirbeside the executable; each must be tracked by Git, and the installer applies the file's mode from the Git index (100755installs as0755, everything else as0644), so a committed mode change shows as asset drift ingitzone format check. A name must not be the executable's file name (binary.installFileName, defaultcliName). Onlyinstall.shinstalls extras, in binary and in source mode; the npm postinstall installs none, so a program that uses such a file must cope with its absence in an npm install. - Release notes: the release description is the tag's section of
changelog.md, the## <date> - <version>headinggitzone releasewrites, up to the next##heading. Generic notes with the install command are used only when the changelog has no section for the version. The workflow extracts it with the managedscripts/gitzone-binary-release.mjs, which the release workflow template also writes. - Old releases:
releaseWorkflow.keepReleases(default 0) is how many published releases stay. With N above 0, the workflow deletes, after this release's assets are uploaded, every published Gitea release older than the newest N (by creation time) together with its assets. Tags are never deleted, drafts are neither counted nor deleted, and the release of the running tag always stays. 0 keeps every release; a value that is not a non-negative integer failsgitzone format. A Gitea API error fails the job. - npm postinstall: with
npmWrapper.enabled, the postinstall downloadsreleases/download/v<version>/<name>for the running platform, where<version>is theversionin the package's ownpackage.json. It runs only when the package is installed as a dependency, that is when its directory lies inside anode_modulesdirectory (npm, pnpm, Yarn and Bun install every package, local or global, there). In the project's own checkout, for development or a release build, it skips the download, because that version is not released yet and the project builds its binaries itself. The release asset is the only source, and it is verified like the installer does: the postinstall reads the release'sSHA256SUMS.txt, downloads the binary and its notice files next to the target, and moves them into place under their release asset names only when every SHA-256 matches; a mismatch, a missing entry, a missing file or missing checksums fail and install nothing. An existing binary is not downloaded again, but missing notice files are. It follows 301, 302, 303, 307 and 308 redirects (relative locations included) up to 10 hops and only to HTTPS, and gives up after 30 s without data or 15 minutes per file. Every failure stops the install with the URL, the version and the cause. The wrapper inbin/runs the binary as a child process and, on Linux and macOS, forwards SIGTERM and SIGHUP to it while it runs. SIGINT is forwarded only when the wrapper has no controlling terminal (opening/dev/ttyfails withENXIO: undersetsid,docker runwithout-t, or adetached: truespawn). With a terminal, a Ctrl+C reaches the binary directly through the process group, so the wrapper ignores SIGINT until the binary ends; a parent'schild.kill('SIGINT')on the wrapper is then ignored as well, because Node does not name the sender: send SIGTERM, or spawn the wrapper withdetached: true. The wrapper passes on the binary's exit status, and when a signal ended the binary it ends with the same signal (or 128 + its number if Node ignores it). The wrapper is the entry point forpnpm add -gandnpx; under systemd or another supervisor, point the service at the installed native binary instead. A wrapper run as a systemd main process should useKillMode=mixed: with the defaultcontrol-group, systemd sends SIGTERM to the binary as well and the wrapper forwards another. For the same reason, signal the wrapper's pid, not its process group: without a terminal, a group-wide SIGINT (tini -g,KillSignal=SIGINT,kill -INT -<pgid>) also reaches the binary twice.
@git.zone/cli.schemaVersion remains the CLI config schema. @git.zone/cli.assets.schemaVersion is scoped to the managed asset model.
Managed assets are applied through the existing formatter workflow:
gitzone format plan --only assets --json
gitzone format check --only assets
gitzone format --only assets --write --yes
If npmWrapper.enabled is set, gitzone format --only packagejson --write also keeps package.json bin, scripts.postinstall, and npm package files entries in sync.
The installer is published as a release asset only: the release workflow stages install.sh into the release artifact directory, lists it in SHA256SUMS.txt and uploads it with the executables. Its usage text shows <repository>/releases/download/latest/install.sh (Gitea, like GitHub, resolves latest to the newest published release; a pinned install adds --version vX.Y.Z), and the release notes show the release's own releases/download/<tag>/install.sh; Gitea serves both without an account, like the executables. No generated file points at a raw/ or src/ URL, which code.foss.global serves to signed-in users only. The release uploads the installer exactly when installer.enabled is not false. A repository that starts publishing the installer has no latest/install.sh until its next release uploads one; until then that URL answers 404. The whole script is one function that only its last line calls, so a download that ends early runs nothing.
GitZone 10 removed installer.distribution, releaseWorkflow.includeInstallerAsset and repository.branch. gitzone format (like gitzone config migrate and gitzone config fix) removes them from .smartconfig.json where removing them changes nothing: repository.branch always, since nothing reads it, installer.distribution: "releaseAsset", and an includeInstallerAsset equal to whether the installer is enabled; an object left empty goes with them. Every other value fails gitzone format and gitzone config doctor with an error that names the step: installer.distribution: "rawBranch" (a raw/branch/ URL) or another value, remove the key; includeInstallerAsset: false with an enabled installer, remove the key or disable the installer; includeInstallerAsset: true with a disabled installer, list the installer file in releaseWorkflow.extraAssets instead (a disabled installer's file name is free for an extra asset).
Set installer.service.removeLegacyUnits to a list of systemd unit names when an installer must disable and remove older service units during upgrades.
installer.versionSelection lists the ways a user picks the version (default ["latest", "version"]): with neither flag the installer takes the newest Gitea release, --version vX.Y.Z a given tag, and adding "major" enables --major N (N or vN, no leading zero), the newest release of that major. --major reads the releases API's list without drafts and pre-releases (?draft=false&pre-release=false), newest first, 50 per page, and takes the first tag that starts with vN. or N.; it reads at most 100 pages and fails when none matches. Without "major" the installer has no --major option.
A version is a SemVer 2.0 version with an optional v (v1.2.3, 1.2.3, v1.2.3-beta.1, 1.2.3+build.5); the installer adds the v of the release tag, so --version 1.2.3 installs v1.2.3. --version latest is the same as no --version. The tag the releases API names for the newest release, or for --major N, must have the same form. Anything else fails before the installer builds a download URL. Every download uses curl with HTTPS only (also on redirects), at most 10 redirects, and the postinstall's limits: 30 s to connect or without data, 15 minutes per transfer. The npm postinstall likewise refuses a package.json version that is not SemVer.
install.sh --dry-run resolves and checks the version like an installation, then checks the release without root and without touching the system: it downloads SHA256SUMS.txt, checks that it lists every file the installation downloads (the platform's executable, its notice files and the installer.extraFiles), and requests the first byte of each, printing <HTTP status> <URL> per file. It exits 0 when every file answers 200 or 206 and has its checksum entry, and 1 otherwise. In source mode it prints the clone URL and tag.
The installer installs and updates in this order, and an update that fails leaves the previous installation running:
- Verify before touching anything. In binary mode it downloads the platform's asset, the third-party notice files tssea wrote for it (
<name>.third-party-notices.txtand.json, the target'snoticeFiles) andSHA256SUMS.txtof the same release, refuses a file the release lacks or whose digest differs or is missing (withsha256sumorshasum; neither present is an error), and runs the new binary with--version. In source mode (installer.modes.source) it clones the tag and runscommandsandvalidate, each a shell command line run withevalexactly as written; the first that fails stops the installation. Any failure here exits non-zero with the running installation and service untouched. - Stop and move aside. It stops the running
installer.service.detectNamesunits and movesinstallDirinto a new directory beside it,installDir.previous.XXXXXX(frommktemp -d); it never overwrites the old installation in place. A backup an interrupted earlier run left behind is never reused, merged into or deleted. Data underinstaller.preservePathslives outsideinstallDirand is not touched. - Install and check. It installs the binary with its notice files and
installer.extraFiles, which keep their release asset names (or the source build, where everyexecutableFilesentry must exist), intoinstallDir, links it intobinDir, createsensureDirs, and runs the installed CLI with--version. - Restart. If a service was running, it runs
installer.service.refreshCommand(when set) and restartsinstaller.service.restartName. The refresh command is a shell command line, written into the installer as one quoted word and run withevalin a subshell, withbinDirfirst onPATHso a bare CLI name runs the CLI just installed, also when the installer'sPATHlacksbinDir(asudosecure_pathcan leave it out) or names another copy first. It may use the installer's variables, such as"$BIN_DIR/<cli>"; its exit status, that of its last command, decides whether the update succeeded.startHintandsuccessHintsare printed exactly as written. - Clean up. Only after the restart succeeded it deletes the moved-aside installation.
If any step from 2 on fails, the installer removes the new files, moves the previous installation back, relinks it, restarts the service it stopped, and exits non-zero with the failed step and whether the previous state was fully restored. A failed first installation leaves nothing behind.
The installer links the CLI into binDir only and never picks another directory. When the PATH it runs with (under sudo, root's PATH) finds no <cliName>, or finds another copy before binDir/<cliName>, it ends with a warning on stderr that names binDir or the other copy; the installation stands and the exit status stays 0.
Migrating from denoBinaryCli
GitZone 9 refuses the removed keys with an error that names the replacement:
| Removed | Replacement |
|---|---|
kind: "denoBinaryCli" |
kind: "binaryCli" |
@git.zone/tsdeno.compileTargets |
@git.zone/tssea.targets (see "Migrating from tsdeno" in the tssea readme); keep each name, and write the .exe of a Windows name |
binary.targets, binary.outDir |
@git.zone/tssea.targets, @git.zone/tssea.outDir |
releaseWorkflow.assetGlobs, releaseWorkflow.include |
releaseWorkflow.extraAssets for further repository files (exact paths, no globs); the executables and their notice files are always released |
releaseAssets |
releaseWorkflow |
rawBranch, repository.rawBranch |
none: the installer is a release asset; remove the key |
installer.delivery |
none: the installer is a release asset; remove the key |
installer.defaultMode |
installer.modes.default |
installer.source |
installer.modes.source |
installer.modes.source.validateCommand |
installer.modes.source.validate |
installer.service.legacyUnitNames |
installer.service.removeLegacyUnits |
managed templates asset_deno_binary_installer, asset_deno_binary_postinstall, asset_deno_binary_release_gitea |
asset_binary_installer, asset_binary_postinstall, asset_binary_release_gitea |
Sealed TsPack releases on Gitea
For a component that already builds and packages with @git.zone/tspack, select
assets.kind: "tspackRelease". GitZone manages the tag-triggered workflow and its
release scripts. Compilation stays in the component's command, and TsPack owns
archive creation and verification. The project must install @git.zone/tspack
1.4.0 or later and ignore both configured output and dist_gitzone_retained/ in Git.
Pin pnpm 11 or later in package.json using packageManager. The workflow installs
that version directly with the pinned official pnpm/setup action before the
explicit frozen dependency install. This avoids an older container-provided pnpm
launcher rewriting the lockfile while selecting the project's package manager.
sealedRelease.nodeVersion is the exact Node.js release (x.y.z) that installs the
dependencies and runs the prepare and verify commands. The workflow installs it with the
SHA-pinned official actions/setup-node v4.4.0 and checks node --version before
anything else runs. It is independent of the Node.js release a SEA build tool embeds.
GitZone 9 refuses the former sealedRelease.denoVersion with a migration error.
{
"@git.zone/cli": {
"assets": {
"schemaVersion": 1,
"kind": "tspackRelease",
"sealedRelease": {
"nodeVersion": "24.13.0",
"outputDirectory": "dist_control_release",
"prepareCommand": ["node", "scripts/release-control.mjs"],
"verifyCommand": ["node", "scripts/package-control.mjs", "--release", "--reuse"]
}
}
}
}
Both commands return exactly one JSON object on stdout with directory and
manifestSha256. Preparation creates a clean tagged release directly beneath
the configured output root and saves every input descriptor needed for reuse.
Verification restores that same result without compiling or repacking; the
workflow runs it right after preparation and requires the same directory and
manifest digest. Commands are explicit executable/argument arrays; GitZone does
not interpolate a shell.
A command's stderr goes straight to the job log, so write build progress and
diagnostics there and keep stdout for the JSON result. The command runs without
GITHUB_TOKEN and ACTIONS_RUNTIME_TOKEN. When it exits non-zero or is killed,
the release step fails with a line that names the command and its exit code or
signal, below the command's own stderr. A command that writes more than 1 MiB to
stdout or runs longer than 30 minutes is stopped and fails the step by name.
Run gitzone format --only assets --write --yes and commit the generated files
(.gitea/workflows/release.yml and three scripts in scripts/). The normal
authorized gitzone release -y pushes the tag that triggers this CI flow.
The draft Gitea release is the retention. The workflow does not use Actions artifacts: Gitea scopes them to one run attempt, so a rerun could not read them, and Gitea 1.27 finalizes an upload by merging its chunks inside the request, which outlasts the artifact client's 3-minute timeout for a few hundred megabytes and then fails every retry because the merge already consumed the chunks. Release attachments belong to the tag, and the job token can write them.
- Discover reads the tag's release with the job token and anonymously. With
no release, the job builds. A release must name its manifest digest in its
description; its
tspack-manifest.jsonattachment must hash to that digest, TsPack must accept the manifest andSHA256SUMS.txtfor the tagged commit, and the attachments must be exactly the sealed set. Discovery then downloads every archive intodist_gitzone_retained/and hashes it against the manifest. A complete set is retained and nothing is built. A draft that is not complete is rebuilt. A published release that is not complete stops the job. - Prepare runs the component's prepare command, verifies the result with
TsPack, runs the verify command on it, and writes the release receipt. Both
commands run with
SOURCE_DATE_EPOCHset to the tagged commit's time, so tools that record or embed a time build the same bytes on every attempt; a job that sets another value stops. - Retain waits until Gitea has processed the pushed tag before it creates a
draft, because Gitea publishes a release it finds for a tag when it processes
the push (the same wait
gitzone releaseuses; see Gitea release attachments before npm). The wait also retries page timeouts, aborts and transient network errors within its 15-minute deadline, reporting the last cause if it expires. The draft's description names the manifest digest. Uploads run one at a time; every upload reads the release again first, then the attachment is downloaded and hashed. Attachments that already match are kept. - Restore moves the retained download into the output root and verifies it with TsPack against the manifest digest and the tagged commit. A resumed job does not run the component's verify command: the restored bytes are hash-bound to the digest the description names, which was written only after the verify command passed on the job that built them.
- Publish checks that the draft holds exactly the attachments this job hashed (Gitea stores an attachment's bytes under its UUID, and its API can only rename one, so id, name and UUID identify hashed bytes), checks the remote tag again, publishes the draft and confirms the release and its attachment list anonymously.
A published attachment is never replaced and a release is never deleted. A release build must be reproducible: a draft whose description names a manifest holds an earlier build of the same commit, and a rebuild that seals a different manifest is refused without changing the draft, naming both digests. Make the build reproducible, or delete the draft to release a new build. Within the same manifest, a draft attachment that conflicts with the sealed set (bytes an interrupted upload left, or a file outside the set) is replaced: right before each deletion the release must still read back as a draft with the token and be invisible anonymously, and the job log names the attachment and both digests. A draft that turns public meanwhile stops the job before the next change.
After an interruption, rerun the job. A rerun after a complete draft or a published release resumes from its bytes without building; a rerun after an incomplete draft rebuilds, and completes it when the rebuild reproduces the manifest the draft names. The remote tag must match the sealed source commit. Unexpected API responses, a publicly visible draft, an incomplete published release and changed source identities stop before publication.
Formatting
gitzone format is dry-run by default. That makes it safe to run in any repo.
# Preview changes
gitzone format
# Emit a machine-readable plan
gitzone format plan --json
# Fail when formatting changes or validator errors remain
gitzone format check
# Run a subset of formatters
gitzone format --only prettier,packagejson
# Apply changes
gitzone format --write
# Apply without prompt
gitzone format --write --yes
# Apply deterministic fixes, then use opencode for remaining issues
gitzone format fix
Formatters include cleanup, smartconfig normalization, dependency license checks, package metadata normalization, template updates, .gitignore, TypeScript config, Prettier, README existence checks, and configured copy operations.
The packagejson formatter writes the files list of package.json. It ships the ts/ and ts_web/ sources without their tsconfig.json: tsrepo build (formerly tsbuild) reads a folder's compiler options from <folder>/tsconfig.json, which extends the root tsconfig.json, and the package does not ship the root file. The list therefore ends with !ts/tsconfig.json and !ts_web/tsconfig.json; pnpm 10 and 12 apply an exclusion only after the entries that include the file.
The owner npm scopes are an exported constant, so no repository keeps a hand-copied list:
import { ownerNpmScopes } from '@git.zone/cli';
// ownerNpmScopes: ['@api.global', '@apiclient.xyz', ..., '@push.rocks', '@serve.zone', ...]
--only, --skip and @git.zone/cli.format.modules.only/skip accept the module names cleanup, smartconfig, license, packagejson, templates, assets, gitignore, tsconfig, prettier, readme, copy and rustfmt; any other name is refused with that list. gitzone format --write writes nothing while the plan carries an error.
The template formatter owns .vscode/settings.json and .vscode/launch.json in every project, and the Gitea workflows, CLI entry points and HTML entry files of the project types that declare them. Workflow and entry files are written from the template as a whole. The html/index.html of a website loads its fonts from https://<module.assetbrokerUrl>/fonts/fonts.css and links https://<module.legalUrl> and its /privacy page; a wcc project's page loads the same fonts. Both keys live under @git.zone/cli.module in .smartconfig.json and hold a host (host[:port][/path], without https:// or a trailing slash), for example "assetbrokerUrl": "assetbroker.example.com". When a page needs a key that is missing, empty or not a host, the plan carries an error that names it, and the page is left alone. The formatter never creates or changes a Dockerfile or .dockerignore: they describe the project's own image. gitzone template service and gitzone template website scaffold a first pnpm-based Dockerfile.
The Gitea workflows verify and never publish. npm and wcc projects get default_nottags.yaml and default_tags.yaml, service and website projects docker_nottags.yaml and docker_tags.yaml. Each runs in code.foss.global/host.today/ht-docker-node:latest: a pinned checkout that keeps no credentials, then pnpm --version, pnpm install --frozen-lockfile, pnpm build, pnpm test and pnpm audit --prod --audit-level=high --registry=https://registry.npmjs.org. The audit fails the workflow when a production dependency has a high or critical advisory; development dependencies do not ship and are not audited. It asks registry.npmjs.org because a private registry may not serve the advisory endpoint, and it runs last, so a newly published advisory never hides a test result. pnpm 12 skips an advisory you have reviewed and accepted when pnpm-workspace.yaml lists its GHSA ID under auditConfig.ignoreGhsas. No setup action installs pnpm: the image's pnpm switches to the version that package.json pins in packageManager, pnpm 10 included, so the printed version is the one that installs, builds, tests and audits. The switch downloads that version from the npm registry, so the runner needs registry access, which the install needs anyway. They use no secrets and build no images; a tag workflow verifies the tagged commit, and gitzone release publishes packages and images. The two .vscode files are merged at the top level instead: every key the template declares is refreshed, and every other top-level key — an editor setting only your project has, such as deno.enable — is kept. Two limits follow from that. A key the template itself declares is taken as a whole, so an array such as json.schemas or configurations is replaced rather than unioned, and a merged file is re-serialized with two-space indentation, so a project that keeps its own key loses hand-made formatting in those two files. A .vscode file that is not valid JSON cannot be merged and is replaced by the template as a whole. That never happens silently: the plan line reads Replace .vscode/settings.json: the current file is not valid JSON (…) instead of the usual Apply template …, and the run prints a warning that keys only your project declares are lost. Both appear in gitzone format, gitzone format check and gitzone format plan --json, so you see them before confirming a write.
A projectType: "rust" repository is formatted for Cargo:
rustfmtrunsrsrepo fmt --check(rsrepo resolved as for releases: the project'snode_modules/.bin/rsrepowhenpackage.jsondeclares@git.zone/rsrepo, otherwisersrepoon PATH) and plans each file rustfmt would change;--writerunsrsrepo fmtonce. A failing check without a formatting diff, such as a missing rustfmt or an unparsable file, is a plan error. Other project types skip the module.- The
.gitignorestandard also ignores Cargo's root/target/. licensereports an error when the repository root carries no license text (license,license.md,LICENSE-MIT,COPYINGand the like, any case).packagejsonleaves apackage.jsonalone, andsmartconfigdoes not ask formodule.npmPackagename.
gitzone format fix intentionally lives outside the default format path. Normal format runs stay deterministic; the fix command uses opencode only after deterministic formatters have done what they can.
Development Services
gitzone services manages local Docker-backed services for development projects.
Supported services:
| Service | Lifecycle/log aliases |
|---|---|
| MongoDB (NoSQLDB engine) | mongo, mongodb |
| ObjectStorage (S3-compatible) | objectstorage, s3 |
| Elasticsearch | elasticsearch, es |
Service-selection commands such as set, enable, and disable also accept
elastic; lifecycle and log commands do not.
# Start configured services
gitzone services start
# Enable specific services non-interactively
gitzone services set mongodb,objectstorage
# Check status
gitzone services status
# Machine-readable status, including connection strings and data sizes
gitzone services status --json
# Print MongoDB Compass connection string
gitzone services compass
# Migrate legacy mongod data into NoSQLDB explicitly (start does it automatically)
gitzone services migrate mongodb --yes
# Show logs
gitzone services logs mongo 50
Service config is stored in .nogit/env.json. Newly created config files use
owner-only permissions; writes are atomic and reject a stale in-memory snapshot
rather than overwriting a concurrent change. MongoDB (NoSQLDB), ObjectStorage,
and Elasticsearch data is stored in .nogit/nosqldbdata,
.nogit/objectstoragedata, and .nogit/esdata, so it stays out of Git.
The mongodb service runs the NoSQLDB engine from the digest-pinned
code.foss.global/lossless.zone/database 2.0.0 image (@lossless.org/nosqldb
10.7.3) as <project>-nosqldb. It
speaks the MongoDB wire protocol, so the .nogit/env.json contract
(MONGODB_URL, MONGODB_HOST, MONGODB_PORT, MONGODB_USER, MONGODB_PASS,
MONGODB_NAME, MONGODB_AUTH_ENABLED) is unchanged and consumers need no code
change. The wire port is published as 127.0.0.1:MONGODB_PORT only. The
engine's management UI is not published; its password MONGODB_ADMIN_PASSWORD
is generated per project, never printed, and redacted from logs and
services config --json. Startup refuses the default admin passwords and an
admin password reused as MONGODB_PASS, hardens .nogit/nosqldbdata to the
engine user with mode 0700, recreates an owned container by immutable ID when
its image, port, data bind, DBST_* environment, restart policy, command,
entrypoint, user, or health check drifted, and waits for an authenticated
readiness round trip through @lossless.org/client/nosqldb. Transactions run on
the single node without a replica set.
ObjectStorage uses S3_PORT for its local S3 API and S3_UI_PORT for its
management UI. S3_REGION defaults to us-east-1; S3_ADMIN_PASSWORD is a
separate randomly generated password for the UI's admin user. Startup rejects
empty credentials, admin/admin S3 defaults, the default admin password, and
an admin password reused as either S3 credential. gitzone services config --json replaces those three ObjectStorage credential values with "***", as
it does the credential-bearing platform names listed under "serve.zone platform
environment" below. It prints every other field as stored, including the
MongoDB and Elasticsearch passwords and the credential-bearing MONGODB_URL and
ELASTICSEARCH_URL, so treat the complete output as sensitive.
GitZone runs a digest-pinned ObjectStorage image with both the S3 API and
management UI published on loopback. S3_HOST, S3_ENDPOINT, S3_USESSL, and
the other generic S3 consumer fields remain user-controlled; service startup
always reconciles the local managed instance through 127.0.0.1 and its
configured local ports.
Before creating or recreating an ObjectStorage container, or starting an owned
stopped container, GitZone uses one fenced root helper to restore recursive
ownership and set .nogit/objectstoragedata itself to mode 0700. An exact
container caught in Docker's restart loop is stopped by immutable ID before that
hardening and restart; if hardening fails, it remains safely stopped. An
already-running canonical container is not modified.
GitZone derives new bucket names from the project name using S3 naming rules.
When loading older configuration it repairs only the exact generated
<project>-documents value if that value is invalid; custom bucket names are
never rewritten. services start s3 validates the bucket and credentials,
recreates an owned container when its pinned image, ports, data bind, controlled
product environment, restart policy, command, entrypoint, user, or health check
drifted, waits for management readiness, then creates and verifies the bucket
through an authenticated S3 client. A GitZone-label mismatch is an ownership
conflict and blocks mutation rather than being treated as repairable drift.
Container setup, management readiness, and the S3 operations share one deadline;
SmartBucket cleanup has its own bounded close. Docker mutations use the
container's immutable ID after exact-name discovery.
Outside the explicit restart-loop recovery above, GitZone stops a pre-existing
container on failure only after that invocation's start returned successfully. A
newly created ID remains invocation-owned across an uncertain start and can be
stopped safely; an uncertain pre-existing start is left adoptable rather than
risking stopping another concurrent invocation. A canonical container created
but not yet started remains stopped for a safe retry.
serve.zone platform environment
.nogit/env.json also carries the environment names a serve.zone database
and objectstorage binding injects (see "Capability environment" in the
serve.zone app-integration guide), so a serve.zone app runs locally against
gitzone services without a hand-edited environment. They are derived from the
GitZone fields on every start, reconfiguration, and write; edit the GitZone
fields, never the derived names, which are overwritten. Other keys you add to
the file are preserved.
| Platform name | Value |
|---|---|
MONGODB_URI, MONGODB_URL, MONGO_URL |
one identical URI: mongodb://MONGODB_USER:MONGODB_PASS@MONGODB_HOST:MONGODB_PORT/MONGODB_NAME?authSource=admin&directConnection=true |
MONGODB_HOST, MONGODB_PORT |
the GitZone fields of the same name |
MONGODB_DATABASE, MONGO_DBNAME |
MONGODB_NAME |
MONGODB_USERNAME, MONGO_DBUSER |
MONGODB_USER |
MONGODB_PASSWORD, MONGO_DBPASS |
MONGODB_PASS |
AWS_ENDPOINT_URL |
the endpoint origin resolved from S3_ENDPOINT (see below), e.g. http://localhost:29000; the scheme's default port is omitted |
S3_ENDPOINT_HOST |
the host of that origin, as written in S3_ENDPOINT; an IPv6 address without brackets |
S3_PORT, S3_REGION, S3_BUCKET |
the GitZone fields of the same name |
S3_USE_SSL |
"true" or "false", the scheme of that origin |
AWS_REGION |
S3_REGION |
AWS_S3_FORCE_PATH_STYLE |
"true" |
S3_ACCESS_KEY, S3_ACCESS_KEY_ID, AWS_ACCESS_KEY_ID |
S3_ACCESSKEY |
S3_SECRET_KEY, S3_SECRET_ACCESS_KEY, AWS_SECRET_ACCESS_KEY |
S3_SECRETKEY |
Two values differ from the platform deliberately:
S3_ENDPOINTkeeps the value you set, by default the bare hostlocalhost, where the platform delivers the endpoint origin. ReadAWS_ENDPOINT_URLfor the origin; it means the same locally and on the platform.- The local MongoDB credential is the root user in
admin, so the URIs authenticate withauthSource=adminwhere the platform names the application database. The path names the database in both.MONGODB_URLgaineddirectConnection=truein the same release, matching the standalone NoSQLDB node; its value is otherwise unchanged.
S3_ENDPOINT is never rewritten, and every non-empty value loads. The origin
is resolved from it in one of three forms:
- a bare host or IP address (
localhost,::1): the scheme comes fromS3_USESSLand the port fromS3_PORT; host:portor[ipv6]:port(minio.local:9000): its port wins overS3_PORT, and the scheme comes fromS3_USESSL;- an origin,
http(s)://host[:port](https://s3.example.test): a complete origin, so its scheme wins overS3_USESSLand its port, or the scheme's default port, overS3_PORT.
The host is kept as written, so IPv4 shorthand such as 127.1 and IPv4-mapped
IPv6 addresses are not rewritten. User info, a path, a query, a fragment, a
scheme other than http/https, and a port outside 1-65535 are not part of an
origin and are not carried over; an endpoint without a host, such as :9000,
takes its host from S3_HOST. S3_PORT stays the managed ObjectStorage's local
API port: GitZone reconciles the local instance through 127.0.0.1 and that
port whatever S3_ENDPOINT says, so when S3_ENDPOINT carries its own port or
is an origin, S3_PORT is not the port of AWS_ENDPOINT_URL.
While MongoDB authentication is disabled, the URIs carry no credentials and the
four credential names (MONGODB_USERNAME, MONGO_DBUSER, MONGODB_PASSWORD,
MONGO_DBPASS) are absent; MONGODB_USER and MONGODB_PASS are kept for when
it is enabled again. gitzone services config --json replaces every platform
name that carries a credential (MONGODB_URI, MONGO_URL, the password pair,
and the six S3 key names) with "***"; MONGODB_URL and MONGODB_PASS keep
their earlier, unredacted output.
Migrating legacy MinIO state
Persisted minio and s3 service selections are rewritten to the canonical
objectstorage value. S3_CONSOLE_PORT is migrated to S3_UI_PORT when the
values do not conflict. The runtime migration also adds the separate admin
password and region fields and repairs only the exact invalid bucket name that
older GitZone versions generated. Legacy registry and data-marker entries are
retained as preserved migration evidence, not converted into active
ObjectStorage ownership. Every selected migration store is preflighted before
the first write. Closed-schema service selection, marker, and registry stores
reject malformed, conflicting, foreign, or future state without rewriting it.
Runtime config validates and migrates its known fields while preserving unknown
application fields.
If <project>-minio, a registry-retained alternate legacy container name, or
.nogit/miniodata still exists, ObjectStorage startup stops before mutating the
ObjectStorage container. In an untargeted services start, an earlier enabled
service such as MongoDB may already have started. GitZone never reuses, removes,
cleans, or prunes legacy MinIO state because its disk layout is not compatible
with ObjectStorage. Recovery is explicit:
- Export every required bucket with the existing MinIO tooling.
- Stop and rename the legacy container, then move
.nogit/miniodatato a preserved backup location. - Run
gitzone services start s3to create the canonical service. - Import through an external S3 client and verify the required objects before disposing of the backup.
An existing .nogit/objectstoragedata directory without a valid ownership
marker blocks creation of a new container and direct cleanup. One exception is
an existing container whose immutable identity and exact canonical GitZone
labels prove that directory belongs to this project; startup or removal repairs
the missing marker before proceeding. An invalid or foreign marker always
blocks mutation. Without that exact container proof, preserve or move the
directory, verify its provenance outside GitZone, start a fresh canonical
service, and import required objects through S3. Do not synthesize a marker for
unverified data.
Migrating legacy MongoDB (mongod) state
Releases before NoSQLDB ran mongo:7.0 as <project>-mongodb on
.nogit/mongodata. The persisted-state migrations move the registry's
containers.mongo claim to legacy.mongoContainer and the marker's mongod
directory to a safeToPrune: false legacy entry; neither is ever treated as
NoSQLDB ownership.
gitzone services start (and the explicit gitzone services migrate mongodb,
which asks first unless --yes is given) migrates the data automatically when
ownership is proven: the container carries the exact legacy GitZone labels for
this project (or has no GitZone labels and exactly one legacy registry claim),
runs mongo:7.0, and binds exactly .nogit/mongodata at /data/db. Data whose
container is gone is proven by the marker's legacy entry or a legacy registry
claim.
The migration journals every step in .nogit/.gitzone-mongodb-migration.json:
- Both images are present before anything is touched.
- Freeze: the legacy container is stopped (by immutable ID, with a 60-second clean-shutdown grace) before anything reads its data, so no application write can land after the snapshot.
- A temporary reader,
<project>-mongodb-migration-<token>, opens.nogit/mongodatawith the legacy container's exact image ID. It runs standalone (no--replSet, so the stored replica-set configuration is never consulted) withrecoverFromOplogAsStandalone, which replays the oplog like the replica-set member would and then keeps the node read-only; data that never ran as a replica set is opened as a plain standalone. The reader has no network, no published port, restart policyno, and no access control: only short-lived fenced helpers that join its network namespace can reach it. - From the reader, an inventory (every non-system namespace with document count
and index names) and a
mongodump --archiveare taken. MongoDB views are refused before the dump with an error naming every view: NoSQLDB has no general views. The reader is then removed. - The NoSQLDB container is created on a new
.nogit/nosqldbdatawith no network, no published port, and restart policyno, andmongorestorerestores everything exceptadmin.*,config.*, andlocal.*with--stopOnError. Credentials reach the tools only through the environment and a private config file, never argv. - The NoSQLDB inventory must match the legacy inventory exactly. The
restore-phase container is stopped, and only then does the journal move
from
runningtocommitting. - The restore-phase container is replaced by the published service container
(loopback port,
unless-stopped) on the same data, the legacy container is removed by immutable ID together with its anonymous/data/configdbvolume, and the journal recordscompleted. The bind-mounted.nogit/mongodatais never deleted. An interrupted commit resumes on the next start.
Invariant: while the journal is running, no application has ever been able to
reach the NoSQLDB data, so it contains only migration writes. Any failure
before the commit therefore rolls back safely: the reader and the NoSQLDB
container and its data directory are removed, the legacy container is started
again by its immutable ID if it was running, and the error carries the redacted
tool output. An interrupted migration is rolled back on the next start. Other
features NoSQLDB does not support (for example collations, text or geo indexes)
surface as restore or verification failures rather than being dropped.
Every attempt writes its evidence to its own
.nogit/mongodb-migration/<timestamp>-<token>/: the helper scripts
(inventory.js, readiness.js), the inventories (source-inventory.json,
target-inventory.json), the tool logs (source-inventory.log, dump.log,
target-readiness.log, restore.log, target-inventory.log), and
dump.archive. Each file exists only if its step ran. A rollback deletes only
dump.archive, a derived copy of data that stays in .nogit/mongodata, and
keeps the scripts, inventories, and logs of the failed attempt. After a
completed migration, the whole directory, dump.archive included, is kept.
.nogit/mongodata and .nogit/mongodb-migration/ are never deleted by any
GitZone command, including clean and prune. Remove them manually once the
migrated data is confirmed. The journal stays as a permanent completion record,
so a later clean never re-imports the old data. If ownership cannot be proven, startup
stops before any Docker mutation and prints the manual recovery steps. Projects
that never run the new CLI keep their running legacy container untouched.
gitzone services stop and stop -g may stop a proven-owned legacy container;
nothing else acts on it.
Consuming a service programmatically
gitzone services status --json emits only JSON on stdout, so a test suite or
script can read a live connection string without parsing human output:
gitzone services status --json | jq -r '.services.mongodb.connectionString'
The ObjectStorage status key is .services.objectstorage; the former
.services.minio key no longer exists. Preserved predecessor evidence is
reported separately under .legacy. minioDataDirectory, minioDataExists,
mongoDataDirectory, mongoDataExists, mongoMigrationDirectory, and
mongoMigrationDirectoryExists are always emitted; minioContainer and
mongoContainer are present only when container evidence exists. Consumers must treat this as a breaking response-schema change rather
than interpreting legacy evidence as an active service.
Cleanup levels
Cleanup is tiered, from fully resumable to irreversible:
| Command | Containers | Data | Notes |
|---|---|---|---|
gitzone services stop |
kept (stopped) | kept | fully resumable |
gitzone services remove |
removed | kept | resumable; --yes to skip the prompt |
gitzone services clean |
removed | removed | irreversible; needs a typed yes or --yes |
gitzone services prune |
see below | see below | machine-wide; dry run unless --apply |
clean and prune first persist an exact deletion intent, then atomically rename
the canonical directory to a tokenized sibling quarantine before deleting any
contents. Native deletion is attempted there. If container-owned files remain,
GitZone uses a short-lived root container scoped to that quarantine; Docker's
--privileged mode is not used. The helper reserves one deterministic name per
bind target, carries a random invocation label, is created stopped, and is
inspected by immutable ID before execution or cleanup. Old stopped helpers,
whether still created or already exited, can be recovered only after their
complete runtime envelope, bind target, age, and immutable ID are proven;
running, fresh, or noncanonical occupants block the operation.
A helper carries exactly one read-write bind of its target. Images such as
mongo:7.0 additionally declare their own VOLUME destinations, for which
Docker attaches anonymous volumes GitZone never requested; those are accepted
only where Docker proves them image-declared and anonymous, namely the local
driver, a random 64-character volume name, and a destination the container's own
image configuration declares outside the bind destination. A named volume, a
second bind, a read-only target bind, or a volume at any other destination
leaves the helper unproven and blocks the operation. Whenever a helper is proven
— after a successful run, when an old helper is reclaimed, and on the rollback
path of a failed run — it is removed together with its anonymous volumes; a
named volume is never removed this way. A helper whose closing inspection
refuses it or times out is left in place and reported as cleanup that could not
be confirmed; it keeps blocking the operation until a later run proves it.
If interruption or helper failure leaves bytes, the persisted intent
lets the next clean or prune --apply resume the same quarantine. A partial
failure therefore never leaves a corrupt directory at the canonical service
path. Startup refuses to create fresh service data while such an intent remains.
Service start, stop, container removal, and data removal share one interprocess lock per project and service. Startup refreshes runtime config and project activity inside that lock, while prune rechecks activity and stopped as well as running service containers after acquiring it. Concurrent lifecycle commands therefore serialize rather than deleting a directory or container while another command prepares, mounts, starts, or stops it.
Reclaiming space across projects
Service data is per project and survives container removal, so it accumulates.
gitzone services prune reports what every registered project holds and what
can be reclaimed. It is read-only unless --apply is passed:
# Report only: what exists, what is reclaimable, and why
gitzone services prune
# Change the inactivity threshold (default 30 days)
gitzone services prune --stale-days 90
# Actually reclaim, non-interactively
gitzone services prune --apply --yes
A project is only a candidate when there is positive evidence it is finished
with: its directory is gone, or it has been inactive past the threshold with no
container running. Anything ambiguous — an unlabeled container claimed by more
than one project, an unreachable Docker daemon, a directory still mounted by a
running container — is reported and skipped rather than reclaimed. Containers
are identified by an exact canonical GitZone label envelope. An unambiguous
registry claim is used only for older Elasticsearch containers with no
git.zone.* labels at all. NoSQLDB and ObjectStorage have no unlabeled
predecessor fallback, and their data is reclaimable only with marker proof;
partial, missing, or conflicting GitZone labels fail closed. Removal
revalidates and uses the container's immutable ID, never its mutable name, so
pruning cannot touch a same-name replacement. If Docker becomes unavailable,
registry claims are preserved as well as containers and data.
Legacy MinIO and mongod containers are reported as preserved migration
resources and are never prune candidates. A legacy MinIO registry reference is
kept permanently. A legacy mongod registry reference is dropped together with
its project's registry entry once the project directory is gone and no
container with that name remains, because it can then identify nothing; while
such a container exists, the entry is kept so the container stays identifiable.
.nogit/miniodata,
.nogit/mongodata, and .nogit/mongodb-migration are deliberately outside the
prune allowlist; current-project status reports their presence, while
machine-wide prune leaves them untouched.
MongoDB authentication
NoSQLDB runs with SCRAM-SHA-256 authentication enabled; the root user is
MONGODB_USER/MONGODB_PASS in the admin database. Authentication can be
disabled per project for
clients that cannot complete a SCRAM handshake:
gitzone services auth mongodb off
gitzone services start mongo
This is opt-in and never implicit. The database is always published on
127.0.0.1 only, and the combination of no authentication with a non-local
MONGODB_HOST is refused outright. Transactions continue to work, and
gitzone services status reports the mode. Changing the mode recreates the
container with the matching engine configuration.
The setting is recorded in .smartconfig.json, so it is committed and a fresh
clone or CI run reproduces it without any manual step:
{
"@git.zone/cli": {
"services": ["mongodb"],
"serviceOptions": {
"mongodb": { "auth": false }
}
}
}
serviceOptions is a sibling of services, never a richer services
value. services must stay a flat array of canonical lowercase strings because
@git.zone/tsdeploy derives a workload's requiredCapabilities from it and
rejects any other shape.
A committed declaration takes precedence over .nogit/env.json, so a stale
local file cannot silently diverge from what the repository declares. When
nothing is declared, an existing local value is preserved. When neither exists,
authentication is enabled. Because a declaration affects everyone who clones the
repository, services status states where the setting came from:
⚠️ Auth: DISABLED, declared in .smartconfig.json (applies to every checkout)
An older CLI that predates serviceOptions ignores the key and starts MongoDB
with authentication enabled — it degrades to the secure default, never the
insecure one.
Checking which version is running
gitzone --version prints the bare version on the first line, followed by the
path it resolved from. A stale copy in a legacy pnpm global root can otherwise
make it look like an older version is installed when it is not:
gitzone --version
# 3.2.1
# resolved from: /home/you/.local/share/pnpm/store/v11/links/@git.zone/cli/3.2.1/…
gitzone --version --json
# {"version":"3.2.1","resolvedFrom":"…"}
gitzone tools update also removes inert copies of managed packages left behind
in legacy global roots, provided the active root already supplies them and no
command shim still points there.
Approved Testing Domains
gitzone testing uses the testing API in dcrouter 19.1.1 or later. An operator
first enables an explicit testing-zone policy, provisions a machine registration,
and approves each exact hostname requested by that registration. Approved grants
survive credential rotation. Wildcards, parent domains, arbitrary TXT records,
and production routes are outside this command's authority.
Inject GITZONE_TESTING_BASE_URL and GITZONE_TESTING_TOKEN from your environment
or secret manager. Admin commands select GITZONE_TESTING_ADMIN_TOKEN instead.
The URL must be an HTTPS origin; plain HTTP is accepted only on loopback for
isolated tests. Credentials are never accepted as command arguments or saved to
project/global configuration. --token-stdin reads at most 8192 bytes from a
non-terminal stdin and requires the selected environment credential to be absent.
gitzone testing --help
gitzone help testing --json
gitzone testing whoami --json
gitzone testing zone list --limit 25 --json
gitzone testing grant request --zone ZONE_ID --hostname alice.testing.example.com --idempotency-key grant-alice-1 --json
gitzone testing grant status --grant GRANT_ID --json
gitzone testing cert ensure --grant GRANT_ID --idempotency-key cert-alice-1 --wait --json
gitzone testing cert status --job JOB_ID --json
Mutation keys are supplied explicitly. Repeat the same key and payload after a
lost response; use a new key only for a new operation. Certificate requests reuse
usable cached material. --wait follows queued/running/retry-wait jobs while
respecting their polling and retry times. --timeout-ms bounds acquisition and
requests (default five minutes, maximum one hour). A local deadline or signal
does not cancel durable server work. Poll the returned operation ID or repeat
the original idempotency key to find its outcome. Unknown or abandoned outcomes
require operator recovery and never trigger automatic mutation resubmission.
Run a development server with disposable certificate files:
gitzone testing cert run --grant GRANT_ID --idempotency-key cert-alice-1 -- node server.js --port 8443
The child receives GITZONE_TESTING_CERTIFICATE_FILE,
GITZONE_TESTING_PRIVATE_KEY_FILE, and GITZONE_TESTING_HOSTNAME. Its environment
excludes the parent's GITZONE_TESTING_* values, including both credentials.
The directory has mode 0700 and both files have mode 0600. GitZone invokes the
program directly, preserves every argument after --, returns its exit status,
and removes the directory after exit or termination. SIGINT/SIGTERM are forwarded
to the child; a child that does not stop is killed after five seconds. JSON output
is rejected for cert run because child stdout is inherited. Certificate renewal
is on demand: this command obtains material once; restart it to obtain renewed
material when due. It does not save a permanent certificate cache.
DNS operations are restricted to records owned by the approved grant. Use new
for creation and the current record revision for replacement/deletion:
gitzone testing dns upsert --grant GRANT_ID --record-key web-v4 --revision new --type A --value 192.0.2.10 --ttl 60 --idempotency-key dns-web-1 --wait --json
gitzone testing dns list --grant GRANT_ID --json
gitzone testing dns delete --grant GRANT_ID --record-key web-v4 --revision 1 --idempotency-key dns-delete-1 --wait --json
gitzone testing dns status --mutation MUTATION_ID --json
Admin enrollment and rotation require a caller-preopened writable non-terminal descriptor of at least 3. Supply it through the parent secret-manager integration; GitZone writes exactly the new token plus a newline to that descriptor, and only registration metadata to ordinary output. No credential can be retrieved again after a lost delivery; list registrations and rotate the relevant credential.
# Descriptor 3 must already be connected to the caller's secret store.
gitzone testing admin registration create --name developer-alice --expires-at 2027-01-01T00:00:00Z --idempotency-key enroll-alice-1 --secret-fd 3 --json
gitzone testing admin registration list --json
gitzone testing admin registration rotate --registration REGISTRATION_ID --generation 1 --expires-at 2027-01-01T00:00:00Z --secret-fd 3 --json
gitzone testing admin zone put --domain DOMAIN_ID --revision new --enabled true --record-types A,AAAA,CNAME --min-ttl 60 --max-ttl 3600 --max-grants 25 --json
gitzone testing admin grant list --state pending --json
gitzone testing admin grant review --grant GRANT_ID --revision 1 --action approve --reason 'Approved development hostname' --json
admin registration revoke and admin grant review --action revoke require a
revision and reason. Revocation blocks future access and schedules cleanup of
proven-owned DNS records; already delivered keys cannot be recalled. Operator
recovery remains revision checked and audited:
gitzone testing admin recovery list --kind certificate --state indeterminate --json
gitzone testing admin recovery status --kind certificate --operation JOB_ID --json
gitzone testing admin recovery action --kind certificate --operation JOB_ID --revision 3 --action recheck --reason 'Inspect interrupted issuance' --idempotency-key recovery-1 --json
For DNS recovery, --operation may identify a mutation or a grant's retained
cleanup operation. Only actions listed by the server are supported. Abandonment
retains unknown effects and their reservations; it does not make them safe to
repeat. Lists accept --after and --limit (25 by default, at most 100) and return
nextAfterId. Exit codes are 0 for success/admitted work, 1 for failure or an
unresolved terminal operation, 2 for invalid input, and 130/143 for interruption.
Templates
Start new projects with built-in scaffolds:
gitzone template npm
gitzone template service
gitzone template website
gitzone template wcc
gitzone template rust
Templates are rendered through SmartScaf and then can be normalized with gitzone format. The TypeScript templates (npm, service, website, wcc) develop with @git.zone/tsrepo: they declare it as their one development tool and call tsrepo build, tsrepo test, tsrepo bundle, tsrepo watch and tsrepo docs instead of tsbuild, tstest, tsbundle, tswatch and tsdoc (tsrepo watch asks for its preset, such as website or element, on the first run and stores it in .smartconfig.json); test files import tap from @git.zone/tsrepo/tapbundle, and a scaffolded cli.ts.js runs through @git.zone/tsrepo/run. The rust template scaffolds a Cargo workspace for @git.zone/rsrepo and gitzone releases; see Rust workspaces and the cargo target. It runs cargo generate-lockfile after writing the files, so commit the resulting Cargo.lock.
Meta Repositories
Use gitzone meta when one workspace coordinates multiple repositories.
gitzone meta init
gitzone meta add frontend https://example.com/org/frontend.git
gitzone meta update
gitzone meta remove frontend
Other Utilities
Docker resources
gitzone docker prune reports Docker containers and volumes created by
git.zone tooling, and reclaims them only when asked:
gitzone docker prune # report only
gitzone docker prune --apply # remove stopped tool-owned containers
gitzone docker prune --volumes # include tool-owned volumes in the report
gitzone docker prune --volumes --apply --yes
Scope is an allowlist: only resources labeled git.zone.tool=<tool> and
git.zone.safe-to-prune=true are ever considered. Anything unlabeled is
invisible to the command. Running containers and attached volumes are never
removed, and images are never removed at all. Volumes hold persisted data, so
they are excluded unless --volumes is passed and require a typed yes or
--yes on top of --apply.
This command deliberately cannot prune the whole machine. To do that, run
docker directly so the blast radius is explicit and yours.
Other utilities
# Open GitLab CI settings or pipelines for the current repo
gitzone open ci
gitzone open pipelines
# Deprecate an old npm package interactively
gitzone deprecate
# Preview an explicit deprecation across both registries
gitzone deprecate --package @example/old --replacement @example/new --registries https://registry.npmjs.org,https://mirror.example.test --plan
# Preview a deprecation for a package without a successor
gitzone deprecate --package @example/old --registries https://registry.npmjs.org,https://mirror.example.test --message "@example/old is no longer maintained; use the platform fetch API instead." --plan
# Prepare a project for local work
gitzone start
# Generate a short unique ID
gitzone helpers shortid
For explicit deprecation, replace --plan with -y to apply the reviewed
operation. --message supplies migration instructions; otherwise the message
names the replacement. A package without an honest successor is deprecated
without --replacement; --message is then required and should say what
users do instead, and the plan shows replacement: none (--json:
"replacement": null). Interactive mode accepts an empty replacement and then
asks for the message. Deprecation applies to every version, prereleases
included: GitZone passes pnpm deprecate the bare package name, never a range
such as *, which would skip prereleases. GitZone verifies
that the old package exists on all registries, and that the replacement, when
named, has an active latest version there, before issuing any change, invokes
pnpm deprecate with literal arguments, and verifies the old versions' metadata
after each registry operation. Every metadata read carries a unique cache-busting
query, because the CDN in front of npmjs ignores no-cache request headers.
Because npmjs serves an accepted deprecation only after replication, verification
waits up to ninety seconds per registry, probing with doubling backoff from two
seconds up to twenty and reporting each wait. If the window ends first, GitZone
stops before the next registry and says that rerunning is safe. A rerun skips any
registry where every version already has the exact requested message, so accepted
deprecations are not submitted again. --json prints the plan without changing it.
Developing GitZone
From this repository, cli.ts.js runs the TypeScript sources through the
project's @git.zone/tsrepo/run. cli.js runs the built dist_ts output. Child
GitZone processes retain the same entry point as their parent.
After committing and verifying a CLI correction on clean, integrated main,
maintainers can use the source entry point to release that correction when the
globally installed CLI is affected by the defect:
node cli.ts.js release --plan
node cli.ts.js release -y
This runs the configured release transaction, including its preflight and publication targets. The usual release authorization and checks still apply.
Troubleshooting
Format only previews changes:
gitzone format --write
Release says there is nothing to release:
# Make sure commits have populated the Pending changelog section
gitzone commit
Packing fails while reading an excluded local service-data directory:
Use a clean release checkout with dependencies installed from the committed
lockfile. pnpm's packlist traversal can visit local data before applying the
package's files list; .npmignore does not prevent that traversal when
files is present. Keep service data and its permissions intact.
Inspect the release journal before retrying. An installed journal must be
resumed with its original artifacts. A failure after the release tag but before
journal installation has not started final publication; gitzone release resume <version> -y prepares the journal from the recorded intent and publishes. A
later source correction can be released as a new version from the clean
checkout.
A release step fails on a Git, pnpm or tSDocker command:
The error names the step, then the command and how it ended, then the end of its error output on one line:
Unable to inspect main at the configured Git push destination. Step: publication state check. `git ls-remote` timed out after 180 s and was terminated by SIGTERM; gave up after 8 attempts over 1560.4 s
The retries bound each step, not the release: a remote that hangs rather than refusing can hold one
ls-remote step for about 26 minutes (8 × 180 s plus 120 s of waits) and one fetch or push for about
42 minutes (8 × 300 s plus waits, and a push re-reads the remote between attempts).
Every git ls-remote of a release — the preflight before the tests, the check
after them, the check before each publication step and the Git target's own
probe — runs under release.targets.git.remoteTimeoutSeconds (default 180, at
most 3600) per attempt; git fetch and git push keep their five-minute bound
per attempt. The preflight reads the remote before the configured tests run, so
a remote that is down fails the release before a long test run; the check
immediately before the push stays authoritative.
Transient remote failures are retried, so a Git server that drops connections
does not end a release. A timeout, an ssh connection closed by the remote host
(Connection to <host> closed by remote host., Connection closed by <address> port <n>, kex_exchange_identification: Connection closed by remote host) or
refused by it (ssh: connect to host <host> port <n>: Connection refused), a
reset, timed-out or unexpectedly ended connection (`Connection reset by
Docker services fail to start:
docker info
gitzone services status
gitzone services reconfigure
Config looks outdated:
gitzone config migrate 2
gitzone config show --json
License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the license file.
Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.