apps/cli/package.json owns the product version. scripts/releases/config.ts
derives the npm channel, native targets, Docker tags, desktop identity and
installer names. Build scripts and the website consume that configuration.
Source workspaces remain private; only staged runtime packages are published.
Use the pinned Node and Bun versions in .github/actions/setup/action.yml.
bun run release:cli
bun run release:wrapper
bun run release:desktopThese commands never publish. macOS public installers require
bun run release:desktop --notarize, with the five EXECUTOR_MAC_* credentials
from the Agents vault. The signing helper owns a temporary keychain and an
in-memory notary key file and removes them after the build.
The Executor releases workflow (release-artifacts.yml) defaults to
build. It reads the committed version, builds all five native platforms,
tests installed CLI archives and desktop restart behavior, checks OS credential
setup and key-loss refusal, and runs the native Docker scenarios on amd64 and
arm64. It also produces the checksum-pinned Git companion source archive.
Every job runs on Blacksmith. macOS builds use Apple Silicon runners; the Intel
job uses x64 Node and Bun under Rosetta and tests the x64 runtime and desktop.
It uses the 12-vCPU Mac runner to give emulation more capacity while preserving
the same startup and scenario deadlines; Apple Silicon uses the 6-vCPU runner.
The pinned Bun installer patch keeps its selected binary architecture under
Rosetta, matching the optional dependencies installed by the package manager.
Windows installers are currently unsigned.
A Bun runtime release built from a pull request's branch, as one that changes the
lockdown patch needs, is the one main's publishing runs use from then on. If that
branch's scripts/releases/bun-runtime/ changed after the build and before merge,
delete the release before merging so main builds it again; see
the release scripts.
Executor 2 desktop installs update from their own channel feed. The channel is
fixed at build time from the version: 2.0.0-beta.N builds follow beta and
stable builds follow latest. There is no in-app channel switch. Install a stable
build to leave beta.
Executor 1 reads GitHub's release list in the same public repository, so v2
never uses the GitHub provider or electron-updater's default latest*.yml
names. scripts/releases/config.ts defines a generic feed in the published
prerelease executor-v2-desktop-updates:
| Platform | Beta file | Stable file |
|---|---|---|
| macOS | executor-v2-beta-mac.yml |
executor-v2-latest-mac.yml |
| Windows | executor-v2-beta.yml |
executor-v2-latest.yml |
| Linux x64 | executor-v2-beta-linux.yml |
executor-v2-latest-linux.yml |
| Linux arm64 | executor-v2-beta-linux-arm64.yml |
executor-v2-latest-linux-arm64.yml |
Each file lists absolute URLs to the versioned release assets and their blockmaps. The feed release never becomes GitHub's latest release, so v1 installs never see it. Its first publication creates it on the release's public commit.
The desktop build writes app-update.yml for Windows, Linux and signed macOS
builds. Unsigned macOS review builds have no feed because Squirrel.Mac only
installs signed updates. Linux checks only inside the AppImage; .deb installs
update from the download page.
Publishing updates the channel last, after the versioned release is public.
scripts/releases/desktop-feed.ts checks every platform's metadata against the
installer bytes, merges the macOS arm64 and x64 entries, and writes the files.
A stable release writes latest and moves beta forward unless beta already
holds a newer version. Rerunning the upload replaces the same files. To roll a
channel back, publish a newer fixed version; installs never downgrade.
The app checks 15 seconds after launch and every four hours, downloads quietly, then offers Restart or Later once per version. Updates → Check for updates… checks at once and offers a version declined earlier. The backend stops before installation. Updates never install on quit.
New apps pin the apps version in packages/apps/package.json, so every host
must ship a version that npm holds with exactly the content this checkout
builds. Every change to the framework, including the workspace libraries
bundled into it, bumps that version in its PR (and the host protocol when the
host boundary changes). Merging allocates the numbers and the deploy from
main publishes them. Never publish apps from a branch or by hand; see
publishing apps.
scripts/releases/apps-published.ts runs after bun run apps:build. It packs
the staged package and compares every file with the published archive of the
same version:
- The deploy workflow's
appsjob runs it with--publishin thereleaseenvironment before the production deploy job. An unpublished version is published with--tag beta; a published one is only compared. - Publishing runs of Executor releases run it without a flag, so the version must already be on npm unchanged.
- Pull request checks run it with
--allow-unpublished: an unpublished bump only warns, while a published version with different content fails.
Pull requests also run scripts/releases/apps-bumped.ts, which builds the base
commit's package and fails when the staged package changed but the version did
not.
The Beta workflow (.github/workflows/beta.yml) publishes betas. It checks
on every push to main and on a schedule at minutes 8 and 38, and
scripts/releases/beta-due.ts decides from npm, the release runs and main's
history. Before deciding, each tick runs scripts/releases/lock-repair.sh, which
pushes Refresh bun.lock [skip ci] when main's bun.lock is not what
bun install writes and the only difference is workspace packages' own versions.
Any other difference is only reported: a warning in the run summary and one open
issue labelled lock-repair-refused (scripts/releases/lock-repair-issue.sh),
which a later tick closes when it finds the lock fresh. The repair never holds a
beta. That commit follows the merge that left the lock stale, so it
never makes a beta due by itself, and its [skip ci] starts no Executor releases
run. A push and a scheduled tick decide alike:
- While any build or publication of Executor releases is queued or running, it waits. A check, which publishes nothing, does not hold it.
- It starts no beta and no further attempt while the release matrix on
mainis red. The matrix runs are the checks, the Executor releases runs that the Release check workflow starts with a repository dispatch after main changed what a release lane builds or runs, paced to one build every two hours, and the runs titled(build), dispatched frommainby hand. Checks list apart from builds and publications, so they never crowd the publishing attempts out of what the decision reads. The newest finished one must have passed itsreleasesjob. A run that tested nothing is skipped: a cancelled one, and a check whosepacejob did not pass or whose matrix did not run. A push that changed nothing a release lane reads starts no check, so it neither holds a beta nor lets one through. A failed run holds every beta, with a warning, until a later run passes. To clear a hold once the cause is fixed, merge the fix, or rungh workflow run release-artifacts.yml --ref main -f channel=build. A run still pacing or building holds nothing, since the publishing run tests every platform on the commit it publishes and publishes only when all pass. - When npm lists the committed version, main has commits after that version's
Release 2.0.0-beta.Ncommit, and npm published it at least six hours ago, it commitsRelease 2.0.0-beta.N+1tomainand dispatches Executor releases with channel beta. - When npm lacks the committed version, it publishes it again instead of
skipping it. An attempt that failed in its build or test jobs is dispatched
again on the next tick. After two failures in a row, the next attempt waits
six hours after the last one and for a new commit on
main, or a day, so a brokenmaincosts at most five publishing runs a day. An attempt that failed in its publish job has its failed jobs re-run once from its own artifacts; after that, inspect it as below. Each skipped retry leaves a warning on the Beta run, and the failed release runs stay red. - Only publishing runs count as attempts. A dispatch with channel build does not publish, so it neither adds to a failure streak nor counts as a success, though it still makes the tick wait while it runs.
- When the last publishing run for the committed version (its commit contains
the
Releasecommit) succeeded but npm still lacks the version, every tick fails red instead of dispatching again: another run would succeed the same way without publishing. Inspect that run's publish job and the registry, fix the cause, then dispatch Executor releases with channel beta by hand.
GitHub delivers this repository's schedules hours late and drops the ticks in
between: in the 30 days to 2026-11-11 a */5 cron ran about five times a day and
an hourly one about four and a half. So merges drive publishing, and the schedule
is a backstop. Without merges, a beta that waits out the six hours, or a retry
after a failed attempt, waits for the next merge or the next scheduled run, which
has come up to 11 hours apart.
Ticks never overlap. While one runs, one more waits; a newer tick replaces the waiting one, and it reads main afresh, so a burst of merges costs a few runs. When a merge lands between a tick's checkout and its bump push, the push is refused; the tick pushes nothing and passes with a notice, because that merge started a tick of its own, which decides again. A refusal while main has not moved fails the tick.
Dispatching Beta by hand publishes new commits at once, skipping the six
hours and the retry back-off. A merge that lands while a manual dispatch
waits behind a running tick replaces it; dispatch it again. The bump commit is pushed over SSH with the
deploy key in the beta environment's BETA_PUSH_KEY secret, the one actor
the main ruleset lets push without a pull request (see
CI configuration). Its message ends in [skip ci], so it starts
no CI, Deploy, public export, Cloud tests or Beta run; the next merge carries it.
To publish a beta by hand instead, set the version to an unused
2.0.0-beta.N, merge the reviewed release changes, and dispatch Executor
releases from main with channel beta. The workflow refuses a channel
that does not match the committed version.
- Build and test every native runtime, desktop installer and Docker architecture.
- Export the filtered source snapshot to the public repository's
v2-releasesbranch. Createexecutor@<version>on that public commit, never a private SHA. - Upload installers, native archives, the npm launcher, bundled Git source and SHA256SUMS to the draft.
- Publish the tested Docker manifest as
:<version>and:beta, then make the GitHub prerelease public withlatest=false. - Publish the npm launcher, which downloads the native archives from that public release. It records each archive's SHA-256; publication checks them against the build and the release's SHA256SUMS, then polls the registry archive and compares its integrity.
- Verify a clean
executor@<version>installation from npm, including its first download. - Point the Homebrew formula and the desktop
betaupdate channel at the public release assets.
The npm latest tag, Docker latest, and Executor 1 desktop updater stay
unchanged. A draft public release blocks accidental repeat publication of the
same version. After a partial failure, re-run the failed publish job. It resumes
only its own release, whose target is this source's unchanged public snapshot:
a draft gets its assets again, and a release already public keeps them, which
npm publication then checks. It skips an npm launcher whose registry integrity
matches the local archive. Any other
existing release or npm version stops publication for inspection. Never
republish an accepted immutable npm version merely because its registry entry is
still propagating.
The homepage resolves desktop downloads in the browser from GitHub's public
release list, choosing the newest published release with this major version's
tag prefix. A merged version bump keeps linking to the previous release until
this one is public; without JavaScript, or when the lookup fails, the buttons
open the releases page. The documentation's install commands name the newest
release npm's dist-tags list, read when the site builds (scripts/releases/published.ts),
so they too keep the previous version until publication moves the tag and the
next deploy from main rebuilds the site. Verify the public npm install, GitHub
assets, Docker manifest and rendered site.
A successful upload alone does not establish public availability.
Get explicit approval for the v2 stable release. Change the version to 2.0.0
and dispatch the same workflow with latest. The version, tags, filenames
and links change together. The fixed desktop identity com.usefulsoftware.executor.v2,
product name Executor 2, profile Executor v2, and CLI data directory
~/.executor/v2/cli remain unchanged. Stable publication writes the desktop
latest channel and moves beta installs forward. Marking v2 as GitHub's latest
release also changes what Executor 1's updater reads; decide v1's path before cutover.
Changesets still orchestrates separately published workspace packages. The product archive includes private workspace packages and uses the CLI manifest as its single release version; the removed v1 release script is not a second publishing route.
apps/hosted/cloud/alchemy.releases.ts owns the eight required secrets in the existing release
environment. It is separate from the unapplied broad CI stack, so
applying releases does not change production credentials, repository policy or
Cloudflare deployment tokens. Missing credentials fail the apply.
From the cloud package, use alchemy deploy alchemy.releases.ts --stage ci --dry-run --no-input to review, then --no-input --yes to apply. Resolve an
ignored Agents reference file through agent-vault run --env-file.
The environment is created once by a repository administrator with a bare
PUT /repos/UsefulSoftwareCo/executor-next/environments/release. It already exists.
Secret providers require it to exist and never change its protection rules.
The IaC token needs Environments read/write and Metadata read on
UsefulSoftwareCo/executor-next. GitHub cannot restrict it to one environment.
Public releases use a GitHub App installed on UsefulSoftwareCo/executor with
Contents read/write. Its installation tokens have their own rate limit, so a
release cannot fail because a person's token is busy elsewhere.
The release stack requires NPM_TOKEN, PUBLIC_RELEASE_APP_CLIENT_ID,
PUBLIC_RELEASE_APP_PRIVATE_KEY,
EXECUTOR_MAC_SIGNING_KEY, EXECUTOR_MAC_SIGNING_CERTIFICATE,
EXECUTOR_MAC_NOTARY_KEY, EXECUTOR_MAC_NOTARY_KEY_ID, and
EXECUTOR_MAC_NOTARY_ISSUER, plus GitHub and Cloudflare state credentials.
Normal main pushes continue exporting to public v2 through
scripts/export-public.sh. Release snapshots use v2-releases so a release
cannot race or replace a newer main export. Both use
scripts/export-public.exclude; private history and internal notes stay private.
A main export also merges the public pull requests that main ports, named by
Public-PR lines in its commit messages; a release export merges none, because
no pull request targets v2-releases.
Public release notes come from the product README, not private PR titles or
GitHub's generated changelog.