* Add daily macOS dev build release channel
Publish once-daily signed macOS builds from main at a dedicated cadence,
separate from hourly (too noisy) and release branches (too infrequent).
Builds are notarized and installable via the updater, but unvetted —
published to stablyai/orca-daily rather than the main repo to avoid
evicting stable/RC entries from the releases feed.
* fix lint
* fix commit
* Add third token mint to daily macOS build workflow
The upload step's 2x45m retry budget can outlive the one-hour token, so a third
is minted after it for verify and cleanup operations. Release notes are moved to
a file to ensure consistency between draft creation and publish. Daily channel
description updated with specific UTC release time.
* feat(updater): add an adhoc release channel for branch builds
Hourly covers main. This covers everything that is not main yet: a
dispatchable macOS build of an unlanded branch, published to
stablyai/orca-adhoc, so the team can run an experimental feature for a
few days instead of reasoning about it from a diff.
Adhoc sits at the bottom of the version order — 'adhoc' < 'hourly' <
'rc' < stable — so no routine check can walk anyone onto somebody's
branch; only an explicit pinned jump reaches one. It gets its own repo
rather than sharing orca-hourly's, because a branch build must not
appear in the list a developer riding main is looking at.
Signed and notarized exactly like hourly, for the same reason: macOS
anchors a notarized app's TCC grants on identifier + team, so an
unnotarized build reads as a new client and silently loses file access
under Documents/Desktop/Downloads.
Tags stamp to the second rather than the minute. Hourly runs under a
concurrency group and cannot overlap itself; adhoc builds are dispatched
on demand, so two people cutting from different branches inside one
minute is ordinary — and a minute-resolution tag would collide and fail
the second build after its whole pack-and-notarize run.
Channel-specific behaviour now derives from one DEDICATED_REPO_CHANNELS
list: repo mapping, macOS-only support, and UpdateSource. The RPC schema
that validates releaseChannelOverride was a hand-copied enum missing the
new channel, which would have rejected the override on its way to the
main process; it reads the predicate now.
* fix(updater): merge the duplicated shared/types import
Co-authored-by: Orca <help@stably.ai>
* fix(ci): default the adhoc build ref to the dispatch branch
The Actions UI puts its own "Use workflow from" branch picker directly
above the ref field, and picking a branch there is what most people read
as "build this". Making the field optional means the obvious action is
also the correct one; naming a branch explicitly still wins, so main's
copy of the workflow runs rather than a stale one on an old branch.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(updater): name hourly releases by version, build number, time, and sha
Hourly releases were titled with their raw tag
(`v1.4.163-hourly.202607312054`), which reads as one opaque digit run and
does not say which commit it came from.
Title them `1.4.163 • 01 • 07-31 13:54 • e698241` instead, and show that
same string in the in-app build picker by having the picker render the
release's stored name rather than deriving its own label. Composing it in
one place means the two surfaces cannot drift.
The build number is monotonic across the channel. It is read as the
highest number already in use rather than as a count of releases: the
prune step trims to 72, so a count would roll backwards after three days
and reissue numbers. Drafts count toward it — unlike in the freshness
check, which asks whether a commit shipped, this asks whether a number is
free, and a stranded draft still holds one.
Times are Pacific while the tag's stamp stays UTC. The stamp is a sort
key and a local one would repeat an hour at every DST fall-back, making
two distinct builds compare equal; the title is only ever read.
* fix(updater): fail the hourly build when the release name is missing
The workflow checks out `ref: main`, but a workflow_dispatch runs the
workflow file from whatever branch was dispatched. A branch that edits
this step while main still carries the old script produces an empty name
and an untitled release — silent, and only visible once someone opens the
releases page. Verified by hitting exactly that on run 30665586904.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
The hourly workflow builds only macOS artifacts, so on Linux and Windows the
channel had nothing to install while still looking selectable.
Adds isChannelSupportedOnPlatform in the shared module so the picker and the
main-process check cannot drift. The segment is disabled with an Apple glyph
and a tooltip rather than hidden, so a dev who has heard of the channel sees
that it exists and why it is unavailable instead of not finding it.
A persisted 'hourly' override arriving off-macOS (settings sync, or a profile
carried from a Mac) now falls back to stable rather than rendering a selected
segment the user cannot act on. checkForPinnedBuild rejects the channel too,
since IPC is reachable regardless of what the renderer disables.
* feat(updater): add hourly dev channel and build switching
Adds an hourly macOS build channel plus a dev-only surface for switching
update channels and jumping to any published build, including older ones.
Hourly builds publish to a separate stablyai/orca-hourly repo. The routine
update path resolves tags from the main repo's releases atom feed, which
exposes only its 10 newest entries — 24 hourly tags a day would evict every
stable/RC entry there and leave real users with nothing to update to.
Hourly artifacts carry the release bundle id and Developer ID signature so
Squirrel.Mac can swap them in place; only notarization is skipped, which
in-place updates never check.
Version tails are stripped to the base (1.4.160-hourly.<stamp>, not
1.4.160-rc.3-hourly.<stamp>) so hourlies sort below both rc.N and stable and
are reachable only by an explicit pinned jump, never by an ordinary check.
The picker is revealed by Option-clicking the Updates header, matching the
Help menu's existing hidden admin affordance. Pinned jumps set allowDowngrade
and release the feed on every settle path so a jump can never leave background
checks permanently deferred.
* chore(hourly): create orca-hourly and add token provisioning script
Adds setup-hourly-release-token.sh, which provisions HOURLY_RELEASE_TOKEN
without the value ever reaching stdout, argv, or shell history: it is read
with `read -rs`, passed to gh through GH_TOKEN in the environment rather than
as an argument (argv is world-readable via ps), piped into `gh secret set` on
stdin, and scrubbed by an EXIT trap.
Verification creates and deletes a draft release in orca-hourly to prove
Contents:write for real rather than trusting the permission checkbox. Drafts
are absent from the releases atom feed, so the probe cannot disturb users.
Refuses to run without a controlling terminal instead of falling through
having set nothing, and refuses to run under xtrace, which would echo the
token on every expansion.
* fix(updater): address review feedback on the hourly channel
Renderer:
- Guard listBuilds against out-of-order responses. activeChannel flips once
getVersion resolves, and rapid channel clicks stack requests, so a slower
earlier load could land last and fill the list with builds from a channel
the picker was no longer showing.
- Selecting the running build's own channel now clears the override instead
of pinning it. There was previously no way back to "follow this build's
channel", so merely opening the panel left background checks pinned.
- Validate releaseChannelOverride on hydration, matching every other
enum-like field in that function.
Main:
- Exclude pinned jumps from recordCompletedUpdateCheck() in update-available.
A dev browsing the picker was persisting lastUpdateCheckAt and suppressing
the next real background check for a full day.
- parseHourlyVersionStamp now anchors on the whole version and round-trips
the parsed fields. It accepted garbage prefixes, and Date.UTC rolled
impossible dates forward, so ...hourly.202602300000 rendered as March 2.
Workflow:
- Publish into a draft and flip it live only after the manifest check. The
window between creating the release and verifying its assets previously
exposed a tag the picker would offer and the download would 404 on; a
draft is invisible to listReleaseBuilds, so a job that dies in that
window — including a hard kill by the job timeout, which runs no cleanup
step — leaves nothing user-visible behind.
- Add a failure handler that discards the draft, gated on the publish step
not having succeeded so a later prune failure cannot delete a live release.
- Align retry budgets with the job timeout (was 60min against a worst case
of ~185min, so a mid-retry kill skipped the cleanup that step exists for).
- Exclude drafts from the freshness and retention queries.
- persist-credentials: false; the job only reads this repo and never pushes.
* refactor(hourly): authenticate with a GitHub App instead of a PAT
A fine-grained PAT expires, and the hourly build would then fail silently on
a schedule nobody watches. A GitHub App's private key has no expiry, so this
is set up once. It is also owned by the org rather than by the person who
created it, so the credential survives that person leaving.
The workflow mints a short-lived installation token via
actions/create-github-app-token and passes it as GH_TOKEN. Installation
tokens live one hour, which is ample: this job runs no tests, no
notarization, and no Windows signing, so it is pack + upload. The retry
budgets and job timeout are re-sized to that reality rather than copied from
the release pipeline, whose 3x45 publish budget exists for notarization and
SignPath.
setup-hourly-release-token.sh now provisions HOURLY_RELEASE_APP_ID and
HOURLY_RELEASE_APP_PRIVATE_KEY. The key is redirected from a file straight
into `gh secret set` on stdin, so its contents never enter a shell variable,
argv, or the terminal.
* fix(hourly): make the xtrace guard fire and cover cancelled runs
The xtrace guard disabled tracing before testing for it, so `[[ -o xtrace ]]`
read the state the previous line had just cleared and never fired. `bash -x`
ran straight through, tracing exactly the key handling the guard exists to
prevent. Test first, then disable.
The draft cleanup only ran on failure(), but a run stopped from the Actions
UI is cancelled(), not failed — a manual cancel mid-publish stranded the
draft. Cover both.