This document covers the maintainer-only workflows for synchronizing the
upstream runtime, building the release tarball, and publishing releases. End
users do not need any of this — see the main README for
installation and usage. GitHub Releases are the only distribution channel: the
package is named zcode-cli and nothing is published to npm.
The same path used by CI can be run locally:
brew install sevenzip
bun run sync -- --platform linux --arch x64Use the committed artifact instead of the latest updater manifest when rebuilding a reviewed release:
bun run sync:lockedThe synchronization command:
- reads the public stable-channel manifest used by ZCode Desktop, with the static CDN manifest as a fallback only when the service response is unavailable or invalid;
- downloads the matching installer;
- verifies its SHA-512 from the manifest;
- extracts
resources/glm; - builds and injects the local
@zcode/tuiadapter; - validates the official CLI version;
- records provenance in
vendor/extraction.json; - records the remote artifact URL and SHA-512 in
zcode-runtime.lock.json; - aligns the package version prefix with the ZCode App version while preserving the independently incremented CLI build revision.
The distributed package is controlled by the files allowlist in
package.json. It contains only:
bin/zcode.js, the bundled executable Node.js launcher;vendor/, the verified officialzcode.cjsruntime, official bundled plugins and the compiled local@zcode/tuiadapter;config.example.jsonandzcode-runtime.lock.json;README.md,LICENSEand the requiredpackage.json.
Tests, GitHub workflows, build scripts, launcher/TUI TypeScript sources, local
config, .release/ artifacts and development node_modules are not packaged.
Installation pulls only the declared pi-tui runtime dependency. The launcher
and TUI are compiled to JavaScript with tsdown; its launcher banner adds the
Node.js shebang directly, with no post-build rewrite. The compiled TUI is
injected into
vendor/ before publication.
Package versions use <app-version>-<build>, for example 3.3.5-2. The prefix
tracks the upstream ZCode App. The globally increasing build revision tracks
fixes and features in this project. Do not use 3.3.5+build.2: SemVer ignores
+build metadata when comparing upgrades.
Increment the project revision before publishing a local fix or feature:
bun run version:buildThe release workflows normally perform that increment for you. The command is also available for local inspection and exceptional manual preparation.
Publishing is split into two workflows so the committed version, the tarball, the Git tag and the GitHub Release all describe the same release:
.github/workflows/prepare-release.ymlextracts and validates the current official runtime, then opens or updates a Release PR containing the exactpackage.jsonversion andzcode-runtime.lock.jsonbuild input;- a maintainer reviews and merges that PR;
.github/workflows/publish.ymlchecks out its merge commit, rebuilds the exact locked runtime, audits and install-tests the tarball, then createsv<version>and the corresponding GitHub Release with the tarball asset.
The generated vendor/ directory remains ignored by Git and is rebuilt in both
workflows. Its updater URL and SHA-512 are committed in
zcode-runtime.lock.json. Preparation resolves the latest manifest; publishing
downloads the committed URL and verifies the locked SHA-512, so a later upstream
update cannot silently change a reviewed release.
The scheduled workflow follows the Desktop stable channel (channel=1) without
credentials or a personal device_mid. Static latest-*.yml files can lag the
service and are therefore recovery inputs, not the primary update signal. If
upstream rolls its stable channel back, synchronization keeps a newer committed
lock instead of silently downgrading it; adopting a rollback requires an
explicit maintainer review of zcode-runtime.lock.json.
The preparation workflow checks upstream once per day at 01:30 in the
Asia/Shanghai timezone, in upstream mode. GitHub documents scheduled
triggers as best effort:
under high Actions load a scheduled event can still be delayed or dropped. The
preparation itself is idempotent:
the fixed release branch is created or updated only when the runtime lock or
package version changes. A same-version upstream repack increments the global
build so the GitHub Release still carries an immutable new version. From the
Actions page, run Prepare ZCode CLI release with one of these modes:
cliincrements the global build and also aligns with the latest App;upstreamchecks for an App update without incrementing the build.
The modes use release/zcode-cli and release/zcode-upstream respectively. If
both PRs are open, merge one and rerun the other preparation mode so its version
is recalculated from the new main branch.
The workflow also runs a least-privilege keepalive job on scheduled events. It calls GitHub's workflow-enable API instead of creating dummy commits, which prevents the public-repository 60-day inactivity rule from disabling this schedule. This cannot eliminate platform-wide outages or dropped events; for a hard delivery deadline, use an external scheduler or run the workflow manually:
gh workflow run prepare-release.yml --ref main -f kind=upstream
gh workflow enable prepare-release.ymlMerging either Release PR publishes automatically. Publish ZCode CLI
release can also be started manually for recovery. Its publish checkbox can
be disabled to run all validation and consistency checks without changing Git
tags or GitHub Releases. Tag creation and GitHub Release creation are
independently idempotent, so a partially completed run can be retried safely.
Local packaging uses the same commands as the publishing workflow. Start from the exact clean commit whose version will be published:
bun install --frozen-lockfile
bun run release:build
git diff --exit-code -- package.json zcode-runtime.lock.json
bun run release:packrelease:build runs TypeScript checking and all tests, downloads the artifact
from zcode-runtime.lock.json, verifies its SHA-512, builds and injects the TUI,
then runs runtime and PTY smoke tests. release:pack runs the offline
prepack guard, creates .release/zcode-cli-<version>.tgz, audits every
included path and executable mode, installs it into a temporary directory, and
runs the installed zcode --version. Its final size, integrity and file count
are written to .release/release.json.
Publishing is tag + GitHub Release only (for example via the /release skill:
annotated v<version> tag, git push origin <tag>, gh release create with
the .release/zcode-cli-<version>.tgz asset); nothing reaches npm. Delete the
uploaded tarball afterwards — it is the audited preview and install-test
artifact and remains archived on the Release.
The README install URLs use
releases/download/v<version>/zcode-cli-<version>.tgz — a tag-pinned URL that
always resolves to that version's asset and never breaks when a newer Release
becomes latest. Update the three READMEs' install URLs together with the
version bump (the URL is only downloadable once that Release exists). Do not
use releases/latest/download/...: the latest pointer moves on every publish
while the asset name carries the version, so historical links go 404.
Synchronization preserves the build when the upstream App version changes. For
example, syncing 3.3.5-12 against ZCode App 3.4.0 produces 3.4.0-12.
Before enabling publication:
- confirm redistribution rights for the extracted ZCode runtime;
- under the GitHub repository's Settings → Actions → General, enable Allow GitHub Actions to create and approve pull requests;
- prepare a
clirelease and merge its Release PR to verify a tag + GitHub Release publication with the tarball asset.
The publisher runs on a GitHub-hosted runner with Node 24. It refuses to reuse a tag that points at another commit and attaches the tarball asset to an existing Release when one is already present.