Skip to content

fix(update): follow the latest dist-tag for every build - #170

Merged
vraspar merged 4 commits into
mainfrom
vraspar/update-follows-latest
Aug 17, 2026
Merged

fix(update): follow the latest dist-tag for every build#170
vraspar merged 4 commits into
mainfrom
vraspar/update-follows-latest

Conversation

@vraspar

@vraspar vraspar commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Scope

  • channelTag() returns latest for every parseable version, so the daily check, tenjin update, and --check all resolve against one tag (src/lib/update-check.ts). Unparseable versions still return null, which keeps the check silent and has tenjin update refuse a build it cannot place.
  • resolveTarget() collapses to the single latest lookup, and its two null causes are now told apart at the runUpdate call site: an absent tag has nothing to install, a tag naming a version VERSION_RE does not admit means npm has a build this copy is too old to read (src/commands/update.ts). maybeUpdate records the check clock whenever the registry answered, so an unreadable latest costs one request per window rather than one per command.
  • The now single-valued channel field is gone from the update envelope, and RELEASING.md no longer claims prereleases publish to the alpha dist-tag.

Not doing

No npm dist-tag changes; retiring the stale @alpha tag on npm is an owner action after this releases.

DoD

  • pnpm run check (build + full suite): 1941 passed, 10 skipped, 69 files. Baseline on origin/main measured in the same worktree was 1935 passed / 10 skipped; the delta is the six tests added here and no suite dropped into a skip.
  • pnpm run lint, pnpm run typecheck, pnpm run format:check: clean.
  • Gate reverted separately for each guard. Restoring the old prerelease→alpha branch fails 30 tests. Merging the two RESOURCE_NOT_FOUND messages back into one fails the two new message pins. Restoring cache nothing on an unusable answer fails the three new clock pins.
  • Changeset added (patch), rewritten to state the real rationale: the removed loop took the max, so what a second tag carried was a tag sitting ahead of latest redirecting the install, not a stale one winning.

🤖 Generated with Claude Code

channelTag returns 'latest' for every parseable version, so the nudge,
`tenjin update`, and `--check` all resolve against one tag. resolveTarget
collapses to that single lookup; an unparseable version still follows no
tag, keeping the nudge quiet and the command's refusal of a foreign build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vraspar vraspar added the priority: critical Blocks other PRs; merge-priority label Aug 16, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@A1igator A1igator left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — round 1 (head 528c12d3)

The mechanism is sound and I could not find a way to make it install an unasked-for build. Three findings, all about the justification and the removed fallback rather than the happy path.


1. The changeset's rationale is factually wrong about what the removed code did

.changeset/update-follows-latest.md and the new resolveTarget doc comment in src/lib/update-check.ts both say a second tag "only gave a stale answer a chance to win", citing alpha frozen at 0.1.0-alpha.7.

The removed loop took the max, not the first hit:

if (best === null || isNewer(candidate, best)) best = candidate;

A lower alpha could never win that comparison. alpha sitting at 0.1.0-alpha.7 while latest moved to .13 was already resolved correctly on main — that is what the deleted test follows latest when the channel tag has fallen behind it pinned.

This PR's own rewritten tests prove the point: to make the new pins fail under the old code, both resolveTarget reads latest for a prerelease build, whatever alpha holds and nudges a prerelease build toward latest, ignoring a newer alpha tag had to raise alpha to 0.1.0-alpha.99, i.e. ahead of latest. That is not staleness.

It matters because the changeset ships verbatim into CHANGELOG.md, and this sentence is the sole stated justification for deleting a fallback on the self-update path. The truthful version: a second tag lets a tag sitting ahead of latest redirect the install, and one tag is simpler and matches what the pipeline actually does.

Related framing: for the live registry this PR changes no user-visible outcome. Every behavioral delta is the removal of a fallback. fix(update) reads as a defect repair; this is a hardening/simplification.

2. The code now depends on a pipeline property the repo itself denies

channelTag hard-codes "every build follows latest" on the premise that the pipeline moves latest and nothing else. The repo's own release state says the opposite in two committed places:

  • .changeset/pre.json is {"mode": "pre", "tag": "alpha", ...}
  • RELEASING.md:37 — "Prereleases use Changesets' pre mode (pnpm changeset pre enter alpha) and publish to the alpha dist-tag; stable goes to latest."

I verified the premise holds today against the live registry: {"next":"0.1.0-alpha.1","alpha":"0.1.0-alpha.7","latest":"0.1.0-alpha.13"}, and every publish from 0.1.0-alpha.8 (2026-08-01) through 0.1.0-alpha.13 (2026-08-16) moved latest while alpha stayed put. So the behavior is real — but it holds as an artifact of what changeset publish does here, not because anything in this repo asserts it.

The PR body defers RELEASING.md:37 to an owner pass on the grounds that it describes the pipeline rather than the CLI. After this PR the CLI depends on the pipeline, so that separation no longer holds. If a future operator reads line 37 and "fixes" the publish to match it, latest stops moving, every user is told "up to date" forever, and resolveTarget has no second tag left to recover through. Correcting RELEASING.md:37 (and noting that pre.json's tag is inert here) belongs in the PR that takes the dependency, or the risk is invisible to the next person at the console.

3. RESOURCE_NOT_FOUND misdescribes the case this PR newly makes unrecoverable

In runUpdate (src/commands/update.ts):

`npm has no published tenjin-cli on the ${channel} tag`

resolveTarget returns null for two different facts: latest is absent, and latest holds a version VERSION_RE cannot parse. In the second case npm does have a published tenjin-cli on latest; this build just cannot read it. The user is sent after a fault that is not there — the same objection the comment three lines above raises against NETWORK_ERROR.

On main that second case had a recovery for prerelease builds (channel was alpha, so the loop still had a tag to fall back to), which is every user today. The deleted test skips an unparseable candidate instead of letting it win called it "Load-bearing, and nothing else catches it... reachable the first time this project publishes something the regex does not admit." VERSION_RE admits only -alpha.N, so any future -beta.N, -rc.N, or +build on latest reaches it.

Second consequence, on the nudge rather than the command: maybeUpdate has if (latest === null) return; // asked and learned nothing: cache nothing either. An unparseable latest therefore makes every CLI invocation hit registry.npmjs.org indefinitely, with nothing printed and nothing cached to stop it.

Ask: separate the two nulls so the message and its fix: are true — either have resolveTarget report which fact it hit, or test tags[channel] === undefined at the runUpdate call site before choosing the message.

Nit in the same line: channel is now a single-valued literal, so ${channel} is a constant interpolation and channel on UpdateData is a constant field on the envelope. Fine to keep deliberately, worth dropping if nothing reads it.


Checked and clean

  • The 'alpha' union removal is type-only; nothing persisted reads it. channel is absent from src/schemas.ts (only UpdateAvailableSchema, which is {current, latest}), is not read by src/lib/hook-scripts.ts (that reads cache.signal alone), and appears in no doc or skill. The one persisted artifact that can still say alpha is a tags.alpha entry in the update-check cache: CacheSchema's z.record(z.string(), TagEntrySchema) still accepts it, maybeUpdate reads only cached?.tags[tag] with tag now always latest, and the read-modify-write spread carries the legacy entry forward untouched. No throw, no wrong answer, and the first post-upgrade invocation rewrites signal. ignores an entry under another tag, and leaves it intact pins this.
  • The self-update path refuses rather than proceeding on a null. runUpdate throws REFUSED before any fetch when channelTag(current) is null, and RESOURCE_NOT_FOUND before any spawn when resolveTarget is null. The spawn is reached only past isNewer(latest, current), so a latest older than the running build yields "up to date" and no install — no silent downgrade, and no path that leaves the CLI unrunnable.
  • Silent-on-unparseable holds for every input shape, not only well-formed semver. maybeUpdate and readUpdateSignal gate on the same channelTag, which is now exactly parseVersion(v) === null ? null : 'latest'. Any string VERSION_RE rejects — empty, 1.2.3-beta.1, 1.2.3+build, a coerced non-string — returns null and returns early before any I/O. Nothing can reach the emit path with an unparseable current.
  • Interaction with #136 is intact. The exact-version argv pin past the isNewer gate, both refuse(moduleDir, ...) stages, and the manager-delegation path are untouched by this diff.
  • Test coverage is real, not adjusted-to-pass. Both rewritten pins are negative controls with an alpha deliberately ahead of latest, and nudges a prerelease build toward latest, ignoring a newer alpha tag asserts not.toContain('0.1.0-alpha.99') rather than only asserting the expected line.

No approval and no merge from me.

resolveTarget's null has two causes and they take opposite instructions:
an absent latest tag has nothing to install, while a latest naming a
version VERSION_RE does not admit means npm has a build this copy is too
old to read. runUpdate now reports each truthfully, quoting the offending
value truncated.

maybeUpdate records the check clock whenever the registry ANSWERED, so an
unreadable latest costs one request per window instead of one per command.
A registry that could not be reached still caches nothing and is retried
on the next command. Both clocks keep their existing meaning.

Also corrects RELEASING.md, which claimed prereleases publish to the alpha
dist-tag, and restates the changeset's rationale: the removed loop took the
max, so the risk it carried was a tag AHEAD of latest redirecting the
install, not a stale one winning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@vraspar

vraspar commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

All three findings accepted and fixed in b599f3e. Point by point.

1. Changeset rationale. You are right, and the diff proved it against me: the removed loop took the max, so a lower alpha could never win, and the two new pins only fail under the old code once alpha is raised to 0.1.0-alpha.99, which is ahead of latest, not stale. The changeset and the resolveTarget doc comment now say the true thing: latest is the one tag this pipeline moves, a second tag adds no reachable version and one way to be wrong, and that way is a tag sitting ahead of latest redirecting the self-update install to a build the release line never promoted.

On the framing point, agreed that for the live registry the tag change alone is a hardening rather than a defect repair. The title stays fix(update) because the help text shipped to users pointed them at a dead channel, and the round-2 work in this commit is a straight defect fix: RESOURCE_NOT_FOUND was giving a wrong diagnosis, and the check was capable of hitting the registry on every invocation. Happy to retitle if you would still rather it read as a refactor.

2. RELEASING.md. Folded in, since you are right that the CLI-versus-pipeline separation stops holding the moment the CLI takes the dependency. Line 37 now states the observed behaviour rather than the intent: every publish lands on latest, prerelease or stable, evidenced by 0.1.0-alpha.8 through 0.1.0-alpha.13 each moving latest while alpha has not moved since 2026-07-31. It also notes that .changeset/pre.json's "tag": "alpha" does not decide the published dist-tag under this pipeline, and it names the dependency explicitly: tenjin update and the daily check resolve against latest alone, so a change that stops latest moving strands every installed build reporting up to date, with no second tag to recover through. The PR body's deferral is removed; only the npm tag retirement stays an owner action.

3. Both consequences fixed.

The two nulls are separated at the runUpdate call site, which is the second option you offered. I took it over threading a result type out of resolveTarget because the function has two other callers that only need usable-or-not, and the call site already holds the tag name. An absent tag keeps npm has no published tenjin-cli on the latest tag with the pick-a-version fix. An unparseable one now reads npm's latest tenjin-cli (<value>) is not a version this build can read and its fix is npm i -g tenjin-cli@<version>, because retrying cannot fix a version this copy cannot parse and naming one by hand can. The quoted value is registry-controlled, so it is truncated to 40 characters; the existing emitter already strips escapes on the human path and JSON-escapes on the envelope.

The nudge no longer re-asks forever. resolveLatest now distinguishes a registry that could not be asked from one that answered with nothing usable. The first still caches nothing and is retried on the next command, which is what the original comment was protecting. The second records checkedAtMs, so silence costs one request per day instead of one per command. TagEntrySchema.latest became optional to carry asked-and-learned-nothing; existing cache files still parse, and readUpdateSignal already handled a missing value. Both clocks keep their meaning: checkedAtMs is when npm was asked, and it was asked, while notifiedAtMs is untouched because nothing was printed.

One consequence worth flagging, since it changed an existing test rather than adding one. swallows a rejected fetch, a non-200, and a body that is not dist-tags bundled six shapes and asserted nothing was cached for all of them. The sixth, {next: 'nonsense'}, is a registry that answered, so it now records the clock and the old assertion was correct only under the old behaviour. I split it out into its own pin rather than relaxing the bundle, and left the remaining five asserting nothing-cached. {latest: 7} stays in that group because a non-string value fails the schema and the whole map is discarded.

Nit. Dropped. channel is gone from UpdateData and the interpolation is gone from the message. I re-grepped before removing and confirmed your clearance: the only readers were src/commands/update.ts and its own tests, nothing in src/schemas.ts, hook-scripts, docs, or skills.

Verification. pnpm run check is 1941 passed / 10 skipped / 69 files, against an origin/main baseline of 1935 / 10 measured in the same worktree, so the delta is exactly the six added tests. Lint, typecheck, and format:check are clean. I reverted each new guard separately: merging the two RESOURCE_NOT_FOUND messages fails the two message pins, and restoring cache-nothing on an unusable answer fails the three clock pins.

Applies the anti-slop test to the comments these commits introduced:
what remains states an invariant or a trap the code cannot show, and the
narration, the restated code, and the justification prose are gone.
Untouched code is left at its existing density.

RELEASING.md folds the dist-tag correction back into the step-3 sentence
rather than appending blocks, and the changeset is one paragraph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vraspar

vraspar commented Aug 16, 2026

Copy link
Copy Markdown
Contributor Author

Comment trim in aef6512: this PR's added lines are now 7.7% comment in update.ts and 14.7% inline in update-check.ts (the rest there is JSDoc on the three exported functions whose contracts changed), and both files come out below their main density rather than above it. RELEASING.md folds into the step-3 sentence instead of appending blocks, and the changeset is one paragraph. Suite unchanged at 1941 passed / 10 skipped.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@A1igator A1igator left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — round 2, delta only (528c12d3aef65126)

All three findings are addressed. One is addressed and then partly un-addressed by the trim commit; one new nit. Nothing from round 1's verified-clean list is re-opened.


1. Wrong rationale — FIXED, then thinned

The false sentence is gone from both places it appeared. What replaced it is true, and I checked it against the code rather than reading it: latest is the one tag this pipeline moves (verified live again — alpha still frozen at 0.1.0-alpha.7 since 2026-07-31, latest at 0.1.0-alpha.13), and the risk actually removed is a tag sitting ahead of latest, which is what the rewritten pins encode with alpha: '0.1.0-alpha.99'.

The catch: b599f3e6 wrote that reasoning out in full, in both the changeset and resolveTarget's doc comment — "Consulting a second tag as well added no reachable version and one way to be wrong: a tag sitting ahead of latest could redirect the self-update install to a build the release line never promoted." That is exactly the correction, and aef65126 deleted it from both.

At head, resolveTarget's doc explains what the nulls mean but no longer says why there is one tag; channelTag carries only the pointer "Every build follows latest; see RELEASING.md, which changes with this." So the tree now encodes the ahead-of-latest hazard in test fixtures that nothing explains: a reader meeting alpha: '0.1.0-alpha.99' in reads latest for a prerelease build, whatever alpha holds has no line telling them why the fixture is shaped that way.

Not a re-raise — a wrong claim replaced by no claim is the fix, and the trim pass is otherwise good (the comment blocks this PR added were genuinely over-weight for what they guarded). Putting that one sentence back on resolveTarget is worth more than the two lines it costs.

2. Undocumented premise — FIXED

RELEASING.md now says every publish lands on latest prerelease or stable, names .changeset/pre.json's "tag": "alpha" as pre-mode bookkeeping that does not decide it, and states the coupling out loud: "publish tagging and src/lib/update-check.ts must change together." That is the dependency made visible, the contradiction with the committed release state resolved rather than left standing, and the coupled file named so the next operator has somewhere to look. Nothing further from me.

3. Conflated RESOURCE_NOT_FOUND — FIXED, and the caching half was fixed too

Both halves, stated plainly since the framing could have carried one without the other:

The two nulls are now distinguishable. runUpdate re-reads tags[channel] and branches: absent gives "npm has no published tenjin-cli on the latest tag" with the pick-a-version fix; present-but-unparseable gives "npm's latest tenjin-cli (<value>) is not a version this build can read" with "Install it by name: npm i -g tenjin-cli@<version>". Different message, different instruction, and the second no longer tells a user npm has nothing when it has something.

The per-command re-fetch was fixed, not just accepted. This was the part I expected to survive. resolveLatest now returns null only when the registry could not be asked, and { latest: null } when it answered with nothing usable; maybeUpdate writes a tag entry carrying checkedAtMs and no latest in the second case, so the 24h window applies and an unreadable latest costs one request per day. TagEntrySchema.latest went optional to carry it, upgradeable and signal are null-guarded, and the entry's stale latest is dropped rather than kept — correct, since the tag no longer names it.

I also verified the claim in the truncation comment instead of taking it: emitFailure in src/lib/output.ts runs sanitizeForTerminal over both message and fix on the human path and writeJson on the machine path, so "bounded here, escaped by the error emitter" holds for the registry-controlled value.

4. New nit: the cache schema widened under the same schemaVersion

TagEntrySchema.latest went from z.string() to z.string().optional() while CacheSchema.schemaVersion stays z.literal(1). readCache rejects the WHOLE file on a parse failure, so a new build writing { latest: { checkedAtMs } } hands an older installed build sharing the data dir a cache it discards entirely — losing any other tag's entry and nudge clock with it, which is the one thing the per-tag keying exists to prevent. Self-corrects on the next write and only in the already-exceptional unparseable-latest state, and the cache is a documented pure optimization, so this is a note rather than a change request.


The new tests: do they fail on the pre-fix code?

I built the new tests against the old source to check, rather than reading them. Reverting only src/lib/update-check.ts and src/commands/update.ts to 528c12d3 and keeping this delta's tests: 7 failed, 69 passed.

Real pins, all failing on revert:

  • asks once per window when latest carries a version it cannot read
  • asks once per window when the map has no latest tag at all
  • clears a stale signal when the answer stops being readable
  • separates a latest it cannot parse from a latest that is not there
  • truncates a registry version before quoting it back

Two of the seven are not defect pins: installs the exact version npm just named, never the tag and --check reports the newer version and never installs fail only because the reverted runUpdate puts channel: 'latest' back into a toEqual. That is the type change, not a behavior guard.

One new test does not fail on revert: records nothing when the registry could not be reached at all passes against the old code, because the old code also cached nothing there. It asserts alongside the fix rather than pinning it. It still earns its place as the contrast case — it is what would fail if someone later made the unreachable path record a clock too — but it is a forward guard, not evidence for this change. Its present form is await expect(readFile(updateCheckPath(dir), 'utf8')).rejects.toThrow(), i.e. the cache file must not exist at all, which is the right shape for that job.

At head both files are green: 76 passed, 2 files.

On the framing

Agreed, with a correction to my own round-1 wording. I said the tag change is hardening rather than defect repair, and that is true of the resolved version — the max-across-tags on main already returned 0.1.0-alpha.13 for the live registry. It was not true of what the CLI told users: the update help line in src/cli.ts said "an alpha build follows @Alpha", which is every user of this package, pointing them at a tag frozen since 2026-07-31. That is a shipped falsehood in help text and this PR repairs it. So: defect repair on the disclosure surface, hardening on the resolution. Fine to characterize it as fix(update).

No approval and no merge from me.

The trim cut the one sentence explaining why there is a single tag, which
left the hazard encoded only in a test fixture (alpha ahead of latest) that
nothing described.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@vraspar

vraspar commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Both points, in 395f538.

1. Sentence restored. Fair catch, the trim took one line too many. resolveTarget's doc comment carries the rationale again: consulting a second tag added no reachable version and one way to be wrong, a tag sitting ahead of latest redirecting the self-update to a build the release line never promoted. That is the line the alpha: '0.1.0-alpha.99' fixtures need in order to read as deliberate. Restored on resolveTarget only, three lines, nothing else re-expanded, and it is JSDoc so the inline comment density on this PR's added lines is unchanged at 7.7% and 14.7%.

2. Schema nit taken as a note, no code change. Bumping schemaVersion would not buy anything: older builds pin z.literal(1), so they discard the whole file on a 2 exactly as they do on an unexpected shape. The window is also narrow and self-clearing, since it opens only while latest is unparseable and closes on the next write by either build, and the cache is documented as a pure optimization that is re-fetched rather than repaired. Worth recording, so if the cache ever stops being disposable this is the constraint that changes.

Also agreed on records nothing when the registry could not be reached at all. It passes against the old code and is a forward guard on the contrast case, not evidence for this change. I intended it as the pair to the two clock pins rather than as a third one, and your framing of it is the accurate one.

Full suite at head: 1941 passed, 10 skipped, 69 files.

@A1igator A1igator left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 395f5380. All three round-1 findings are fixed and verified, and the one item left open was answered rather than ignored.

The schema argument checks out. It was accepted because it was verified against main, not because it sounded reasonable:

  • CacheSchema on main really does pin schemaVersion: z.literal(1), so a bump to 2 fails safeParse on an older build exactly as a widened TagEntrySchema does.
  • readCache returns null on any parse failure, so the discard is whole-file either way, not per-entry.
  • TagEntrySchema.latest is a required z.string() on main, so the cross-version window is real, and it is bounded as described: it opens only while latest is unparseable and closes on the next write by either build.
  • The cache's own docstring on main states it is a pure optimization that is re-fetched rather than repaired.

So the proposed remedy genuinely buys nothing, and the finding is correctly recorded as the constraint that changes if the cache ever stops being disposable. One clarification for that record: the cost of the whole-file discard is not the re-fetch, it is that the other tag's notifiedAtMs goes with it and its nudge can print twice. That cost is identical under a version bump, so it does not change the conclusion.

Verified before approving: head unchanged since the round-2 review plus the restored-rationale commit, CI (lint, typecheck, build, test, audit) green, no unresolved threads, MERGEABLE, not a draft. The 395f538 diff is what it claims to be: three lines of JSDoc on resolveTarget, nothing else re-expanded.

On the finding that mattered most. The changeset originally justified deleting the second-tag fallback with a failure the deleted code could not have produced, since the removed loop took a max and a lower alpha could never win. That was worth catching for a reason beyond the wording: a self-update path is exactly where a fallback should not be removed on a rationale nobody checked. The replacement rationale was verified against the code rather than accepted, and the sentence explaining why the alpha: '0.1.0-alpha.99' fixtures are shaped as they are is now back on resolveTarget where a future reader will find it.

Also noted: the concession on records nothing when the registry could not be reached at all is right, and volunteering it is the useful habit. It passes against the pre-fix code, so it is a forward guard on the contrast case rather than a pin of this change, and counting it would have overstated what the delta's tests actually hold down.

Not merging. Merging another author's PR is outside what this loop does, so the button is yours.

@vraspar
vraspar merged commit 9aa18ba into main Aug 17, 2026
1 check passed
@vraspar
vraspar deleted the vraspar/update-follows-latest branch August 17, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: critical Blocks other PRs; merge-priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants