Files
open-multi-agent/.github/RELEASING.md
T
Jack Chen 8fa3bb8362 feat(release): add OMA-powered release bot (#483)
* feat(release): add OMA-powered release bot

* fix(release): harden bot execution boundaries

* chore(release): schedule bot for Friday evening
2026-08-10 17:18:29 +08:00

13 KiB

Releasing

This repository publishes three packages from one npm workspaces root. A fourth private workspace, @open-multi-agent/release-bot, prepares and executes the repository-specific workflow but is never published. This file records what ships, in what order, and what CI proves at each stage. The day-to-day contribution flow is in CONTRIBUTING.md.

What ships

Package Version track Tagged Published
@open-multi-agent/core The trunk. Git tags and GitHub Releases track this version. vX.Y.Z Every release
@open-multi-agent/otel Independent. Depends on core through a semver range, so a core release does not force a republish. No tag Only when it changes
create-oma-app Independent. Its templates pin core exactly, so it ships alongside every core release. No tag Every core release

The root package.json is private and is never published. Each published package sets publishConfig.access to public, so no extra access flag is needed at publish time.

Release bot and authority boundary

packages/release-bot uses OMA itself to prepare a release. Its fixed runTasks() graph runs change analysis and compatibility review in parallel, then a planner and an independent reviewer. All four agents use DeepSeek V4 Flash. Only the two evidence roles receive narrow, read-only tools: immutable evidence, the release contract, and one deterministic risk-ranked, size-bounded diff bundle whose paths are selected by code rather than the model. The planner and reviewer consume the immutable summary and structured dependency reports. Model output is restricted to version bump classes and changelog prose; deterministic code owns concrete versions, the allowed file set, template pins, validation, Git/GitHub mutations, package order, registry checks, tags, and GitHub Releases.

Each DAG task has one task-level attempt; malformed structured output may use OMA's single in-run correction, but the whole role is not restarted. Per-role turn and output budgets bound model work, and the complete planning DAG aborts after ten minutes. When core changes but packages/create-oma-app does not, deterministic policy forces a create-oma-app patch bump for the template pin. The planner's scaffolder bump class is authoritative only when that workspace has merged changes of its own; normalization occurs before independent review.

The weekly workflow creates a ready release PR automatically. A maintainer reviews and merges it manually. Merge is the release approval, but it is not a model tool approval: publication happens in a separate deterministic workflow only after CI succeeds on the exact merged release commit.

Repository content is treated as untrusted evidence. Agents never receive bash, write tools, GitHub credentials, npm credentials, or publish tools. A rejected or invalid structured plan fails closed without creating a branch.

One-time activation

The workflows require all of the following external configuration:

  1. Install a repository-scoped GitHub App with Contents: read/write and Pull requests: read/write. Set its client ID as the repository variable RELEASE_BOT_APP_CLIENT_ID and private key as the secret RELEASE_BOT_APP_PRIVATE_KEY.
  2. Add DEEPSEEK_API_KEY as a repository Actions secret.
  3. Create a GitHub environment named npm-release. Environment reviewers are optional; merging the release PR is already the required human approval.
  4. For each of the three npm packages, configure a GitHub Actions trusted publisher for organization open-multi-agent, repository open-multi-agent, workflow filename publish.yml, environment npm-release, and the npm publish action.

The GitHub App token is required rather than the repository GITHUB_TOKEN so the automatically created PR starts normal CI and the published GitHub Release starts release-smoke.yml. npm publication uses short-lived OIDC credentials; there is no NPM_TOKEN. Trusted publishing currently requires a GitHub-hosted runner, Node 22.14 or newer, npm 11.5.1 or newer, and id-token: write.

Versioning and tags

  • Only core is tagged. Tags are vX.Y.Z matching the core version, lightweight rather than annotated, pointing at the release commit.
  • @open-multi-agent/otel and create-oma-app carry their own version numbers and are not tagged.
  • CHANGELOG.md is a single root file keyed by core version, with an ## Unreleased section at the top.

Breaking changes

Decide whether a release contains one before the release commit, not while writing release notes. A change is breaking when it can stop working for a caller who did nothing but upgrade:

  • the engines floor rises
  • a published direct dependency crosses a major version
  • input that an earlier release accepted is now rejected
  • a public export is removed, renamed, or has its signature narrowed

A conventional-commits ! marker on a merged commit signals that one landed, but it is not a substitute for the steps below, because nothing carries it through to a reader of the release.

When a release contains a breaking change:

  • CHANGELOG.md opens that version with a ### Breaking changes section above ### Added, naming what breaks and what the caller has to do.
  • The GitHub Release repeats it as its first section, not as a note near the end.
  • Weigh the version number against how far the change reaches. 1.14.0 raised the engines floor to Node 20 and moved openai from v4 to v6 while shipping as a minor, so every ^1.x caller on Node 18 received it automatically. npm treats an engines mismatch as an EBADENGINE warning rather than an install failure, which means those projects install successfully and fail later at run time.

The release commit

Version bumps land on main through a normal pull request before anything is published. A release commit contains:

  • the version bump in packages/core/package.json
  • the version bump in packages/otel/package.json, when otel is part of this release
  • the version bump in packages/create-oma-app/package.json
  • the new core version pinned in every create-oma-app template manifest:
    • packages/create-oma-app/templates/demo/package.json
    • packages/create-oma-app/templates/pr-review/package.json
    • packages/create-oma-app/templates/security/package.json
    • packages/create-oma-app/template/package.json, the shared base
  • the CHANGELOG.md entry, moved out of ## Unreleased
  • the regenerated package-lock.json

The three package manifests are what users receive, and their package versions remain independent as described above. The package job in ci.yml asserts that every templates/*/package.json overlay pins the current core version; it does not require the otel or create-oma-app package version to equal core. The base template manifest is not covered by that assertion and is not user-facing either, because every overlay ships its own package.json that overwrites the base copy at scaffold time. Keep it in sync anyway so local tooling and the base layer do not disagree with the release, but a stale pin there does not reach a generated project. See packages/create-oma-app/AGENTS.md for the full set of template traps.

Order

  1. Prepare a release PR. release-bot.yml runs each Friday at 10:00 UTC (18:00 Asia/Taipei) and is also manually dispatchable. It exits successfully without calling a model when a release PR is already open, no commits exist after the latest core tag, or packages/core did not change. Otherwise it plans the release, updates the known manifests, pins, changelog, and lockfile, runs root lint, test, and build validation, pushes a release-bot/core-vX.Y.Z branch, and opens a ready PR.
  2. Review and merge the release commit into main. Branch protection and the normal PR CI remain authoritative. Never merge a release proposal merely because its model reviewer approved it.
  3. Wait for CI on the exact merged release commit. A successful CI workflow run on main triggers publish.yml. The publisher verifies that both core and create-oma-app versions increased relative to that commit's first parent; later unrelated commits cannot accidentally publish.
  4. Publish to npm in order: core, then otel when its declared version is not already live, then create-oma-app. The publisher checks the public registry before every action and waits for each new version to resolve before continuing. Rerunning the exact release commit resumes a partial publication rather than attempting to republish an immutable npm version.
  5. Tag the release commit vX.Y.Z and push the tag. The lightweight tag is still created only after every expected package is visible. An existing tag must point to the exact release commit, and a tag with missing packages is a hard failure.
  6. Publish the GitHub Release last. The GitHub App event triggers release-smoke.yml, which resolves the real registry bytes. An existing matching Release is treated as an idempotent resume.

For recovery, manually dispatch publish.yml with the exact merged release commit SHA. The workflow verifies that the SHA is reachable from main and has an already successful CI run; it does not accept a branch name, failed CI, or an arbitrary working tree.

Release notes are not a copy of the changelog

The two are rendered under different rules. CHANGELOG.md is hard-wrapped at 80 columns, which is correct for a repository file because GitHub joins a single newline into a space there. A release body is rendered with GFM hard line breaks, where every newline becomes a <br>, so pasted wrapped text turns into a column of lines that look truncated at 80 characters.

Unwrap each paragraph and list item onto a single line before publishing, and check the draft first:

jq -Rs '{text: ., mode: "gfm"}' < notes.md | gh api /markdown --input - | grep -c '<br>'

That endpoint matches what the release page renders. A correct release body returns 0.

What CI proves

Before merge

ci.yml runs on every push and pull request targeting main.

  • package builds every workspace, then:
    • imports each core entry point, runs the CLI help, and exercises the evaluation gate on both its pass and fail paths
    • asserts the core, otel, and create-oma-app tarballs ship exactly the expected files
    • packs core, installs it, and smoke-tests the installed oma bin
    • asserts templates/*/package.json pin the current core version, and typechecks the template against core
    • resolves the lowest core version allowed by otel's dependency range, packs it from npm, and installs the real core and otel tarballs into clean consumers
  • scaffold-e2e runs npm run test:scaffold: pack, scaffold, install, and run, all from local tarballs.
  • lint, test across Node 20/22/24, and coverage cover the rest.

Running npm pack --dry-run inside a package locally mirrors the tarball assertion CI performs.

After the GitHub Release

release-smoke.yml triggers on release: published and runs two independent jobs against the real registry.

npx-scaffold proves the published bytes work from a user's point of view:

  • npx create-oma-app@latest <project> --template pr-review --provider cloud
  • the generated package.json pins @open-multi-agent/core at exactly the release tag without its leading v
  • npm install resolves that same core version
  • npm run demo succeeds with no API key set, driving the real scheduler and report generation from scripted responses
  • reports/ contains Markdown, JSON, and HTML output carrying the expected demo-mode markers

The whole chain retries up to three times with a delay, so a stale npm cache or CDN propagation lag becomes a retry rather than a failure.

otel-consumer covers @open-multi-agent/otel, which has no tag of its own and would otherwise get no post-publish coverage:

  • reads the otel version from packages/otel/package.json at the release commit, since the release tag only names the core version
  • installs that otel version together with the released core and pinned OpenTelemetry packages into a clean consumer
  • asserts the resolved core is the released one, which catches a core release that has outgrown otel's dependency range (npm would quietly resolve an older core rather than fail the install)
  • emits a span record through createOtelTraceSink and asserts it reaches an InMemorySpanExporter

When otel did not change in a given release, its version is already live from an earlier one and this job still proves the pairing holds against the new core.

This is a post-publish alarm, not a gate. The release is already out when either job runs. A red run means the published bytes are broken and a fix release is needed; it does not block anything.