npm-release
Skill0 starsChanged 48 days agoApache-2.0View on GitHub
Operate and change the manual, environment-gated npm publication path, where scope, order, integrity and registry facts are all proven before any credential exists.
5 other projects use a version of this.
Adopted by
Most recently taken up by
- edloidas/skillstheir own version, first seen Nov 1, 2026
- Dhruv2mars/skillstheir own version, first seen Oct 20, 2026
- CanglongCl/dsh-tool-retrytheir own version, first seen Oct 20, 2026
- jscraik/Agent-Skillstheir own version, first seen Sep 27, 2026
- luochang212/skill-zootheir own version, first seen May 2, 2026
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 5
- luochang212/skill-zoo★ 125same name
- jscraik/Agent-Skills★ 9same name
- CanglongCl/dsh-tool-retry★ 4same name
- Dhruv2mars/skills★ 3same name
- edloidas/skills★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- changesets-releaseSkill
- local-ci-paritySkill
- package-architectureSkill
- package-artifact-validationSkill
- pnpm-supply-chainSkill
- provider-adapter-developmentSkill
- public-api-evolutionSkill
- runtime-contract-evolutionSkill
- runtime-lifecycle-coordinationSkill
- workspace-lifecycleSkill
The file
We split this skill into the rules it gives an agent
4 rules. 0 are common, 2 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 13
Use this skill when a change touches any of:
- Distinctive: line 22
Do not use this skill when:
- Very specific: line 145
Unrecognised header _formats_ are refused: Only the POSIX ustar signature (ustar\0 + 00) is accepted.
- Very specific: line 277
Read docs/release.md for publication policy and current registry status.
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
npm release
Trigger
Use this skill when a change touches any of:
.github/workflows/release.yml- anything under
tools/release/ docs/release.mdordocs/adr/ADR-0017-manual-npm-release.md- the question "how would these packages actually reach the registry?"
Counter-trigger
Do not use this skill when:
- you are deciding a version number or writing version intent — use
changesets-release; this skill never chooses a version, it only publishes one that a commit already carries - you are changing what a package ships — use
package-artifact-validation - you are changing the canonical gate or the validation workflow — use
local-ci-parity - you want to publish now: publication is a human decision with an environment approval,
not something an agent may initiate. All eight public packages have immutable
0.2.0and0.3.0versions; none is ever republished or named in a dispatch again. A 2026-09-27 registry observation foundlatestat0.3.0for all eight. Provider0.4.0is not published.
Owns
.github/workflows/release.yml— the only workflow that may reach a publish credentialtools/release— preflight, staging verification, publication, the npm tool identity and the workflow policy checkdocs/release.md— the operator runbook and the outstanding-approval recorddocs/adr/ADR-0017-manual-npm-release.md— the decision record for this path
Does not own
.github/workflows/gate.yml— owned bylocal-ci-paritytools/repo/gate.ts— owned bylocal-ci-parity; this skill contributes one step to itpackages/*/package.json#version— owned bychangesets-release.changeset— owned bychangesets-releasetools/repo/check-artifacts.ts— owned bypackage-artifact-validation
Relationships
boundary-with→local-ci-parity— that skill owns the workflow directory and the canonical gate; this skill owns the one release workflow inside it and thereleasegate step.boundary-with→changesets-release— that skill decides versions; this skill refuses to run while any version intent is still pending.depends-on→package-artifact-validation— the gate's tarball checks are what makes a packed artifact publishable at all.depends-on→pnpm-supply-chain— publication installs frozen and with lifecycle scripts denied.
Procedure
- Nothing publishes itself. The workflow has exactly one trigger,
workflow_dispatch, onmain. There is no push, tag, schedule orworkflow_runentrance, and no job or step may carry anif:— a release step that can be skipped is a release step that can be green without running. The operator states every fact. Four required inputs, none defaulted: the full 40-character
source_sha, an explicitpackagesscope ofname@versionentries, thedist_tag, and aconfirmphrase that restates the count, the commit and the tag:publish <count> package(s) from <source_sha> to <dist_tag>A typo in any input invalidates the confirmation, because the expected phrase is derived from the parsed inputs.
Two jobs, one credential.
verifyis ungated: it checks out the named commit, runs the canonicalpnpm gate, packs exactly the named packages, runs the fail-closed preflight and uploads the staged plan plus tarballs.publishneedsverify, runs in thenpm-releaseenvironment, downloads that exact artifact id (withdigest-mismatch: error, which the action drives from artifact metadata — it takes no expected-digest input), re-verifies everything locally, and only then runs one step that can seesecrets.NPM_TOKEN. Building never happens in a step that holds the credential.Both jobs install the workspace from the lockfile, frozen and script-free. In the gated job that install is a control, not a convenience: it is where the pinned npm comes from, and without it there is no tool whose identity the job can prove (see step 5).
- Preflight refuses; it never repairs. Findings are refusals, and there is no
override input. It refuses pending version intent, a commit that is not main's tip, a
dirty tree, a version the commit does not carry, an artifact whose identity or contents
are not the reviewed ones, a dependency range it cannot resolve to one exact version, a
scope that is not closed over its own dependency graph, a dependency cycle, an existing
version, a dist-tag regression, and any registry answer that is not a definitive 404 or
a well-formed packument. A packument missing
dist-tags, naming a tag that is not an exact version, or pointing a tag at a version it does not list is malformed: the dist-tag is the mutable half of a packument and reading it as "unset" is how a release moveslatestbackwards while reporting success. For linked packages, only packages entering a Changesets plan are aligned; an adapter-only plan can contain two packages. An artifact has exactly one identity, or it is refused. What this repository reads out of a tarball and what npm extracts from it must never be two different packages. Paths are normalised the way an extractor normalises them and collisions are refused, header checksums are verified, appended data and content outside
package/are refused, and unsupported archive semantics are refused rather than skipped.pacote-differential.test.tsasserts the resulting property against npm's own bundled reader, offline: same identity, or refusal. Never relax that test to "we parse tar the same way" — the invariant is about disagreement, not parsing.Which npm, exactly. That argument is worth nothing unless the npm the test reads with is the npm that publishes.
tools/release/lib/npm-tool.tsis the single answer to "which npm": the package pinned in the catalog, taken from the explicit pathnode_modules/npm, proven before use, and used by bothpacote-differential.test.tsandpublish.ts. It never falls back toPATHand takes no environment override — an unresolvable, malformed, mislocated or wrong-versioned tool is a refusal, and refusals name every reason at once because the reader is usually looking at a runner they cannot poke at. npm is a declared devDependency here rather than something inherited from the runtime becausepnpm/setupinstalls Node from thenodepackage, whose payload is thenodebinary alone: guessing atprocess.execPath's neighbours found no npm and made the suite unloadable (run 34699256419), whilespawn('npm')published through whatever the runner image happened to ship. Those were the same bug seen from two ends, and splitting the tool apart again reopens it.Locate; do not resolve, and do not stop at the manifest. Two rules earned the hard way, when a review broke the first version of this module twice:
createRequire(root).resolve('npm/…')answers "what wouldrequirefind", which is not "what does this repository depend on". It walks every ancestornode_modulesand thenNODE_PATH, and with no npm installed in the workspace at all, both were accepted as the publishing tool. So: the root manifest must declaredevDependencies.npm: "catalog:", the package is read from exactly<repoRoot>/node_modules/npm, and its real path must be owned by<repoRoot>/node_modules.node_modules/npmis a symlink under pnpm and must stay usable, so the rule is about where the link lands, not that it exists.- A package's
mainandexportsare part of its identity, so containment onpackage.jsonproves nothing about the code that loads. A bundledpacotewhosemainwas../../../../outside.cjs, or whoseexportstarget was a symlink out of the tree, passed inspection and then loaded foreign code. Each reader's entry point is therefore resolved, realpath'd, confined to that reader's own directory and recorded;loadNpmModuleloads that exact file and refuses any id not proven here. Never hand a caller a barerequirerooted in npm — that re-runs resolution at load time, which is precisely where the checked answer gets replaced by an unchecked one.
Two rules of different kinds keep that true, and confusing them breaks something:
- Unrecognised header formats are refused. Only the POSIX ustar signature
(
ustar\0+00) is accepted. A review built a secondpackage/package.jsonheader with valid checksums, a populatedprefixand no signature: npm ignored the prefix and let it overwrite the real manifest; this reader applied the prefix and saw a harmless path. Refusing the format closes that. - Inside ustar, npm's field semantics are reproduced literally — the
prefixsplit including its byte-475 155-vs-130 branch, and the rule that a regular-file entry whose name ends in/is a directory.prefixis not refused: node-tar genuinely emits it for deepdist/paths, so refusing it would reject artifactspnpm packactually produces. Refusing what npm accepts breaks the release; accepting what npm reads differently breaks the approval.
- The approval is not a snapshot. An approval can sit for hours while
mainadvances, a dist-tag moves and a dependency is unpublished. Every fact the approval rested on is re-established in the gated job, and again immediately before each individual upload: the dist-tag not already pointing at something newer, every packed dependency still resolvable at its exact version — including dependencies this run published itself, because "we uploaded it" is memory and the registry is fact — andsource_shastill being main's tip on the remote, read withgit ls-remoterather than from the checkout's cachedorigin/main, which cannot change once the job has started. An unanswerable remote refuses; it is never read as agreement. - Publication is ordered, explicit and non-overwriting. One tarball per
npm publish, in dependency order, with--ignore-scripts --access public --provenance --tag <tag> --registry https://registry.npmjs.organd a temporary--userconfignpmrc that interpolates the token from the environment and is removed in afinally. Each upload is followed by a registry readback of identity, integrity, dependency ranges and dist-tag before the next package is attempted. A zero exit is
accepted_pending, immediately, and the upload never happens twice. npm scans a newly published package before serving it: usually about five minutes, documented as possibly fifteen or more. Run 34753860073 attempt 3 read the registry five times separated by 3000 ms — four sleeps, about twelve seconds — and failed a publication that had already succeeded;@relvo-labs/agent-protocol@0.2.0first answered 200 publicly 466 s later, byte-identical to the reviewed artifact. So the momentnpm publishexits zero the version is recorded asaccepted_pendingand is never described as unpublished, andpublishReleaserefuses to hand the same subject tonpm publisha second time in a run.What follows is read-only reconciliation (
tools/release/lib/visibility.ts): exact registry lookups on a deterministic backoff — 5 s doubling to a 30 s cap, the last delay clipped to the bound — for a budget of 20 minutes per package, deliberately longer than the 15 minutes npm documents, because a bound equal to the documented worst case fails on exactly the runs it exists to survive. Only the two expected post-acceptance answers are retried: the package absent (404), or a well-formed packument that does not yet list the exact version. A network failure, 5xx, timeout, rate limit, auth failure or malformed packument ends it at once (readback_unavailable) — an unanswerable registry is not a registry that is still scanning, and spending the delay budget on a 401 turns a credential problem into a job timeout. An identity, integrity, shasum, dependency, peer-dependency or dist-tag mismatch ends it at once too (readback_mismatch): the registry answered, and more reads cannot change the answer.classifyReadbackis fail-closed — an unrecognised finding code is a stop, not a wait.The bound is a bound. When it expires the run stops non-zero as
visibility_not_confirmed, before any dependent is attempted, and reconciliation becomes a human step: check the npm account's notifications for a manual-review or blocked-package notice and use the appeal path offered. Never republish..github/workflows/release.yml'spublishjob timeout is derived from the same constants — 8 × (20 m budget + 2 m upload and pre-upload rechecks) + 14 m setup, download and staged verification = 190 minutes — andworkflow-policy.tsasserts that exact number. A timeout below the wait the publisher is willing to make is the same defect as a readback window below the scan delay, with the runner rather than the program abandoning an accepted upload.- Partial failure stays partial, and acceptance is not verification. On any failure
the run stops non-zero. A zero exit from
npm publishmeans the registry accepted the bytes; a readback means it serves what was reviewed. The report keeps those apart —published,acceptedPending, andunknownfor an upload it could not adjudicate — because reporting an accepted-but-unconfirmed upload as "not published" invites a recovery dispatch naming a version that is already public and immutable. Nothing is unpublished, no version is overwritten, and recovery is a new human-reviewed dispatch whose scope names only what is confirmed still unpublished. Anything logged about an upload is a bounded line of plan facts written through the redacting sink inpublish.ts; never command output, npmrc contents or the environment. - Changing this path is a reviewed change.
pnpm release:checkdescribes the reviewed workflow as an exact structure: allowed job and step keys, exact jobenvandoutputsbindings, exact action inputs, the exact command sequence per job, and an allow-list of every${{ … }}expression that may appear anywhere in the file. It is exhaustive on purpose — a field the policy does not assert is a field an attacker chooses, andcontinue-on-error, ashell:override, an extrarun:, a constant in place of a dispatch input andsecrets['NPM_TOKEN']were each demonstrated to pass a policy that only checked a list of properties. Editing the workflow therefore means editingEXPECTED_JOBSinworkflow-policy.tstoo; that is the review step. Add a new action toALLOWED_ACTIONSonly with an independently verified commit SHA, and never pass an action input the pinned revision does not declare. Job timeouts are part of that structure:EXPECTED_JOB_TIMEOUT_MINUTESasserts each one exactly, and the gated job's value is computed from the visibility constants rather than typed, so the workflow and the publisher cannot drift apart.
Verification
pnpm release:check # structural policy for the release workflow, job timeouts included
pnpm exec vitest run tools/release # tool identity, preflight, ordering, registry, readback, visibility
pnpm gate # both of the above, plus everything else CI runsVisibility reconciliation is tested through publishRelease itself — the function
tools/release/publish.ts calls — with the upload and registry transports replaced and a
virtual clock, so a beyond-15-minute delay and a bounded expiry are both asserted in
milliseconds of real time and no npm process exists. Keep it that way: a test that only
exercises a polling helper proves nothing about the orchestrator that waits, and the
property worth protecting is "exactly one upload, however long the wait". Never add a test
that can reach a real registry, and never assert visibility by publishing something.
A local pnpm gate passing proves less about the release path than it looks like it does
if your Node came from nvm or a system package: that layout has a bundled npm, and the
runner's does not. When changing anything about how the tool is located, reproduce the CI
layout rather than trusting the local one — install the pinned pnpm executable, run
pnpm runtime set node <matrix version> -g into a scratch PNPM_HOME, and run the gate
with that node first on PATH.
Rehearse preflight locally against real packed artifacts, without credentials. Report
local facts honestly: never supply workflow_dispatch or refs/heads/main to impersonate
a hosted dispatch. The 0.2.0 scope below deliberately names an already-published immutable
version so registry preflight must reject it; never copy it into a real dispatch.
A local run must refuse release eligibility.
Packing still precedes diagnostic evaluation; issue #26 tracks that separate limitation.
export RELEASE_EVENT_NAME=local_nonpublishing_verification
export RELEASE_REF=$(git symbolic-ref -q HEAD || printf detached)
export RELEASE_SOURCE_SHA=$(git rev-parse HEAD) RELEASE_RUNNER_SHA=$(git rev-parse HEAD)
export RELEASE_PACKAGES='@relvo-labs/agent-protocol@0.2.0'
export RELEASE_DIST_TAG=latest
export RELEASE_CONFIRM="publish 1 package(s) from $RELEASE_SOURCE_SHA to latest"
node tools/release/preflight.ts --staging /tmp/release-stagingThis one-package example is a rejection-only diagnostic rehearsal, not a release scope.
Read docs/release.md for publication policy and current registry status.
The library release inventory does not assert feature coverage: the gate owns that proof.
Raw pending-file checks remain required so empty or malformed intent cannot authorize
publication through an empty release plan.
Provenance
- Source: independent — authored for this repository against the npm CLI documentation
(
publish,dist-tag,--provenance,--userconfig), npm's documented publish-time malware-scanning availability delay (usually about five minutes, possibly fifteen or more), the npm registry packument format, and the GitHub Actions documentation forworkflow_dispatchinputs, environments, job outputs and artifact digests. - Observed-evidence: run 34753860073 attempt 3 of this repository — accepted upload, 466 s to public visibility, matching integrity — is the case the visibility budget and the derived job timeout are sized against.
- Reviewed-not-copied:
actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c— itsaction.ymlcontract andsrc/download-artifact.tswere read to establish that the pinned revision declares nodigestinput and derives the expected hash from artifact metadata. No text was incorporated. - Compared-against:
pacoteandtar, as bundled with thenpmpinned inpnpm-workspace.yaml's catalog — the same package this repository publishes with. They are loaded offline, out of that package, for the identity-agreement test. npm is a declared devDependency (one lockfile entry; it vendors its own dependencies) and no code from it is incorporated. It was previously loaded from whichever npm happened to sit besideprocess.execPath, which assumed an official Node distribution's layout.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

