[QA Methodology] Release a hotfix of an already-merged-and-released fix into the stable bundles the operator names, as patch releases cut from support/<X.Y> branches. Use when asked to 'выпустить хотфикс для релиза' / release a hotfix, backport a fix to a stable bundle, or cut a patch on a support line. Not for finding bundles that miss a patch (/qa-bundle-check) or delivering a released hotfix onto environments (/qa-hotfix-check). Gated writes; never auto-merges.
Installs into .claude/skills of the current project.
Are you the author of Qa Hotfix?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/virtocommerce-qa-hotfix)
---
name: qa-hotfix
description: "[QA Methodology] Release a hotfix of an already-merged-and-released fix into the stable bundles the operator names, as patch releases cut from support/<X.Y> branches. Use when asked to 'выпустить хотфикс для релиза' / release a hotfix, backport a fix to a stable bundle, or cut a patch on a support line. Not for finding bundles that miss a patch (/qa-bundle-check) or delivering a released hotfix onto environments (/qa-hotfix-check). Gated writes; never auto-merges."
argument-hint: "VCST-XXXX [v12,v14] [--repo=<name>] [--pr=<ref>] [--dry-run]"
---
# Release a Hotfix into Frozen Bundles
A VirtoCommerce **stable bundle** (`vc-modules/bundles/vN`) pins every module, the Platform, and
the Theme to one frozen generation. When a fix lands on `dev` and ships in the current release, a
frozen bundle does **not** get it for free — the bundle intentionally trails `master`. To deliver
the fix to a frozen bundle you cut a **hotfix**: a new patch `X.Y.(Z+1)` on the bundle's existing
`X.Y` line, produced from a `support/<X.Y>` branch.
This skill productizes the manual flow Oleg described (choose branch → find commit → cherry-pick →
commit → "Release Hotfix"). Terminal entry: [`/qa-hotfix`](../../commands/qa-hotfix.md).
> Twin relationship: `/qa-hotfix` is to hotfix releases what [`/qa-bundle-check`](../qa-bundle-check/SKILL.md)
> is to hotfix *detection*. `bundle:check` tells you a bundle is **missing** a same-line patch;
> `qa-hotfix` **produces** that patch for a specific task. They share `config/module-repo-map.json`,
> the `GIT_TOKEN` auth model, and the same-line semantics.
## VirtoCommerce hotfix mechanics (verified 2026-06-23)
- **Branch convention:** `support/<major.minor>` (e.g. order `3.1000.3` → `support/3.1000`,
catalog `3.904.11` → `support/3.904`). Confirmed via release `target_commitish`.
- **The "Release hotfix" workflow** is a `workflow_dispatch` shipped in every repo; the filename
varies by repo kind (discover it by workflow **name** "Release hotfix", don't hardcode):
- module repo → `module-release-hotfix.yml`
- `vc-platform` → `platform-release-hotfix.yml`
- `vc-frontend` → `theme-release-hotfix.yml`
Run **on the support branch** with `incrementPatch=true` → publishes `X.Y.(Z+1)`,
`makeLatest=false`, then auto-commits the bumped version back.
- **`gh` is not required** — writes go through local `git` + the token-authenticated
GitHub REST API (`workflow_dispatch` = `POST /actions/workflows/{id}/dispatches`).
## Why a deterministic script (+ a thin agent layer)
Resolving "task → PR → commit", "is it merged & released", "which support branch & next patch" is
mechanical — parse JSON, query GitHub, compare integers. So:
- **`scripts/hotfix/hotfix-precheck.ts`** does all the read-only analysis and emits a per-bundle verdict.
- **`scripts/hotfix/hotfix-release.ts`** does the one deterministic write (dispatch the Release-hotfix
workflow) and verifies the published patch contains the fix.
- The **agent/orchestrator** only does what needs judgment: read the JIRA task, run the cherry-pick
(resolving conflicts), and gate every write behind explicit human confirmation.
## Step 1 — resolve the fix (linked PR is primary)
`GIT_TOKEN` is read from `.env.local` (5000 req/h; without it 60/h → likely rate-limited).
PR resolution order:
1. **PRIMARY — the PR linked to the task:** GitHub search `org:VirtoCommerce <TASK> type:pr`
(unscoped, so it also matches PRs that carry the key only in a comment). A single product-repo
hit (`vc-module-*`, `vc-platform`, `vc-frontend`) is used directly.
2. **FALLBACK — the JIRA description:** only when search finds nothing / can't disambiguate, parse
the PR link out of the issue **description**. The
script does this via JIRA REST when `JIRA_EMAIL` + `JIRA_API_TOKEN` are in `.env.local`; the
`/qa-hotfix` command does it via the Atlassian MCP when running interactively.
3. **MANUAL — last resort:** `--pr=<owner/repo#num | url>` or `--repo=<name>`.
## Step 2 — gate the fix: MERGED and SHIPPED (run FIRST, no bundles yet)
This is the original "check the PR is merged and the release is out" step. Run the precheck **without**
`--bundles` — it does the gate phase only, then stops to ask for bundles:
```bash
npm run hotfix:precheck -- VCST-5082 # gates-only: PR → merged → shipped
# overrides if the linked-PR search needs help:
npx tsx scripts/hotfix/hotfix-precheck.ts VCST-5082 [--repo=<name>] [--pr=<owner/repo#num|url>] [--json]
```
1. The PR is **MERGED** (else STOP — merge first; a hotfix cherry-picks the merged commit).
2. The fix has **SHIPPED** in a normal release (the merge commit is contained in a published
release tag) — else STOP, run the normal "Release" workflow on the base branch first
(ask for the release to be cut first).
Both pass → the script prints `✓ Gates passed` and prompts for bundles → go to Step 3.
## Step 3 — establish which bundles are the latest stable (ASK, never assume)
Only after the gates pass. **The set of stable bundles is not fixed — it changes as new generations
ship.** `v12`/`v14` are only an illustrative pair; do **not** hardcode them.
1. Ask the user: **"which release bundles are currently the latest stable?"** (the operator
names them — e.g. "v12 and v14"). Accept whatever they name (`v13,v15`, a single `v16`, …).
2. If unsure what exists, candidates live at `vc-modules/bundles/<vN>/package.json` on `master` —
but *which are the supported stable lines* is the user's call, not a guess. When in doubt, ask.
Every `vN` below is a placeholder for whatever the user named.
## Step 4 — per-bundle precheck (read-only)
```bash
npm run hotfix:precheck -- VCST-5082 --bundles=<the bundles the user named>
```
Per bundle: pinned version → line `X.Y` → does `support/X.Y` exist → highest patch on the line →
the patch a hotfix would produce (`highest + 1`) → whether the fix is already on the branch.
**Two layers of "can we hotfix this?":**
1. **By branch (physical possibility):** the `Possible?` column — a hotfix runs on `support/X.Y`.
If the branch is missing → `✗ no` → **create it first** (gated write step 0 below) from the
line's base tag, then proceed. The pinned tag must be real on that line (that's the base).
2. **By code (will it apply):** the `Code check` block — every file the fix MODIFIES/REMOVES must
still exist on the support branch (added files are excluded). All present → clean cherry-pick
likely; a missing file → the line diverged → cherry-pick will likely conflict. This is a
no-clone heuristic; the **definitive** conflict check is the actual `git cherry-pick` at the
write step (clone in `.fix-workspace/`, conflict → STOP, never force a risky resolution).
### The fix-shape gate — is this fix safe to hotfix? (developer's checklist)
Before ANY hotfix, the fix itself must clear the checklist a VC release engineer runs by hand. The
precheck prints it as the **`Fix-shape check`** block (computed once from the fix commit's own diff,
so it's repo/task-level, not per-bundle):
| # | Check | How it's verified | On FAIL |
|---|-------|-------------------|---------|
| 1 | **Fix in a single module** | the diff stays inside one `src/<Project>` tree | multi-project → **STOP + hand off** |
| 2 | **No breaking changes** | HEURISTIC flag: touches a contract-bearing file (`module.manifest`, `.csproj`, `Directory.Build.props`, `*Dto.cs`, `Models/`, `Contracts/`, `I*.cs`, `*Client.cs`) | flagged → a **developer MUST confirm** it isn't breaking before proceeding |
| 3 | **Doesn't bump other modules' dependency versions** | scans the diff for a changed `VirtoCommerce.*` version in a manifest/`.csproj`/props | a raised pin → **STOP + hand off** (a hotfix must not drag dependency versions) |
| 4 | **cherry-pick applies clean** | the actual `git cherry-pick` at the write step | conflict beyond trivial → **STOP + hand off** |
| 5 | **`vc-build compress` passes** | the **"Release hotfix" workflow** builds + tests the artifact; `hotfix:release --poll` exits `1` if the run is red | red run → **STOP**: report the run URL + failing step; a re-run needs a new confirmation |
| 6 | **Regression environment** | after the release deploys, run regression on the support line | RED → hand off |
Checks 1–3 are mechanized in the precheck (no clone); 4–6 are enforced downstream (write step /
release workflow / regression). **Golden rule (Oleg): if anything looks like it could break —
STOP, analyze, and involve a developer.** A `⚠` on check 1 or 3 makes the precheck exit `1` and
**suppresses the "Next steps" write plan** — it is not a hotfix candidate until a human clears it.
A `⚠` on check 2 is advisory (heuristic) but still requires a developer's explicit "not breaking"
before you proceed.
### Verdicts & exit codes
| Verdict | Meaning |
|---|---|
| `✓ READY → … → X.Y.(Z+1)` | support branch exists, fix not yet on it — proceed to the write steps |
| `◯ already-applied` | the fix is already on `support/X.Y` — nothing to cherry-pick. Established in three layers, and the badge names which one fired (`sha` / `trailer` / `content` match): the original commit is an ancestor of the branch · a `cherry-pick -x` trailer on it names the original SHA · **or the fix’s own diff is already present in the branch’s files**. Content is the layer that fires after a plain `git cherry-pick` — which rewrites the SHA and leaves no trailer — so without it a just-published hotfix keeps reading `READY` and invites a duplicate release of a shipped fix (measured on VCST-5940, 2026-09-10). Content answers *is this change present*, not *did we pick it*: true because the line never had the bug is the same operational answer. A binary or too-large diff leaves it inconclusive, which reads as `READY` — never as applied |
| `✗ no support/X.Y` | the branch doesn't exist yet — **create it first** (gated write step 0), branching from the line's base tag, then proceed with the hotfix |
| `— not in bundle` | the repo isn't pinned in that bundle — nothing to hotfix there |
- Exit `0` = every requested bundle is `ready`/`already-applied` · `1` = a gate is blocked (incl. a
fix-shape signal — multi-module or dependency bump) · `2` = tool error.
- `--json` emits `{ task, repo, fixSha, prGate, releaseGate, fixShape, bundles[] }` for the orchestrator
(`fixShape` = `{ modules, multiModule, dependencyBumps[], contractFiles[] }`).
## The write steps (gated — confirm before EACH)
For `READY` bundles, and for `✗ no support/X.Y` bundles after their branch is created (step 0).
Work in `.fix-workspace/` (gitignored). **Triple-guarded no-auto-merge culture applies**
(`.claude/knowledge/execution/quality-gates.md`): every write needs explicit human confirmation — sequentially,
one confirmation per write; in parallel, a single **batch** confirmation covering all lanes before
any push (see [`parallel-lanes.md`](parallel-lanes.md) §Parallel vs sequential). Either way, nothing is pushed unconfirmed.
**Step 0 — create the support branch when it's missing (`✗ no support/X.Y`).** A hotfix needs a
`support/X.Y` line; if it doesn't exist yet, create it (this used to be a hand-off, now it's a
gated write). Branch from the line's **base tag** = the highest released `X.Y.*` tag, or — if the
bundle's pinned `X.Y.Z` is all that exists on the line — the pinned tag itself. Never branch from
`dev`/`master` (that would pull in unreleased work).
```bash
# base = highest existing X.Y.* release tag (fallback: the bundle-pinned X.Y.Z)
git fetch origin --tags
git branch support/X.Y <base-tag> # e.g. git branch support/3.1011 3.1011.0
git push origin support/X.Y # ← confirm before this push
```
Confirm the base tag with the user before pushing (it decides what the new support line contains).
After the branch exists, re-run the precheck for that bundle → it should now read `✓ READY`, then
continue with steps 1–4.
For each ready bundle (line `X.Y`) — one **lane**; with ≥2 `READY` bundles run the lanes in
parallel (own worktree each) per [`parallel-lanes.md`](parallel-lanes.md) §Parallel vs sequential, after a single batch confirmation:
1. `git fetch && git checkout support/X.Y`
2. `git cherry-pick <fixSha>`
- **Conflict:** resolve only if trivially mechanical; anything risky → **STOP + hand off**
(the fix touched code that diverged on the support line — a human must decide).
3. Show the diff → **confirm** → `git push origin support/X.Y`
4. **confirm** → trigger + verify the release:
```bash
npm run hotfix:release -- --repo=<name> --branch=support/X.Y --expect-commit=<fixSha> --poll
```
`scripts/hotfix/hotfix-release.ts` discovers the "Release hotfix" workflow, dispatches it on the support
branch (`incrementPatch=true`), polls the run, and verifies the published `X.Y.(Z+1)` release
contains `<fixSha>`. Exit `0` = released + verified · `1` = run failed or commit not in release.
**Parallel lanes.** With ≥2 `READY` bundles, read [`parallel-lanes.md`](parallel-lanes.md) §Parallel vs
sequential before the batch confirmation — what runs concurrently, what stays serial, lane isolation.
### After the hotfix
- Re-run the precheck → the bundle should now read `◯ already-applied`.
- **Confirm before the tracker writes.** The outcome comment, the "Need hotfixes" flag and the status
transition below are outward writes to a shared board: show the comment text, the flag and the
target status, and get one explicit yes before writing any of them. On a no, write nothing and say
so in the close-out.
- Report per bundle: new patch version + release URL. Comment the outcome on the JIRA task (English; Markdown, never Jira wiki markup; clear/brief/outcome-first per `knowledge/execution/tracker-ops.md` §5a **Comment & body style**).
- **Advance the JIRA status to `Hotfix ready`** (right after the outcome comment) — **only for
issue type `Bug`.** The `Wait hotfixes` / `Hotfix ready` statuses live only in the Bug workflow;
a hotfix can also target a **Story** (or any other type), and those have no such statuses — for
them **leave the status untouched** (still post the outcome comment, still skip the flag). Check
the issue's `issuetype.name` first; if it isn't `Bug`, do nothing here. For a Bug, the path is
**`Tested → Wait hotfixes → Hotfix ready`**, but the middle hop is driven by a field, not a
transition:
1. **Set the "Need hotfixes" flag** (find the field by its name `Need hotfixes` in the issue's edit
metadata and set its `true` option; on VCST that is `customfield_10181` = option `{id: "10151"}`,
an example only — never hardcode it). A JIRA post-function then **auto-moves `Tested → Wait hotfixes`** (the
"Wait hotfixes" status only exists while this flag is set — that's why there is no manual
`Tested → Wait hotfixes` transition).
2. **Take the live-discovered transition whose target status is `Hotfix ready`** (VCST: the
"Hotfix released" transition, id `15`). Discover it with `getTransitionsForJiraIssue` — never
hardcode the id; match by `to.name === "Hotfix ready"`.
- **Never move a ticket backwards.** If the ticket is already at or past `Hotfix ready` (e.g.
`Testing on stable`, `Done`), no `Hotfix ready` transition is offered → leave the status as-is.
Setting the flag on such a ticket is harmless (it still needed hotfixes) but do not force a
backward transition.
- Tracker-agnostic: this is the VCST (Jira) workflow; discover the field + transitions live and
skip the step on a tracker/project that has no `Hotfix ready` status.
- **Theme/frontend caveat:** a vc-frontend hotfix release asset is named `vc-frontend-X.Y.Z.zip`
(not `vc-theme-b2b-vue-*`). If a bundle's
`ThemeB2BVue` pin must then be bumped, take the URL from the release's real `assets[]`, never by
string-replacing the version. (Bumping the bundle pin itself is `/qa-bundle-check` territory.)
## Final step — offer self-diagnostics (consent-gated)
At the very end of every run — released, STOPPED or BAILed — follow
[`self-check-offer.md`](self-check-offer.md): one Yes/No, run `/vc-self-check` only on an explicit Yes,
never auto-trigger.
## Hard rules (STOP/BAIL is a success, not a failure)
- **Never auto-merge.** The pipeline ends at a published hotfix release; merging the *bundle bump*
PR (if any) is a separate human action. `merge_pull_request` / `gh pr merge` are denied.
- **Create a missing `support/X.Y` branch (gated), don't hand off.** When the line has no support
branch, create it from the line's **base tag** (highest released `X.Y.*`, else the bundle-pinned
`X.Y.Z`) with explicit human confirmation of that base — never silently, and never off
`dev`/`master`. (Write step 0.)
- **One repo / one module per task.** Multi-repo or multi-module fixes → STOP + hand off (fix-shape
check 1).
- **A hotfix never bumps a dependency version.** A raised `VirtoCommerce.*` pin (fix-shape check 3)
→ STOP + hand off — bumping the bundle's pins is `/qa-bundle-check` territory, not a hotfix.
- **STOP on:** PR not merged, fix not released, a fix-shape signal (multi-module / dependency bump /
unconfirmed breaking-change surface), cherry-pick conflict beyond trivial, failed release run, or
the published patch not containing the fix. Leave the JIRA task where it was with a one-line reason.
- **If anything looks like it could break — think first.** Analyze it and involve a developer before
hotfixing; do not force a risky change onto a frozen support line.
## Reporting
This is tooling output, not one of the tracked report categories (`.claude/rules/reports.md` §1)
— print the precheck table and the released versions to the user / JIRA comment; do **not** create a
file under `reports/`. A hotfix that changes a release decision flows into the normal release report.
## Maintenance
- `config/module-repo-map.json` (shared with `bundle:check`) maps bundle module Id → repo and
self-heals for new modules.
- Tag scheme assumed: bare `Major.Minor.Patch`; branch scheme `support/Major.Minor`. If a repo
diverges (e.g. `v`-prefixed tags, a different support-branch name), adjust `tagOf()` / the
`support/${line}` construction in `scripts/hotfix/hotfix-precheck.ts`.
- The "Release hotfix" workflow is discovered by name, so a renamed file keeps working as long as
the workflow's `name:` stays "Release hotfix".