Skip to content

Releasing

A release ships the local client (chronos + chronosd) and the desktop app. The pipeline lives in .github/workflows/release.yml; this page is the operator’s checklist.

Scope of v1: macOS only (arm64 + x86_64). The Windows build compiles in CI, but the sandbox has no Windows isolation yet, so shipping a Windows client would sell isolation that isn’t there. The enterprise panel (cloud/) deploys separately (Railway) and is not part of this artifact.

  1. Make sure main is green (just ci) and the workspace crate version is the one you want to ship (crates/chronos-cli/Cargo.toml) — the workflow refuses a tag that does not match it.

  2. Update the changelog (see below): draft the section, curate it, and commit the change so it ships in the tarball’s user guide.

  3. Tag and push:

    Terminal window
    git tag v0.1.0
    git push origin v0.1.0
  4. The release workflow builds both macOS targets and packages, per target:

    • chronos-<version>-<target>.tar.gz — CLI + daemon binaries + the built user guide (staged at share/chronos/docs, served by the dashboard at /docs);
    • Chronos-<version>-<target>.dmg — the desktop app, with chronosd bundled inside as a sidecar (drag to Applications; unsigned builds need right-click → Open the first time).

    It collates SHA256SUMS and creates a draft GitHub release with generated notes.

  5. Review the draft (assets, checksums, notes) and publish it manually. This is the human-facing release — release notes and the .dmg for a first install. The in-app update rollout is separate (see below): it goes through the panel’s channel router, not GitHub.

A workflow_dispatch run of the same workflow is a dry run: it builds and uploads the artifacts to the run (with a <version>-dev.<sha> version), but creates no release.

The customer-facing changelog is curated, not auto-generated. To draft the next version’s section:

Terminal window
just changelog # since the last tag
just changelog v0.1.0 # an explicit base ref

That prints a Markdown block, bucketed into Novedades / Correcciones / Seguridad / Rendimiento from the range’s Conventional Commits. It is a draft: de-jargon each line, drop what customers don’t need, remove the (#NN) / (CHR-NN) references, and paste the result into apps/docs/src/content/docs/changelog.md. The script never writes the page and never touches git — the propose→curate→publish gate is a person, like everywhere else in the product. The committed changelog reaches clients two ways: the public docs site at docs.getchronos.dev/changelog, and the copy bundled into the release tarball for offline use inside the app.

Publishing the docs site (docs.getchronos.dev)

Section titled “Publishing the docs site (docs.getchronos.dev)”

The site lives at docs.getchronos.dev, served from the Railway service chronos-docs (project chronos-panel). It is the same Astro Starlight source as the offline build; only the base path differs, set by env at build time:

  • PublicDOCS_BASE=/ DOCS_SITE=https://docs.getchronos.dev (the apps/docs/Dockerfile bakes these in). Author-written links are root-absolute (/guide/merge/); a small rehype plugin in astro.config.mjs re-adds the base so the same content also works under /docs.
  • Offline — the default DOCS_BASE=/docs, which just docs-build produces for the dashboard and the release tarball.

The service is wired to GitHub (branch main, root apps/docs, watch apps/docs/**), so merging a docs change to main auto-deploys. Manual out-of-band redeploy, from apps/docs:

Terminal window
railway up # linked to the chronos-docs service

TLS terminates at Cloudflare (orange-cloud proxy on the getchronos.dev zone); the docs record is a proxied CNAME onto the service’s Railway domain, with the custom domain registered on the service so Railway routes the host. Full runbook: docs/docs-site.md.

Installed desktop clients update themselves from the enterprise panel’s channel router; the signed bundles live in a private Railway bucket. Releasing a version and rolling it out are two steps: registering makes a build downloadable, promoting a channel is what actually offers it to clients.

Channels are stable (default), internal and alpha (plus beta, promotable but not yet a Settings option). They are independent pointers — alpha can run ahead of stable off the same signed bundles.

Terminal window
just version 0.2.0
git commit -am "chore: 0.2.0"
git tag v0.2.0 # stable: registered, NOT auto-promoted
git push origin v0.2.0

To roll out to a non-stable channel automatically, tag a prerelease whose identifier is the channel name:

Terminal window
git tag v0.2.0-internal.1 # → auto-promoted to the `internal` channel
git tag v0.2.0-alpha.3 # → auto-promoted to `alpha`
git push origin --tags

CI builds from the tagged commit, so the version’s code (including the pinned updater public key) must be on main first — merge before tagging.

release.yml:

  1. builds and minisign-signs the .app.tar.gz per target,
  2. uploads each to the release bucket under releases/<version>/… (path-style),
  3. registers the version with the panel (POST /v1/admin/releases),
  4. for a prerelease, auto-promotes it to its channel (-internal.Ninternal, …).

It also drafts the GitHub release with the .dmg for humans / first installs.

A plain (stable) tag is registered but not promoted — rolling stable out is deliberate. In the panel → Releases tab, promote the version to a channel; that click is the rollout gate. To pause a bad release, promote the channel back to the previous version (clients only ever move forward, so there is no forced downgrade).

A client on the target channel is offered the update a few seconds after launch (and every 6 h): a card bottom-right with Update and restart. To put a tester on a non-stable channel: install the app once (hand them the .dmg), then Settings → Version → Update channel → Internal / Alpha. Their first install must be a build that carries the pinned public key (i.e. from this code onward), or its updater is fail-closed.

The channel is a client-local choice today; governing it per org/team from the panel is tracked in CHR-49.

There are two independent signatures, don’t conflate them: the minisign signature (above) is what the in-app updater verifies — required for updates, already configured. The Apple Developer ID signing + notarization here is what clears Gatekeeper on first install — optional, and tracked in CHR-48.

The build signs and notarizes only when the repo secrets are present; otherwise the tarball carries an UNSIGNED.txt marker and the installer prints the Gatekeeper workaround. Recommended before external users. Required secrets:

SecretContent
MACOS_CERTIFICATEbase64 of the Developer ID Application .p12
MACOS_CERTIFICATE_PWDpassword of that .p12
MACOS_SIGNING_IDENTITYe.g. Developer ID Application: ACME (TEAMID)
APPLE_API_KEYApp Store Connect API key id (e.g. ABCD1234)
APPLE_API_ISSUERthe API key’s issuer id (a UUID)
APPLE_API_KEY_P8base64 of the .p8 private key (downloaded once)

Notarization uses an App Store Connect API key rather than an Apple ID + app-specific password: the key is owned by the organization, carries a scoped role, and is revocable on its own — so releases don’t break when a person’s Apple account changes. Create it in App Store Connect → Users and Access → Integrations → App Store Connect API.

When present:

  • The CLI and daemon binaries (for the tar.gz) are codesigned (hardened runtime + timestamp) and submitted to notarytool with the API key.
  • The desktop app is fully hardened: the bundled chronosd sidecar is signed first, then Tauri signs the .app with the hardened runtime + Entitlements.plist, notarizes it and staples the ticket (so Gatekeeper clears it offline, no right-click needed), and packages the stapled app into the .dmg. This is driven by handing cargo tauri build the APPLE_SIGNING_IDENTITY + APPLE_API_KEY/APPLE_API_ISSUER/APPLE_API_KEY_PATH credentials.

Without the secrets the .app/.dmg are unsigned and the first launch needs the Gatekeeper workaround (right-click → Open, or xattr -dr com.apple.quarantine).

just package reproduces the CI packaging for the host target: it builds the release binaries and the user guide, stages the same tarball layout under target/dist/, and writes the tarball plus SHA256SUMS. Always unsigned — signing/notarization only happens in CI with secrets. If the docs toolchain is not installed, it packages without the user guide and says so.

End users (or pilot admins) run:

Terminal window
curl -fsSL https://raw.githubusercontent.com/renaiss-ai/aluxion-chronos/main/scripts/install.sh | sh

While the repo is private the script needs an authenticated channel: it prefers a logged-in gh CLI and falls back to GITHUB_TOKEN. It verifies the SHA-256 checksum, installs to ~/.local/bin, and places the user guide in the daemon’s data dir so the dashboard serves it at /docs.