From 356de3f40bc23378c03b36bbaabf9db57185a995 Mon Sep 17 00:00:00 2001 From: Arjun Pramanik Date: Sun, 1 Nov 2026 22:00:13 +0000 Subject: [PATCH] docs: document a new package's first release and point the guard at it A package's first release can't go through CI: npm trusted publishing needs an existing package, so the Release job fails with ENEEDAUTH, and a publish without a valid login fails with 404 rather than 401. CONTRIBUTING now has the one-time bootstrap (manual publish, trusted publisher with "Allow npm publish", tag and GitHub release, the 0.0.0-stage placeholder), and the guard's plan line for a first publish points there. Also: the V2 package count and the CI description in CONTRIBUTING, a note to verify through the registry API, and AGENTS.md's agent workflow now links to existing docs instead of skills that don't exist. --- AGENTS.md | 8 ++++---- CONTRIBUTING.md | 20 +++++++++++++++----- tools/release-channel-guard.test.ts | 4 ++++ tools/release-channel-guard.ts | 7 ++++++- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4379349..f42bf12 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 -- ` also lists merges that deleted the path; prove artifact absence with `git rev-list --objects --all --reflog` plus `git cat-file -e `, 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. @@ -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. \ No newline at end of file +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)). \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 641529c..e2a7dd1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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`); `npm view` can lag. +4. Verify on the registry with a direct curl (for example `curl -s https://registry.npmjs.org/@capybearista%2F`); `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/@` 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. @@ -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 diff --git a/tools/release-channel-guard.test.ts b/tools/release-channel-guard.test.ts index 58691db..9654a22 100644 --- a/tools/release-channel-guard.test.ts +++ b/tools/release-channel-guard.test.ts @@ -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 () => { diff --git a/tools/release-channel-guard.ts b/tools/release-channel-guard.ts index 19037b8..be2b779 100644 --- a/tools/release-channel-guard.ts +++ b/tools/release-channel-guard.ts @@ -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"; @@ -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}` }`, ); }