Releasing @unbrained/pm-cli
This page is for maintainers cutting npm and GitHub releases. It assumes release work is tracked with pm.
For local progressive-disclosure routing, install guide-shell with pm install guide-shell --project, then use pm guide release.
Agent Quick Context
- Release versioning is calendar SemVer-compatible: one production version
(
YYYY.M.D) per UTC day. Older history can containYYYY.M.D-Nordinals, but release preparation no longer creates them. - Daily release preparation is owned by the GitHub Actions auto-release workflow.
- Publishing is owned by the tag-driven GitHub Actions release workflow.
- Do not run manual
npm publish. - Run local parity gates before pushing release tags.
- Treat
pnpm sdk:surface:checkas a release compatibility gate; never regenerate an unexplained public API diff. - Use
pm guide release --jsonfor machine-readable release docs routing afterguide-shellis installed.
Tracked documentation work: pm-u9d0, pm-4s24d2, pm-39cqqx, stable peer compatibility pm-csuce0, and artifact budgets pm-998juj, plus exact-tag recovery pm-lwnifd, and SDK-bound reliability classification pm-dqtzva. The local/hosted gate selection contract is tracked by pm-ei6x66. Release-candidate analyzer provenance is tracked by pm-u1baah, and authoritative blocker-recovery run selection by pm-db8onn, and queued automatic same-day recovery by pm-dm2vfz. Bounded Sentry request latency is tracked by pm-b9g2cs, and exact-tag changelog stabilization by pm-e63v1x. Dual-phase package sizing around Sentry injection is tracked by pm-llbnua.
Version Policy
Examples:
- first release on 2026-05-01:
2026.5.1 - the next production release after
2026.5.1is the next UTC day's calendar version; do not create2026.5.1-2
Inspect the next SemVer-compatible calendar version for diagnostics:
pnpm version:next
This diagnostic preserves validation compatibility with historical ordinal
tags, but never proposes one. When today's stable release already exists it
fails with immutable-tag recovery guidance, because an ordinal would be a
SemVer prerelease excluded from ordinary stable package peer ranges. An
explicit pipeline --version must equal the current UTC calendar date; past,
future, malformed, and ordinal targets fail before Git inspection.
Validate the current package version:
pnpm version:check
One-Time Setup
- Use npm trusted publishing for
.github/workflows/release.ymlso GitHub-hosted release jobs exchange their OIDC identity for a short-lived npm publish credential and publish signed packages. Keepid-token: write, thereleaseenvironment, Node 24 or newer, npm 11.5.1 or newer, andnpm publish --access public --provenance. The npm Trusted Publisher must match repositoryunbraind/pm-cli, workflowrelease.yml, and environmentrelease. The release job performs a tokenlessnpm publish --dry-runOIDC exchange before the long release gates and fails early with npm's sanitized exchange diagnostic when the registry refuses the configured identity. Do not passregistry-urltoactions/setup-node: its generated token-placeholder npmrc can suppress npm's OIDC exchange when no token is present. Keep@unbrained/pm-clipublic; trusted publishing authorizesnpm publish, notnpm access, so restore package visibility manually before retrying if an anonymous metadata probe fails. Do not exposeNODE_AUTH_TOKENorNPM_TOKENto the publish step; if private dependencies ever require a read token, scope it only to the install step. - Add
RELEASE_PATto thereleaseenvironment from a maintainer token withcontents:writeand branch-protection bypass rights. Auto Release creates a checked version/changelog commit and tag onmain; the defaultGITHUB_TOKENhascontents:writebut cannot satisfy protected-branch required status checks for that freshly-created commit. The workflow does not persist this elevated token during checkout or dependency installation;run-release-pipeline.mjsscopes it to the git push process. - Add
SENTRY_AUTH_TOKENas an optional GitHub Environment or repository secret when Sentry release creation and sourcemap upload should run. AddSENTRY_PERSONAL_ADMIN_TOKENfor the GitHub-hosted Sentry issue-threshold gate; CI-scoped release tokens may not have issue-read scope. The release workflow skips Sentry upload cleanly whenSENTRY_AUTH_TOKENis absent, but fails the reliability threshold gate whenSENTRY_PERSONAL_ADMIN_TOKENis absent; local maintainers should still run the token-backed Sentry gate before release. - Keep any
releaseenvironment compatible with free GitHub features. This repository is public, so environment secrets and tag/branch deployment rules are compatible with the free GitHub path; do not add paid-only release gates. - Ensure
GITHUB_TOKENhascontents: writefor GitHub Release creation. - Keep
package.jsonrepository, homepage, and bugs URLs aligned withhttps://github.com/unbraind/pm-cli. - Keep npm publishing compatible with provenance. The release workflow must keep
id-token: write, a GitHub-hosted runner, Node 24 or newer, npm 11.5.1 or newer, tokenless public metadata probes, andnpm publish --access public --provenancethrough the configured Trusted Publisher.
Automated Daily Driver
.github/workflows/auto-release.yml runs once per day and can also be dispatched manually.
Independent morning dispatch
Tracked by pm-9url9h. An independent
operator-managed timer can invoke scripts/release/dispatch-daily-release.mjs
at 04:45 Europe/Vienna (02:45 UTC in summer, 03:45 UTC in winter).
The aim is completion before 06:30–07:00 local time; host availability, GitHub
runner capacity and release gates can prevent that target. The host must be
awake, online and able to authenticate with gh unattended.
The existing 02:35 UTC GitHub cron remains enabled as a fallback. Either clock can arrive first. The dispatcher checks both release workflows for active runs and skips an existing exact UTC-day tag. Tag existence is explicitly not publication proof: incomplete publication still requires the normal immutable-tag recovery procedure. Hosted concurrency and the one-release-per-UTC-day guard remain the final protection if a native run races the external request.
The standalone dispatcher requires an absolute private state directory:
node scripts/release/dispatch-daily-release.mjs --state-dir /absolute/private/state --check
--check reads prerequisites without dispatching or writing state, including
outside the morning window. Without it, the dispatcher accepts only
04:45–05:15 Vienna time and sends workflow_dispatch to auto-release.yml on
main with explicit push=true, dry_run=false, and telemetry_mode=off,
matching scheduled production policy. It never builds from the operator's
working branch, changes release secrets or bypasses hosted gates.
An exclusive daily intent file is written before sending the request. A timeout can mean GitHub accepted the request, so an uncertain attempt is retained and never automatically resent that UTC day. Inspect hosted run history before any manual recovery. Pre-dispatch read failures leave no intent; a timer may try again within the morning window. Late persistent-timer catch-up is a no-op. Install a reviewed copy outside development checkouts so branch edits cannot silently change an unattended service.
Keep dispatcher output in the service journal: dispatch_accepted proves only
API acceptance. Verify the hosted run, GitHub Release and exact npm version
separately. These runs remain workflow_dispatch; they do not improve or erase
the native schedule denominator in Release Reliability (repository-only, excluded from npm).
Disable the independent timer to roll back; the original hosted schedule and
publication path continue unchanged. Host-specific unit files and authentication
configuration belong in private operator documentation.
Hosted release policy
Pinned tool downloads retain their existing versions and mandatory acceptance
checks. Codecov CLI v11.3.1 uses its immutable GitHub release asset API endpoint,
with the reviewed SHA-256 verified before execution. The Release workflow first
requires uncached pinned Bun bootstrap and exact version checks on native macOS
and Windows runners. Both must pass before the Build, Test, Publish job can run;
a failed download or mismatched runtime therefore blocks npm publication. The
later installed-package acceptance remains a separate check of the published
artifact. macOS Bun acceptance uses
the official pinned asset URL with ?download=1 after repeated HTTP 504s on the
ordinary route; binary caching stays disabled. A failed download still fails the
job. Recover an incomplete release against its existing tag after reviewed
transport changes land; a download failure does not authorize another automatic
version. A completed, explicitly skipped publication job with an empty step list
remains eligible for first-publication recovery. Missing job evidence is still
refused, and accepted publication in any other attempt still blocks a second
publication. Tracked by pm-gh1694
(repository-only tracker record, excluded from npm).
- release only when commits exist after the latest release tag
- ignore tracker-governance-only commits for publish eligibility:
.agents/pm/**and the mechanically generatedCHANGELOG.mdprojection do not create a package release by themselves, while any product, test, documentation, workflow, or other changed path remains release-relevant - create at most one production tag and npm version per UTC day; if no tag was
created, a non-
github-actions[bot]closure of the exact bot-createdAuto Release blockedissue on the same UTC day triggers one preparation retry - if today's tag already exists, a queued schedule or blocker closure bypasses candidate preparation and accepts the immutable result only after independent public-distribution evidence confirms the exact release; incomplete publication continues through bounded recovery instead of being accepted from tag presence alone
- release preparation must pass all quality and compatibility gates before commit+tag push
- before dependency installation or build, auto-release verifies that the candidate has exact-commit analyzer results or an immutable tree-identical reviewed PR head with successful DeepScan and CodeFactor results
- after the automatic version commit lands, main CI accepts its parent's analyzer evidence only when the commit is the exact immutable-tagged release projection: the canonical commit message, every expected distribution manifest changed by version substitution alone, and only the generated changelog heading changed. Any other path or content still fails closed
CHANGELOG.mdis generated by the latest npmpm-changelogpackage (pm install npm:pm-changelog --project, thenpm changelog generate --mode replace --all-release-tags) from closed tracker items across git release tag windows and checked in CI; do not edit it by hand- Release preparation passes
--date-from-versionwith the target calendar version so the generated heading already has its UTC release date before the tag exists. This keeps the preparation commit consistent with canonical post-tag generation and the normalmainchangelog check. - protected-branch pushes require
RELEASE_PAT; Auto Release fails fast before the expensive release gates whenpush=true,dry_run=false, and that secret is not configured - release reliability gating requires
SENTRY_PERSONAL_ADMIN_TOKENfor issue-threshold checks; Auto Release fails before creating the version commit/tag when the token is missing andpush=true, while sourcemap upload remains optional throughSENTRY_AUTH_TOKEN - after creating and pushing a new tag, auto-release waits for the tag-push
.github/workflows/release.ymlrun to finish instead of dispatching a second publish workflow - native scheduled and declared morning-dispatcher failures open or update the
same daily blocker with trigger origin, preflight state (
push,dry_run,release_pat_configured, andsentry_personal_admin_token_configured) and detected cause. Successful automatic recovery closes it only after exact publication verification; operator dispatch does not manage this lifecycle. This behavior is tracked by pm-automatic-release-alerts. - when either automatic origin fails across multiple UTC days, auto-release supersedes a stale open blocker with a fresh current-day blocker so same-day retry detection follows the latest automatic failure
- closing the exact bot-created
Auto Release blocked: scheduled run failedissue as a maintainer or agent records a same-day retry marker, then either retries preparation when no tag exists or watches an active exact-tag Release workflow. If only failed or missing evidence exists, it dispatches the reviewed currentrelease.ymlfrommainwith the immutable tag input; it never reruns historical workflow code. It comments with the recovered tag on success and reopens the same issue on failure. A second close on the same UTC day is refused before release mutation and reported asretry_already_attempted, and workflow cleanup closures bygithub-actions[bot]are ignored. - after a scheduled run publishes a tag and the downstream release workflow succeeds, auto-release closes any open
Auto Release blockedissue so the GitHub tracker reflects current release health - workflow concurrency can leave a delayed schedule queued behind another production run. The queued schedule resolves the current UTC-day immutable result before candidate provenance, proves publication, and exits without evaluating the generated release commit as a new candidate
Dependency advisory diagnostics are tracked by
pm-z20n3s. Both Auto Release and Release
retain the original .cache/dependency-security/receipt.json after the gate,
including failures, as dependency-advisory-<run-id>-<attempt> for seven days.
Only that public receipt is uploaded, with explicit hidden-file inclusion.
It records registry and OSV findings or unavailable evidence without raw
stderr, credentials, telemetry, or sibling caches. Missing receipts warn rather
than masking the original failure. Retention never changes the gate verdict.
Pipeline entrypoint:
node scripts/release/run-release-pipeline.mjs
Tracked by pm-prrlce: hosted preparation
uses --push --defer-push to write an exact producer binding. Auto Release uploads
release-producer-binding-<attempt> before verifying and atomically pushing its
prepared refs. Publication independently fetches the verified run's artifact and
compares the tag name, target commit, tag object and declaration. Missing, expired,
ambiguous or mismatched evidence fails closed; artifact retention is 90 days.
A branch advance after sealing refuses publication rather than rebasing into a
different target. Local --push refuses before Git inspection or asset mutation;
local preparation without pushing remains supported. Historical admission remains
limited to the reviewed exact-object inventory.
The pipeline performs:
- change detection + one-release-per-day guard
- a single
YYYY.M.Dversion bump; ordinal targets and the removed--allow-same-day-releaseoverride fail closed - latest
pm-changeloginstall and main changelog refresh through package-owned full-history generation; the release pipeline passes--release-version,--all-release-tags, and the canonical--exclude-tag changelog-excludepolicy so the pending release section matches post-tag CI checks - build, clone-local merge-driver installation, then the remaining strict gates (typecheck, docs/skills freshness, coverage, static quality, compatibility, security, smoke checks, reliability gate); this ordering makes the checkout-owned CLI available before bootstrap, matches CI, and prevents fresh-clone tracker measurements from observing undeclared merge-driver repairs
- release note generation from changelog + pm evidence
- commit and tag creation (plus optional push)
The generated changelog includes clickable pm item links to the tracked .toon files. Missing release evidence should be fixed in pm item history, not by hand-editing CHANGELOG.md.
Changelog Classification (Contributor-Facing)
pnpm changelog:pm routes closed pm items into keep-a-changelog sections using pm-changelog classification logic. Use explicit type/tag metadata when you want deterministic section routing.
Signal tiers:
- Strong signals: item
type+tags - Weak signal: item
title(after stripping CLI-flag-like tokens such as--add)
Category precedence (first matching bucket wins):
| Priority | Category | Trigger terms (from strong + weak signals unless noted) |
|---|---|---|
| 1 | Security |
security, cve, vulnerability |
| 2 | Deprecated |
deprecated, deprecation |
| 3 | Removed |
removed, remove, deleted, delete |
| 4 | Fixed |
fix, fixed, bug, bugfix, hotfix, regression |
| 5 | Added |
feature, feat, added, add, new |
| 6 | Changed |
strong-signal change, changed, refactor, update, updated, improve; for non-bug-like types only, title fallback is also used |
| 7 | Other |
no classifier match |
Bug-like default:
- Items with type
Issue,Bug,Bugfix, orDefectdefault toFixedunless a higher-priority category already matched. - This bug-like default runs before title-only
Changedfallback to avoid misrouting command-name issue titles (for example "pm update ..." issue reports).
Practical examples:
Issue+ titlepm update --add-tags fails->FixedTask+ tagrefactor->ChangedFeature+ tagsecurity->SecurityIssue+ tagfeature->Added(explicit stronger signal beats default)
If a changelog routing rule appears incorrect, fix classifier behavior in the pm-changelog package/repo and then consume the updated package here. Do not patch generator internals in this repository.
Local Release Parity Checklist
- Confirm the UTC calendar date. Do not use an ordinal diagnostic as a production target.
pnpm version:next
- Verify previous-version tracker compatibility in a temporary project before release asset edits.
Create representative data with the latest published package and then read, mutate, run linked tests, validate, and health-check the same temp PM_PATH with the current build. The temp run must use isolated PM_PATH and PM_GLOBAL_PATH; never point compatibility tests at the repository's real tracker data.
Minimum coverage:
- parent and dependency links
- comments, notes, learnings, body, reminders, events
- linked files, docs, and tests
- legacy markdown item files (including external YAML wrappers before JSON front matter) migrating cleanly to TOON
- closed issue metadata and history drift checks
- current-build write mutation and item-count preservation
- Review private reliability signals.
Use maintainer-only local workflows for reliability checks and incident triage. Keep operational details, infrastructure topology, and raw diagnostics out of tracked release documentation and release notes.
Run the public Sentry/telemetry threshold gate through the package script alias:
pnpm sentry:telemetry:gate -- --telemetry-mode best-effort
The Sentry threshold gate reads the latest event for each issue and classifies
expected handled failures from the SDK error catalog. An event is ignored only
when it is handled, its pm.error_code resolves to a declared canonical code,
its pm.exit_code exactly matches that code's transport contract, and the
semantic class is usage, not_found, or conflict. Message and title prose
never participate. Unknown codes, missing or mismatched exits, unhandled
events, and every generic_failure or dependency_failed remain blocking.
This keeps rewording independent from release policy and makes stale or broad
message allowlists impossible. The gate skips issues whose events are all test
traffic: environment=test (opted-in CI tests) or pm.source_context=test (a
test child that delivers with another environment such as ci). An issue with
any other event counts whenever any of its events falls inside the activity window. Activity and
environment are separate Sentry queries, because Sentry applies lastSeen after
an environment filter: a combined query would also skip an issue whose
production event predates the window while a test event falls inside it. When
the listing of issues with non-test events may be truncated, nothing is skipped,
and a failed listing fails the gate. Both listings follow Sentry's next-page links
up to --sentry-limit (default 200); when active issues remain beyond it, the
gate fails, because it cannot judge issues it never read.
Sentry API requests use a 120-second deadline by default. Operators can set
--sentry-request-timeout-ms between 1 and 300000 when reproducing
provider latency, while the emitted gate receipt records the effective value.
The release workflow pins 120000; query timeouts remain fail-closed and must
not be treated as an empty issue set.
If private reliability checks identify repeated user friction, either confirm the current release already contains the remediation with regression coverage or fix it before continuing.
The build writes dist/cli-bundle/bundle-manifest.json atomically with SHA-256 digests for every emitted bundle file. At startup, pm reports bundle_integrity_torn_install only when a module-loader failure is accompanied by manifest proof that an upgrade or rebuild changed, removed, or corrupted the active bundle. Reinstall @unbrained/pm-cli and retry after that diagnostic. Ordinary ERR_MODULE_NOT_FOUND and export failures with an intact manifest remain unexpected failures and must continue to block reliability gates.
- Run the same release pipeline locally.
Runtime install reproducibility is tracked by
pm-gh1417. The committed
runtime-dependencies.json records only the exact production closure from the
tested pnpm-lock.yaml, including integrity, nested conflicts and optional
edges. After changing dependencies, run pnpm install, then
node scripts/release/runtime-lock.mjs apply. Static and publication gates reject
drift; never generate the ledger by resolving fresh registry ranges.
pnpm pack:distribution builds an isolated publication tree, copies the exact
installed runtime closure into physical package directories and sets npm's
bundleDependencies. It excludes pnpm's development store and source maps.
The release workflow publishes this staged tarball with provenance; CI and
packed smoke tests use the same staging command. Application distribution and
bundled runtime have separate committed size and file-count limits, so runtime
payload cannot hide application growth. The ledger ships with the tarball for
installed-version verification. pm-iceees
keeps selected optional-peer subtrees complete: npm deliberately skips peer edges
when gathering bundled files, so staged runtime manifests record the exact tested
peer bindings as ordinary or optional dependency entries. Package code, source
and installed manifests, and peer declarations remain intact.
pm-2csj4k (repository-only tracker record excluded from npm) adds one root entry per required
peer: Bun installs resolve a package's dependencies from the registry instead of
its bundleDependencies, so the published root manifest pins every non-optional
peer target that the ledger installs at the root to its tested version and
bundles it. A peer resolved only in a nested node_modules is not pinned,
because the root manifest may name only root-installed packages, which the
installed-runtime verifier enforces. Without the pin,
@sentry/node's @opentelemetry/api ^1.9.1 resolved to 1.10.0 and Bun warned
against the bundled OpenTelemetry packages' <1.10.0 peer ranges. Optional-only
peers and direct dependencies are unchanged, and the packed smoke runs a verbose
bun add and fails on any peer-range warning. A real archive regression resolves the peer's descendant
from an extracted package and the installed-runtime verifier still refuses any
missing locked package. npm 12 ignores published shrinkwraps; shipping
a shrinkwrap alone therefore cannot enforce this contract. SDK consumers
supply the bounded optional Node types peer themselves.
Physical entrypoint verification is tracked by pm-tl7vlu (repository-only tracker record excluded from npm). The dependency advisory CLI resolves both its invocation and module to physical paths, so directory aliases and Node's symlink-preserving main-module mode still execute verification and return a receipt or failure status. Module imports expose the collector without starting verification.
Push the final implementation commit through a reviewed pull request first, wait for DeepScan and CodeFactor to finish on that reviewed SHA, then run the canonical registry-owned preflight:
pnpm verify:preflight
The registry supplies the ordered executable plan, command arguments, environment, capture policy, and explicit skip policy. Its receipt distinguishes passed checks from declared skips. The same registry maps named PR, nightly, and release workflow gates; hosted-only entries must explain why no faithful local equivalent exists. Analyzer provenance is the first executable step, so an unreleasable candidate fails before build, coverage, or package work.
The preflight includes the mandatory local hosted-analysis proof:
pnpm quality:hosted-analysis
The gate accepts only DeepScan's explicit zero-new-issue status and
CodeFactor's explicit no-issues result. Both contexts are required by main
branch protection, and the release pipeline reruns the same immutable-tree
proof. The gate reads branch protection through REST first; if that transport
is unavailable or its core API quota is exhausted, it queries the equivalent
GraphQL rules across every cursor-paginated page and still requires one exact
main rule with strict updates plus both analyzer contexts. Malformed,
truncated, missing, or ambiguous admin policy data fails closed. GitHub Actions'
ephemeral token cannot request the repository Administration: read permission
required by both of those policy surfaces. When both admin transports reject
that token, the gate uses the contents-readable GET /branches/main summary
and requires that the exact candidate is the current protected branch head,
protection is enabled and enforced, and CodeFactor plus DeepScan are effective
required contexts. That summary intentionally cannot attest the admin-only
strict-update setting: its receipt reports strict: null,
strict_verified: false, and verification_scope: effective_required_checks
instead of inventing a positive result. Exact, immutable identical-tree, or
fully validated deterministic-release analyzer provenance remains mandatory
and is emitted beside the policy receipt,
so the restricted-token recovery cannot admit an unanalyzed or different-tree
candidate. A maintainer token with administration-read access still verifies
and reports strict: true through REST or paginated GraphQL. It reads the
release commit first. When GitHub does not copy app results
onto a merge commit, the gate may reuse a reviewed merge-parent or squash-PR
head only when its immutable Git tree SHA exactly matches the release commit.
Squash provenance additionally requires one unambiguous GitHub association to a
closed PR merged into main with the release commit as its merge commit.
Missing, ambiguous, or different-tree provenance fails closed.
The automatic release commit is the sole non-identical-tree derivation. The
gate requires one parent, the canonical release subject and body, an immutable
v<version> tag resolving to the candidate, the complete expected manifest
inventory, exact old-version to new-version substitutions in every manifest,
and an exact Unreleased changelog-heading replacement. Fixed distribution
paths come from the same inventory as version synchronization and staging.
Plugin runtime package manifests are mandatory when present in the immutable
parent tree; historical parents that predate those files remain verifiable.
The package-generated
replacement may be ## <version> or the historical
## <version> - <YYYY-MM-DD> form, with the date matching the calendar version.
The complete heading must match and every other changelog byte must remain
unchanged. The gate then resolves the parent through the same exact or reviewed
identical-tree analyzer contract.
Added, deleted, renamed, missing, or otherwise modified paths are refused, as
are malformed manifests, changelog edits, untagged commits, and parents without
successful analyzer evidence. The receipt reports
analysis_source: deterministic_release_transform and names both the release
parent and the parent's analyzer source; it never describes the changed release
tree as identical to the analyzed tree.
The supported default-branch arrival path is a reviewed pull request. A direct
push to main is releasable only if both analyzers attached successful evidence
to that exact commit or it is the narrowly validated automatic release
projection above; when neither condition holds, the gate explicitly refuses the
candidate instead of letting the next scheduled release discover the missing
precondition after expensive work. CI runs Release analyzer readiness (main)
on every product-relevant push to main, providing continuous visibility
between release attempts. If GitHub's commit-association endpoint has not yet
published a new merge, the gate reads the pull-request number from GitHub's
standard merge message and accepts it only after the immutable PR resource
confirms that exact merge SHA, main base, and reviewed head. Remediate a
refused direct-main candidate by landing the next tree-changing fix through a
reviewed PR; never copy, synthesize, or bypass analyzer evidence.
Reviewed Delivery Closeout
The reviewed delivery includes its PM evidence, item closure, and generated changelog. Complete those mutations on the implementation branch before merge whenever their evidence is already available. A pull request is not complete while known tracker or changelog closeout remains queued for a direct push.
When a repository update genuinely depends on the merge SHA or a post-merge
hosted result, create a normal follow-up branch from main and land that update
through another reviewed pull request. Do not push .agents/pm/** or
CHANGELOG.md closeout directly to main. Although tracker-only commits do not
create package-release eligibility by themselves, the direct commit still
becomes the default-branch head and lacks the reviewed analyzer evidence needed
by the next release-relevant candidate. Release analyzer readiness (main)
must fail closed in that state; its failure is a provenance control, not an
analyzer approval. Restore readiness only with a tree-changing, main-based,
reviewed pull request—never by copying statuses, weakening the gate, or
synthesizing a pull-request association.
# Read-only parity check
pnpm release:pipeline:dry-run
# Full local preparation (version/changelog mutation + local commit/tag)
pnpm release:pipeline
The static phase includes pnpm sdk:surface:check,
pnpm benchmark:sdk-entrypoints:check, and
pnpm benchmark:transport:check. The packaging phase runs the
package-artifact preflight step (scripts/release/package-artifact-gate.mjs,
the gate behind pnpm quality:package-artifact) before any version commit or
tag exists. It evaluates npm's actual packlist against the committed
unpacked-size, file-count, required-runtime-file, and forbidden source-map
budgets, so an over-budget candidate never receives an immutable tag. The
package-artifact-sentry-injected step then runs the same gate with
--profile=sentry-injected --simulate-injection. It runs the installed
sentry-cli on a disposable copy of dist/ and adds each packed file's
measured growth to the packlist, so it checks the bytes the release will
publish without rewriting the checkout. An incremental rebuild does not
re-emit unchanged modules, so injecting the checkout itself would leave
injected files behind. Release
additionally reruns the packlist gate immediately after Sentry debug-ID
injection and before any Sentry upload or finalization, using the derived
sentry-injected profile: the base ceiling plus max_bytes_per_injected_file
for every packed dist/**/*.js file. sentry-cli sourcemaps inject dist/
adds one fixed-length snippet to each such file (358 bytes with
@sentry/cli 3.8.2), so a fixed allowance stopped covering the transform once
the file count grew and blocked the already-tagged 2026.11.5
(pm-998juj).
tests/unit/scripts/release/sentry-injection-allowance.spec.ts runs the
installed sentry-cli against a fixture, so an upgrade that grows the snippet
fails in the pull request that upgrades it, not on a tag. CI constrains the base
build, publication constrains the exact mutated artifact users install, and a
rejected package cannot leave finalized observability artifacts. Additive SDK
exports require a reviewed
snapshot refresh. A removal or semantic signature change fails until the
maintainer supplies
pnpm sdk:surface:update -- --acknowledge-breaking "<release rationale>";
pair that acknowledgement with migration guidance, compatible extension
version bounds where applicable, and the next eligible date-based release.
Build finalization removes redundant indentation from emitted .d.ts files
(pm-chw71s). String and template type
contents, comments, JSDoc, symbols and module boundaries retain their original
tokens. Source declarations remain readable, and packed TypeScript consumers
verify the emitted surface before publication. Artifact ceilings remain unchanged.
Whitespace after explicit declaration delimiters is also compacted, while
newlines that may supply automatic semicolons are retained. Literal contents
and documentation remain intact; the publication-weight owner is
pm-998juj
and this delimiter-compaction tranche is
pm-5aze0q
(repository-only tracker records excluded from npm).
The generated error-code catalog ships as compact rows
(pm-998juj, a repository-only tracker record excluded from npm).
scripts/generate-error-code-catalog.mjs omits every derivable field: the
meaning sentence, the class implied by the exit code, the shared recovery
guidance, provisional stability, every-command emitters, a self canonical code
and empty aliases. expandPmErrorCodeCatalogRows restores them at load time,
so PM_ERROR_CODE_CATALOG and every serialized projection stay byte-identical.
The catalog appears once in the tsc emit and once in each CLI bundle, and the
compact rows removed about 320 KB from the packed distribution.
The publication tree ships only the emitted declarations a consumer can reach
(pm-998juj).
exportedDeclarationClosure in scripts/release/package-distribution.mjs
walks every exported types entry through its relative from and import()
specifiers. The exports map refuses unexported subpaths, so a consumer cannot
import any other dist/**/*.d.ts: the CLI internals behind ./cli, the MCP
servers, and internal SDK and core modules. Packing leaves those out, and the
checkout keeps every declaration. A manifest without an exports map ships every
declaration, because any subpath then stays importable. The walk fails closed. An exported or
referenced declaration missing from the packlist, or a relative specifier that
is not a .js module path, refuses the pack instead of publishing a dangling
type reference. The packed smoke's strict TypeScript consumer imports every
exported types entry, so it compiles the complete shipped closure. The cut
dropped 212 declarations (266,135 bytes) from the 2026.11.6 distribution.
Artifact ceilings remain unchanged. The tsc .js emit still ships, because the
pm and MCP bins and the ./cli export import it directly.
- Land the implementation through a reviewed pull request, then use hosted preparation for publication.
Local preparation without --push is a diagnostic preview. Its local producer
declaration cannot authorize publication. Once the reviewed implementation is on
main, dispatch the existing Auto Release workflow from that default branch:
gh workflow run auto-release.yml --ref main \
-f push=true -f dry_run=false -f telemetry_mode=off -f trigger_origin=operator
Hosted preparation runs the mandatory gates, creates the version commit and
annotated tag, and seals their exact producer binding in an immutable artifact
before verifying and atomically pushing either ref. It waits for the single
tag-driven Release workflow to publish and accept that exact package. Do not push
locally prepared tags or invoke npm publish separately.
If today's immutable tag already exists, use the exact-tag recovery procedure below. A local diagnostic tag does not prove hosted preparation or publication; never move a published tag or mint a second automatic version for the same UTC day.
GitHub Workflow
.github/workflows/release.yml runs on v*.*.* tags and handles:
- full-history checkout
- manual
workflow_dispatchby tag for recovery. An isolated anonymous exact-version probe keeps already-published recovery on the reviewed dispatch-timemainsource; when the immutable tag exists but npm publication never completed, recovery preserves the reviewed package gate and budget in runner-temporary storage before checking out that exact tagged source. The controls inspect the untouched tagged package without becoming part of it, so recovery applies the current fail-closed distribution policy while retaining the original version guard and immutable package bytes. - pnpm install with frozen lockfile
- version policy and tag guard
- secret scan
- build, clone-local merge-driver installation, typecheck, test, and coverage
- generated changelog verification and
pm-changeloginstallation before the tracker-bearing static gate, so a clean checkout does not misclassify the managed extension's linked files as missing. Every exact-tag source, including an ordinary tag push and recovery of an unpublished immutable tag, regenerates onlyCHANGELOG.mdwith the tagged checkout's canonical package script and fails if that operation changes any other tracked source path (apart from managed-extension install metadata). Reviewedmainrecovery sources retain the non-mutating drift check. - static quality gate (shared complexity, duplication, dead/orphan module, file/folder hygiene, source/exported docstring coverage profile)
- temporary-project compatibility gate against latest published tracker data
- reliability threshold gate (Sentry severity threshold, bounded to a recent-activity window via
--sentry-window-days(default14,0= unbounded) so a stale benign unresolved issue cannot block every scheduled release; Sentry requests use the fail-closed bounded--sentry-request-timeout-mscontract (default120000, maximum300000);--telemetry-modegate policy:off|best-effort|required). Scheduledauto-release.ymlfailures open/update anAuto Release blockedGitHub issue so blocked daily releases are never silently skipped. - extension grammar acceptance (pm-gh1466, pm-gh1467): the existing flag parity gate includes a synthetic extension population, and packed smoke runs the same independent echo oracle on Node and Bun. Immediately before publication,
extension-flag-acceptance.mjsextracts the exact tarball passed tonpm publish, resolves its public SDK exports and manifest executable, and checks help, machine contracts, handler options, remaining arguments and refusals. Any failed or missing case stops publication. The retained receipt records executed case identities, runtime, resolved entrypoints, fixture digest and tarball SHA-256. This proves candidate behavior; published-installed acceptance remains a separate claim. Exact-tag recovery preserves both the reviewed runner and its fixture outside the tagged checkout. - diagnostic privacy acceptance (pm-geywoc): the same candidate runner requires the independent synthetic diagnostic fixture through the installed public SDK. A disposable loopback collector verifies real serialized error events, streamed spans, and finalized logs, including scope attributes. Quoted keys, Unicode escapes in keys/delimiters/separators, nested serialized JSON, containers and incomplete values must lose their sensitive markers; safe text, numeric/boolean attributes and caller scope must remain intact. Opaque encoded boundaries redact the remaining text, and a long backslash control bounds scanning cost. Missing, failed or duplicate case receipts block acceptance. Recovery preserves the diagnostic fixture with the runner. This local export proof is separate from hosted Sentry ingestion and does not change user consent.
- sandboxed
pmcoverage - a base npm pack dry run, optional Sentry debug-ID injection, a second
sentry-injectedpacklist budget over the exact publishable bytes, and only then optional Sentry release metadata/upload/finalization and the npx tarball smoke test. Sentry release metadata and sourcemaps are uploaded only from tag sources. Recovery of an already-published version from reviewedmainpreserves that version's existing Sentry commits and sourcemaps; local debug-ID injection still validates the candidate artifact budget without changing the published release's symbolication data. - generated release notes from changelog plus sanitized tracker metadata. The tracker read projects only release-relevant fields and explicitly removes row and token limits. The generator validates the completeness receipt before summarizing; partial, unreadable, or malformed input produces a visible warning instead of claiming no items closed. The regression suite exercises a real workspace exceeding the default JSON budget. Tracked by pm-svxdsx.
- artifact uploads
npm publish --access public --provenance --tag latest, skipped on retry only when the exact version is anonymously visible from a fresh npm cache. If the package is public but the target version is absent, the workflow publishes immediately without attempting a package-access mutation. A dispatch may do so only when its source-selection preflight pinned the checkout to the requested immutable tag; reviewed-main recovery continues to refuse publication of a missing target. Only when neither the target nor package metadata is anonymously visible, the same-tag recovery path fails closed and instructs the maintainer to restore public package visibility outside the workflow. Trusted publishing does not authorize package-access mutations. Permission, authentication, and registry failures stop the workflow instead of risking an immutable-version overwrite. The checked-out tag'spackage.jsonsupplies the canonical package identity to both the publish guard and the post-publish npm/npx/bunx verifier so those identities cannot drift. The explicit stable dist-tag also preserves correctlatestbehavior when rerunning historical ordinal tags.- post-publish npm/npx/bunx verification in the separate
publicationjob, which needs the publishing job and owns a 110-minute limit, so a slow build phase can no longer cut the registry wait short. npm's publish-time malware scan holds a new version for about five minutes and sometimes far longer (2026.11.8 became visible 55m22s after publish); the verifier polls for about ninety minutes, and an exhausted wait names the version, the scan and the publication-only re-run (pm-0jzl5t). The job checks out the exact commit the release job published (recorded after exact-tag source selection), so the verifiers read the samepackage.jsonidentity as the publish guard; previous-release selection keeps the reviewed dispatch-source selector. The verifier runsscripts/release/verify-published-release.mjs, using isolated empty npm and Bun caches plus an empty npm user config so maintainer credentials and cached metadata cannot mask a public-registry outage. The verifier dispatches a realpm contractscommand through both explicit-bin and package-default invocations, performs stateless JSON-RPCserver/discoveragainst the symlink-resolvedpm-mcpbin under both npx and bunx, and launches the exact publicpm-mcp-httpbin under both executors on an isolated loopback port for a real Streamable HTTPserver/discoverexchange. Both transports require canonical2026-07-28metadata/result envelopes. HTTP startup is bounded to two 20-second attempts per executor so the complete retry budget remains below the hosted step timeout. Signal-aware process-group cleanup escalates fromSIGTERMtoSIGKILLafter a bounded grace period, including when the outer evaluator times out or an intermediate executor exits before its server descendant. Direct executor exit is not treated as process-group cleanup. The verifier derives bin coverage frompackage.json, and proves missing-bin and missing-command controls fail. - exact-package installed acceptance through
scripts/release/verify-installed-agent-session.mjs. Separate npm and Bun install roots must contain the resolved executable, then each drives the cold-startinit -> context -> create -> claim -> annotate -> files -> close -> validate -> get -> contextloop. The structured report identifies the failing step and records per-step output ceilings and estimated token cost. - GitHub Release creation
- GitHub Release metadata verification through the same local verification script
The Auto Release parent has a 290-minute orchestration limit: 60 minutes for preparation and gates, 30 for aggregate runner queue delays, the serial 60-minute build-and-publish, 110-minute publication verification, 15-minute acceptance and 5-minute advertisement job limits, and 10 minutes for orchestration overhead. Runner queue time consumes the waiting parent's wall time even when no child job is executing. The reserve accommodates delays without extending a child job or changing a quality gate. Extreme queue delays can still exhaust the parent; recover the existing immutable tag after checking its publication evidence rather than creating another daily version. Tracked by pm-29bwud.
Monitor:
gh run list --workflow Release --limit 5
gh run watch <run-id> --exit-status
Verified Coverage Uploads
Tracked by pm-2x67z9. CI downloads the
official Codecov CLI from an immutable GitHub release and checks the reviewed
SHA-256 before making it executable. Both coverage and test-result uploads
require that verifier to succeed and retain mandatory upload failures plus
the exact PR-head identity. The pinned action's supported url input selects
the official https://codecov.io Cloud host for the current coverage and
test-result APIs; certificate validation remains required for every request.
Both uploads disable verbose logging because debug output can expose signed
storage upload URLs in public Actions logs. Normal upload-result diagnostics
remain available, and upload errors still fail the required gate.
Failed tests can still upload diagnostic reports
after successful verification. Update the release URL and digest together;
verify the official artifact and its adoption age before changing those pins.
The shell regression executes the real checksum with approved and corrupt
download fixtures; a failed check must prevent executable permission and uploads.
Post-Release Verification
Use the rolling reliability report (repository-only, excluded from npm) as well as the latest release result. Its first-attempt denominator, explicit outcome receipts, and UTC dispatch policy preserve failures even after a recovery succeeds.
npm view @unbrained/pm-cli@<version> version dist.integrity dist.unpackedSize --json
npx --yes --package @unbrained/pm-cli@<version> -- pm --json --no-extensions contracts --summary
npx --yes @unbrained/pm-cli@<version> pm --json --no-extensions contracts --summary
bunx --silent --bun --package @unbrained/pm-cli@<version> pm --json --no-extensions contracts --summary
bunx --silent --bun @unbrained/pm-cli@<version> pm --json --no-extensions contracts --summary
pnpm release:verify-installed-agent -- --version <version> --manager both --json
gh release view v<version> --json tagName,name,isDraft,isPrerelease,url
pnpm release:verify-published -- --version <version>
The executable remains pm even though the npm package is scoped.
Use the npm registry package for maintainer global updates. Do not use npm install -g https://github.com/unbraind/pm-cli.git as the normal update path; npm can leave a stale shim while replacing git-sourced global installs. If a workstation is already in that state, run bash scripts/install.sh --repair or npm uninstall -g @unbrained/pm-cli && npm install -g @unbrained/pm-cli@latest.
Failure Handling
- If local gates fail, fix and rerun before tagging.
- Treat failed scheduled Nightly Validation jobs as release-health blockers until triaged. The nightly workflow opens or updates a GitHub issue for each failing scheduled OS/Node matrix entry, with the run URL and commit SHA, so cross-platform regressions do not rely on someone manually scanning the Actions tab.
- A time-bounded waiver that expires fails
repository-defect-evidence-requiredat the next release gate; the failure record names the item or boundary and its expiry. The nightlyWaiver notice windowstep fails about two weeks earlier, so renew or retire the waivers it names through a reviewed pull request first. See Waiver Notice Window. - Treat a green manual Auto Release dry-run (
push=falseordry_run=true) as gate confidence only. It does not prove the protected-branch publish path; scheduled production runs still requireRELEASE_PATandSENTRY_PERSONAL_ADMIN_TOKENto be configured in thereleaseenvironment. - If release preparation fails before creating a tag, fix the cause and retry preparation on the same UTC day.
- Once a tag exists, never move or replace it and never create a same-day
ordinal recovery version. Rerun
.github/workflows/release.ymlwithworkflow_dispatchandtag=v<version>(or close the current bot-created blocker once to trigger the guarded exact-run recovery). The workflow skips npm publication for a version visible to its isolated anonymous probe, keeping reviewed dispatch-timemain. A definitive missing-version response can pin source to the immutable tag only after admission reads complete workflow and job inventories, validates totals and unique identities, and checks matching attempts. Prior successfulPublish to npmevidence refuses publication while metadata is unavailable: acceptance can precede visibility. Unavailable, malformed, incomplete or inconsistent evidence also refuses it. Only the executing attempt is excluded; its earlier attempts remain checked. This admission check also runs before first publication, using reviewed controls retained outside recovery checkout. Push and dispatch publishers share exact-tag concurrency. See pm-publication-acceptance-fence. A never-published tag then reapplies the version guard, installs the managed changelog extension, regenerates the package changelog with the tagged checkout's canonical policy under a tracked-path mutation guard, and permits first publication only from that tagged source. Ordinary tag-push releases use the same guarded regeneration because their tag did not exist when the release commit's changelog was prepared. Other registry failures stop before source selection or publication. - If the
publicationjob fails because npm has not yet made a version thatPublish to npmaccepted visible, the package is held by npm's publish-time scan, not lost. Do not dispatch a recovery or touch the tag. Oncenpm view @unbrained/pm-cli@<version> versionsucceeds, rungh run rerun <run-id> --failed: only the publication job and the skipped acceptance and advertisement jobs run again, without rebuilding or republishing. The failure message prints the exact command (pm-0jzl5t). Close the bot-createdAuto Release blockedissue by hand after that run succeeds; the parent run that recorded the failure stays red. - If an immutable published package contains a defect that cannot be repaired by rerunning the same tag workflow, document the incident and ship the code fix in the next UTC day's release.
- A manual exact-tag
workflow_dispatchrecovery uses isolated anonymous registry probes and never mutates package access. A visible package with a missing target version proceeds directly to exact-tag OIDC publication. When neither the package nor target version is anonymously visible, recovery fails closed until a maintainer restores public visibility outside the workflow. An already-visible immutable version is still verified and never republished. Recovery starts from the dispatch-time commit SHA and fails unless the dispatch ref is the repository default branch (main). It remains on that reviewed source when the exact npm version exists. When the version is definitively absent, it switches to the resolved commit behindRELEASE_TAG, requirespackage.jsonto match the tag, installs the clone-local merge driver and managed changelog extension, and may publish that exact source after every gate passes. - Record failure evidence and remediation in the release
pmitem.
Silent skip debugging
When auto-release exits green but does not cut a version, inspect the pipeline's JSON skip reason from scripts/release/run-release-pipeline.mjs (or rerun locally with pnpm release:pipeline:dry-run -- --json):
- tracker-only skip family:
tracker_only_changes_since_last_tag(all changed paths are.agents/pm/**and/or the generatedCHANGELOG.mdprojection; a product-visible path is the required negative control) - changelog-empty skip family:
empty_generated_changelog_section_for_target_version(generated release section exists but has no non-empty entries)
pm-changelog is maintained in a separate repository/package. Classifier or release-window bugs must be fixed and released there first, then consumed here via the latest npm package (pm install npm:pm-changelog --project) before rerunning release generation.
Before local changelog regeneration diagnostics, always refresh tags first:
git fetch --tags --force
Without a forced tag refresh, local tag windows can drift from origin and produce misleading changelog or skip diagnostics.
Aus docs/RELEASING.md in pm 2026.11.13. Auf GitHub ansehen