# 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](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`](../packages/release-bot/README.md) 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 thirty minutes. When core changes but `packages/create-oma-app` does not,
deterministic policy maps core's bump to a create-oma-app bump by breaking
nature: a core major bumps create-oma-app minor (its 0.x minor position carries
the "breaking" signal), and any non-breaking core bump bumps create-oma-app
patch. 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`](../CHANGELOG.md) is a single root file keyed by core version,
with an `## Unreleased` section at the top.
## What earns a changelog entry
[`CHANGELOG.md`](../CHANGELOG.md) documents what a consumer of the published
packages experiences. The test is mechanical: an entry is warranted when the
change reaches a consumer through the public API surface, runtime behavior, or
a file that ships in an npm tarball. Each package's `files` field is the
authority on what ships; for `@open-multi-agent/core` that is `dist`,
`README.md`, and `LICENSE`.
Warranted:
- a public export added, removed, renamed, or changed in signature
- runtime behavior an unchanged caller can observe
- content in a shipped file, including `README.md` and the JSDoc that `dist`
carries into the published `.d.ts` files
Not warranted:
- `@open-multi-agent/release-bot`, which is private and never published
- the release process and repository automation
- examples, fixtures, tests, CI configuration, and tsconfig files, none of
which ship
- a defect introduced and fixed between two releases, because no published
version ever carried it
A comment-only diff changes a shipped file only when that comment reaches
`dist`. Documentation under `docs/` does not ship; describe a provider or
runtime behavior it documents only when that behavior is itself part of the
release.
When a change is genuinely ambiguous, leave it out. The release PR diff carries
every commit regardless, so a maintainer still sees it.
## 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`](../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`](../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:23 UTC
(18:23 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`, which reads the `version`
field of the core and create-oma-app manifests at that commit and at its
first parent and stops before the publish job unless both moved. The
publisher then re-derives the same fact and refuses a commit whose versions
did not increment, so later unrelated commits cannot accidentally publish.
Reading the version rather than asking whether those manifests changed at
all is what keeps an ordinary dependency or metadata edit from starting a
job that mints a write-scoped token and then fails closed.
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`](../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 `
`, 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:
```bash
jq -Rs '{text: ., mode: "gfm"}' < notes.md | gh api /markdown --input - | grep -c '
'
```
That endpoint matches what the release page renders. A correct release body
returns `0`.
The published body is the version's changelog section followed by three sections
the changelog does not carry. `## Packages` names every workspace version and
says which ones were not republished. `## Thanks` credits contributors from
outside the project, resolved from the commits between the previous release tag
and the release commit, with the maintainer's own commits and bot commits
excluded. Each contributor is credited by GitHub login: a noreply address
carries it, and any other address is resolved by asking GitHub which account
claims the commit, falling back to the commit's display name when no account
does. `## Install` is copy-pasteable. All three are derived from the release
commit and its own manifests, so no model output reaches them.
## 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 --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.