@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
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 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 never takeslatest(steppreflight.npmDistTags). - 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 version files and 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 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.
- 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 is skipped when the project has no tstest installed, and a project opts out with
{"@git.zone/tstest": {"cleanup": {"onRelease": false}}}in.smartconfig.json. 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. A project with both apackage.jsonand adeno.jsonis an npm project here and owes the same proof. - A Deno-only project — a commit with a
deno.jsonand nopackage.json— neither needs a pnpm lockfile nor installs anything: its checkout runs the configured build and test commands against the committed tree, and pinning dependencies stays the project'sdeno.lockor import map. The plan of such a release has nopreflight.lockfilestep. Preflight and the checkout both read the release commit rather than the working folder, so an ignoredpackage.jsonnever makes a release demand a lockfile its checkout would not install; in that one case the plan, which reads the working folder, still lists the step, and the step then does nothing. - 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.packNpmArtifact.
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
# 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, 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 |
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 |
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 or deno.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. 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 or deno.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 such as "32.0.0-rc.1"; GitZone releases only canonical versions |
noncanonical-version |
A floor is configured but the current package.json or deno.json version is not a canonical x.y.z version to compare against |
current-version-changed |
The release checkout's package.json or deno.json version differs from the version the release was resolved from |
gitzone config show lists the configured floor.
Maintenance lines
A maintenance line releases an older major from its own branch under its own
npm dist-tag, so that latest never moves. 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 is lowercase letters and digits joined by hyphens and starts with a letter; it is neverlatestand 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 only to Git and npm. Docker targets promote aliases such as
latest, and Gitea marks its newest release as the latest one, so a line with an enableddockerorgiteaAssetstarget 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.- 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.
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.
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
tstest 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
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.
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.
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. Assets are uploaded in manifest order. Put corresponding source and relinking materials before native executables, since public Gitea attachments also distribute those binaries.
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 public, non-draft, non-prerelease tag release.
Every upload has a revisioned journal attempt. Each accepted attachment must
download anonymously with the expected size and SHA256; authenticated redirects
are rejected and anonymous downloads never carry the token. 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 |
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 npm. 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. 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.
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. Any 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:
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.
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.
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
},
"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.
Managed Assets
Projects can opt into generated, updateable repository assets through @git.zone/cli.assets. The first supported kind is denoBinaryCli, which manages installer scripts, npm binary wrappers, postinstall downloaders, and Gitea release workflows for Deno-compiled CLI binaries.
{
"@git.zone/cli": {
"schemaVersion": 2,
"assets": {
"schemaVersion": 1,
"kind": "denoBinaryCli",
"cliName": "onebox",
"displayName": "Onebox",
"repository": {
"host": "code.foss.global",
"path": "serve.zone/onebox",
"branch": "main"
},
"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,
"assetGlobs": ["dist/binaries/*", "dist/package/*"]
}
}
},
"@git.zone/tsdeno": {
"compileTargets": [
{
"name": "onebox-linux-x64",
"entryPoint": "binary/onebox.ts",
"outDir": "dist/binaries",
"target": "x86_64-unknown-linux-gnu",
"permissions": ["--allow-all"],
"noCheck": true,
"selfExtracting": true
},
{
"name": "onebox-linux-arm64",
"entryPoint": "binary/onebox.ts",
"outDir": "dist/binaries",
"target": "aarch64-unknown-linux-gnu",
"permissions": ["--allow-all"],
"noCheck": true,
"selfExtracting": true
}
]
}
}
@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.
Set installer.distribution to "releaseAsset" when the installer itself should be published and documented as a Gitea release asset instead of a raw-branch file. That mode automatically stages install.sh into the release artifact directory unless releaseWorkflow.includeInstallerAsset is set explicitly.
Set installer.service.removeLegacyUnits to a list of systemd unit names when an installer must disable and remove older service units during upgrades.
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.1.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.
The image's Deno CLI installs sealedRelease.denoVersion with deno upgrade --force.
The next command verifies the effective compiler version before dependency installation;
this setup does not require a separate Deno action.
{
"@git.zone/cli": {
"assets": {
"schemaVersion": 1,
"kind": "tspackRelease",
"sealedRelease": {
"denoVersion": "2.9.4",
"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. 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.
The normal authorized gitzone release -y pushes the tag that triggers this CI flow.
The runner must support Gitea's v4 artifact protocol through the stock artifact
actions: use Gitea Runner 3.3.2 or later with its cache/results service enabled and
reachable from job containers, and keep runner.patch_actions enabled. The pinned
actions run on Node.js 24. Follow the Gitea runner upgrade guide
when upgrading an older runner; legacy v3 artifacts are absent from Gitea's REST
artifact inventory and cannot satisfy this workflow's retention check.
Before any release mutation, the workflow retains the complete output root for 90 days using Gitea's v4 Actions artifact protocol, downloads it into a fresh directory, checks the component's reuse result, and verifies every archive through TsPack. The remote tag must match the sealed source commit. Publication creates a draft, downloads and checks existing attachments, uploads only missing files, and publishes after complete readback. It never replaces an attachment or deletes a release. HTTPS object-store redirects do not receive the Gitea job token.
Retry the original Gitea run after an interruption. A retained set from another run identifies the run to resume. Expired or missing retention, conflicting attachments, unexpected API responses, and changed source identities stop before publication. Keep the original artifact until publication is verified; if its retention has expired, recover its exact bytes before retrying. Rerunning an already published release only verifies its existing attachments.
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, Deno dependency-age exclusions, 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: @git.zone/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 deno formatter keeps Deno's minimum dependency age (24 hours by default since Deno 2.9) from holding back our own freshly published packages. When the project has a root deno.json or deno.jsonc, it makes minimumDependencyAge.exclude contain npm:<scope>/* for every owner scope and sets the owner's age when the project sets none (list shortened here):
{
"minimumDependencyAge": {
"age": "P7D",
"exclude": ["npm:@api.global/*", "npm:@git.zone/*", "npm:@push.rocks/*", "npm:zod"]
}
}
The owner scopes and the owner's age are exported constants, so no repository keeps a hand-copied list:
import { ownerMinimumDependencyAge, ownerNpmScopes } from '@git.zone/cli';
// ownerNpmScopes: ['@api.global', '@apiclient.xyz', ..., '@push.rocks', '@serve.zone', ...]
// ownerMinimumDependencyAge: 'P7D'
Project-specific entries stay, and the list is deduplicated and sorted. ownerMinimumDependencyAge is 7 days and mirrors the owner's pnpm minimumReleaseAge of 10080 minutes, so third-party packages wait as long under Deno as under pnpm (Deno's own default is 24 hours). It is written only when the age is missing or null; an age the project sets is kept as it is, false and 0 included. A scalar "minimumDependencyAge": "P1D" becomes { "age": "P1D", "exclude": [...] }, and the object always lists age before exclude. Deno reads the setting from the workspace root only, so workspace members are never written: a member that sets it is reported as a warning, and a run inside a member plans nothing. A project without a Deno config gets none. A config that is not plain JSON (comments, trailing commas) is reported as an error and left untouched, so its comments are never dropped.
Tools must never pass --min-dep-age or --minimum-dependency-age to Deno: the flag replaces the whole setting and discards the exclusions in deno.json. .npmrc (min-release-age) and NPM_CONFIG_MIN_RELEASE_AGE set only the age; the exclusions stay in effect.
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 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.
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
runtimes whose node:crypto cannot complete a SCRAM handshake (notably Deno):
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
Templates are rendered through SmartScaf and then can be normalized with gitzone format.
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. 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.
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 3 attempts over 565.2 s
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. A timeout, a reset or dropped connection, and a transport failure Git reports
as "RPC failed" (other than HTTP 401, 403 or 404) are retried twice, after 5 and 20 seconds;
authentication failures, a missing repository and Git's own answers are final
at once. The error names the release step, the per-attempt bound, and the
attempts and time spent. 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. A failure after the
release tag is resumable with gitzone release resume <version> -y. git fetch and git push keep their five-minute bound. Check the remote with the
same command, for example git ls-remote <push-url> refs/heads/main, and
retry once it answers promptly. An exit code comes with Git's own message,
such as does not appear to be a git repository for a wrong push URL. The
quoted output is shortened to its last 500 characters, and credentials in URLs
(https://user:password@host becomes https://***@host), npm _authToken
settings and Authorization headers are replaced 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.