Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ rg -n "api\.slots\.register|sidebar_content|app_bottom" packages/
- Keep the remote feature branch after merging (disable automatic branch deletion on GitHub); delete only the local branch unless the user explicitly asks to remove the remote one.
- Safety-net-blocked git forms have recoverable equivalents: `git stash -u` before `worktree remove` (never `--force`), `git update-ref` for pointer-only moves on verified-identical trees, `git merge --ff-only` where ancestry allows, `git branch -d` (never `-D` in chained commands)
- Merge commits also need conventional messages — the commitlint hook rejects empty subject/type on merges too
- A package's first npm release can't go through CI: trusted publishing needs an existing package, so the Release job fails with `ENEEDAUTH`. A publish without a valid npm login fails with 404, not 401. Follow CONTRIBUTING.md "First release of a new package"
- `git log --all -- <path>` also lists merges that deleted the path; prove artifact absence with `git rev-list --objects --all --reflog` plus `git cat-file -e <blob>`, not log output
- Release-channel policy edits never move npm dist-tags (guard returns `noop` for published versions); promotion is an authenticated `npm dist-tag` op — E401 in unattended sessions means handing exact commands to an operator
- The "Version Packages" PR is opened by `github-actions[bot]`, so its CI runs sit at "action_required" until a maintainer clicks "Approve and run workflows" on the PR.
Expand Down Expand Up @@ -199,9 +200,8 @@ This repository uses **Bun Workspaces** for dependency management and **Turborep
- **Versioning:** This monorepo uses **Changesets** to manage independent versioning and publishing for each plugin.

### Agent Workflow
1. **Scaffolding:** Use the `init-plugin` skill to create a new plugin directory in `packages/`.
2. **Development:** Use the `create-plugin` skill. Work inside the specific package directory.
1. **Scaffolding:** Model a new package on an existing one of the same kind (V2 server, TUI, or server + TUI), start its README from [`README-template.md`](README-template.md), read [`docs/opencode-v2-plugin-runtime.md`](docs/opencode-v2-plugin-runtime.md), and add its entry to [`tools/release-channels.json`](tools/release-channels.json) (the guard rejects a workspace package without one).
2. **Development:** Work inside the package directory and follow its `AGENTS.md`. To try it in a real OpenCode, see [CONTRIBUTING.md › Testing in OpenCode](CONTRIBUTING.md#testing-in-opencode).
3. **Execution:** Run commands from the **root** using Turbo for speed, or within a package directory for specific tasks.
- Root: `bun run build`, `bun run test`, `bun run lint`
4. **Releasing:** Use the `release-plugin` skill.
- Run `bun changeset` at the root to record change intents.
4. **Releasing:** Run `bun changeset` at the root to record change intents, then follow [CONTRIBUTING.md › Changesets and releases](CONTRIBUTING.md#changesets-and-releases); a new package's first release is manual ([First release of a new package](CONTRIBUTING.md#first-release-of-a-new-package)).
20 changes: 15 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,29 +55,39 @@ Plugin majors track OpenCode majors, not strict semver. A package in release cla

- Use `patch` for fixes and `minor` for features **and for breaking changes** within the same OpenCode major. Say "Breaking" in the changeset text so it lands in the changelog.
- Use `major` only to move a package to a new OpenCode major. Change its `releaseClass` in `tools/release-channels.json` in the same PR (the guard rejects a class above the current OpenCode major, `CURRENT_OPENCODE_MAJOR` in `tools/release-channel-guard.ts`).
- A new package needs one entry in `tools/release-channels.json`; the guard rejects a workspace package without one. Its first release is a first publish (npm has never seen it), which CI's `Release plan` step shows before merge.
- A new package needs one entry in `tools/release-channels.json`; the guard rejects a workspace package without one. Its first release is a first publish (npm has never seen it), which CI's `Release plan` step shows before merge; CI can't publish it, so see [First release of a new package](#first-release-of-a-new-package).

`bun run release:changesets` checks pending changesets and package manifests against this rule without touching the registry. The Changesets workflow runs it on pull requests.

### Release channels

- The five V2 ports are published as `2.x` on `latest`.
- The V2 packages (five ports, plus auto-advisor, which is V2-only) are published as `2.x` on `latest`.
- The one V1-only package (`opencode-agent-prompt-inheritance`) continues to publish under `latest`. The output-styles V1 line is frozen at `1.0.2`. The loader and timeline V1 releases stay available at `1.0.0` and `1.0.1` by exact pin.
- Keep V1 and V2 version/release batches separate. The guard checks **all unpublished public workspace versions**, not just the packages named in a changeset or Git diff. A mixed batch is rejected before publishing.

Publishing is CI-only. The release workflow uses the `npm-publish` environment and npm trusted publishing, and runs `bun run changeset:publish` through the pinned Changesets Action. Version application and publication require maintainer authorization; do not run a real-registry publish locally.
Publishing is CI-only, except a package's first release (see [First release of a new package](#first-release-of-a-new-package)). The release workflow uses the `npm-publish` environment and npm trusted publishing, and runs `bun run changeset:publish` through the pinned Changesets Action. Version application and publication require maintainer authorization; do not run a real-registry publish locally.

The release flow on `main`:

1. Merge a PR that carries a changeset. The Release workflow opens (or updates) a **Version Packages** PR with the version bumps and changelog entries.
2. That PR is opened by `github-actions[bot]`, so its checks wait at "action_required". Click **Approve and run workflows** on the PR, then merge it once they pass.
3. The Release workflow publishes the unpublished versions with provenance, then pushes `name@version` tags and creates a GitHub release per package.
4. Verify on the registry with a direct curl (for example `curl -s https://registry.npmjs.org/@capybearista%2F<name>`); `npm view` can lag.
4. Verify on the registry with a direct curl (for example `curl -s https://registry.npmjs.org/@capybearista%2F<name>`); `npm view` can lag. Use the registry API, not the npmjs.com website, which is behind a bot check.

The toolchain is Changesets CLI 3.x with `changesets/action` v2. They are upgraded together: action v2 rejects CLI v2. The guard pins the CLI version as well (see [AGENTS.md](./AGENTS.md)).

Run `bun run release:check` to inspect the complete unpublished set and selected channel. It queries public registry metadata but does not publish or create Git tags. It rejects unsupported package/version combinations (including a version outside its class major), ambiguous batches, and failed or malformed registry responses.

### First release of a new package

npm trusted publishing can only be configured for a package that already exists, so CI cannot publish a package's first version: the Release job fails with `ENEEDAUTH`. `bun run release:check` (and CI's `Release plan` step) lists the package as `first publish -> latest` with a pointer here. A maintainer bootstraps it once:

1. From a clean checkout of `main`, run `bun install --frozen-lockfile`, then in the package directory `bun run build`, `npm pack --dry-run` (check the file list) and `npm publish --access public`. `npm whoami` must print `capybearista`; log in with `npm login` first if it doesn't. A publish without a valid login fails with **404 Not Found**, not 401.
2. On npmjs.com, add the package's trusted publisher: GitHub Actions, `capyBearista/opencode-plugins`, workflow `release.yml`, environment `npm-publish`. Tick **Allow npm publish** (the Release job runs `npm publish`); leave **Allow npm dist-tag** off. These fields cannot be edited later. A new connection expires unless it completes a publish within 2 days, so set it up shortly before the next release.
3. Create the `@capybearista/<name>@<version>` tag on the commit you published from, and its GitHub release, which the Release job would otherwise have created.

npm may add a `0.0.0-stage` version ("temporary package placeholder for staged publishing"). `latest` is unaffected; leave it. The manually published version has no provenance attestation; later CI releases do.

### Publication failures

npm publication is not atomic across packages. If a job fails, inspect registry versions, dist-tags, and CI output before retrying. Changesets v3 reports failures per package and still creates Git tags for the packages that did publish, so a partial job leaves tags for what reached the registry. Re-running the Release workflow publishes only the versions still missing from the registry. Check that each published version also has its GitHub release; create any missing one from its tag. Do not bypass the guard or move a `latest` tag to recover a failed release.
Expand Down Expand Up @@ -128,7 +138,7 @@ Install and update instructions belong to the root README and the package README

Husky installs a single **commit-msg** hook that enforces conventional commits via `commitlint`.

**CI** runs typecheck, lint, and test on pushes and pull requests. Markdown, docs, asset, and changeset-only changes skip CI through path filters. The **Changesets** workflow runs `bun run release:changesets` on pull requests that touch changesets, package manifests, or the release policy, so changeset-only PRs are still checked. Releases run separately in the `release` workflow, only from `main`.
**CI** runs typecheck, lint, test and the release plan (`bun run release:check`, read-only; it needs npm reachable) on pushes and pull requests. Markdown, docs, asset, and changeset-only changes skip CI through path filters. The **Changesets** workflow runs `bun run release:changesets` on pull requests that touch changesets, package manifests, or the release policy, so changeset-only PRs are still checked. Releases run separately in the `release` workflow, only from `main`.

## Monorepo Structure

Expand Down
4 changes: 4 additions & 0 deletions tools/release-channel-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1669,6 +1669,10 @@ describe("new plugins", () => {
[packageNames.advisor, true],
]);
expect(output.join("\n")).toContain(`${packageNames.advisor}@2.0.0 first publish -> latest`);
// CI cannot do a first publish (trusted publishing needs an existing package): say what to do.
expect(output.join("\n")).toContain(
'needs a one-time manual publish; see CONTRIBUTING.md "First release of a new package"',
);
});

test("a first publish must still use its class major", async () => {
Expand Down
7 changes: 6 additions & 1 deletion tools/release-channel-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ const execFileAsync = promisify(execFile);
*/
const CURRENT_OPENCODE_MAJOR = 2;
const RELEASE_CHANNELS: readonly string[] = ["latest", "opencode2"];
/** Trusted publishing needs an existing package, so CI can't publish a package's first version. */
const FIRST_PUBLISH_HINT =
'needs a one-time manual publish; see CONTRIBUTING.md "First release of a new package"';

type ReleaseClass = `v${number}`;
type ReleaseChannel = "noop" | "latest" | "opencode2";
Expand Down Expand Up @@ -316,7 +319,9 @@ function printPlan(plan: ReleasePlan, output: (line: string) => void) {
`${item.name}@${item.version} ${
item.published
? "published"
: `${item.firstPublish ? "first publish" : "unpublished"} -> ${plan.channel}`
: item.firstPublish
? `first publish -> ${plan.channel} (${FIRST_PUBLISH_HINT})`
: `unpublished -> ${plan.channel}`
}`,
);
}
Expand Down
Loading