From 201962661821eee74588a9aaba0c4a0c2cb3ed2a Mon Sep 17 00:00:00 2001 From: Benjamin Diedrichsen Date: Tue, 1 Sep 2026 12:31:17 +0200 Subject: [PATCH] [feat] release: interactive release client, drop the CI linked-deps guard `pnpm run release` (scripts/release.mjs, zx + enquirer + commander) replaces the hand sequence of bump, changelog, gate, tag, push. It picks packages from a list annotated with what npmjs already has, computes versions from the manifest, collects notes in $EDITOR seeded with the commits since the package's last tag, and prepends them to CHANGELOG.md in the format release.yml's parser expects. The gate (lint:ci -> typecheck -> test:coverage -> build -> verify-pack) runs against the bumped tree *before* the commit, so a failure leaves nothing to unpick -- it offers to restore instead. Tags go out dependency-first, and each version is polled on npmjs before the next tag is pushed. That polling is what lets release.yml lose its `check linked deps are released` step: the ordering is now enforced before CI ever sees a tag, rather than after. linked-deps.mjs stays as a hand-check. The accepted cost is that a tag pushed some other way is no longer caught. Three things found by running it rather than reading it: - Tags are annotated (`-a -m`). A lightweight tag is rejected outright under tag.forceSignAnnotated, which is set on the machine this was written on. - pnpm 11 forwards the `--` in `pnpm run release -- --dry-run` literally, and commander reads a bare `--` as "the rest are positionals". The script takes no positionals, so it strips it and both spellings work. - Prompts refuse with a message naming the flag that avoids them when stdin is not a TTY, instead of hanging as an unsettled top-level await. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ --- .gitea/workflows/release.yml | 34 +- CLAUDE.md | 58 ++- README.PUBLISH.md | 92 +++- package.json | 7 +- pnpm-lock.yaml | 12 + scripts/linked-deps.mjs | 9 +- scripts/release.mjs | 866 +++++++++++++++++++++++++++++++++++ 7 files changed, 1016 insertions(+), 62 deletions(-) create mode 100644 scripts/release.mjs diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index f9d3471..5fa85a3 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -125,32 +125,14 @@ jobs: - name: Install run: pnpm install --frozen-lockfile - - name: Check the linked workspace packages are already released - env: - NAME: ${{ steps.target.outputs.name }} - DIR: ${{ steps.target.outputs.dir }} - run: | - set -euo pipefail - # `pnpm publish` turns `workspace:*` into the version the linked - # package declares at this commit. If that version is not on the - # registry yet, the release installs to a broken tree — and npmjs - # only lets you unpublish for 72 hours. Release the dependency first: - # nopy-cubes, then nopy, then any bundle. - # - # npmjs only: it is the irreversible one, and it needs no credentials - # to read, which this step does not have yet. - missing=0 - for spec in $(node scripts/linked-deps.mjs "$DIR" | tr ' ' '@'); do - # Scoped, not `--registry`: `@scope:registry` outranks it, so a bare - # flag can be silently overridden by any project-level .npmrc. - if npm view "$spec" version --@bitsquare:registry="$NPMJS_REGISTRY" >/dev/null 2>&1; then - echo "${spec} is published" - else - echo "::error::${NAME} depends on ${spec}, which is not on npmjs. Release it first." - missing=1 - fi - done - exit "$missing" + # There used to be a *check linked deps are released* step here, refusing + # to publish a package whose `workspace:` dependency was not yet on npmjs. + # It was removed: `scripts/release.mjs` is what creates release tags now, + # and it already pushes them dependency-first and waits for each version to + # resolve on npmjs before pushing the next — so the ordering is enforced + # before CI ever sees a tag, rather than after. `node scripts/linked-deps.mjs + # ` still prints what a package would bake in, if you want to check by + # hand. A tag pushed some other way is no longer caught. - name: Lint run: pnpm run lint:ci diff --git a/CLAUDE.md b/CLAUDE.md index fd8dc61..28db603 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -142,13 +142,27 @@ One pass per invocation, `nopy.main.ts` orchestrating: variables (prompt, or read them back from the session on replay) → run `before` hooks → resolve `manifest.dependencies(vars)` (dynamic: it receives the *collected* variables) → emit the deploy call → run `after` hooks. There - is no separate topological sort; ordering falls out of the recursion, and a - `${cubeId}:${host}` set makes emission idempotent. Hooks get a `HookContext` + is no separate topological sort; emission is post-order, so the ordering *is* + topological without an algorithm computing it, and a `${cubeId}:${host}` set + makes emission idempotent. Hooks get a `HookContext` whose `exec(id, vars)` re-enters `resolveCube`, so a hook can pull in a cube that is not a declared dependency. -6. **`nopy.executor.ts`** — runs the built `pyinfra -y --data K=V ... --chdir