@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:

  1. Analyze the working tree.
  2. Suggest commit type, scope, and message.
  3. Write a human-readable entry into changelog.md under ## Pending.
  4. Stage and create one semantic source commit.
  5. 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:

  1. 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 a package.json also proves here that the source commit carries a pnpm-lock.yaml its checkout can install with --frozen-lockfile. An npm release then reads every registry's dist-tags for every package it publishes: latest never moves downwards, and a line never takes latest (step preflight.npmDistTags).
  2. Read changelog.md ## Pending entries and infer or accept a semver bump, raised to release.versionFloor when that is higher.
  3. Run configured tests.
  4. With --merge, fast-forward and lease-push main before release metadata is created.
  5. Update version files and baked commit info.
  6. Move pending changelog entries into the new version section.
  7. 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).
  8. 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.
  9. 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.
  10. 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.
  11. 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.
  12. 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.
  13. 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. Run pnpm build yourself 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 files globs 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 applying files, and the checkout does not contain that data.
  • A release commit that carries a package.json must install from its own committed lockfile. Preflight proves that before anything is created: the source commit has to carry a tracked pnpm-lock.yaml, and pnpm 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 run pnpm install and commit it, then release again. The version bump only changes version, which never changes a dependency specifier, so a lockfile accepted in preflight is still accepted in the release commit. A project with both a package.json and a deno.json is an npm project here and owes the same proof.
  • A Deno-only project — a commit with a deno.json and no package.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's deno.lock or import map. The plan of such a release has no preflight.lockfile step. Preflight and the checkout both read the release commit rather than the working folder, so an ignored package.json never 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 .npmrc is 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> — what gitzone config doctor suggests 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 with gitzone release recover <version>, and other configurations of that age delete the tag and drop the release commit (git tag -d v<version> and git reset --hard HEAD^ on main) and release again. Confirm first that HEAD is the release commit — git log -1 --format=%s prints v<version> — so that a commit made on top of it is never dropped.
  • The --plan step list names the isolated work: preflight.lockfile for a project with a package.json, then core.releaseCheckout, core.releaseCheckout.build, then core.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.line takes exactly branch and distTag. The branch is a canonical branch name other than main. The dist-tag is lowercase letters and digits joined by hyphens and starts with a letter; it is never latest and never a semver range such as v4 or x, which npm refuses as a tag.
  • The release must run on the branch the line names. On any other branch, main included, the release stops before anything is created. A branch without release.line still releases only main.
  • 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 enabled docker or giteaAssets target is refused, and so are --merge and gitzone release recover. A line release records its intent, so gitzone release resume <version> -y continues it.
  • A line keeps its major. A Pending ### Breaking Changes section, --major or a release.versionFloor that 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 latest with 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 terminal dist-tag-conflict and nothing is published.
  • After the upload the line's tag must name the version, or a later version of the same major, and latest must still carry a higher major. A line whose version took latest is a terminal dist-tag-conflict. gitzone never writes dist-tags: the error names the exact command that restores latest, pnpm dist-tag add <package>@<previous latest> latest --registry=<registry>, where the previous latest is the version the release recorded when it claimed the upload.
  • release.targets.npm.alreadyPublished applies unchanged: identical bytes already published, with the line's tag on them and latest still a higher major, verify without an upload under success and stop under error. 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, resume and recover) qualifies from pnpm view --help in 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 resume and recover — pnpm whoami --registry=<registry> must succeed for every private destination. Otherwise the release stops with NpmRegistryAuthenticationError, which names the registry, the login step and the command to run next, and quotes the redacted pnpm whoami failure.
  • A destination is verified when pnpm view <name>@<version> --json reports the packed artifact's dist.integrity and dist.shasum, a dist.tarball on 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 whoami confirms the authentication, because a registry answers an unauthenticated read of a private package with 401, 403 or 404 alike. whoami proves 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 publish makes the publish command fail, and the target is recorded as failed like any rejected publish. Fix the credential and run gitzone release resume <version> -y.
  • A scope registry in an .npmrc (@scope:registry=…) overrides --registry for 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.json descriptor that names a package, in tspublish's dependency and order sequence;
  • then the root package, unless package.json sets "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_ENDPOINT keeps the value you set, by default the bare host localhost, where the platform delivers the endpoint origin. Read AWS_ENDPOINT_URL for 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 with authSource=admin where the platform names the application database. The path names the database in both. MONGODB_URL gained directConnection=true in 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 from S3_USESSL and the port from S3_PORT;
  • host:port or [ipv6]:port (minio.local:9000): its port wins over S3_PORT, and the scheme comes from S3_USESSL;
  • an origin, http(s)://host[:port] (https://s3.example.test): a complete origin, so its scheme wins over S3_USESSL and its port, or the scheme's default port, over S3_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:

  1. Export every required bucket with the existing MinIO tooling.
  2. Stop and rename the legacy container, then move .nogit/miniodata to a preserved backup location.
  3. Run gitzone services start s3 to create the canonical service.
  4. 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:

  1. Both images are present before anything is touched.
  2. 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.
  3. A temporary reader, <project>-mongodb-migration-<token>, opens .nogit/mongodata with the legacy container's exact image ID. It runs standalone (no --replSet, so the stored replica-set configuration is never consulted) with recoverFromOplogAsStandalone, 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 policy no, and no access control: only short-lived fenced helpers that join its network namespace can reach it.
  4. From the reader, an inventory (every non-system namespace with document count and index names) and a mongodump --archive are taken. MongoDB views are refused before the dump with an error naming every view: NoSQLDB has no general views. The reader is then removed.
  5. The NoSQLDB container is created on a new .nogit/nosqldbdata with no network, no published port, and restart policy no, and mongorestore restores everything except admin.*, config.*, and local.* with --stopOnError. Credentials reach the tools only through the environment and a private config file, never argv.
  6. The NoSQLDB inventory must match the legacy inventory exactly. The restore-phase container is stopped, and only then does the journal move from running to committing.
  7. 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/configdb volume, and the journal records completed. The bind-mounted .nogit/mongodata is 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

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.

S
Description
the main git.zone cli
Readme
20 MiB
Languages
TypeScript 98.3%
JavaScript 1%
Shell 0.5%
HTML 0.2%