Release Patch — back-port a fix to an existing release line

SkillDev tools

Lets your agent apply bug fixes to an older released version and publish a new patch release through pull requests.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Release Patch — back-port a fix to an existing release line skill

About this capability

Back-port fix(es) onto an existing release line and cut a patch release. Create the new release branch from the base with NO direct commits, land the fix(es) and the version+changelog bump via PRs into it, then publish via the `release` skill. Use for patching a shipped version (e.g. what an older S

What this skill tells your AI

The instructions your AI receives, as published by tetherto/qvac in packages/ocr-ggml/.agent/skills/release-patch/SKILL.md and read by ahel’s review.

Cut a patch release on an existing release line (a shipped x.y.z a consumer still depends on) by back-porting one or more already-merged fixes, without dragging in later main-line changes.

Use this when, e.g., an older SDK release pins @qvac/<pkg> ^0.36.3 and needs a fix that only landed on main/a newer minor. The output is a new patch (e.g. 0.36.4) published under the line's maintenance dist-tag — latest is left pointing at the newest version.

<package> is the directory name under packages/ (e.g. llm-llamacpp). <fix-PR-or-commit> is one or more merged fix PRs (or their squash-merge SHAs). [base-release-branch] is the release line to patch (e.g. release-llm-0.36.3); if omitted, discover it in Step 1.

Golden rule

Never commit or git push directly onto a release-* branch. The release-* ruleset requires all changes to arrive via a merged PR (a bare branch creation push is allowed; subsequent commits are rejected — GH013 … Changes must be made through a pull request). So: create the release branch as an exact copy of the base, push it once, then land every change (fix cherry-picks, version bump) via PRs whose base is that release branch.

Workflow

Step 1 — Identify the target release line + next version

  1. Determine the version the consumer needs — read its dependency spec, e.g. the SDK's packages/sdk/package.json: "@qvac/<pkg>": "^0.36.3" → the 0.36 line, and the patch must be >= 0.36.3 < 0.37.0.
  2. Find the actual release-line ref. ⚠ Naming is not uniform. The release skill's default is release-<package>-<version> / tag <package>-v<version>, but real patch lines have used a short prefix: branch release-llm-<version> + tag llamacpp-llm-v<version>. Do not assume — verify:
    git ls-remote origin 'release-*' | grep -i <pkg>        # candidate branches
    git ls-remote --tags origin | grep -iE '<pkg>|llm'      # candidate tags
    
    Pick the branch/tag that actually holds the target version (confirm with git show <ref>:packages/<pkg>/package.json | grep '"version"'). That commit is the base.
  3. Next patch version = base patch + 1 (must satisfy the consumer range and stay below the next minor). Record it as <newver> and the base branch as <base>.

Step 2 — Resolve the source commits (use the SQUASH-merge SHAs on main)

For each fix PR, take the squash-merge commit on main, not the PR's individual branch commits:

gh pr view <N> --repo tetherto/qvac --json number,title,mergeCommit --jq '{n:.number,title,sha:.mergeCommit.oid}'

Do not cherry-pick a version/changelog-only PR (e.g. a prior x.y.z bump) — that bump is redone for this line in Step 6.

Step 3 — Assess cherry-pick cleanliness BEFORE touching anything

For each source commit, diff its touched files between the base and the commit's parent:

git diff --numstat <base> <squash-sha>~1 -- <touched-paths>

Empty output ⇒ that file is identical at the base ⇒ the hunk applies cleanly. Non-empty ⇒ that file diverged ⇒ expect a conflict there (typically test files). If the core source files are identical, the back-port is safe; a few diverged test files are resolved by grafting in Step 5. If everything is heavily diverged, stop and reconsider (the fix may need manual porting, not a cherry-pick).

Step 4 — Create the release branch from the base — NO changes — and push it

git -C <repo> fetch origin <base>
git -C <repo> worktree add <wt> origin/<base>
git -C <wt> checkout -b release-<pkg>-<newver>     # exact copy of the base, no edits
git -C <wt> config user.email "<you>"; git -C <wt> config user.name "<you>"
git -C <wt> push origin release-<pkg>-<newver>     # branch-creation push (allowed)

Commit identity = the releaser; never add an AI signature / Co-Authored-By. Confirm HEAD == base commit. Do not bump the version here — leave it at the base value (publish-safe).

Step 5 — PR #1: the fix(es), into the new release branch

Work on a normal-named branch (not release-*, so it isn't push-protected):

git -C <wt> checkout -b <TICKET>/backport-<pkg>-<newver>
git -C <wt> cherry-pick <squash-sha>...

Resolve only the expected diverged-file conflicts by grafting the commit's added block into the base file (keep the base file's structure; do not reformat unrelated lines). All identical-at-base files apply automatically.

Validate completeness (that the cherry-pick captured the source, nothing missed):

  • Cleanly-applied files — resulting blob byte-identical to the source commit's post-image: git rev-parse <squash-sha>:<file> == git rev-parse HEAD:<file> for each.
  • Manually-resolved files — the added lines equal the commit's added lines (git show <squash-sha> -- <file> +-lines vs git diff <base> HEAD -- <file> +-lines).

Push the helper branch; open PR #1 with base = release-<pkg>-<newver> (four-emoji body; link the source PR(s)).

CI: add the matrix labels — prebuilds, run-cpp-addon-tests, run-desktop-addon-tests, run-mobile-addon-tests — to run the fix-validating jobs. Do not add verified: ci-router reads only those four names, so verified selects no stage and fails silently, costing a CI round. (The label still exists and its siblings' descriptions still read "requires verified" — that text is stale; the label gate was retired, and ci-trust-policy.test.mjs asserts it stays retired.) Auto-approve the release environment deployment when it appears. Always read failures from full logs via gh api repos/tetherto/qvac/actions/jobs/{id}/logsgh run view --job --log silently truncates (~1.1 MB of ~9 MB) and can fake a "hang". Triage flaky-vs-real; pre-existing environment failures (e.g. test-darwin-x64 macOS-x64-VM timeouts) are non-gating and not blockers — confirm the fix's own tests pass. Merge PR #1 into the release branch.

Step 6 — PR #2: version + CHANGELOG bump, into the release branch

On another helper branch off the (now fix-carrying) release branch:

  • packages/<pkg>/package.json: base version → <newver>.
  • packages/<pkg>/CHANGELOG.md: add ## [<newver>] - <YYYY-MM-DD> (bracketed heading — the release workflow requires this format) above the base entry; reuse the original fix's changelog wording, crediting the fix PR(s). package.json version MUST equal the new heading.
  • Keep the version at the base value until this PR so nothing publishes early.
  • Lint gate varies by line era — run npm run lint in packages/<pkg> (older release lines use standard, newer use prettier); match whatever that branch enforces.

Open PR #2 (base = release-<pkg>-<newver>).

Step 7 — Release (merging PR #2 auto-triggers publish; use the release skill to verify)

Merging the bump PR into the release-* branch is a trusted pushon-merge-<pkg>.yml runs and publishes automatically. No labels are needed on a merge/push: push is a trusted event in ci-router, which short-circuits label parsing and enables every stage on its own. Publication is gated by version>npm + release-merge-guard. Follow the release skill for the monitor/verify mechanics (its Steps 4–6), with these nuances:

  • release-skill latest-guard caveat. The release skill's Step 1 compares the local version to npm latest and stops if not higher. A back-port (e.g. 0.36.4 < latest 0.38.0) fails that check — so the publish path for a maintenance patch is the on-merge auto-trigger from the PR merge, and you verify against the line's maintenance dist-tag, not latest. Use the release skill for its monitor/verify steps; don't let the latest-guard block a legitimate back-port.
  • dist-tag. The npm-dist-tag-determination action publishes an older-than-latest patch under a maintenance tag (e.g. release-<major.minor>) and must NOT move latest. This is correct — consumer semver ranges (^0.36.3) resolve by version, so they still pick up the new patch.
  • create-tag job fails (HTTP 403/422). It uses secrets.GITHUB_TOKEN, which is not authorized to create the protected release tag (no repo tag-ruleset; a user/PAT can). So the git tag is NOT created automatically — create it manually with a user/PAT credential:
    git tag <pkg-tag-prefix>-v<newver> <release-branch-sha>   # e.g. llamacpp-llm-v0.36.4
    git push origin <pkg-tag-prefix>-v<newver>
    
    (Longer-term fix: change create-release-tag.yml to use secrets.PAT_TOKEN, like label-gate does.)

Step 8 — Verify

  • npm view @qvac/<pkg>@<newver> version → the new patch exists.
  • npm view @qvac/<pkg> dist-tagslatest unchanged; the new patch under its maintenance tag.
  • The release git tag exists.
  • The consumer's range now resolves to the new patch (npm view @qvac/<pkg>@'<range>' version).

Nuances & gotchas (all hit for real; do not relearn them)

  • PR-only changes to release-* — the ruleset blocks direct commits after branch creation. Land everything via PRs whose base is the release branch (helper branches are normal-named).
  • Cherry-pick the squash-merge SHA, not PR-branch commits.
  • gh run view --log truncates — use gh api …/actions/jobs/{id}/logs; sanity-check job duration vs timeout before believing a "hang". (See reference_gh_job_log_truncation.)
  • Merging into release-* auto-publishes (on-merge, trusted push) — the version bump is the point-of-no-return; keep version at base until then.
  • Back-port dist-tag goes to a maintenance tag, never clobbering latest.
  • create-tag 403/422 → create the tag manually (GITHUB_TOKEN can't; PAT/user can).
  • release-skill Step 1 latest-guard doesn't fit back-ports — verify against the maintenance tag.
  • No AI signatures / Co-Authored-By in commits or PRs.
  • Lint gate erastandard on older release lines, prettier on newer; run npm run lint.

Worked example — @qvac/llm-llamacpp 0.36.4 (QVAC-22472)

SDK 0.15 pinned ^0.36.3; the n_predict-inside-reasoning fix was only on main/0.37+.

  • Base: branch release-llm-0.36.3 = tag llamacpp-llm-v0.36.3 (84be415c, v0.36.3).
  • Source: PR #3318 squash 0d782b12c (the fix). PR #3327 (0.37.1 bump) NOT cherry-picked.
  • Cleanliness: all C++/unit files identical at base; only qwen3-5.test.js diverged → grafted the one new test block; blob-identity confirmed for the rest.
  • Branch release-llm-0.36.4 created from base (no changes) → PR #3337 (fix) → PR #3342 (0.36.4 bump).
  • Merge auto-published: run 29749326488; npm 0.36.4 under dist-tag release-0.36; latest stayed 0.38.0. create-tag failed 403/422 → tag llamacpp-llm-v0.36.4 created manually via SSH.

Error handling

  • Heavy cherry-pick conflicts across core source (not just tests) → the fix predates too much divergence; stop and port it manually or reconsider the base.
  • release-merge-guard fails on merge → version not bumped or CHANGELOG heading missing/mis-formatted.
  • Publish didn't run after merge → confirm the merge was a push to release-* and packages/<pkg>/** changed; check on-merge-<pkg>.yml runs.
  • Publish ran but latest moved to the patch → the dist-tag logic mis-fired; restore with npm dist-tag add @qvac/<pkg>@<real-latest> latest.

Signals

GitHub stars
612
Forks
112
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
release-patch
Source
github.com/tetherto/qvac