Skip to content

build(release): an open release-hold issue stops the release - #329

Merged
nissa-seru merged 5 commits into
anima-research:mainfrom
nissa-seru:build/release-holds
Oct 14, 2026
Merged

nissa-seru merged 5 commits into
anima-research:mainfrom
nissa-seru:build/release-holds

Conversation

@nissa-seru

Copy link
Copy Markdown
Contributor

An open issue labelled release-hold now stops a release. Whoever finds that releasing main as it stands would harm someone running it files one, saying what has to happen first, and closes it once that has happened.

@antra-tess @Anarchid, this changes how you release, so please look before it merges. npm version and npm publish now ask GitHub for open holds first, and the publish workflow asks again for a tag.

Why

Nothing in the release read such a thing before. #300, the uncapped retryAfterMs sleep, was opened on Oct 12 at 16:15Z and closed on Oct 14 at 04:08Z, once #251 merged. 0.21.1 (Oct 13, 13:37Z) and 0.22.0 (Oct 13, 18:32Z) were both published while it was open. 0.22.0 required membrane ^0.5.87, the first release carrying antra-tess/membrane#56, which attaches a provider's whole retry-after hint to the errors it returns. #300 itself said hosts shouldn't take that membrane before the fix, and a host that installed 0.22.0 got the sleep.

The change

  • scripts/release-holds.mjs lists the repository's open issues labelled release-hold, reading every page, and exits 1 naming each while any is open. A pull request with the label isn't a hold, since it isn't on main until it merges. The repository is the GitHub repository that package.json's repository field names, as a URL or in npm's shorthand, so a fork asks the package's home. GITHUB_REPOSITORY is used only when package.json names none. A token in GH_TOKEN or GITHUB_TOKEN is sent when present, and a public repository's issues read without one.
  • If it can't read the holds, it stops too, since an open hold can't be ruled out. Its message says what to do: for a network error, its code; for an answer GitHub refused, the token, the rate limit, or the setting that named the repository.
  • npm version runs it as preversion, so a held release stops before anything is bumped, committed or tagged. With no hold open, it costs one request to GitHub.
  • npm publish runs it as prepublishOnly, before the build, so a release published by hand stops too.
  • The publish workflow runs it for a tag in its build job, and again in each job that releases. The npm job checks through prepublishOnly, and the release-notes job has a step of its own. A held tag reaches neither npm nor the GitHub release notes, and a hold filed after the build job's check still stops both. Each of the three jobs gets issues: read and passes the job's token. A dispatch on a branch releases nothing, so it isn't held.
  • The stop message says how to go on where it ran: re-run the workflow for the tag, or run npm version or npm publish again.
  • CONTRIBUTING says when to file a hold and what it stops, and that npm's --ignore-scripts skips it, as it skips every lifecycle script.

It doesn't stop a hazard nobody files, or a publish with npm's scripts turned off.

Tests

test/release-holds.test.ts runs the real script against a local stand-in for GitHub's issues endpoint, each case in a throwaway directory with its own package.json. It covers:

  • no open hold, a hold beside a labelled pull request, only labelled pull requests, several holds, and a hold behind a full page of labelled pull requests;
  • which repository it asks: each form npm reads in the repository field, package.json over GITHUB_REPOSITORY, GITHUB_REPOSITORY when package.json names none, a field that names no GitHub repository, a package.json that isn't JSON, and no repository at all;
  • the token it sends, from GH_TOKEN, or from GITHUB_TOKEN alone;
  • what each refusal tells the maintainer, with and without a token, including a 404 that names the setting that named the repository; a server error; an answer that isn't a list; GitHub out of reach; and an API URL with a trailing slash;
  • the stop message's next step in the workflow, under npm version and under npm publish.

The last three cases run npm itself with this repository's preversion and prepublishOnly entries. With a hold open, npm version stops before the bump and npm publish refuses. With none, npm version bumps.

Evidence

At cb2a3365, on Linux:

  • npm run typecheck and npm run build are clean.
  • The full suite passes 2010/2010 across 140 files on Node 24.21.0, with --test-force-exit and without, and on Node 20.20.2 with it. release-holds passes 22/22 in each run.
  • With the 404 hint's earlier wording put back, the test for the setting that named the repository fails, and the other 21 pass.

Ezra-2423 also ran it by hand. With a hold open, npm version patch exits 1 and leaves the version as it was. Against GitHub itself, with no token, the check exits 0.

Review

The review workflow's two batteries read the change at 97487d58, and the fixes for its findings are in e1525a84. Ezra-2423 (Claude Opus 5.5) read each version, 94d02ee9, 97487d58, e1525a84 and cb2a3365, and approved each. His six points on the first are in 693459f1 and 97487d58, and cb2a3365 takes his note on the 404's wording.

We'll merge this once our pre-merge checks pass on its merge with the then-current main, and not before 15:00 UTC on October 14. After it merges, we'll create the release-hold label.

— Ada-1017 (Claude Opus 5.5)

Ada-1017 added 5 commits October 14, 2026 03:06
When releasing main as it stands would harm someone running it, an issue
labelled release-hold says what has to happen first. Nothing in the
release read such a thing before: 0.21.1 and 0.22.0 were both published
while anima-research#300, the uncapped retryAfterMs sleep, was open.

scripts/release-holds.mjs lists the repository's open issues with the
label (pull requests aren't holds: one isn't on main until it merges) and
exits 1 naming each, or when the holds can't be read. It runs as npm's
preversion hook, so npm version stops before it bumps, commits or tags
anything, and as the publish workflow's first step for the pushed tag,
which both releasing jobs wait on. The repository is GITHUB_REPOSITORY or
the one package.json names; GH_TOKEN or GITHUB_TOKEN is sent when set,
and the workflow's build job gets issues: read for it.

The tests run the real script against a local stand-in for GitHub's
issues endpoint: no hold, a hold beside a labelled pull request, only
pull requests, several holds, GITHUB_REPOSITORY and the token, an error
answer, and no repository to ask. CONTRIBUTING says when to file a hold
and what it stops.
… publish meets them too

From the read of 94d02ee:
- The repository is the one package.json names, so a fork's workflow or
  a copy asks the package's home; GITHUB_REPOSITORY is used only when
  package.json names none.
- prepublishOnly runs the check, so npm publish from a maintainer's
  machine stops while a hold is open, as npm version and the workflow do.
- The workflow's step runs for a tag only: a dispatch on a branch releases
  nothing, so an open hold doesn't fail it.
- Messages say what a maintainer can do: an unreachable GitHub names the
  network error's code (or 'no answer within 30 s'); 401 points at the
  token; 403 and 429 at the token's access or the rate limit; 404 at
  package.json's repository field or a private repository's token.
- Tests: package.json over GITHUB_REPOSITORY, GITHUB_REPOSITORY when
  package.json names none, a 404, and GitHub out of reach (a port just
  freed: fetch refuses the discard port as 'bad port' without
  connecting).
prepublishOnly now runs the release-hold check inside the publish job's
npm publish too. That job had no GITHUB_TOKEN for it and no issues: read,
so the request went out unauthenticated from a shared runner address,
where GitHub's limit of 60 an hour could fail the publish after the build
job's check had passed. The job gets issues: read, and its Publish step
passes GH_TOKEN, so the check there asks as the build job's does.
…ms, every page, both release jobs

From the review of 97487d5 (sw-af-holds):
- The repository field is read in each form npm reads: a github.com URL,
  github:owner/repo, or owner/repo. A field that names no GitHub
  repository the check can ask stops it, saying so, rather than falling
  back to GITHUB_REPOSITORY, which is used only when package.json names
  none.
- Every page of open labelled items is read, so a hold behind a hundred
  labelled pull requests still counts (up to 5,000 items, past which it
  stops and says so).
- The GitHub release notes job checks the holds too, with issues: read and
  the job's token, so a hold filed during the run stops the notes as
  prepublishOnly stops the npm publish.
- The stop message says how to go on where it ran: re-run the workflow for
  the tag, run npm version again, or run npm publish again.
- Refusals: 403 and 429 without a token say to set GH_TOKEN, with one that
  the token can't read the issues or its rate limit is spent; 404 says
  which; a 5xx says to retry; another status no longer promises a retry
  helps. A body that can't be read is named as such.
- The message is written synchronously before the exit, so a piped stderr
  gets it whole.
- CONTRIBUTING: someone who can't label asks a maintainer; close a hold
  that no longer applies; both release jobs refuse; the check also stops
  when it can't read the holds; npm's --ignore-scripts skips it.
- Tests (21): npm itself runs this repository's preversion and
  prepublishOnly in a throwaway package (npm version stops before the bump
  with a hold open and bumps without one; npm publish --dry-run stops),
  each repository form, a field naming no GitHub repository, a package.json
  that isn't JSON, GITHUB_TOKEN alone, the refusals with and without a
  token, a 200 that isn't a list, a trailing slash on the API URL, a hold
  on the second page, the message per caller, and the request asserted
  for labelled pull requests alone. The throwaway directories are
  removed.
When package.json names no repository, the hold check asks the one
GITHUB_REPOSITORY names. A 404 for it then said to check package.json's
repository field, which named nothing. The hint now names whichever setting
named the repository: package.json's repository field, or GITHUB_REPOSITORY.

A test runs both: package.json naming a repository (with GITHUB_REPOSITORY
set to another, which the check ignores), and package.json naming none.
Under the old wording the second case fails.
@greptile-apps

greptile-apps Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5 Tier: apex

[Critical impact] The PR appears safe to merge; no actionable issues were found.

Summary

Adds checks for open release-hold issues before versioning, publishing, and creating GitHub release notes.

  • Open release-hold issues now stop npm and tag releases.

Reviews (1) · Last reviewed commit: "build(release): a 404 names the setting ..." · Reviewed by Greptile

@nissa-seru
nissa-seru merged commit f8956a4 into anima-research:main Oct 14, 2026
6 checks passed
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 14, 2026
…nima-research#286's branch

anima-research#333 (a continuing start keeps the turn's routing) conflicts with this
branch at the two sites where both change how deferred messages are
stored:

- At the tool boundary, this branch drains only injectable messages,
  stores them through writeDrainedInOrder (queue order, cut at the first
  that can't be stored) and injects what was stored. anima-research#333 added
  noteForOtherResidents for each message stored there, and recorded each
  injected message's id (injectedIds) for a boundary that leaves its
  injections to the turn's next start as arrivals. Both are kept: the
  write records each stored message's id, and the loop over what was
  stored notes it for the other residents and records its injection's id.
- At the turn-start flush, this branch stores through
  writeDrainedInOrder, and anima-research#333 noted each stored message for the turn
  now starting (noteArrival) and for the other residents. Kept, in the
  loop over what was stored, so a store that throws notes nothing and the
  entries it cuts stay queued.

anima-research#333's flush at a start that ends before its stream, in
startAgentStream's finally, merged as text but not as types. It re-added
through addMessage with deferredWriteId, which this branch replaced with
fromQueue { id, seq }, stored each message on its own, logging and
dropping one the store refused, and was gated on every agent's tool cycle
(pendingAssistantBlocks.size === 0). It now goes through readdDrained, in
queue order and cut per target as every other flush is, and is gated on
the agent's own cycle, as the turn-end and withdrawn-turn flushes are.

Its tests in present-while-acting follow:
- The test that a message deferred in a failed start lands then now has
  another agent's tool cycle pending while the start fails, and asserts
  the flush stored the message then.
- The test that the flush stored each message on its own, the one behind
  a refused message stored and the refused one lost, is replaced by its
  form under the cut: the store refuses the first of two messages, both
  stay queued in order, nothing behind the refused one is stored, and once
  the store takes it the next start stores both in order. (Its refusal
  hook keyed on deferredWriteId, which no longer exists, so it refused
  nothing here.)

The fragment names that flush among those that wait only on their own
agent's state.

addMessage merged without a conflict: anima-research#333's noteArrival and
noteForOtherResidents after a store sit beside this branch's fromQueue
re-deferral and per-target gate. anima-research#329 (release holds) changes no file this
branch changes.
nissa-seru pushed a commit to nissa-seru/agent-framework that referenced this pull request Oct 16, 2026
…pawn wait and transport listeners, and TypeScript 7

main since cd036e0 brings anima-research#329, anima-research#333, anima-research#273, anima-research#368, anima-research#343, anima-research#340, anima-research#365, anima-research#341,
anima-research#352, anima-research#338, anima-research#274 (TypeScript 7), anima-research#347, anima-research#346, anima-research#348 and anima-research#344 (no hang after a
signal kill; no uncaught transport or spawn errors). Merged, not rebased, so
the commits slimepriestess and the round's readers read stay as they were
(Ada-1017, room-203 #105340). `git show --remerge-diff` shows the resolution
alone.

Conflicts, three files:
- src/mcpl/transport.ts:
  - anima-research#344's listeners for a read error on the child's stdout or stderr go in
    beside the stdin listener this branch already had.
  - close() stays this branch's reap (SIGTERM, then SIGKILL, then an
    explicit failure; one shared attempt). anima-research#344's version waited for an exit
    that kill() made likely, and its change there, counting a signalCode as an
    exit (anima-research#331), the reap already does.
- src/mcpl/server-connection.ts:
  - handshake keeps this branch's signature (the abort signal and the
    negotiated protocolVersion) and its abortable open, and takes anima-research#344's
    logAfterSettled. That listener takes over 'error' once the initialize
    race settles, comes off just before a successful return, and stays on a
    failed handshake's transport.
  - The race's cleanup keeps both lines: this branch's removal of the abort
    listener and anima-research#344's logAfterSettled.
  - The reconnect attempt keeps this branch's reconnectOnce, which reaps the
    previous launch before another starts and closes a handshaken transport
    itself when close() has ended the attempt. anima-research#344's version kept that
    transport for close() with a logging listener. That intent carries over
    as a listener put on before the attempt's own close, since the
    transport isn't wired and the handshake's listeners are off.
- package.json: devDependencies take both @babel/parser (anima-research#274) and
  @modelcontextprotocol/server (this branch).

Beyond the conflicts, for main's async StdioTransport.spawn (anima-research#344):
- SpawnerStdioTransport.start() awaits it. So a modern stdio command that
  can't be spawned rejects start() with its spawn error, and the connect
  fails with that error instead of waiting out its 30 s discover probe. This
  is R1-4's fix (Anarchid's review, room-364 #105270), and its test follows
  this merge.
- The tests that wrap spawn to make a child unreapable await it, in
  mcp-modern-outcomes, mcp-server-ownership and mcpl-legacy-lifetime. So
  does the unreapable-close test's own spawn. tsc can't see the old
  synchronous wrappers, since they reach the child through a cast, and they
  hung those suites.
- anima-research#344's test of a transport kept for close() during a reconnect now
  follows this branch's reconnectOnce. The attempt closes that transport
  itself and close() waits for it, so the test resolves the held close
  before it awaits the attempt. It's retitled for what it checks: the
  transport logs the errors it reports while it closes, and after.

tsc (TypeScript 7) is clean. A trial of this resolution on main bdc5dc4
with anima-research#344's head 8387393, the head that merged, passed every test file on
Node 24.21.0 and Node 20.20.2 (J10968). The only failures were 69 in a
stale dist/test/mcp-modern-http-2025.test.js that the VPS copy kept from
anima-research#337's runs. The full check of this commit follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant