Commit Graph
11 Commits
Author SHA1 Message Date
Benjamin DiedrichsenandClaude Opus 5 2810d4491f [fix] release: stop the snapshot job starving the release job
Publish snapshot / snapshot (push) Successful in 1m5s
Pushing a release pushes the branch and then the tags seconds apart.
publish-snapshot.yml keys its concurrency group on the branch and
release.yml keys its own on the tag, so the two never gate each other —
on a single runner they race for it and the branch push always wins.

On 1.0.1 the snapshot job wedged extracting a layer of the runner image,
the release job never started, release.mjs gave up after its 20-minute
wait, and two of three tags were left unpushed. The report still printed
a bold "Done" above an empty shipped list, so it read as a success.

- publish-snapshot.yml skips commits whose message starts with "release:".
  A snapshot of a release commit is the same tree the tag is about to
  publish properly, so skipping costs nothing and removes the race.
- waitForRelease() offers to keep waiting instead of giving up. No
  timeout value survives a wedged runner, so the real choice is between
  asking and making the operator finish the release by hand. --yes and a
  non-interactive run still give up; the latter matters because confirm()
  answers with its default without a terminal, which would extend the
  deadline forever.
- The final header says "Blocked" when it is, and labels the packages
  that did ship before the blockage.
- --wait-timeout defaults to 2400s rather than 1200s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 15:28:12 +02:00
Benjamin Diedrichsen eac44b637e release: @bitsquare/nopy-cubes@1.0.1, @bitsquare/nopy@1.0.1, @bitsquare/nopy-cubes-core@1.0.1
Publish snapshot / snapshot (push) Canceled after 37m36s
Release / release (push) Successful in 1m7s
2026-09-01 13:05:46 +02:00
Benjamin Diedrichsen a28b75f522 bump versions 2026-09-01 13:03:39 +02:00
Benjamin Diedrichsen 41f4e49aa6 add rules to claud md 2026-09-01 13:01:13 +02:00
Benjamin DiedrichsenandClaude Opus 5 05f2d6aa56 [docs] nopy: record the seven findings this branch closed
The documentation half of the same work: `DOCS-AUDIT.md` marks §1.3, §1.5,
§2.3, §4.2 (all three points), §5.1, §6.1, §6.2 and §6.5 closed, each keeping
its original text as the record with what closed it quoted underneath, and the
"suggested order of attack" is rewritten to what is actually left — §5.2, §5.3,
the two missing cube READMEs, and the two findings (§2.7, §4.4) that are stated
accurately in `docs/API.md` while the code still behaves as they describe.

`docs/API.md` drops the two entries from its *Known gaps* list that are no
longer gaps, documents the argv and the absent shell, describes the resolution
stack and the error it raises, and inverts the `.default()`/`.describe()`
warning: the order used to matter and no longer does, which is worth saying
outright since the old advice is in the reader's memory and in 15 manifests.

The README's "topological sorting" becomes "in dependency order, with cycle
detection" — the sort never existed, but until this branch neither did the
thing a sort would have been for — and `--no-history` is spelled
`--no-save-history` wherever it appears.

One line of code rides along, because it is what a `docs/API.md` note has been
asking for: `CubePackageRef` is re-exported from `src/index.ts`, so importing
`NopyConfig` from `@bitsquare/nopy` no longer gives you a type whose own
members you cannot name. The note in `docs/API.md` saying it is missing goes
with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 12:49:30 +02:00
Benjamin DiedrichsenandClaude Opus 5 b5702e423a [fix] cubes: service:autostart reads its data, and its README describes it
Closes DOCS-AUDIT §6.1 and §5.1.

**The script could not run.** It read `APP` off `host.data` and then used
`SERVICE_NAME` and `AUTOSTART` as if they were in scope, so the very first
statement — `if AUTOSTART:` — raised `NameError`; `server` was used in the else
branch but never imported. Three lines: import `server` alongside `systemd`,
read the two names next to `APP`. The logic underneath was always right.
`python3 -m py_compile` passes.

**The README documented a different cube.** It was titled "TypeStack Install
Cube" and described cloning a git repository, `yarn install`, `yarn build`,
`docker compose up -d` and PM2 — none of which this cube does, and it listed
parameters (`USER`, `REPO`, `ENV`, `NODE_PATH`) the manifest does not have,
carrying someone's private repository URL and username as defaults.

Rewritten from the manifest and the now-working script: the three parameters
that exist, and the thing the old text obscured by describing a deploy
pipeline — this cube does not create the unit file, it enables and starts one
that is already installed. `SERVICE_NAME` is documented as what it is, a label
that never reaches systemd, so getting it wrong is cosmetic rather than a cube
managing the wrong unit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 12:48:55 +02:00
Benjamin DiedrichsenandClaude Opus 5 89450cb7bc [fix] nopy: --no-save-history, so -H keeps its id
Closes DOCS-AUDIT §6.2.

Commander derives an option's destination from its long flag with `no-`
stripped, so `--no-history` wrote to the same `options.history` that
`-H, --history <id>` reads. `nopy install -H abc --no-history` set it to
`false`, the id was discarded without a word, and the run fell through to a
full interactive session instead of replaying anything.

The two cannot share a destination, so one spelling had to change, and it is
the boolean that moved: `-H <id>` is what the help text, the README and
`docs/API.md` all use, and "save history" is what the flag actually suppresses
— next to `-s, --save-session`. The old spelling now fails loudly instead of
silently.

Verified by running the CLI, since `nopy.cli.ts` is argv wiring and excluded
from coverage:

    install --no-history                          -> error: unknown option '--no-history'
    install -H nonexistent-id --no-save-history   -> Session not found: nonexistent-id

The second line is the finding: the id used to be destroyed there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 12:48:44 +02:00
Benjamin DiedrichsenandClaude Opus 5 09554b6785 [fix] nopy: find the prompt label through zod's wrappers
Closes DOCS-AUDIT §2.3.

The cube contract says each schema field is `.describe()`d and that the
description is the prompt label. Whether it was depended on the order the
manifest happened to chain in: zod 4 keys a description to the schema
*instance* and `.default()` returns a new `ZodDefault` around the described
type rather than mutating it, so the wrapper carries no description of its own
and the prompt, reading the outer node, fell back to the bare key.

    z.boolean().describe('Update package cache').default(false)  ->  'UPDATE'
    z.boolean().default(false).describe('Update package cache')  ->  the sentence

15 of the 22 core cubes are written the first way, so most prompts showed a key.
`promptLabel()` walks down through `default` / `optional` / `nullable` looking
for a description, which makes the two orders equivalent — the answer that
cannot regress, where re-ordering every manifest and hoping the next one written
gets it right can. It discriminates on `zodKind`, not `instanceof`, for the
reason recorded on that helper: a manifest built by a different zod copy fails
every `instanceof` in the module.

The mocked test asserts all four shapes, including a doubly-wrapped
`describe().optional().default()` and a field with no description at all. The
pty test is the one that carries the weight: its probe schema is written in the
losing order, and it now waits for `First value` on a real enquirer render, so
removing the unwrapping fails a test that talks to an actual terminal rather
than to a mocked `Form`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 12:48:16 +02:00
Benjamin DiedrichsenandClaude Opus 5 bb8b1bfa5c [fix] nopy: detect a dependency cycle instead of overflowing the stack
Closes DOCS-AUDIT §6.5, and the substantive half of §1.5. `docs/API.md` has
documented a circular-dependency error since it was written; nothing raised it.
Two mutually dependent cubes recursed until V8 gave up, and a `RangeError`
names no cube — it reads as a nopy crash rather than as a manifest that says
something impossible.

`BuildContext` now carries a resolution stack: `resolveCube` pushes its
(cube, host) pair, delegates the body to `visitCube`, and pops in a `finally`.
A pair re-entered while it is still on the stack raises a `NopyUsageError`
naming the whole path — `Circular dependency on host1: a → b → c → a`. The
whole path, not just the repeated cube, because dependencies are declared
dynamically and a hook may `exec` anything at all, so the edge that closed the
loop is rarely the one you would guess from the two ends.

It has to be a structure of its own. `resolvedCubes` is written by
`buildDeployCall`, which runs *after* the descent, so a cycle never reaches it;
and it cannot be widened into a "seen" set, because re-entering a *finished*
cube with different `param` overrides is exactly what a dependency or a hook is
for. That distinction is what the diamond test pins: `shared` is entered twice
under `left` and `right` and must still resolve, while `a → b → a` must not.

There is still no topological sort and there does not need to be — emission is
post-order, so the order already is a topological one. Cycle detection was the
one thing a sort would have given that the recursion did not.

Six tests: self-dependency, a three-cube loop, the loop reported as usage
rather than as a stack overflow, a loop closed by a hook's `exec` rather than a
`dependencies()` entry, the diamond, and the same cube on two hosts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 12:48:06 +02:00
Benjamin DiedrichsenandClaude Opus 5 4daf27a3cd [fix] nopy: spawn the pyinfra argv, never a shell string
Closes DOCS-AUDIT §4.2 (point 3, the last one open) and §1.3.

`executeCall` joined `DeployCall.command` and ran it through
`execa({shell: true})`, which made every value on that command line shell
syntax. The finding framed it as a quoting problem "in the password"; it was
wider than that. `--data` values were interpolated inside double quotes, so a
backtick or a `$(…)` in *any* variable value was command substitution and a `;`
ended the command and began another.

`buildDeployCall` now emits a true argv — one element per argument, nothing
pre-quoted — and the executor spawns `execa(command[0], command.slice(1))` with
no `shell` option at all. pyinfra is still found on PATH and stdio stays
inherited, so live output is unchanged.

`maskCommand()` walks the argv by position instead of pattern-matching a joined
string, which closes a leak of its own: it used to bound a secret's value on the
closing `"` the builder had written two modules away, so a value containing a
`"` leaked its own tail. It is now the only thing that turns the command back
into a string, for display, and it shell-quotes as it goes so `--print-only`
output stays pasteable.

Also in `buildDeployCall`: `logConfigToFlags()` finally has a caller (§1.3). It
was exported and unit-tested with nothing consuming it, so `log.verbosity` and
`log.debug` in `.nopyrc.json` did nothing at all. The flags are prefixed onto
the argv right after `-y`. Consequence worth knowing rather than discovering:
`packages/nopy/.nopyrc.json` has always asked for `"verbosity": "trace",
"debug": true`, so a run from that directory now really does get `-vvv --debug`.

The tests move with it — the mock is `execa(file, args, opts)` with no factory
to unwrap, and the new cases are the ones that would have caught this: an argv
element holding `$(id); rm -rf /` stays one element, a secret whose value
contains a quote is masked whole, and `execa` is asserted never to be asked for
a shell.

What remains is not fixable here: the value still reaches pyinfra on its command
line, so it is visible in `ps`. That is pyinfra's `--data` interface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 12:47:50 +02:00
Benjamin DiedrichsenandClaude Opus 5 2019626618 [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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCzYTAm9QUhvLNr2EpdagJ
2026-09-01 12:31:17 +02:00