diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 31a7439..d9dad07 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -6,7 +6,7 @@ }, "metadata": { "description": "Skills for OrcaRouter products.", - "version": "1.5.0" + "version": "2.0.0" }, "plugins": [ { diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 68671fc..8aa4918 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "orca-code-review", "description": "Set up, reconfigure, troubleshoot, and remove OrcaCode Review — AI pull-request review powered by OrcaRouter — in any GitHub repository.", - "version": "1.5.0", + "version": "2.0.0", "author": { "name": "Continuum-AI-Corp", "url": "https://github.com/Continuum-AI-Corp" diff --git a/.github/workflows/unpublish.yml b/.github/workflows/unpublish.yml new file mode 100644 index 0000000..817405d --- /dev/null +++ b/.github/workflows/unpublish.yml @@ -0,0 +1,135 @@ +# Withdraw versions of `@orcarouter/code-review` from npm, by hand, on purpose. +# +# This is a demolition tool. It exists because npm's unpublish window is 72 +# hours and the credential that can use it is the `NPM_TOKEN` repository secret +# — not something on any maintainer's laptop. Running it from CI keeps the token +# where it already lives and leaves a run log of who withdrew what. +# +# An unpublish CANNOT be undone: the version number is burned forever, and +# anyone who pinned it gets an E404 rather than a warning. Prefer +# `npm deprecate`, which leaves the tarball installable and prints a warning. +# Reach for this only for a version that is days old with no known consumers. +# +# Two guards, because the failure mode is deleting the live release: +# +# 1. `confirm` must be typed as the exact word "unpublish". A dispatch button +# is one click and this workflow's blast radius is the whole package. +# 2. The version in `package.json` on the default branch is refused. That is +# what `dist-tags.latest` points at and what every `npx` resolves; the job +# fails before touching the registry rather than after. +# +# Order matters when withdrawing a run of versions: list DEPENDENTS FIRST. Up to +# 1.5.0 this package declared a dependency on ITSELF at ^1.0.2, and npm reads +# dependents when deciding whether a version may go. +# +# Inputs reach the shell through `env`, never through `${{ }}` inside a `run` +# block — an interpolated dispatch input is a command-injection hole, and this +# job holds a publish token. + +name: Unpublish + +concurrency: + group: publish-npm # never race the publish job + cancel-in-progress: false + +on: + workflow_dispatch: + inputs: + versions: + description: "Versions to withdraw, space-separated, dependents first (e.g. 1.5.0 1.4.0 1.0.2)" + required: true + type: string + confirm: + description: 'Type "unpublish" to confirm. Irreversible.' + required: true + type: string + +permissions: + contents: read + +jobs: + unpublish: + runs-on: ubuntu-latest + env: + VERSIONS: ${{ inputs.versions }} + CONFIRM: ${{ inputs.confirm }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + registry-url: "https://registry.npmjs.org" + + - name: Guard + run: | + set -euo pipefail + if [ "$CONFIRM" != "unpublish" ]; then + echo "::error::confirm must be the word \"unpublish\". Nothing was touched." + exit 1 + fi + + PKG_NAME=$(node -p "require('./package.json').name") + PKG_LIVE=$(node -p "require('./package.json').version") + echo "PKG_NAME=$PKG_NAME" >> "$GITHUB_ENV" + echo "PKG_LIVE=$PKG_LIVE" >> "$GITHUB_ENV" + + for v in $VERSIONS; do + if [ "$v" = "$PKG_LIVE" ]; then + echo "::error::$v is the version in package.json — that is the live release. Refusing." + exit 1 + fi + done + echo "::notice::withdrawing: $VERSIONS (keeping $PKG_LIVE)" + + - name: Unpublish + run: | + set -uo pipefail + FAILED="" + for v in $VERSIONS; do + echo "--- $PKG_NAME@$v" + if npm unpublish "$PKG_NAME@$v" --force; then + echo "::notice::withdrew $PKG_NAME@$v" + else + # Keep going: a version outside the 72-hour window, or already + # gone, must not strand the rest of the list half-withdrawn. + echo "::warning::could not withdraw $PKG_NAME@$v" + FAILED="$FAILED $v" + fi + done + echo "FAILED=${FAILED# }" >> "$GITHUB_ENV" + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + # The packument is what `npm install` reads. Asserting against it — and + # anonymously — is the only proof that the withdrawal took effect and that + # the version everyone installs is still there. + - name: Verify the registry + run: | + set -euo pipefail + sleep 15 + SLUG=$(node -p "encodeURIComponent(process.env.PKG_NAME)") + curl -fsSL "https://registry.npmjs.org/$SLUG" -o packument.json + node -e ' + const p = require("./packument.json"); + const left = Object.keys(p.versions).sort(); + const latest = p["dist-tags"].latest; + const live = process.env.PKG_LIVE; + console.log("versions on the registry:", left.join(", ")); + console.log("latest:", latest); + if (!left.includes(live)) { + console.error(`::error::${live} is GONE from the packument — the live release was withdrawn`); + process.exit(1); + } + if (latest !== live) { + console.error(`::error::latest is ${latest}, expected ${live}`); + process.exit(1); + } + const survivors = (process.env.VERSIONS || "").split(/\s+/).filter((v) => v && left.includes(v)); + if (survivors.length) console.error(`::warning::still present: ${survivors.join(", ")}`); + ' + + if [ -n "${FAILED:-}" ]; then + echo "::error::these versions could not be withdrawn: $FAILED" + exit 1 + fi diff --git a/RELEASE.md b/RELEASE.md index 44f0e0f..f05d88e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -66,6 +66,29 @@ The repo and the action stay `orca-code-review`. `--access public` ships it private and the documented install command 404s for everyone outside the org. +#### The `1.x` line was withdrawn; `2.0.0` is the floor + +`@orcarouter/code-review` starts at **2.0.0** on the registry. 1.0.2 through +1.5.0 were published on 25–26 Aug 2026 and unpublished inside npm's 72-hour +window, so the packument shows one version and its history looks truncated — +it is not. What went with them: + +- **App mode.** Up to 1.4.0 the skill offered a GitHub App install as an + alternative to the Action. Installing an App is a permission grant only a + human can approve on a web page, so the agent could only hand over a link; + 1.5.0 dropped it and 2.0.0 is the first version published without it. The + major marks that break rather than leaving it in a minor nobody can install. +- **The self-dependency.** 1.1.0 through 1.5.0 declared + `@orcarouter/code-review: ^1.0.2` as a dependency *of itself*, so every + `npx` pulled a second, older copy of the CLI. Removed in 2.0.0 and pinned by + a test — see "the CLI ships with no dependencies" in + `scripts/installer.test.mjs`. + +Unpublishing was the right call only because the line was two days old with no +known consumers. It is not the default: outside the 72-hour window npm refuses, +a withdrawn number can never be reused, and anyone pinned to it breaks with an +E404 rather than a warning. **Deprecate instead** — see below. + #### The unscoped `orcacode-review` name is retired 1.0.0 and 1.0.1 were published unscoped under a personal account, before the diff --git a/package-lock.json b/package-lock.json index ba7b5a6..e702600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,27 +1,12 @@ { "name": "@orcarouter/code-review", - "version": "1.0.2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@orcarouter/code-review", - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "@orcarouter/code-review": "^1.0.2" - }, - "bin": { - "orcacode-review": "bin/orcacode-review.mjs" - }, - "engines": { - "node": ">=18.17" - } - }, - "node_modules/@orcarouter/code-review": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@orcarouter/code-review/-/code-review-1.0.2.tgz", - "integrity": "sha512-PsM9S+qAN6bDLzRU600fGNrSzjy+DfGMcel49iFFe8FsbZ4JRtoWbfMfEq5yemRD3KbR3FbkBW/xB1GEHhs+FQ==", + "version": "2.0.0", "license": "MIT", "bin": { "orcacode-review": "bin/orcacode-review.mjs" diff --git a/package.json b/package.json index 263741d..557725b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@orcarouter/code-review", - "version": "1.5.0", + "version": "2.0.0", "description": "One-command installer for OrcaCode Review — AI pull-request review powered by OrcaRouter.", "bin": { "orcacode-review": "bin/orcacode-review.mjs" @@ -38,8 +38,5 @@ "license": "MIT", "publishConfig": { "access": "public" - }, - "dependencies": { - "@orcarouter/code-review": "^1.0.2" } } diff --git a/scripts/installer.test.mjs b/scripts/installer.test.mjs index eb6253b..2627a2b 100644 --- a/scripts/installer.test.mjs +++ b/scripts/installer.test.mjs @@ -162,6 +162,18 @@ test("a scoped package declares public access", () => { assert.equal(PKG.publishConfig?.access, "public"); }); +test("the CLI ships with no dependencies", () => { + // `npx` downloads the whole tree before running anything, so a dependency is + // latency on every install. 1.1.0 through 1.5.0 shipped with the package + // depending on ITSELF at ^1.0.2 — every npx fetched a second, older copy of + // the CLI into node_modules, and nothing failed, so five releases carried it. + // A self-reference is also a registry dependent, which is why this is a test + // and not a note in RELEASE.md. + for (const field of ["dependencies", "peerDependencies", "optionalDependencies"]) { + assert.deepEqual(Object.keys(PKG[field] ?? {}), [], `${field} must stay empty`); + } +}); + test("the installed command is short even though the package name is scoped", () => { // `npm i -g` creates a command named by the bin KEY, not the package name. assert.deepEqual(Object.keys(PKG.bin), ["orcacode-review"]);