diff --git a/.github/workflows/release-node.yml b/.github/workflows/release-node.yml index 1f828408..f02df723 100644 --- a/.github/workflows/release-node.yml +++ b/.github/workflows/release-node.yml @@ -378,6 +378,62 @@ jobs: # shellcheck disable=SC2086 pnpm publish --access public --no-git-checks $DIST_TAG + # AAASM-3840: keep the npm `latest` dist-tag current. + # + # The publish steps above route each build to a *channel* dist-tag derived + # from its SemVer pre-release identifier (alpha/beta/rc), and only a bare + # GA `X.Y.Z` publish moves `latest` implicitly. Nothing ever advances + # `latest` for a pre-release, so on a pre-1.0 project that has only ever + # shipped pre-releases, `latest` stays frozen at whatever the very first + # publish happened to be — making a bare `npm install @agent-assembly/sdk` + # resolve to an ancient alpha (the bug this fixes). + # + # POLICY (pre-1.0 / pre-GA): `latest` always points at the *highest* + # SemVer version currently on npm, across every channel. SemVer precedence + # (alpha < beta < rc < GA) makes this self-correcting and monotonic: + # - a GA publish wins over any pre-release of the same base, so `latest` + # naturally tracks the newest GA once one exists; + # - while only pre-releases exist, `latest` tracks the newest pre-release + # (e.g. rc.2 over beta.5 over alpha.9.1) instead of the oldest; + # - it never regresses `latest` to an older build, even if an out-of-band + # hotfix to an older channel is published after a newer one. + # Runs in BOTH publish modes (gated only on dry_run, like the main publish + # step) so every real publish re-asserts the invariant. + - name: Update `latest` dist-tag to newest published version + if: steps.tag.outputs.dry_run != 'true' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + set -euo pipefail + PKG="@agent-assembly/sdk" + # All versions on npm (the just-published one included). `npm view + # versions --json` returns an array, or a bare string if exactly one + # version exists — normalize both to newline-delimited output. + mapfile -t VERSIONS < <(npm view "$PKG" versions --json | node -e ' + const raw = require("fs").readFileSync(0, "utf8").trim(); + const data = JSON.parse(raw); + for (const v of Array.isArray(data) ? data : [data]) console.log(v); + ') + if [[ ${#VERSIONS[@]} -eq 0 ]]; then + echo "::error::no published versions found for $PKG" + exit 1 + fi + # `semver` sorts ascending and includes pre-releases when passed + # explicitly, so the last line is the highest by SemVer precedence. + NEWEST=$(pnpm dlx semver "${VERSIONS[@]}" | tail -n1) + CURRENT=$(npm view "$PKG" dist-tags.latest 2>/dev/null || echo "") + echo "newest published=$NEWEST current latest=$CURRENT" + if [[ -z "$NEWEST" ]]; then + echo "::error::could not determine newest version for $PKG" + exit 1 + fi + if [[ "$NEWEST" == "$CURRENT" ]]; then + echo "latest already points at $NEWEST — nothing to do" + else + npm dist-tag add "${PKG}@${NEWEST}" latest + echo "moved latest: ${CURRENT:-} -> $NEWEST" + fi + # AAASM-2955: cut a git tag + GitHub Release at the published npm # version so the README "GitHub release" badge tracks the current # release. Runs only on the real-publish path (dry-run skips it, same diff --git a/docs/release/npm-dist-tags.md b/docs/release/npm-dist-tags.md new file mode 100644 index 00000000..3e65ad8f --- /dev/null +++ b/docs/release/npm-dist-tags.md @@ -0,0 +1,56 @@ +# npm dist-tag policy — `@agent-assembly/sdk` + +This package publishes to npm via [`.github/workflows/release-node.yml`](../../.github/workflows/release-node.yml). +Each release is routed to a **channel** dist-tag derived from its SemVer +pre-release identifier, and the floating `latest` tag is kept current +automatically. + +## Channel dist-tags + +The publish steps derive the channel tag from the version's pre-release +identifier: + +| Version example | dist-tag | +| --------------------- | ------------------ | +| `0.0.1-alpha.9.1` | `alpha` | +| `0.0.1-beta.5` | `beta` | +| `0.0.1-rc.2` | `rc` | +| `0.1.0` (bare `X.Y.Z`) | `latest` (implicit) | + +Install a specific channel with, e.g., `npm install @agent-assembly/sdk@rc`. + +## `latest` policy (pre-1.0 / pre-GA) + +While the project is pre-1.0 and has shipped only pre-releases, `latest` always +points at the **highest SemVer version currently on npm, across every channel**. + +SemVer precedence (`alpha < beta < rc < GA`) makes this rule self-correcting and +monotonic: + +- A GA publish wins over any pre-release of the same base, so `latest` naturally + tracks the newest GA once one exists. +- While only pre-releases exist, `latest` tracks the newest pre-release + (e.g. `rc.2` over `beta.5` over `alpha.9.1`) — never the oldest. +- It never regresses `latest` to an older build, even if a hotfix to an older + channel is published after a newer one. + +The release workflow re-asserts this invariant on every real (non-dry-run) +publish, in both `all` and `main-only` publish modes. + +## One-time manual correction + +The workflow only advances `latest` on the **next** publish. If `latest` is +currently stale (the bug tracked in AAASM-3840 left it pinned at +`0.0.1-alpha.3`), a package owner with an npm publish token must run the +correction once, out of band: + +```bash +# Replace with the highest version currently on npm +# (e.g. the current rc), then: +npm dist-tag add @agent-assembly/sdk@ latest +``` + +After that one-time fix, the workflow keeps `latest` current going forward. + +See the canonical docs at https://docs.agent-assembly.com for end-user install +guidance.