feat(release): weekly release train — channels, onboarding, dashboard banner, cron#1781
feat(release): weekly release train — channels, onboarding, dashboard banner, cron#1781suraj-markup wants to merge 9 commits intomainfrom
Conversation
Test Coverage Report
Per-file breakdown
Uncovered lines
|
Greptile SummaryThis PR implements the full release pipeline: weekly stable releases via changesets, nightly canary publishes via cron, and a CLI/dashboard update experience with channel awareness (stable/nightly/manual), session guards, and an onboarding prompt.
Confidence Score: 5/5Safe to merge. All previously flagged issues are confirmed fixed; the single new observation is a minor style-guide deviation that does not affect correctness. The release infrastructure, channel-aware update pipeline, active-session guard, and dashboard banner are all well-structured. All previously flagged issues from prior review rounds are confirmed fixed. The core version comparison logic lives in a single tested location in No files require special attention. The only note is the
|
| Filename | Overview |
|---|---|
| packages/web/src/app/api/update/route.ts | Active-session guard + detached spawn of ao update. Windows PATHEXT fix applied; detached: true always violates cross-platform guide's detached: !isWindows() rule. |
| packages/cli/src/commands/update.ts | Adds active-session guard and channel-aware npm/pnpm/bun update routing. shell: isWindows() + windowsHide: true correctly applied to runNpmInstall. |
| packages/cli/src/lib/update-check.ts | Channel-aware version checking, cache management, homebrew/bun path classifier; isVersionOutdated re-exported from core. |
| packages/core/src/version-compare.ts | New shared isVersionOutdated implementation with prerelease segment comparison; correct SHA-suffix ordering via lexical fallback. |
| packages/web/src/app/api/version/route.ts | Cache-only GET for banner; correctly handles git installs via cached.isOutdated fallback, channel mismatch, and legacy cache entries. |
| packages/web/src/components/UpdateBanner.tsx | Banner hides on manual channel, dismissed version, and after update starts. handleDismiss resets phase so the hide condition fires from blocked/error state. |
| packages/cli/src/lib/update-channel-onboarding.ts | Ask-once gate for update channel; uses createDefaultGlobalConfig() (platform-aware runtime default) when no config file exists yet. |
| .github/workflows/canary.yml | New cron-driven nightly canary workflow; creates a minimal temp changeset when none exist, publishes under @nightly tag. |
| .github/workflows/release.yml | Stable release via changesets/action on CI pass on main; Version Packages PR flow; correctly gates on push event and successful conclusion. |
Sequence Diagram
sequenceDiagram
participant CLI as ao CLI (startup)
participant Cache as ~/.cache/ao/update-check.json
participant Registry as npm registry
participant Dashboard as Next.js Dashboard
participant Banner as UpdateBanner.tsx
CLI->>Registry: fetchLatestVersion(channel) [background]
Registry-->>CLI: latestVersion
CLI->>Cache: "writeCache({ latestVersion, channel, installMethod, isOutdated })"
Dashboard->>Banner: mount
Banner->>Dashboard: GET /api/version
Dashboard->>Cache: readUpdateCheckCacheRaw()
Cache-->>Dashboard: cached version info
Dashboard-->>Banner: current, latest, channel, isOutdated
alt isOutdated and channel not manual
Banner->>Banner: show update strip
Banner->>Dashboard: POST /api/update
Dashboard->>Dashboard: ensureNoActiveSessions()
alt sessions active
Dashboard-->>Banner: 409 activeSessions message
Banner->>Banner: phase blocked show error
else no active sessions
Dashboard->>CLI: spawn ao update detached
Dashboard-->>Banner: 202 ok true
Banner->>Banner: phase started hide banner
end
end
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
packages/web/src/app/api/update/route.ts:79-84
Per the cross-platform guide (`docs/CROSS_PLATFORM.md`): "Use `detached: !isWindows()` rather than always-`true` or always-`false`." On Windows, `detached: true` creates a new console process group, which behaves differently from POSIX detachment. `child.unref()` already prevents the Node.js event loop from waiting for the child on both platforms, so setting `detached: !isWindows()` achieves the same "fire and forget" result while respecting the Windows process-group convention.
```suggestion
const child = spawn("ao", ["update"], {
detached: !isWindows(),
stdio: "ignore",
shell: isWindows(),
windowsHide: true,
});
```
Reviews (7): Last reviewed commit: "chore(release-train): cosmetic — workflo..." | Re-trigger Greptile
… banner, cron Implements the full release pipeline described in release-process.html (supersedes #1525, which only had the workflow scaffolding). A. Release infrastructure — .github/workflows/canary.yml triggers on a cron ('30 17 * * 5,6,0,1,2', i.e. 23:00 IST Fri–Tue) plus workflow_dispatch, without the stale-SHA guard or the merged-PR-comment step from #1525 (cron has no merged-PR context). release.yml uses changesets/action. .changeset/config.json adds the snapshot template and moves the private @aoagents/ao-web to ignore[]. B. Channel awareness (packages/cli/src/lib/update-check.ts) — new updateChannel field in the global-config Zod schema (stable | nightly | manual; defaults to manual so existing users see no surprise installs). fetchLatestVersion now reads dist-tags[channel] from the registry; isVersionOutdated compares prerelease segments numerically + lexically so SHA-suffixed nightlies sort correctly. maybeShowUpdateNotice and scheduleBackgroundRefresh skip entirely on manual. C. Active-session guard (packages/cli/src/commands/update.ts) — before any handle*Update proceeds, sm.list() filters for working/idle/ needs_input/stuck and refuses with `N session(s) active. Run `ao stop` first.` instead of auto-stopping (per the design doc: surprise-killing user work is worse than refusing). D. Soft auto-install + onboarding — handleNpmUpdate skips the confirm prompt on stable/nightly. New packages/cli/src/lib/update-channel- onboarding.ts prompts once on the first `ao start` after this lands; ask-once gate keyed on the absence of updateChannel in the global config; dismissal persists `manual`. New `ao config set updateChannel <value>` command (also handles installMethod). E. Dashboard banner — packages/web/src/app/api/version/route.ts reads the same cache file the CLI writes (~/.cache/ao/update-check.json, XDG-aware) and rejects cache entries from a different channel. packages/web/src/app/api/update/route.ts duplicates the active-session guard so the dashboard can return a structured 409. New UpdateBanner component wired into Dashboard.tsx — Tailwind only, var(--color-*) tokens, dismissible per-version via localStorage, deferred fetch so it doesn't shift the call order in existing dashboard tests. F. Bun + Homebrew detection (update-check.ts) — new classifiers for ~/.bun/install/global/ (auto-installs `bun add -g @aoagents/ao@<channel>`) and /Cellar/ao/ (notice-only — `brew upgrade ao`, never auto-install because brew owns the symlinks). New installMethod override field in the global config to pin detection when path heuristics fail. Tests: +155 (B/C/F unit, onboarding ask-once gate, /api/version + /api/update, UpdateBanner visibility/dismiss/click). pnpm test, pnpm typecheck, pnpm lint all green for the changes; the same 10 pre-existing test failures observed on main are still present (all environment-dependent: ~/.cache/ao state, codex binary install, /private path canonicalization on macOS). Closes #1525 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI fixes:
- Web /api/update spawn ENOENT — attach `child.on("error", ...)` so the
asynchronous spawn-error event from a missing `ao` binary doesn't bubble
up as an unhandled error and crash vitest. The route already returns 202
before the error fires; on real installs the user sees "no version change"
if the install fails.
- start.test.ts pollution — runStartup calls `maybePromptForUpdateChannel`,
which (with isHumanCaller mocked to true) writes to the real
~/.agent-orchestrator/config.yaml on the CI runner via persistUpdateChannel.
Subsequent tests then load that newly-created (empty-projects) config and
report "No projects configured" instead of the expected "project not found".
Fix: stub `update-channel-onboarding.js` in start.test.ts so runStartup
is a no-op for the channel prompt.
Review feedback:
- (P1) `runtime: "tmux"` hardcoded default in `persistUpdateChannel` and
`loadOrInit` would lock Windows users into a non-functional tmux config
when they dismiss the channel prompt. Both now use `getDefaultRuntime()`,
matching `makeEmptyGlobalConfig` in core's global-config.ts.
- (P2) `hasChosenUpdateChannel` JSDoc inverted — the second "True when"
bullet actually described the False case. Rewritten with separate
True/False sections that match the implementation.
- (P2) `isVersionOutdated` was duplicated between the CLI and the dashboard
/api/version route. Moved to a new shared module
`packages/core/src/version-compare.ts`, exported from `@aoagents/ao-core`,
consumed by both CLI (re-exports as `isVersionOutdated`) and the web route
directly. Added 14 unit tests in core for the canonical implementation.
Defensive: `maybePromptForUpdateChannel` now validates the prompt result via
`UpdateChannelSchema.safeParse` before persisting — never writes `undefined`
or an unrecognized string to disk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…back
- (P1) `ao update` silently never ran on Windows because `spawn("ao", ...)`
doesn't consult PATHEXT, so npm's `ao.cmd` shim wasn't found and the
async ENOENT was swallowed by the error handler. Add `shell: isWindows()`
+ `windowsHide: true` per the cross-platform guide.
- (P1) Dismiss button was inert when the banner was in the `blocked` (409)
or `error` phase — `setDismissedFor` set the localStorage flag but the
hide condition required `phase === "idle"`, so the banner stayed pinned
until reload. `handleDismiss` now resets phase to idle (and clears the
error message) so the existing condition fires. Added a regression test
covering dismiss from the 409 path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9f29131 to
8d0984c
Compare
…resolves npm.cmd (P1) The dashboard /api/update spawn got `shell: isWindows()` + `windowsHide: true` in 9f29131, but `runNpmInstall` in the CLI's `ao update` command was still missing the same fix. On Windows, `spawn("npm", ...)` without a shell wrapper doesn't consult PATHEXT, so npm/pnpm/bun's `*.cmd` shims never resolve and the install silently ENOENTs. Mirror the fix into runNpmInstall — it's the single spawn site behind every non-git, non-homebrew install path (npm-global, pnpm-global, bun-global, unknown), so this one change covers all four install methods. Tests: - Mock `isWindows` from @aoagents/ao-core so the spawn options can be inspected per-platform. - Assert `shell: true, windowsHide: true, stdio: "inherit"` on Windows. - Assert `shell: false` on macOS / Linux. - Parametrize over pnpm-global / bun-global to confirm the same options flow through every npm-style install command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed 3c2f706 — mirrored the Same single spawn site handles every npm-style install command (npm-global, pnpm-global, bun-global, unknown), so this one change covers all of them. Added 4 unit tests asserting the spawn options on Windows vs macOS/Linux, parametrized across npm/pnpm/bun. |
…alls
(P1) The dashboard banner never appeared for git-installed users because
`/api/version` ran `isVersionOutdated(current, "origin/main")`, and
`parseVersion("origin/main")` produces NaN parts that the early-exit guard
catches with `return false`. Git installs cache `latestVersion` as a git
ref (not a semver) and a precomputed `isOutdated` flag from `git fetch +
merge-base`; the CLI special-cases this in `update-check.ts`. Mirror the
same pattern here:
cached.installMethod === "git"
? cached.isOutdated === true
: isVersionOutdated(current, latest)
Also extend the local CacheData with `installMethod?: string` and
`isOutdated?: boolean` so the new branch type-checks. Kept as `string`
rather than importing the CLI's `InstallMethod` type — the literal "git"
compare is the only thing that matters here, and the web package shouldn't
take a dep on @aoagents/ao-cli.
Two new tests cover the git-install path: one asserts isOutdated=true is
trusted from the cache, the other asserts isOutdated=false (current with
origin) is trusted too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…flag guard #3 — ensureNoActiveSessions now consults loadGlobalConfig() first as a quick "any projects registered?" check, then routes through loadConfig(globalPath) only when the registry actually has projects (loadConfig dispatches to buildEffectiveConfigFromGlobalConfigPath when given the canonical global path — see packages/core/src/config.ts). Defends against AO_GLOBAL_CONFIG override to a non-canonical path. Three new tests cover: registered-projects path fires the guard correctly; empty registry returns early without building a SessionManager; missing global file returns early without even reading it. #4 — Restored the rejection of git-only flags on non-git installs. Users copy/pasting `ao update --skip-smoke` from older docs would silently no-op on npm/pnpm/bun installs. Now exits non-zero with: "--skip-smoke only applies to git installs (current install: npm-global)." Test it.each across npm/pnpm/bun/homebrew/unknown plus a positive test that git installs still accept the flag. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a stable user runs `ao config set updateChannel nightly` and then
`ao update`, isVersionOutdated(0.5.0, 0.5.0-nightly-abc) returns false (per
semver, prerelease < stable on equal base). The old code printed "Already
on latest nightly" and exited without installing — confusing, because the
install command we'd run is genuinely a different dist-tag.
Fix: snapshot the previously-cached channel BEFORE forcing a refresh, then
detect a switch via `previousChannel !== activeChannel && !info.isOutdated`.
On switch:
- Don't take the "already on latest" early-return.
- Print a yellow "Channel switch detected: was X, now Y." notice.
- Force a confirm prompt regardless of stable/nightly soft-install,
defaulting to "no" (channel-switch should be explicit). Manual users
still see their normal prompt.
Onboarding copy now includes one line about channel switches: "switching
later prompts before installing the other channel's build."
4 new tests: explicit switch fires the prompt + installs on yes; declines
on no; same-channel doesn't fire (back to "Already on latest"); first-ever
update with no previous cache doesn't fire either.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…are cache, dedup export #5 — UpdateBanner no longer wraps its mount fetch in setTimeout(0). Production code shouldn't bend to test mock ordering. Instead, the two brittle Dashboard tests that relied on `mockImplementationOnce` queue ordering now route by URL via `mockImplementation`, and the cadence test asserts "no other endpoints were touched" instead of "no fetch was touched at all". Also added a deliberate "no interval / re-fetch" comment per #6. #7 — Promoted core's `makeEmptyGlobalConfig` to the public `createDefaultGlobalConfig` (kept the internal alias for back-compat). Both the CLI's `persistUpdateChannel` and `loadOrInit` (in `ao config`) now call it instead of inlining the same defaults block. Single source of truth. #8 — New `packages/core/src/update-cache.ts` exports `getUpdateCheckCachePath`, `readUpdateCheckCacheRaw`, and `getInstalledAoVersion`. The CLI's `update-check.ts` keeps its richer install-method/channel/git-rev validation but now delegates path resolution and version lookup to core. The dashboard's `/api/version` route drops its duplicated `getCachePath`/`readCache`/`getCurrentVersion` and consumes from core directly. Cache layout is one file, not two. #9 — Removed the duplicate `export { isManualOnlyInstall }` from `update.ts` (also dropped the unused import). The canonical export lives in `update-check.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… changeset trim #1 release.yml: added a comment above `workflows: [CI]` warning that GitHub matches by name (not filename) and silently no-ops on mismatch — so a rename of ci.yml's `name:` field would mean releases stop triggering. #10 UpdateBanner: replaced text-[13px] / text-[12px] with text-sm / text-xs to match the dashboard's chrome scale. #6 Banner refresh: noted in the existing useEffect comment that we don't re-fetch — re-evaluate if "user kept tab open for days, missed an update" becomes a real complaint. #11 .changeset/release-train.md: dropped @aoagents/ao-web from the version bump list. The package is `private: true` and in changeset's ignore[], so listing it was cosmetic and would just clutter the eventual release notes with a non-published artifact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed bd25890, 655b6db, dfc7cb1, 3786ba5 addressing all 10 findings. MUST fix (commit bd25890)
SHOULD fix (commit 655b6db)
POLISH (commit dfc7cb1)
COSMETIC (commit 3786ba5)
Test status
Nothing deferred to follow-up. |
Summary
Implements the full release pipeline described in
release-process.html. Combines what would otherwise be five separate PRs into one cohesive landing.Supersedes #1525 — incorporates that PR's release infrastructure (canary + release workflows, changeset config, publishConfig on every package) with the cron / no-stale-SHA-guard / no-merged-PR-comment modifications called out in the design doc.
Sections (mapped to the design doc)
A. Cron tweaks + release infrastructure
.github/workflows/canary.yml—cron: '30 17 * * 5,6,0,1,2'(23:00 IST Fri–Tue) plusworkflow_dispatch. No stale-SHA guard (cron usesref: main); no merged-PR-comment step (no merged-PR context from cron)..github/workflows/release.yml—changesets/actionopens a "version packages" PR; merging it publishes@latest..changeset/config.json— addssnapshot.prereleaseTemplate: '{tag}-{commit}', moves the now-private@aoagents/ao-webtoignore[], includes all currently-shipped plugins in the linked group.packages/web/package.json—"private": truesochangeset publishnever tries to push the dashboard.publishConfig: { access: "public", provenance: true }.B. Channel awareness —
packages/cli/src/lib/update-check.tsupdateChannelfield inGlobalConfigSchema(stable | nightly | manual, defaultmanual). Schema is.optional().catch(undefined)so legacy / typo'd values don't break config load.fetchLatestVersion(channel)now readsdist-tags[channel]from the registry (full package doc, not the per-tag URL) and falls back tolatestifnightlyisn't published yet.isVersionOutdatedcompares prerelease segments numerically + lexically per semver — SHA-suffixed nightlies (0.5.0-nightly-abc < 0.5.0-nightly-def) sort correctly.maybeShowUpdateNoticeandscheduleBackgroundRefreshskip entirely onmanualso opted-out users see no traffic and no nudges.channelfield; entries from a different channel are ignored.C. Active-session guard —
packages/cli/src/commands/update.tsensureNoActiveSessions()runs before any install. Lists sessions, refuses withN session(s) active. Runao stopfirst.if any are inworking/idle/needs_input/stuck. Never auto-stops.POST /api/updateso the dashboard returns a structured409 { activeSessions, message }.D. Onboarding question +
ao config setpackages/cli/src/lib/update-channel-onboarding.ts— prompts once on firstao start. Ask-once gate keyed on the absence ofupdateChannelin the global config; dismissal persistsmanualso we don't re-ask.ao config set updateChannel <stable|nightly|manual>— also handlesinstallMethod. Newpackages/cli/src/commands/config.ts.runStartupcallsmaybePromptForUpdateChannel()after preflight.E. Dashboard banner
GET /api/version(packages/web/src/app/api/version/route.ts) reads the same cache file the CLI writes ($XDG_CACHE_HOME/ao/update-check.json). Cache-only; never makes a network call inside a request handler.POST /api/update(packages/web/src/app/api/update/route.ts) runs the active-session guard, then spawnsao updatedetached.UpdateBanner.tsx— top-of-page strip wired intoDashboard.tsx. Visible only whenisOutdated. Click POSTs to/api/update. Dismissal persists per-version inlocalStorage; a newer version reappears the banner. Tailwind only,var(--color-*)tokens, no inline styles.F. Bun + Homebrew detection
classifyInstallPathadds/Cellar/ao/(homebrew, notice-only —brew upgrade ao, never auto-install because brew owns the symlinks) and~/.bun/install/global/(auto-installsbun add -g @aoagents/ao@<channel>). Cellar check runs first because brew nests the npm tree underlib/node_modules/.installMethodglobal-config field overrides path detection for users with custom prefixes.Test plan
New tests added (155 total, all green):
update-check.test.ts— channel resolution, prerelease compare (rc.1 < rc.2, nightly SHA suffixes),dist-tagsfetch, channel-mismatched cache, manual-channel skip, bun/homebrew classifiers,installMethodoverride, channel-awaregetUpdateCommand.update.test.ts— active-session guard (refuse on working/idle/needs_input/stuck, allow terminal statuses), soft auto-install (skip prompt on stable/nightly, prompt on manual), homebrew notice-only.update-channel-onboarding.test.ts— ask-once gate, dismissal persistsmanual, no re-prompt after persistence.version-update-api.test.ts— channel-aware cache reading, channel-mismatched entries ignored, 409 active-session refusal, 202 happy path.UpdateBanner.test.tsx— hides on up-to-date, hides onmanual, hides when dismissed for current latest, re-appears for new version, POST + 409 surface.CI:
pnpm typecheckcleanpnpm lintclean (0 errors; 50 pre-existing warnings, none in changed code)pnpm --filter @aoagents/ao-cli test— 92 update-check + 34 update-command + 11 onboarding tests pass; same 10 pre-existing failures observed onmainstill present (environment-dependent:/privatepath canonicalization,~/.agent-orchestrator/state).pnpm --filter @aoagents/ao-web test— 18 new tests pass; same 2 pre-existingprojects-routefailures still present.Manual verification needed
The release workflows themselves can only be exercised by merging — the cron schedule won't trigger from a feature branch and the release environment requires
NPM_TOKEN. The dashboard banner can be exercised locally by writing a stub cache file:Closes #1525
🤖 Generated with Claude Code