How a Zenith release goes out. A release is a v* tag pushed by a person; nothing publishes on its own.
Every command here is for Windows PowerShell 5.1, and each block runs on its own. Unless a block says
otherwise, run it from the repository root.
One tag starts three workflows:
| workflow | what it publishes | where |
|---|---|---|
docs-publish.yml |
the docs site, the web app and the JSON Schemas, as one Pages artifact | https://libraries.horizon36596.org/zenith/, /zenith/app/, /zenith/schema/v1/<kind>.json |
release.yml |
Zenith-Setup-<version>.exe, Zenith-Portable-<version>.exe and SHA256SUMS.txt, with the notes from CHANGELOG.md |
https://github.com/Horizon-36596/zenith/releases |
npm-publish.yml |
the seven @horizon36596/zenith-* packages, with provenance |
https://www.npmjs.com/org/horizon36596 |
The fourth channel, the robot runtime on JitPack, builds on demand from the tag (see The robot runtime on JitPack).
All three workflows fail before building anything when the tag does not match the version in the root
package.json, and release.yml also fails when CHANGELOG.md has no section for that version.
The public Horizon-36596/zenith starts from one commit: a snapshot of the private development
repository, built from an explicit allowlist and scrubbed. These steps happen once. Steps 1, 2 and 13
run in the private development repository; the rest run anywhere with gh signed in as an owner of
the Horizon-36596 organisation.
In the development repository, on the commit being released:
pnpm release:checkIt makes the public snapshot, scrubs it (Horizon robot specifics, private repository names, local
paths, personal names and emails, secrets), then installs, builds, tests and builds the Pages artifact
inside the snapshot with nothing else on hand. The last line must read release-check: ready. Any
FAIL line is a stop.
A fresh snapshot of the same commit, which is what gets pushed. The one release:check built in has
node_modules and build output in it.
Remove-Item -Recurse -Force "$env:TEMP\zenith-public" -ErrorAction SilentlyContinue; node scripts/public-snapshot.mjs "$env:TEMP\zenith-public"; if ($?) { node scripts/scrub-check.mjs "$env:TEMP\zenith-public" }The last line must read scrub: clean.
The name zenith is needed for the public repository.
gh repo rename zenith-dev --repo Horizon-36596/zenith --yesGitHub redirects the old name only until a new repository takes it, which step 4 does. Point the development clone at the new name now:
git remote set-url origin https://github.com/Horizon-36596/zenith-dev.git; if ($?) { git fetch origin }gh repo create Horizon-36596/zenith --public --description "Plan an FTC autonomous on the real field, check it, and run it on the robot without editing Java. Horizon (FTC 36596)." --homepage "https://libraries.horizon36596.org/zenith/"The topics, Discussions (the issue template links to them) and private vulnerability reporting (what
SECURITY.md tells people to use):
gh repo edit Horizon-36596/zenith --add-topic ftc --add-topic first-tech-challenge --add-topic robotics --add-topic path-planning --add-topic autonomous --add-topic pedro-pathing --add-topic solverslib --add-topic mcp --add-topic typescript --add-topic java --enable-discussionsgh api -X PUT repos/Horizon-36596/zenith/private-vulnerability-reportingThe commit's author is set explicitly, so the name and address are a choice rather than whatever the
machine's git config holds. Both are public for good. Set $authorName and $authorEmail on the
first line; the block refuses to run until the address is changed.
$authorName = 'Horizon (FTC 36596)'; $authorEmail = 'CHANGE-ME'; if ($authorEmail -eq 'CHANGE-ME') { Write-Error 'Set $authorEmail to the address the first commit should carry.' } else { Set-Location "$env:TEMP\zenith-public"; git init -b main; git add -A; git -c user.name="$authorName" -c user.email="$authorEmail" commit -m "chore: release Zenith 0.1.0"; if ($?) { git remote add origin https://github.com/Horizon-36596/zenith.git; git push -u origin main } }If the push is refused with GH007: Your push would publish a private email address, the address is
marked private on GitHub (Settings, Emails, "Block command line pushes that expose my email"). Either
untick that setting, or use the account's users.noreply.github.com address shown on the same page.
Check what was pushed: one commit, the Horizon author, and the README rendered with the wordmark.
gh api repos/Horizon-36596/zenith/commits --jq '.[] | .commit.author.name + " <" + .commit.author.email + "> " + .commit.message'Source GitHub Actions. The Custom domain field stays empty: the landing repository
Horizon-36596/Horizon-36596.github.io owns libraries.horizon36596.org, and a project site with no
domain of its own is served under it at the repository's name, /zenith/. Setting a domain here would
take the domain root from every library.
gh api -X POST repos/Horizon-36596/zenith/pages -f build_type=workflowIn the UI: Settings, Pages, Build and deployment, Source: GitHub Actions. Leave Custom domain
empty. Do not add a CNAME file.
Turning Pages on creates a github-pages environment that allows the default branch only. A tag is
not a branch, so without this the tag's deploy is rejected before any step runs, with Tag "v0.1.0" is not allowed to deploy to github-pages due to environment protection rules. That message is an
annotation on the run, not a line in a log.
gh api -X POST repos/Horizon-36596/zenith/environments/github-pages/deployment-branch-policies -f "name=v*" -f "type=tag"In the UI: Settings, Environments, github-pages, Deployment branches and tags, Add rule, ref type
Tag, pattern v*. Check it:
gh api repos/Horizon-36596/zenith/environments/github-pages/deployment-branch-policies --jq '.branch_policies[] | .type + " " + .name'The list must include tag v*.
On https://www.npmjs.com/: create the organisation horizon36596 (free for public packages), then
a granular access token with Read and write on the @horizon36596 scope's packages and an
expiry you will remember. Then store it as the NPM_TOKEN secret. gh asks for the value and does
not echo it:
gh secret set NPM_TOKEN --repo Horizon-36596/zenithIf the secret was already set before step 3, it went with the rename to zenith-dev. The public
repository needs its own copy (the command above), and the development repository should not keep
one, because it carries the same publish workflow and a v* tag pushed there would publish from it:
gh secret delete NPM_TOKEN --repo Horizon-36596/zenith-devIn the public clone from step 5:
Set-Location "$env:TEMP\zenith-public"; git tag v0.1.0; if ($?) { git push origin v0.1.0 }gh run list --repo Horizon-36596/zenith --event push --limit 6Each one, until it finishes; a non-zero exit means it failed:
gh run watch --repo Horizon-36596/zenith --exit-status $(gh run list --repo Horizon-36596/zenith --workflow docs-publish.yml --limit 1 --json databaseId --jq '.[0].databaseId')gh run watch --repo Horizon-36596/zenith --exit-status $(gh run list --repo Horizon-36596/zenith --workflow release.yml --limit 1 --json databaseId --jq '.[0].databaseId')gh run watch --repo Horizon-36596/zenith --exit-status $(gh run list --repo Horizon-36596/zenith --workflow npm-publish.yml --limit 1 --json databaseId --jq '.[0].databaseId')A deploy job with zero steps and no log is the environment rule in step 7; read the annotation in the Actions tab.
Open https://jitpack.io/#Horizon-36596/zenith, find v0.1.0 under Releases or Tags, and press
Get it. Or ask for the POM, which starts the build and waits for it (a few minutes):
Invoke-WebRequest -UseBasicParsing -TimeoutSec 900 https://jitpack.io/com/github/Horizon-36596/zenith/v0.1.1/zenith-v0.1.1.pom | Select-Object StatusCodeThe robot runtime on JitPack says what a good build log looks like.
The docs site answers, and its footer carries the version:
$page = Invoke-WebRequest -UseBasicParsing https://libraries.horizon36596.org/zenith/; "$($page.StatusCode) footer has 0.1.0: $($page.Content.Contains('Zenith 0.1.0'))"The web app answers and loads its assets from under /zenith/app/. Open it in a browser afterwards
and press Load example; the field picture should draw.
$app = Invoke-WebRequest -UseBasicParsing https://libraries.horizon36596.org/zenith/app/; "$($app.StatusCode) assets under /zenith/app/: $($app.Content.Contains('/zenith/app/assets/'))"Each JSON Schema is served at its own $id:
foreach ($kind in 'auto', 'robot', 'field', 'waypoints', 'link') { $url = "https://libraries.horizon36596.org/zenith/schema/v1/$kind.json"; $id = (Invoke-RestMethod $url).'$id'; "$kind $($id -eq $url)" }The npm packages are at the new version:
foreach ($name in 'schema', 'core', 'season-biobuzz', 'seasons', 'github', 'cli', 'mcp') { "@horizon36596/zenith-$name $(npm view "@horizon36596/zenith-$name" version)" }The Release has its three files, and the checksums match the downloads:
Remove-Item -Recurse -Force "$env:TEMP\zenith-release" -ErrorAction SilentlyContinue; gh release download v0.1.0 --repo Horizon-36596/zenith --dir "$env:TEMP\zenith-release"; if ($?) { Get-Content "$env:TEMP\zenith-release\SHA256SUMS.txt" | ForEach-Object { $hash, $file = $_ -split ' '; "$file $((Get-FileHash -Algorithm SHA256 "$env:TEMP\zenith-release\$file").Hash -eq $hash)" } }The JitPack build log ends in success:
(Invoke-WebRequest -UseBasicParsing https://jitpack.io/com/github/Horizon-36596/zenith/v0.1.0/build.log).Content -split "`n" | Select-String 'BUILD SUCCESSFUL', 'FAILED'Once the public repository is the one work goes into. An archived repository is read-only, so do this after the last change you want in its history.
gh repo archive Horizon-36596/zenith-dev --yesThe landing page is index.html in Horizon-36596/Horizon-36596.github.io. Add this card inside
<main>, alongside the other library cards:
<a class="library" href="/zenith/" aria-label="Zenith - read the documentation">
<h3>Zenith</h3>
<p>
Plan an FTC autonomous on the real field, see whether it will work, and run it on the robot
without editing Java. Draw paths and commands in the editor, on the web or on Windows, or let an
agent edit the same file over MCP; every change is checked against the field, the game rules
and the 30 second period.
</p>
<div class="meta">
<span class="tag beta">beta</span>
<span class="tag">TypeScript + Java</span>
<span class="tag">MIT</span>
<span class="tag">com.github.Horizon-36596:zenith</span>
</div>
<p class="go">Read the documentation</p>
</a>Clone it on a branch:
gh repo clone Horizon-36596/Horizon-36596.github.io "$env:TEMP\horizon-landing"; if ($?) { Set-Location "$env:TEMP\horizon-landing"; git switch -c add-zenith }Paste the card into $env:TEMP\horizon-landing\index.html, then commit and open the pull request:
Set-Location "$env:TEMP\horizon-landing"; git commit -am "feat: list Zenith on the library site"; if ($?) { git push -u origin add-zenith; gh pr create --fill }After the merge, https://libraries.horizon36596.org/ shows the card and its link opens the docs.
-
Move the
[Unreleased]entries inCHANGELOG.mdunder a new## [x.y.z] - YYYY-MM-DDheading and add its link at the bottom. -
Set the version in the root
package.json, stamp it everywhere and check for drift (see The npm packages), commit, and let CI go green. -
Tag and push the tag:
$version = (Get-Content package.json -Raw | ConvertFrom-Json).version; git tag "v$version"; if ($?) { git push origin "v$version" }
-
Do steps 10, 11 and 12 above with the new version.
A published version is never moved or reused. The fix for a bad release is a new patch version; what follows only limits the damage until it is out.
-
Something private went public. Make the repository private at once, then fix the snapshot and push again. Treat any exposed secret as leaked and revoke it, whatever the history says.
gh repo edit Horizon-36596/zenith --visibility private --accept-visibility-change-consequences
-
The docs site or the web app is broken. Redeploy the previous good tag from the Actions tab (Publish documentation site, Run workflow, pick the tag), or from here. There is no earlier tag for v0.1.0; to take the site down instead, turn Pages off (
gh api -X DELETE repos/Horizon-36596/zenith/pages) and redo steps 6 and 7 when fixed.gh workflow run docs-publish.yml --repo Horizon-36596/zenith --ref v0.1.0
-
The desktop release is bad. Turn it back into a draft, which hides it and keeps the files:
gh release edit v0.1.0 --repo Horizon-36596/zenith --draft
-
An npm package is bad. Deprecate the version, so installs warn, and publish a fixed patch.
npm unpublishworks only within 72 hours and blocks that version number forever, so it is for a leaked secret, not a bug.foreach ($name in 'schema', 'core', 'season-biobuzz', 'seasons', 'github', 'cli', 'mcp') { npm deprecate "@horizon36596/zenith-$name@0.1.0" "Broken release; use 0.1.1." }
-
The JitPack build failed. It is cached against the tag. Fix it on a new commit and tag a new version, or delete the failed build from the JitPack page (see below).
-
The library card is wrong. Revert its pull request in the landing repository.
-
Starting the public repository over. Delete it on GitHub (Settings, Danger zone;
gh repo deleteneeds thedelete_reposcope), then rename the development repository back if it is wanted under the old name, and begin again at step 1:gh repo unarchive Horizon-36596/zenith-dev --yes; if ($?) { gh repo rename zenith --repo Horizon-36596/zenith-dev --yes }
JitPack builds the Java runtime from the tag the first time anyone asks for it, following jitpack.yml
(JDK 17, then ./robot/gradlew -p robot :auto-runtime:publishToMavenLocal). The one artifact holds both
runtimes: the Ivy classes in org.horizon36596.zenith.ivy, the SolversLib classes in
org.horizon36596.zenith.solverslib, and the shared classes in org.horizon36596.zenith. The
:ivy-runtime module is not published; it compiles the Ivy side with no SolversLib on its classpath
and runs the Ivy tests. Teams on either library install the same line:
implementation 'com.github.Horizon-36596:zenith:v0.1.1'The version is the tag exactly as written, v included. JitPack only builds public repositories on its
free tier, so the repository must be public before this works.
-
Root
package.jsonsays the version you are about to tag ("version": "0.1.0"forv0.1.0). The Gradle build reads its version from there, so a tag that disagrees publishes a POM with the wrong version inside it. -
Both runtimes build, their tests pass (the SolversLib and conformance tests in
:auto-runtime, the Ivy tests in:ivy-runtime) and the artifact publishes locally. This is the command CI'srobotjob runs:.\robot\gradlew.bat -p robot build :auto-runtime:publishToMavenLocal :ivy-runtime:testDebugUnitTest
It needs JDK 17 on
JAVA_HOMEand the Android SDK (Android Studio's, orANDROID_HOME). The result lands in~\.m2\repository\org\horizon36596\zenith-runtime\<version>\, one AAR with the classes of both runtimes. -
The
robotjob in CI is green on the commit you are tagging.
-
Push the tag:
git tag v0.1.0; if ($?) { git push origin v0.1.0 }
-
Open https://jitpack.io/#Horizon-36596/zenith, find
v0.1.0in the Releases or Tags list, and press Get it. That starts the build. It takes a few minutes. -
Press the Log icon next to the tag and read the log to the end. A good build ends with
BUILD SUCCESSFULand lists the artifacts it found in the local Maven repository: one AAR, its sources jar and its POM, all fromorg/horizon36596/zenith-runtime. That one AAR is both runtimes; there is no second artifact for Ivy. A red icon means the build failed; the log says why, and nothing is served for that tag. -
Check that the artifact is served under the repository's name:
Invoke-WebRequest -UseBasicParsing -Method Head https://jitpack.io/com/github/Horizon-36596/zenith/v0.1.1/zenith-v0.1.1.pom | Select-Object StatusCode
200means teams can install it. A404after a green build means JitPack published it under a different name; the log's file list shows which, androbot/README.mdandsite/docs/robot-runtime.mdthen need the real coordinate. -
Optional, the real consumer check: in a robot project, add the snippet from
robot/README.md, sync Gradle and build TeamCode.
A failed JitPack build is cached against the tag. After fixing the cause on a new commit, delete the failed build on the JitPack page (the log view has a delete control for the owner) or tag a new version.
.github/workflows/npm-publish.yml publishes the seven packages on the same v* tag, in dependency
order: @horizon36596/zenith-schema, -core, -season-biobuzz, -seasons, -github, -cli and
-mcp. Each goes out with an npm provenance statement. The job fails before publishing anything
when the tag does not match the root package.json version, and it skips a version npm already has,
so a run that stopped halfway can be re-run.
- Create the
horizon36596organization on https://www.npmjs.com/ (the scope must match@horizon36596). - Create a granular access token with read and write permission on the
@horizon36596packages, and add it to the repository as the Actions secretNPM_TOKEN(Settings, Secrets and variables, Actions).
-
The version is stamped everywhere and the drift check passes:
node scripts/stamp-version.mjs; if ($?) { node scripts/check-version.mjs }
-
The packages build, pack and install cleanly. This packs all seven, installs the tarballs into an empty folder and runs
zenith --version,zenith validateand azenith-mcpsession:pnpm install; if ($?) { pnpm build }; if ($?) { node scripts/smoke-pack.mjs }
-
Optional: run the workflow by hand from the Actions tab with publish unticked. It builds, tests and smoke-tests, then prints what it would publish.
-
The tag push starts the npm publish workflow. Watch it in the Actions tab.
-
Check that each package is on npm at the new version:
npm view @horizon36596/zenith-cli version; npm view @horizon36596/zenith-mcp version
-
Check the install a team would do, from an empty folder:
npx -y @horizon36596/zenith-cli --version