| name | release-fabric-consumers |
| description | Release the qvac-fabric consumers to npm after the bundled version-bump PR merges — one release branch + on-merge dispatch per consumer, stopping at the npm approval gate. Supports --exclude to hold packages back. Phase C after rollout-phase-b. |
| argument-hint | [base-branch] [--exclude <pkg>[,<pkg>...]] |
| disable-model-invocation | true |
Release qvac-fabric consumers to npm
Publish the qvac-fabric consumers to npm in one coordinated pass. This is the
final phase of a fabric rollout — the step after rollout-phase-b's bundled
version-bump PR (vcpkg version>= + package.json + CHANGELOG.md for the full roster)
has merged to main. It applies the single-package release workflow to every consumer
in the release set at once.
It reuses the same mechanics as /release (cut a release-* branch → dispatch
on-merge-<package>.yml → the on-merge workflow builds prebuilds, publishes to npm with
the latest tag, and creates a git tag), but for the whole release set, and it is aware
of the per-consumer quirks and the mandatory human approval gate.
The release set is the full consumer roster (see the table below) minus anything named
in --exclude. Every step below operates on the release set, not the roster.
Usage
/release-fabric-consumers [base-branch] [--exclude <pkg>[,<pkg>...]]
/release-fabric-consumers
/release-fabric-consumers main --exclude model-fit
/release-fabric-consumers main --exclude model-fit,vla-ggml
[base-branch] — optional; the branch the version bumps landed on. Defaults to main.
--exclude <pkg>[,<pkg>...] — optional; comma-separated packages/ directory names
(the first column of the consumer table) to hold back from this pass. Everything else in
the roster is released.
Validate --exclude before touching anything. A value that does not match a packages/
dir name in the table is a hard error, not a silent no-op — a typo like
--exclude modelfit would otherwise quietly release the very package the user meant to hold
back. Report the unmatched name and stop before creating any release branch. Excluding
every consumer is likewise an error, not an empty success.
Prerequisites
- The consumer bump is merged to
<base-branch> — either rollout-phase-b's own bundled PR
(e.g. qvac#3334) or, in --on-top-of-pr mode, the feature PR the bumps rode in on (e.g.
qvac#3725). The target versions live on origin/<base-branch>.
- For each consumer in the release set,
origin/<base-branch>'s package.json version is
higher than the current npm latest (i.e. npm is one bump behind). If not, the version
bump PR hasn't merged yet — stop.
- Each release-set consumer's
CHANGELOG.md has a ## [<version>] - <date> section (bracketed
heading — the release-merge-guard requires this exact format).
These are checked only for the release set — a package held back by --exclude is never inspected,
which is the point of excluding it.
Watch for a never-published consumer
The "npm is one bump behind" check can pass for the wrong reason. A package whose npm latest
reads 0.0.0 with no release tag has never actually been published — 0.0.0 is a name placeholder,
so any real version compares as "higher" and the precondition passes while the package is in a
first-publish state this skill otherwise assumes away.
npm view @qvac/<pkg> dist-tags.latest
git -C <repo> ls-remote --tags origin "<tag-prefix>-v*"
Both signals together (0.0.0 and no tag) mean a first release. 0.0.0 here is a sentinel,
not a version that drifts — it is npm's placeholder for a name reserved but never published, so it
stays literal. Which package is in that state does drift, so run the two commands above across the
roster rather than trusting a name written here; model-fit was the instance when this was written
and has since published. When a package is in that state, its first release needs two things settled
before dispatch:
- a dated
## [<version>] - <date> entry — a CHANGELOG.md holding only an ## [Unreleased]
block is what the release extractor will not accept as a version section;
- a decision on whether the version already on
origin/main publishes as-is, or is superseded by
the rollout bump. Do not bump past an unpublished version without asking — that silently burns a
version number.
Until both are settled, hold it back with --exclude <pkg> rather than dispatching it and
dealing with a failed release-merge-guard. That is the supported way to release the rest of the
roster on schedule while a first-publish package waits.
The consumers
This table is the full roster. The release set is this list minus --exclude.
Package dir (packages/) | npm name | on-merge workflow | git-tag created |
|---|
embed-llamacpp | @qvac/embed-llamacpp | on-merge-embed-llamacpp.yml | llamacpp-embed-v<ver> |
fabric | @qvac/fabric | on-merge-fabric.yml | fabric-v<ver> |
llm-llamacpp | @qvac/llm-llamacpp | on-merge-llm-llamacpp.yml | llamacpp-llm-v<ver> |
model-fit | @qvac/model-fit | on-merge-model-fit.yml | model-fit-v<ver> |
ocr-ggml | @qvac/ocr-ggml | on-merge-ocr-ggml.yml | ocr-ggml-v<ver> |
translation-nmtcpp | @qvac/translation-nmtcpp | on-merge-translation-nmtcpp.yml | v<ver> (bare) |
vla-ggml | @qvac/vla-ggml | on-merge-vla.yml ⚠️ | vla-v<ver> |
The workflow-name and git-tag columns are not uniform — see Restrictions & nuances.
classification-ggml is not in this list. It dropped the qvac-fabric vcpkg
dependency and now consumes the published npm package @qvac/fabric, so it is not part
of a fabric rollout — see Release fabric first below for the one follow-up it needs.
Working repo & the golden guardrail
- Run all
git/gh operations in the code working repo (e.g. ~/repo/qvac) — NOT a
stale/read-only worktree. Verify the repo is on the right remote (tetherto/qvac).
- Base every release branch on
origin/<base-branch>, never on local HEAD (local
main is often behind).
- GOLDEN RULE — a release branch modifies ZERO files. It is an exact copy of
origin/<base-branch>. Create it without a checkout, then assert an empty diff before
pushing. Never edit anything under .github/workflows/**. If the diff is non-empty,
STOP and report — do not push.
Workflow
Do the whole release set in parallel unless told otherwise (version-only bumps were already
build-validated on the fabric sync PR; a canary-first order is a fine alternative).
Release fabric first
@qvac/fabric is the shared runtime addon the others build against, and it is also a
caret dependency of @qvac/classification-ggml. Publish it ahead of the rest so
downstream installs resolve against the new build rather than the previous one.
Then check the caret. Read both values rather than assuming either — they move every
release, so any version written here would be wrong by the time you read it:
git -C <repo> grep -h "@qvac/fabric" origin/main -- packages/classification-ggml/package.json
git -C <repo> grep -h '"version"' origin/main -- packages/fabric/package.json
On a 0.x version a caret locks the minor: ^0.<m>.<p> absorbs later 0.<m>.x
automatically but will not cross to 0.<m+1>.0. So if this release moves
@qvac/fabric's minor past the one classification-ggml's caret pins,
classification-ggml needs a manual dependency bump — track it as a follow-up after the
release, not as part of the rollout PR. This has happened before and is expected to
recur; packages/classification-ggml/CHANGELOG.md records each crossing.
If fabric is in --exclude, this ordering step does not apply to that pass and the
classification-ggml caret follow-up does not arise — release the rest of the set in
parallel and say in the report that the ordering constraint was moot.
Step 1 — Sync and read target versions
git -C <repo> fetch origin <base-branch>
Resolve the release set first (roster minus --exclude, with --exclude validated per Usage),
then for each consumer in the release set read the version off origin/<base-branch>
(git -C <repo> show origin/<base-branch>:packages/<pkg>/package.json) and confirm
npm view @qvac/<pkg> version is lower. Also confirm no target release-* branch already
exists on the remote (git -C <repo> ls-remote --heads origin "release-<pkg>-<ver>").
State the resolved release set and the excluded packages before proceeding, so the user can
catch a wrong set before any branch is pushed.
Step 2 — Create + assert + push each release branch
git -C <repo> branch release-<pkg>-<ver> origin/<base-branch>
git -C <repo> diff --stat origin/<base-branch>..release-<pkg>-<ver>
git -C <repo> push origin release-<pkg>-<ver>
Step 3 — Dispatch the on-merge workflow (uses the workflow-name map)
gh workflow run "<on-merge-wf>" --repo tetherto/qvac --ref release-<pkg>-<ver>
The workflow file for vla-ggml is on-merge-vla.yml. Pushing a fresh release-* branch
usually does not auto-trigger (path filter sees no new commits), so the explicit
dispatch is the trigger — but check gh run list --branch release-<pkg>-<ver> and, if a
push-triggered run already exists, do NOT double-dispatch.
Step 4 — Monitor to the approval gate (do NOT approve)
Poll until each run pauses at publish-npm. Read the run URL straight from gh rather than
hand-assembling it from a run id:
gh run list --branch release-<pkg>-<ver> --repo tetherto/qvac --limit 1 --json databaseId,url,status,conclusion
gh api repos/tetherto/qvac/actions/runs/<run-id>/pending_deployments
Job order per run: label-gate (auto) → publish-logic + release-merge-guard (env
release, branch-policy only) → build (prebuilds, ~9 platforms) → publish-npm (env
npm, required reviewers) ← PAUSES HERE → create-tag.
When a run reaches the npm pending-deployment gate, STOP and hand off to the user.
Required output format. Emit the CI run links as one line per addon, in the order of the
consumer table, and nothing else on those lines:
<addon>: <CI run link>
<addon> is the packages/ directory name (fabric, embed-llamacpp, vla-ggml,
translation-nmtcpp, …) — not the npm name, not the workflow name.
<CI run link> is the run's url from the gh run list --json call above, i.e.
https://github.com/tetherto/qvac/actions/runs/<run-id>.
- Only the release set appears. Packages held back by
--exclude get no line.
- Every addon in the set gets a line, even if its run has not yet reached the gate — append a
short state marker (
— awaiting gate, — build running, — failed) so the list is never
partial and a missing package is always a real problem rather than a timing artifact.
Worked example (--exclude model-fit):
fabric: https://github.com/tetherto/qvac/actions/runs/31390432306
embed-llamacpp: https://github.com/tetherto/qvac/actions/runs/31390431726
llm-llamacpp: https://github.com/tetherto/qvac/actions/runs/31390432200
ocr-ggml: https://github.com/tetherto/qvac/actions/runs/31390430929
translation-nmtcpp: https://github.com/tetherto/qvac/actions/runs/31390430912
vla-ggml: https://github.com/tetherto/qvac/actions/runs/31390432221 — build running
After the list, note the gate detail once — gated job publish-npm, environment npm, and which
runs are already pending approval. That is context; the list is the hand-off artifact.
Never approve the deployment yourself (the automation token has can_approve=false anyway;
only a qvac-internal-release member can).
Step 5 — After the user approves, verify
Once publish-npm + create-tag complete for a consumer:
npm view @qvac/<pkg> dist-tags.latest
git -C <repo> ls-remote --tags origin "<git-tag>"
Public-access check (token-less): install each @qvac/<pkg>@<ver> into a clean dir with a
bare registry .npmrc and an isolated HOME (so the user's token is never read),
--ignore-scripts --no-package-lock; confirm the installed package.json version matches.
Put this loop in a script file and run bash <file> (see Bash discipline).
Step 6 — Report
Consolidated table: package, npm latest, git tag, public-install ✓, CI run link. Release
branches are never merged back to main; offer to delete the leftover local branches.
State explicitly which packages were excluded and why (e.g. model-fit — first publish, awaiting dated CHANGELOG entry and version decision). A pass that silently covers six of seven is
indistinguishable later from one that dropped a package by accident.
Restrictions & nuances (hard-won)
- Workflow-name map is not uniform. 6 consumers use
on-merge-<pkg>.yml, but
vla-ggml uses on-merge-vla.yml (short name, no -ggml). This is the release
(on-merge) workflow — distinct from the PR-validation workflow on-pr-vla.yml.
- git-tag naming varies per package — do NOT assume
<pkg>-v<ver>. Use the table:
llamacpp-embed-v<ver>, fabric-v<ver>, llamacpp-llm-v<ver>, model-fit-v<ver>,
ocr-ggml-v<ver>, bare v<ver> for translation-nmtcpp, and vla-v<ver>.
The tag comes from create-release-tag.yml, which builds <repo_name>-v<published_version>
from the repo_name each on-merge-*.yml passes it — read that input if a new consumer
appears rather than guessing from the directory name.
- label-gate auto-authorises release dispatches. The
label-gate/Authorise job treats
push and workflow_dispatch as trusted events (authorised=true) — so a
release-* branch dispatch needs no labels at all. PR events are the contrasting
case, and they do not want verified either: they need the granular stage labels
(prebuilds plus the four run-*), which are the only names ci-router reads.
verified selects nothing anywhere — the label gate was retired repo-wide.
- The npm approval gate is mandatory and human-only.
publish-npm runs in the npm
environment, protected by required_reviewers (team qvac-internal-release). Every run
pauses there. Detect via .../pending_deployments. Never approve — hand off to the
user. (Earlier jobs use env release, which has only a branch_policy allowing
release-*, so they never gate.)
- No GitHub Release objects are created. This pipeline produces npm publishes + git
tags only. returns "release not found" — that is EXPECTED, not a
failure. (The workflow is unrelated/defunct on another
branch.) Do not treat a missing GitHub Release as an error.
Error handling
release-merge-guard fails → version not bumped or CHANGELOG.md missing the
## [<version>] - <date> section. Fix on main (new PR) and re-dispatch.
- A prebuild platform fails →
gh run view <run-id> --repo tetherto/qvac --log-failed.
- npm publish fails → the version may already exist (someone released it) or
NPM_TOKEN
is invalid; check npm view @qvac/<pkg> versions.
- Never delete/skip tests or weaken CI to get a release through.
Reference — the PR #3334 run (worked example)
qvac#3334 (qvac-fabric 9840.0.0) released all 6 consumers of that time via this exact
flow. Runs: classification-ggml 29736362703, embed-llamacpp 29736364449,
llm-llamacpp 29736366470, ocr-ggml 29736373159, translation-nmtcpp 29736374927,
vla-ggml 29736376662. All paused at publish-npm; after a qvac-internal-release member
approved, all 6 published to npm latest and created their (differently-named) git tags.
This is a historical record, not the current roster — classification-ggml was still a
fabric consumer then, and fabric / model-fit had not been added. The flow it demonstrates
is unchanged; use the table above for who to release.