Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
8035964e1This file is pinned to the selected release. Download the original to open it in your design tool.
# Releasing the CLI
Running daemons self-update from a public GCS bucket (`src/lib/selfUpdate.ts`). Releasing is pushing a
tag: `.github/workflows/release.yml` bundles the CLI, publishes it to that bucket, and cuts the GitHub
Release. **The tag IS the version — CI never bumps.**
Tags are `vX.Y.Z_cli`, not plain `vX.Y.Z`: the backend now lives in this same repo under `backend/`
and releases as `vX.Y.Z_backend` (see `../backend/README.md`), so the suffix is what tells each tag-push
trigger which workflow to run. It's stripped before anything treats it as a version — the published
version everywhere below (manifest, release title, `ADAPTER_VERSION`) is always a bare `X.Y.Z`.
```bash
make release-cli # bump the patch, tag, push — CI does the rest
make release-cli ARGS="--dry-run" # print the version it would cut and the notes, do nothing
make release-cli ARGS="0.2.0" # release an explicit version
make release-cli ARGS="--notes-file notes.md" # hand-written release notes
```
Nothing is built locally and no GCS credentials are needed: the only things `cli/scripts/release-cli.sh`
touches are git and a public HTTPS read of the manifest. See [`../docs/cicd.md`](../docs/cicd.md) for
what the release workflow itself does, step by step.
**Why the version comes from two places.** `cli/scripts/release-cli.sh` takes the highest of the last
git tag and the version published under the `cli` key of `harness/cli/metadata.json`. The two can
drift — a scratch publish that slipped, or a manifest edited by hand, leaves the manifest ahead of the
last tag. Bumping from tags alone there would produce a version LOWER than what's already served —
every daemon refuses it (`shouldUpdate` / `semverGt` in `src/lib/selfUpdate.ts`) while the release
still reports success.
Unlike `autonomous-harness-desktop`'s `make release`, there is no `--minor` flag: `shouldUpdate` is a
plain "strictly newer" check with no major/minor-triggers-a-forced-update concept, so every release
just needs to outrank what's live.
`package.json`'s `version` field is never touched by this script; the published version is baked into
the bundle via `ADAPTER_VERSION` at build time (see "The publishing step" below).
## The publishing step
`cli/scripts/upload-cli.sh` is the publishing step, and CI is its only caller: `release.yml` runs it
with the version taken from the tag. There is deliberately no `make` target for it — publishing from a
laptop created no git tag and bumped from the remote manifest, so the repo stopped reflecting what was
served, and a stale local tag then broke the next `make release-cli` at `git fetch --tags`. If CI is
down, fix CI; a version with no tag is worse than a late one.
What the script does, for reading the workflow:
1. Reads the current version from the live manifest (falling back to `package.json` if the manifest is
unreadable), then bumps or takes the version it was given.
2. Bundles with `ADAPTER_VERSION="$VERSION" npm run bundle` and asserts `node dist/cli.js version`
reads back that exact string before uploading anything.
3. Uploads `cli.js` and `notify.mjs` to `harness/cli/<version>/`, then download-merge-reuploads
`metadata.json` in a single write, touching only the `cli` key.
A failed build stops the release; nothing is uploaded and no version is consumed.
## The `curl | bash` installer
`cli/scripts/install.sh` is what `curl -fsSL https://cdn.autonomous.ai/harness/cli/install.sh | bash`
runs: it installs the managed Node runtime and the CLI bundle from the same manifests the release
publishes to. It is a static file, versioned by nothing, and does **not** ship with a release — publish
it on its own whenever it changes:
```bash
make upload-cli-install-sh # cli/scripts/install.sh -> harness/cli/install.sh in the bucket
curl -fsSL https://cdn.autonomous.ai/harness/cli/install.sh | head -5 # confirm the CDN edge moved
```
Its command contract (flags, exit codes, what it refuses) is pinned by `cli/src/scripts/install.spec.ts`,
which runs with the rest of `make cli-test`. Not to be confused with `cli/scripts/install-cli.sh`, the
local dev loop below.
## Local install (no upload)
`bash cli/scripts/install-cli.sh` bundles **this working tree** into `~/.harness/cli` — the same
layout the public installer produces — and restarts the daemon on it, so `harness` on this machine
means your code without publishing anything:
```bash
make install-cli # bundle + install + restart the daemon
make install-cli ARGS="--no-restart" # skip restarting the daemon
make install-cli ARGS="--no-updates" # pin self-update off (see below)
```
Self-update stays **on** by default: the local build is labelled with the currently published version,
which the release you're level with cannot outrank, so your bytes survive until a genuinely newer
version ships — then the daemon quietly swaps back to it. `--no-updates` pins your build instead.
## GCS layout and manifest shape
```
gs://s3-autonomous-upgrade-3/harness/cli/metadata.json
gs://s3-autonomous-upgrade-3/harness/cli/<version>/cli.js
gs://s3-autonomous-upgrade-3/harness/cli/<version>/notify.mjs
```
The manifest is always read straight off the GCS origin (`CDN_ASSET_BASE_URL` in `upload-cli.sh` does
not apply to it) — it's polled every ~60s by every running daemon, and this product's CDN caps any
cacheable response at ~31 days regardless of origin headers, so it must never be CDN-fronted. The bundle
files it points at are the opposite: immutable once published, so their `url` fields point at
`cdn.autonomous.ai` instead, and are uploaded with a long `Cache-Control` on purpose.
```json
{
"cli": {
"version": "0.1.72",
"url": "https://cdn.autonomous.ai/harness/cli/0.1.72/cli.js",
"sha256": "<64 hex>",
"size": 2103552
}
}
```
The bucket must be public-read — that is bucket policy, not something the scripts set.
## How a running daemon self-updates
1. Polls `ADAPTER_UPDATE_URL` every `ADAPTER_UPDATE_CHECK_MS` (default 60 s) — see `src/config/env.ts`.
2. Compares the manifest's `cli.version` against its own — strictly newer only (`semverGt`), so
republishing an old build can never downgrade a fleet.
3. Downloads `cli.js` + `notify.mjs` and verifies each sha256/size against the manifest **before**
anything is swapped in. A mismatch is discarded.
4. Swaps the bundle in when the daemon is idle and restarts, with rollback on a bad build.
**There is no undo.** A bad release is rolled back by publishing a *higher* version containing the
older code — never by deleting the new one, which just leaves every daemon pointed at a 404. Disable
self-update on one machine with `ADAPTER_UPDATE_DISABLE=true`.
## Rolling out safely
Publish to a scratch manifest before touching the real one, then point a local build at it:
```bash
gh workflow run release.yml -f version=9.9.9 -f metadata_path=harness/cli/metadata-test.json
```
```bash
ADAPTER_UPDATE_URL=https://storage.googleapis.com/s3-autonomous-upgrade-3/harness/cli/metadata-test.json harness start
```
No daemon polls `metadata-test.json`, so nothing self-updates from it. The *artifacts* still go to
their real home (`harness/cli/<version>/` is hardcoded in `upload-cli.sh`), so use an unmistakable
version and delete `gs://s3-autonomous-upgrade-3/harness/cli/9.9.9/` afterwards.