refactor(release): split publish into two-repo model — GitHub release public, npm publish private (#1815)
* refactor(release): split publish into two-repo model Org compliance forbids npm publish credentials in public repositories. Move the npm publish step out of this repo and into a private ComposioHQ/ao-publisher repo, triggered via repository_dispatch. Public repo (this one) now only: - Bumps versions via changesets/action (no `publish:` argument) - Creates git tags via `pnpm changeset tag` - Creates the GitHub release (stable or prerelease) - Dispatches `publish-npm-stable` / `publish-npm-nightly` to ao-publisher The only secret needed here is PUBLISHER_DISPATCH_TOKEN, a fine-grained PAT scoped to ao-publisher with `repository_dispatch:write`. NPM_TOKEN lives in ao-publisher's secret store and never enters this repo. Removed from both workflows: NODE_AUTH_TOKEN env, NPM_CONFIG_PROVENANCE env, and `registry-url` on setup-node — none are needed when no npm publish happens here. Canary also gains a skip-if-unchanged guard so cron ticks during quiet stretches don't republish identical SHAs. CONTRIBUTING.md "Release Setup" → "Release Architecture": documents the two-repo split, secret layout, rotation procedure, and the stable vs. nightly flows. Requires PUBLISHER_DISPATCH_TOKEN secret + ao-publisher repo setup by maintainers (out of scope for this PR — see PR description). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(release): address Greptile review findings on two-repo split Three review findings on PR #1815: 1. (P1) canary.yml missing git commit before tagging `pnpm changeset version --snapshot` modifies package.json but does not commit. `pnpm changeset tag` would then tag the pre-snapshot HEAD, so ao-publisher would check out un-bumped versions. Add a "Commit snapshot version bumps" step with `git diff --cached --quiet || git commit` (defensive: skip if nothing to commit) and the `[skip ci]` marker. The commit is never pushed to main — only the tags are pushed and the orphan commit travels with them. 2. (P2) release.yml `--target main` race condition If a commit lands on main between `git push --follow-tags` and `gh release create --target main`, the release commitish drifts and auto-generated notes pull in unrelated commits. Create an explicit `vX.Y.Z` git tag pointing at the version-bump commit, push it, and drop `--target`. `gh release create` then resolves the commitish from the existing tag — no race window. 3. (P2) canary.yml skip-guard suppresses post-stable nightlies `gh release list --limit 1` returns the most recent release by date, which could be a stable from `release.yml`. An explicit `workflow_dispatch` nightly right after a stable cut would be suppressed. Filter to prereleases only: gh release list --json tagName,isPrerelease \ --jq '[.[] | select(.isPrerelease)][0].tagName // empty' If no prerelease exists yet, the jq returns empty and the guard falls through naturally (LAST_SHA empty → condition false → nightly proceeds). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: retrigger checks (event dropped on previous push) * fix(release): make release.yml idempotent and dispatch reachable on re-run The previous design used a single `released` output (based on `after > before` tag count) to gate both `Create GitHub release` and `Dispatch npm publish`. On a re-run, all tags are already on the remote and `fetch-depth: 0` brings them down, so `pnpm changeset tag` adds nothing, `after == before`, `released=false`, and both steps are skipped — breaking the "re-run recovers cleanly" claim, especially the common case where the first run failed only at dispatch because `PUBLISHER_DISPATCH_TOKEN` was missing. Refactor into a `Determine release state` step that emits three independent signals: - `is_release_commit` — version-bump signal. Detected by comparing `packages/ao/package.json` version against its value in HEAD^. A Version Packages merge changes this; a regular commit does not. This is the filter that prevents the dispatch from firing on every commit to main (which all have `hasChangesets == 'false'`). - `tag_on_remote` — whether the `vX.Y.Z` tag exists at origin. - `release_exists` — whether the matching GitHub release exists. Each downstream step is gated on its own piece of state: - Tag push: `is_release_commit && !tag_on_remote` - Release create: `is_release_commit && !release_exists` - Dispatch: `is_release_commit` (always fires on a release commit) The dispatch fires unconditionally on a release commit, even when tag and release already exist on the remote. That guarantees recovery from the most common failure mode (first run succeeded everywhere except dispatch). The publisher must be idempotent against already- published versions for this to be safe — `pnpm changeset publish` already has this property since it skips packages whose current version is already on the registry. Update CONTRIBUTING.md → "Release Architecture": - Add "Idempotency contract for ao-publisher" section spelling out the no-op-on-already-published requirement. - Add "Recovery" section explaining that re-running the failed workflow is the canonical recovery path, plus a manual `gh api` fallback for cases where re-running isn't practical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(canary): skip-guard anchors on tag parent, not the orphan snapshot The previous P1 fix added a "Commit snapshot version bumps" step before `pnpm changeset tag`, which moved the snapshot tag onto an orphan commit (the version-bump commit) rather than main HEAD. The skip-if-unchanged guard still compared `git rev-list -n 1 "$LAST_TAG"` against `GITHUB_SHA`, but `LAST_TAG` now resolves to the orphan snapshot commit's SHA, never the main SHA. The two could never be equal → `skip=true` was unreachable → every cron tick republished regardless of whether main had advanced. Use `${LAST_TAG}^` to anchor on the snapshot commit's first parent — which is the main HEAD at the time the previous nightly ran — and compare that against the current `GITHUB_SHA`. Now the guard fires correctly when main hasn't advanced since the last nightly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(release): single umbrella tag per release, drop per-package tags `pnpm changeset tag` creates one tag per publishable package (~27 here) on every release. At 5 nightlies/week × 52 weeks × 27 packages that's roughly 7 000 tags/year just from canary — pure decoration since `ao-publisher` only consumes the umbrella `vX.Y.Z` tag. The per- package tags also cause partial-recovery conflicts: `git push --tags` on a re-run trips over tags that were pushed by the prior run. Drop the `pnpm changeset tag` call from both workflows and replace `git push origin --tags` with `git push origin "v$version"`. Push exactly one umbrella tag per release. CONTRIBUTING.md → "Release Architecture" updated: - Flow diagram replaces "changeset tag → push" with "push vX.Y.Z tag" - New paragraph spells out the single-tag-per-release policy and why we skip `pnpm changeset tag`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(canary): workflow_dispatch bypasses skip-guard for recovery When a nightly pushes the tag but fails at `gh release create` or dispatch (e.g. `PUBLISHER_DISPATCH_TOKEN` missing), a re-run via `workflow_dispatch` was silently skipped: `LAST_TAG` resolves to the just-pushed nightly, `${LAST_TAG}^` equals `GITHUB_SHA`, and the binary skip guard fires before any of the downstream steps we want to retry. `release.yml` handles the equivalent case by gating each step on independent state. Canary has a single binary gate, so it needs a different escape hatch: `workflow_dispatch` always proceeds. The human trigger is itself the signal that we want to run regardless of whether the source SHA looks unchanged. Cron-driven runs keep the SHA-equality dedup so quiet stretches don't republish the same SHA on every tick. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(release): replace ao-publisher dispatch with AO cron poll model Remove the ao-publisher dispatch steps and PUBLISHER_DISPATCH_TOKEN from both release.yml and canary.yml. npm publishing is now handled by an AO cron job on a private server that polls GitHub releases and publishes when a new tag is ahead of the current npm version. Changes: - release.yml: remove dispatch step, keep tag + GitHub release - canary.yml: remove dispatch step, keep tag + GitHub prerelease - CONTRIBUTING.md: rewrite Release Architecture for two-stage model (public CI → GitHub release, private cron → npm publish) * fix(release): address review — remove env/id-token, scope git add, fix wording - Remove environment: release from both workflows (no npm publish here, could block on required reviewers) - Remove id-token: write permission (unused, no OIDC needed) - Scope git add in canary to package.json and .changeset/ only (avoid staging build artifacts) - Fix 'orphan commit' wording → 'snapshot commit' (not actually orphan) - Fix nightly version format 0.0.0-* → X.Y.Z-nightly-<sha> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: i-trytoohard[bot] <1484917231245856808@users.noreply.github.com>
This commit is contained in:
parent
d95c9def10
commit
15158aa7b2
|
|
@ -1,10 +1,21 @@
|
|||
name: Canary
|
||||
|
||||
# Nightly canary publishes the current tip of `main` to npm under @nightly.
|
||||
# Two-stage release pipeline (nightly canary side).
|
||||
#
|
||||
# Nightly canary creates snapshot versions, tags, and a GitHub prerelease.
|
||||
# npm publishing is handled by a private cron job (AO) that polls GitHub
|
||||
# releases and publishes when a new prerelease tag is ahead of the current
|
||||
# npm nightly version.
|
||||
#
|
||||
# No NPM_TOKEN or publisher dispatch secrets are needed in this repo.
|
||||
# The only secret used is GITHUB_TOKEN (automatic).
|
||||
#
|
||||
# Cron schedule: 23:30 IST = 18:00 UTC, on Fri,Sat,Sun,Mon,Tue
|
||||
# (DOW 5,6,0,1,2). Wed/Thu are the bake window — no scheduled publishes.
|
||||
# `workflow_dispatch` lets the release captain re-cut a nightly during bake
|
||||
# when a fix lands and the Discord cohort should test the patched candidate.
|
||||
# `workflow_dispatch` lets the release captain re-cut a nightly during
|
||||
# bake when a fix lands and the Discord cohort should test the patched
|
||||
# candidate.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 18 * * 5,6,0,1,2"
|
||||
|
|
@ -15,38 +26,86 @@ concurrency:
|
|||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
canary:
|
||||
name: Publish canary
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
|
||||
# Skip-if-unchanged guard: if the most recent prerelease in this
|
||||
# repo was cut from the current tip of main, there's nothing new
|
||||
# to publish — short-circuit the whole job. Prevents republishing
|
||||
# the same SHA on every cron tick during quiet stretches.
|
||||
#
|
||||
# We filter to prereleases only (`isPrerelease == true`). If the
|
||||
# most recent release were a stable cut from `release.yml`, an
|
||||
# explicit `workflow_dispatch` nightly right after the stable
|
||||
# release would otherwise be suppressed — which we don't want.
|
||||
#
|
||||
# The previous-nightly tag points at the snapshot commit created
|
||||
# by the "Commit snapshot version bumps" step below. The snapshot
|
||||
# commit's first parent is the source main HEAD, so `${LAST_TAG}^`
|
||||
# is the right anchor to compare against `GITHUB_SHA` (main HEAD
|
||||
# at this run).
|
||||
#
|
||||
# `workflow_dispatch` always proceeds — recovery path for partial
|
||||
# failures. A human triggered the run, they want it to run.
|
||||
- name: Check if main has new commits since the last nightly
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
if [ "$GITHUB_EVENT_NAME" = 'workflow_dispatch' ]; then
|
||||
echo 'Manual trigger via workflow_dispatch — bypassing skip guard.'
|
||||
echo 'skip=false' >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
LAST_TAG=$(gh release list --json tagName,isPrerelease --jq '[.[] | select(.isPrerelease)][0].tagName // empty')
|
||||
LAST_SHA=""
|
||||
if [ -n "$LAST_TAG" ]; then
|
||||
LAST_SHA=$(git rev-parse "${LAST_TAG}^" 2>/dev/null || echo "")
|
||||
fi
|
||||
if [ -n "$LAST_SHA" ] && [ "$LAST_SHA" = "$GITHUB_SHA" ]; then
|
||||
echo "Last prerelease ($LAST_TAG) was cut from $GITHUB_SHA — skipping."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
- uses: actions/setup-node@v4
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
registry-url: https://registry.npmjs.org
|
||||
# No `registry-url`: this workflow does not publish to npm.
|
||||
|
||||
- run: echo "HUSKY=0" >> $GITHUB_ENV
|
||||
- if: steps.check.outputs.skip != 'true'
|
||||
run: echo "HUSKY=0" >> $GITHUB_ENV
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- if: steps.check.outputs.skip != 'true'
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- run: pnpm -r build
|
||||
- if: steps.check.outputs.skip != 'true'
|
||||
run: pnpm -r build
|
||||
|
||||
# Pre-publish guard: same as release.yml — catches workspace:* deps on
|
||||
# private packages before they'd silently break a published install.
|
||||
- run: node scripts/check-publishable-deps.mjs
|
||||
# Pre-publish guard: same as release.yml — catches workspace:* deps
|
||||
# on private packages before they'd silently break a published
|
||||
# install. Still runs here so the public repo blocks a malformed
|
||||
# snapshot before it reaches npm.
|
||||
- if: steps.check.outputs.skip != 'true'
|
||||
run: node scripts/check-publishable-deps.mjs
|
||||
|
||||
- name: Create snapshot versions
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
# If no changesets exist (e.g. right after a Version Packages merge),
|
||||
# create a minimal one. `changeset version --snapshot` consumes and
|
||||
|
|
@ -58,8 +117,52 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish canary
|
||||
run: pnpm changeset publish --tag nightly --no-git-tag
|
||||
# `pnpm changeset version --snapshot` modifies package.json files
|
||||
# on disk but does NOT create a git commit. Without this commit,
|
||||
# the umbrella tag we push below would point at the pre-snapshot
|
||||
# HEAD, so when the npm publisher checks out the tag it would see
|
||||
# the un-bumped versions and either publish wrong version numbers
|
||||
# or fail trying to republish an existing version.
|
||||
#
|
||||
# The commit is NEVER pushed to main — only the tag below is
|
||||
# pushed, and the snapshot commit travels with it as part of the
|
||||
# tag's reachable history. `[skip ci]` is defensive in case any
|
||||
# future change ever pushes this commit to a branch.
|
||||
- name: Commit snapshot version bumps
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git add packages/*/package.json packages/plugins/*/package.json .changeset/
|
||||
git diff --cached --quiet || git commit -m 'chore: snapshot version bump [skip ci]'
|
||||
|
||||
# Push the umbrella `vX.Y.Z` tag pointing at the snapshot commit.
|
||||
# The npm publisher resolves this tag to the snapshot commit with
|
||||
# the bumped package.json versions.
|
||||
#
|
||||
# We deliberately skip `pnpm changeset tag`: it would create one
|
||||
# tag per publishable package (~27 here) every night, which adds
|
||||
# up fast on the nightly cadence. The npm publisher only consumes
|
||||
# the umbrella tag, so the per-package tags are pure decoration.
|
||||
- name: Tag snapshot versions
|
||||
id: tag
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
run: |
|
||||
version=$(node -p "require('./packages/ao/package.json').version")
|
||||
git tag "v$version"
|
||||
git push origin "v$version"
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Public-facing GitHub prerelease. `--prerelease` flags it as such
|
||||
# so consumers and tooling that filter by stability stay correct.
|
||||
# No `--target`: the tag was just pushed and GitHub resolves the
|
||||
# commitish from it (avoids the race where another commit lands
|
||||
# on main between the tag push and this step).
|
||||
- name: Create GitHub prerelease
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "v${{ steps.tag.outputs.version }}" \
|
||||
--prerelease \
|
||||
--generate-notes
|
||||
|
|
|
|||
|
|
@ -1,8 +1,17 @@
|
|||
name: Release
|
||||
|
||||
# Stable @latest publishes when CI passes on `main` and a Version Packages PR
|
||||
# is merged. The changesets/action below opens/updates that PR automatically;
|
||||
# merging it runs `changeset publish` and pushes to npm.
|
||||
# Two-stage release pipeline.
|
||||
#
|
||||
# This public repo is responsible for version bumps, tagging, and creating
|
||||
# the GitHub release. npm publishing is handled by a private cron job (AO)
|
||||
# that polls GitHub releases and publishes when a new tag is ahead of the
|
||||
# current npm version.
|
||||
#
|
||||
# No NPM_TOKEN or publisher dispatch secrets are needed in this repo.
|
||||
# The only secret used is GITHUB_TOKEN (automatic).
|
||||
#
|
||||
# See CONTRIBUTING.md → "Release architecture" for the full picture.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
# Depends on the workflow named "CI" in .github/workflows/ci.yml — if
|
||||
|
|
@ -20,13 +29,11 @@ concurrency:
|
|||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
|
|
@ -41,24 +48,105 @@ jobs:
|
|||
with:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
registry-url: https://registry.npmjs.org
|
||||
# No `registry-url`: this workflow does not publish to npm.
|
||||
- run: echo "HUSKY=0" >> $GITHUB_ENV
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm -r build
|
||||
# Pre-publish guard: catches the case where a publishable package has a
|
||||
# workspace:* runtime dep on a `private: true` package — pnpm would
|
||||
# rewrite the dep on publish to a version that doesn't exist on npm,
|
||||
# breaking `npm install -g @aoagents/ao`.
|
||||
# breaking `npm install -g @aoagents/ao`. Still runs here so the
|
||||
# public repo blocks a malformed version bump before it reaches npm.
|
||||
- run: node scripts/check-publishable-deps.mjs
|
||||
|
||||
# changesets/action manages the "Version Packages" PR — when there
|
||||
# are pending changesets it opens/updates the PR; when there are
|
||||
# none (i.e. that PR was just merged) it would normally invoke the
|
||||
# `publish:` command. We deliberately omit `publish:` so the action
|
||||
# never runs `changeset publish`. npm publishing is handled by a
|
||||
# private cron that detects the GitHub release.
|
||||
- uses: changesets/action@v1
|
||||
id: changesets
|
||||
with:
|
||||
publish: pnpm changeset publish
|
||||
version: pnpm changeset version
|
||||
# Creates/updates a PR titled "chore: version packages" when changesets
|
||||
# are pending. Merging that PR publishes to npm under @latest.
|
||||
title: "chore: version packages"
|
||||
commit: "chore: version packages"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
|
||||
# Determine release state. Each downstream step (tag push, GH
|
||||
# release creation) is gated on its own piece of state so the
|
||||
# workflow is idempotent and recovers cleanly on re-run after a
|
||||
# partial failure.
|
||||
#
|
||||
# `is_release_commit` is the crucial signal: it filters out
|
||||
# regular commits to main (which also have `hasChangesets ==
|
||||
# 'false'` but should NOT trigger a publish). We detect a
|
||||
# version-bump commit by comparing the umbrella package's
|
||||
# version against its value in the parent commit — a Version
|
||||
# Packages merge changes that version, a regular commit does not.
|
||||
# The umbrella `@aoagents/ao` is the canonical version source for
|
||||
# the `vX.Y.Z` tag scheme and is part of the linked group, so
|
||||
# any release that bumps the cohort will bump this file.
|
||||
- name: Determine release state
|
||||
id: state
|
||||
if: steps.changesets.outputs.hasChangesets == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
version=$(node -p "require('./packages/ao/package.json').version")
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
prev_version=""
|
||||
if git rev-parse HEAD^ >/dev/null 2>&1; then
|
||||
prev_version=$(git show HEAD^:packages/ao/package.json 2>/dev/null | jq -r .version 2>/dev/null || echo "")
|
||||
fi
|
||||
if [ -n "$prev_version" ] && [ "$prev_version" != "$version" ]; then
|
||||
echo "is_release_commit=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_release_commit=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
if git ls-remote --tags --exit-code origin "refs/tags/v$version" >/dev/null 2>&1; then
|
||||
echo "tag_on_remote=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tag_on_remote=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
if gh release view "v$version" --json tagName >/dev/null 2>&1; then
|
||||
echo "release_exists=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "release_exists=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# Push the umbrella `vX.Y.Z` tag only if this is a fresh
|
||||
# version-bump commit AND the tag isn't already on the remote.
|
||||
# Skipped on re-runs where the tag was pushed on a prior run
|
||||
# (idempotent recovery).
|
||||
#
|
||||
# We deliberately skip `pnpm changeset tag`: it would create one
|
||||
# tag per publishable package (~27 here) on every release, which
|
||||
# creates partial-recovery conflicts on re-run when `git push --tags`
|
||||
# tries to re-push existing per-package tags. The npm publisher
|
||||
# only consumes the umbrella tag, so the per-package tags add no
|
||||
# value.
|
||||
- name: Tag versioned packages
|
||||
if: steps.state.outputs.is_release_commit == 'true' && steps.state.outputs.tag_on_remote == 'false'
|
||||
run: |
|
||||
git tag "v${{ steps.state.outputs.version }}"
|
||||
git push origin "v${{ steps.state.outputs.version }}"
|
||||
|
||||
# Public-facing GitHub release. Stable channel — not a prerelease.
|
||||
# No `--target`: the `vX.Y.Z` tag is on the remote at the
|
||||
# version-bump commit, and `gh release create` resolves the
|
||||
# commitish from the existing tag. Avoids the race where another
|
||||
# commit lands on main between the tag push and this step,
|
||||
# pulling unrelated commits into the auto-generated release notes.
|
||||
# Skipped if a release for this version already exists.
|
||||
- name: Create GitHub release
|
||||
if: steps.state.outputs.is_release_commit == 'true' && steps.state.outputs.release_exists == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create "v${{ steps.state.outputs.version }}" \
|
||||
--generate-notes
|
||||
|
|
|
|||
|
|
@ -70,13 +70,57 @@ ao update
|
|||
|
||||
`ao update` fast-forwards the local install repo, reinstalls dependencies, clean-rebuilds `@aoagents/ao-core`, `@aoagents/ao-cli`, and `@aoagents/ao-web`, refreshes the global launcher with `npm link`, and finishes with CLI smoke tests. Use `ao update --skip-smoke` when you only need the rebuild step, or `ao update --smoke-only` when validating an existing install.
|
||||
|
||||
## Release Setup (maintainers only)
|
||||
## Release Architecture (maintainers only)
|
||||
|
||||
The canary and stable release workflows require one secret configured in GitHub repo settings:
|
||||
AO uses a **two-stage release pipeline**. This public repo handles version bumps, git tags, and GitHub releases. npm publishing runs on a private server (AO cron job) that polls GitHub releases and publishes when a new tag is ahead of the current npm version. Org compliance forbids npm publish credentials in public repositories, so `NPM_TOKEN` never enters this repo.
|
||||
|
||||
- `NPM_TOKEN` — an npm automation token with publish access to the `@aoagents` org. Add it at **Settings → Secrets and variables → Actions → New repository secret**.
|
||||
### Where things happen
|
||||
|
||||
Without this secret, both `release.yml` and `canary.yml` will fail at the publish step.
|
||||
| Stage | Where | Responsibility |
|
||||
| ------------------------ | ------------------------------ | ------------------------------------------------------------------------ |
|
||||
| Versioning + GitHub release | This repo (public, CI) | Changesets version bumps, git tags, `gh release create` |
|
||||
| npm publish | Private server (AO cron) | Detects new GitHub releases → builds → `pnpm changeset publish` |
|
||||
|
||||
The flow on every release:
|
||||
|
||||
```
|
||||
This repo (public CI) Private server (AO cron)
|
||||
────────────────────── ─────────────────────────
|
||||
release.yml: Polls gh release list
|
||||
changeset version Detects new vX.Y.Z tag
|
||||
push vX.Y.Z tag Compare to npm @latest/@nightly
|
||||
gh release create vX.Y.Z If behind → checkout tag → build → publish
|
||||
|
||||
canary.yml: Same cron, detects prereleases
|
||||
changeset version --snapshot Publishes with --tag nightly
|
||||
commit snapshot bump + tag
|
||||
gh release create --prerelease
|
||||
```
|
||||
|
||||
Each release pushes a single umbrella `vX.Y.Z` git tag pointing at the version-bump commit. We deliberately do **not** run `pnpm changeset tag`, which would emit one tag per publishable package (~27) every release — fine for stable's monthly cadence, noisy on the nightly cadence (~7 000 tags/year). The npm publisher only consumes the umbrella tag, so the per-package tags add no value.
|
||||
|
||||
### Secrets
|
||||
|
||||
This repo requires **no additional secrets** beyond the automatic `GITHUB_TOKEN`. `NPM_TOKEN` lives only on the private server.
|
||||
|
||||
### How releases are cut
|
||||
|
||||
- **Stable**: merge the "chore: version packages" PR opened by `changesets/action`. `release.yml` tags the bumped packages and creates a `vX.Y.Z` GitHub release. The AO cron detects the new release and publishes to npm `@latest`.
|
||||
- **Nightly**: `canary.yml` runs on cron (23:30 IST Fri–Tue) or via `workflow_dispatch`. It snapshots versions to `X.Y.Z-nightly-<sha>` format (e.g., `0.6.1-nightly-7c46dc92`), tags, and creates a prerelease GitHub release. The AO cron detects the new prerelease and publishes to npm `@nightly`.
|
||||
|
||||
There is no path from this repo that calls `npm publish` directly.
|
||||
|
||||
### Idempotency
|
||||
|
||||
`release.yml` is idempotent: each step (tag push, GitHub release creation) is gated on whether that piece of state already exists, so a re-run after a partial failure picks up only the missing steps.
|
||||
|
||||
The AO cron is also idempotent — `pnpm changeset publish` skips packages whose current version is already on the registry, so re-running after a partial publish is safe.
|
||||
|
||||
### Recovery
|
||||
|
||||
If `release.yml` fails after the GitHub release was created, **re-run the failed workflow**: the state-detection step will see that the tag and release already exist and skip those steps.
|
||||
|
||||
If the AO cron fails to publish, it will retry on the next poll cycle (every 15 minutes). No manual intervention needed for transient failures. For persistent issues, check the cron logs on the private server.
|
||||
|
||||
## Testing your changes
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue