Skip to content

fix(release): publish the signed release's npm tarball, not a rebuild - #175

Merged
siracusa5 merged 21 commits into
mainfrom
c/npm-publish-release-tarball
Sep 17, 2026
Merged

siracusa5 merged 21 commits into
mainfrom
c/npm-publish-release-tarball

Conversation

@siracusa5

@siracusa5 siracusa5 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #180 (Wave A CLI: #176#178#180, all on #173). Targets main so CI runs. Merge those first; this diff then shrinks to its own commits. It was merged forward onto #180 only to resolve an adjacent scripts/test.mjs suite entry.

Summary

Part of plan step 4 (first npm publication), stacked on #173. The npm workflow rebuilt the package on the publish runner, so what reached the registry could differ from the tarball in the signed release's SHA256SUMS. It now publishes that exact tarball after checking it. It also upgrades npm to a version trusted publishing supports.

Changes

  • scripts/verify-npm-tarball.mjs (new, unit-tested) checks the release tarball:

    • its SHA256SUMS line
    • package name and version
    • every entry is under package/, with no node_modules
    • no lifecycle scripts

    It prints the registry-style sha512- integrity.

  • .github/workflows/npm-publish.yml:

    • gh release download the tarball and SHA256SUMS, verify, then npm publish "$TGZ" --provenance --access public. No rebuild.
    • npm@11, asserting npm ≥ 11.5.1 and Node ≥ 22.14.0 (npm docs). Node 22 bundles npm 10, which cannot use trusted publishing.
    • Idempotent: if the version exists on npm, succeed only when dist.integrity matches the release tarball; otherwise fail.
    • Optional publish_pointer input for the context-cake pointer package, which no workflow published.
    • Header documents the one-time prerequisites.
  • scripts/tests/release-workflow.test.mjs pins the new shape; new npm-tarball suite registered in the release group.

Test Plan

  • node scripts/test.mjs release: 4/4 suites
  • New tests reject a checksum mismatch, a missing SHA256SUMS line, a lifecycle script, and a wrong version
  • CI checks pass
  • First real run after John reserves the package name (cannot be exercised before that)

Notes

  • npm's docs don't say whether trusted publishing accepts npm publish <tarball>. It is standard npm CLI usage; the first real run confirms it.
  • The workflow stays manual (workflow_dispatch), after the signed release exists.

Open questions for John

  • Reserve the names now: hand-publish contextcake@0.0.0 (and try context-cake@0.0.0) with 2FA, then attach the trusted publisher (repo ContextCake/context-cake, workflow npm-publish.yml, environment npm-publish) and disable token publishing.

🤖 Generated with Claude Code

siracusa5 and others added 4 commits September 16, 2026 23:20
…bution

Record the 2026-09-16 platform decisions as dated amendments beside the
original text:

- distribution spec: Intel Mac (x64) and Linux .deb (x64) in scope, with
  EARS criteria; AppImage, a Windows desktop app, and non-macOS
  self-update stay out. Fix the stale npm package name.
- distribution design: new §11 (one release platform table, per-platform
  install locations and CLI channel, Linux .deb specifics, split release
  jobs).
- control-plane: npm publishes before Homebrew; the CLI dispatcher moves
  into the engine; the npm package ships the policy fixtures; milestone 7
  starts with the Windows read path.
- channels and site specs: native Intel Mac and Linux downloads, npm CLI
  as the no-app route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
CRLF: every frontmatter reader tested startsWith("---\n"), so a file saved
with Windows line endings parsed as having no frontmatter and silently lost
its type, title, and dates; past the fence, the last field kept a trailing
\r and was dropped too. okf-local, files, layer-files, and promote now go
through one splitFrontmatter helper, and frontmatter writes keep the file's
line endings.

Paths: new platform-paths.mjs answers where config, data, and cache live on
macOS, Linux (XDG), and Windows, per control-plane spec §5.12, with
CONTEXTCAKE_* overrides. The app-bundled CLI (always the macOS path before)
and the npm CLI (~/.config on Windows before) both read their default
manifest from it.

Promotion bindings move to the data directory. Their macOS home was a
"Local State" folder inside Electron's userData, where Chromium keeps a file
of that name, so staging a promotion failed with ENOTDIR on any Mac that had
run the app. Bindings staged in the old location are still read and removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The SQLite store reuses postings by file fingerprint (size, mtime), which a
parser change never touches. Without a format bump, an unchanged CRLF note
indexed before the frontmatter fix would keep postings and a `type` column
from its old frontmatter-less parse until someone edited it. Bump
FORMAT_VERSION to 6, test that an older-format store re-analyzes every
document, and record the rule in CLAUDE.md.

Also note in the manifest trust gotcha that CONTEXTCAKE_MANIFEST and
CONTEXTCAKE_CONFIG_DIR select the CLI's default manifest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
npm-publish.yml rebuilt the package on the publish runner, so the registry
copy could differ from the tarball listed in the release's SHA256SUMS. It now
downloads that tarball, checks its checksum, name, version, entries, and
lifecycle scripts (scripts/verify-npm-tarball.mjs, unit-tested), and runs
`npm publish <tgz>`.

Also:
- upgrade to npm 11 and assert npm >= 11.5.1 and Node >= 22.14.0, which
  trusted publishing requires; Node 22 bundles npm 10
- a re-run for a version npm already has succeeds only when the published
  dist.integrity matches the release tarball, and fails loudly otherwise
- optional input to publish the context-cake pointer package, which no
  workflow published before
- document the one-time prerequisites (placeholder 0.0.0, trusted publisher)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
@siracusa5 siracusa5 added the bug Something isn't working label Sep 17, 2026
@siracusa5
siracusa5 changed the base branch from c/multi-platform-downloads-ca2eb1 to main September 17, 2026 03:34
@siracusa5 siracusa5 closed this Sep 17, 2026
@siracusa5 siracusa5 reopened this Sep 17, 2026
siracusa5 and others added 2 commits September 16, 2026 23:58
…and profiles

Wave A slices (a) and (b) of the control-plane spec.

- packages/core/src/cli.mjs + cli/: one command table that routes argv
  and generates `help --json`. Table commands answer with the versioned
  JSON envelope, typed ControlErrors, the §5.2 exit codes, and redaction
  of tokenEnv values, provider token shapes, and Authorization headers.
  `mcp` accepts only its serving flags. `--timeout` is refused on
  mutating commands before they start.
- The app CLI and the npm bin become thin wrappers. The app passes a
  wrapSpawn hook so mcp and doctor still run through its observability
  launchers.
- `init` creates a v2 manifest and never migrates or overwrites.
- `profile` family over control/profiles.mjs: the original six commands
  keep their text; show, rename, clone, and purge-state are new. Delete
  retires sidecar state; purge-state --confirm removes it. Mutations
  accept --expect-revision, checked under the manifest lock.
- `account status` reports a typed disabled state.
- The npm package ships the capture and context policy fixtures. The
  distribution test asserts the exact packed file list, and a new e2e
  test installs the packed tarball and runs help, init, and MCP.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
siracusa5 and others added 11 commits September 17, 2026 00:21
MCP's list_concepts, get_links, read_file markdown, and rule merge, plus
the service's /api/resolve disposition decoration, move into
concept-queries.mjs so the CLI query family answers from the same code.
Behavior is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
concept list|search|read|links and file list|read answer through the
resolver, the retained search store, and layer-files.mjs, with coverage
naming every source a read could not reach and --require-complete
exiting 6. Parity tests pin the answers to /api/resolve, /api/search,
/api/files, /api/file, and MCP list_concepts and get_links.

doctor runs in-process: manifest and quarantined layers, the selected
profile, source reachability, config/data/cache writability, and every
contextcake on PATH with its version. A failed check exits 8. The app's
wrapSpawn hook still adds the observability check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Source operations take { profileId, expectRevision }; null is default, the
only profile the HTTP service reads, so its routes behave as before.

- addSource stages a fresh clone and promotes it under the manifest lock
  after the revision and name checks.
- removeSources can keep unreferenced clones (retainClones), and
  pruneClones deletes one only when nothing references it and git reports
  no changes, stash, or unpushed commits.
- syncSource moves out of service.mjs so the CLI runs the same code.
- listSources, configurePendingSource, withSourceSession, testSources.
- settingsView reports which tier each value came from; patchSettings
  takes expectRevision.
- repairContextManifest takes profileId and a precondition;
  syncPackAssignmentLevel takes a profileId.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
`contextcake source list|show|add|update|level|reorder|remove|test|sync|
prune|pending-list|pending-configure|pending-dismiss` and `contextcake
settings list|get|set|reset`, both experimental, over the shared control
operations. Parity tests drive the CLI and the HTTP service over the same
fixtures. The packed-tarball e2e now adds its folder with `source add` and
checks MCP list_concepts serves it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…and profile state

- Spawned entrypoints refuse dispatcher flags they do not implement.
  `write --json --dry-run` used to read --dry-run as the value of --json
  and write for real. --timeout on a spawned write exits 2
  (TIMEOUT_REFUSED); doctor keeps its own --json.
- ctx.signal aborts on --timeout and interrupts. A read is abandoned and
  the process exits; a write always runs to the end, so an answer never
  says INTERRUPTED over a mutation that lands. A second SIGINT exits.
- Redaction honors toJSON, keeps Error message and code, and only marks
  true cycles as circular.
- migrateManifestToV2 allocates the profile id under its lock and throws
  PROFILE_EXISTS and PROJECT_MAPPED instead of plain errors. The test
  harness now fails any answer whose code the command does not declare.
- Writes report the revision they wrote (ctx.noteManifestWrite); reads
  answer from the manifest the envelope read.
- profile delete writes the manifest before retiring state and reports a
  failed retire as a warning. purge-state checks and deletes under the
  manifest lock.
- New table trait requireComplete for commands that must exit 6 on
  partial coverage. The e2e test runs the Windows .cmd shim through a
  shell.
- Site CLI reference documents the new profile commands and the
  envelope and exit-code change for `contextcake profile`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…cli-wave-a-query

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Query and doctor operations take the manifest the envelope read instead
of reading the file again, stop on --timeout and interrupts through
ctx.signal, and report the revision they read. Site docs describe the
concept, file, and doctor commands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Adapts the source and settings families to the foundation fixes:
- every manifest write reports the revision it wrote under the lock
  (MANIFEST_REVISION on the operation result, ctx.noteManifestWrite);
- `source test` uses the requireComplete trait instead of throwing;
- adds and pending configures check ctx.signal before the locked write,
  and `source test` aborts its reads on it;
- the tolerant manifest read computes the file's revision itself now that
  currentManifestRevision is gone.

Also documents the two families in the site CLI reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
`source test` and `source sync` register their session close with
ctx.onClose when the dispatcher offers it, so an MCP child cannot outlive
a read cut off by --timeout. Clones and pulls take ctx.signal; an aborted
clone removes its partial directory and answers with the abort reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
/api/resolve hashed raw section text while MCP read_file and the
Discrepancy Center record fingerprint() (JSON-quoted), so a section
acknowledged in the Discrepancy Center read "reopened" over HTTP. The
shared decorator now uses fingerprint(), and MCP calls it instead of its
own copy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
…tings

- ctx.onClose(fn) registers async cleanups. The dispatcher runs them
  newest first, 1.5 s in total, after run() settles and before it
  answers a timeout or interrupt; an abandoned read waits 1 s before
  exiting. Read sessions register their close there and also close on
  ctx.signal, so a timed-out query no longer orphans an MCP child that
  ignores SIGTERM.
- Aggregate queries list each source once and read from that listing,
  so coverage describes the data; a read failure after listing marks
  the source partial.
- doctor never runs what it finds on PATH. control/installs.mjs skips
  relative entries, counts one install per folder (PATHEXT names only
  on Windows), dedupes by realpath, caps the scan, and reads the version
  from package.json or Info.plist.
- doctor lists MCP and keychain-credential sources as notProbed without
  failing --require-complete, uses the query coverage vocabulary, and
  refuses a data folder under a regular file.
- file read answers INVALID_INPUT for an empty path segment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
siracusa5 and others added 3 commits September 17, 2026 02:57
…firmed host

Review findings on the source family:

- prune: cloneState runs git with user config off and counts ignored
  files, hidden index flags, in-progress merges or rebases, and
  reflog-only commits as work. Prune and the app's remove rename a clone
  into .cache/repos/.trash under the lock and check and delete it after
  releasing it; a clone that changed goes back.
- pending-configure refuses to attach a token to a GitHub source whose
  apiBase is not the default unless --api-base restates it, and the
  pending listing always shows apiBase. The probe override never gets a
  token unless a test opts in.
- add re-checks a reused clone under the lock and refuses a slot holding
  a clone of another repository; sync refuses a non-clone folder in the
  slot; a failed clone only removes a directory it created.
- nested scrub markers count as missing; testSources drops its abort
  listener.
- HTTP tests for clone add, sync, remove, SYNC_FAILED, and the new 409.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Stack the source/settings families on the query/doctor families so the Wave A
PRs merge in a straight line (#176#178#180) instead of conflicting.

Conflict resolution:
- cli/families/index.mjs: register source, settings, concept, and file.
- CLAUDE.md: take #178's dispatcher row and ctx.onClose gotcha, and #180's
  control-operations row and clone/token gotchas.
- reference/cli.md: one description naming every family.

Follow-ups the merge made possible:
- source.mjs calls ctx.onClose directly now that the dispatcher provides it.
- cli-doctor test: the "no source family" case builds a table without it,
  and the "with source family" case uses the real registry instead of a stub.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
Stack the npm publish hardening on the Wave A CLI PRs so every open PR merges
in one straight line. The only conflict was scripts/test.mjs, where both sides
added a release suite next to each other; both suites are kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
The Wave A CLI PRs landed as squash commits; the only conflict was the test
registry, where this branch adds the npm-tarball suite beside them. Verified
no suite from main was dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: John Siracusa <siracusa5@users.noreply.github.com>
@siracusa5
siracusa5 merged commit 7e1fe9b into main Sep 17, 2026
7 checks passed
@siracusa5
siracusa5 deleted the c/npm-publish-release-tarball branch September 17, 2026 20:27
@siracusa5 siracusa5 mentioned this pull request Sep 17, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant