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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills for OrcaRouter products.",
"version": "1.5.0"
"version": "2.0.0"
},
"plugins": [
{
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
135 changes: 135 additions & 0 deletions .github/workflows/unpublish.yml
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 2 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -38,8 +38,5 @@
"license": "MIT",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@orcarouter/code-review": "^1.0.2"
}
}
12 changes: 12 additions & 0 deletions scripts/installer.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand Down
Loading