Skip to content
Back to skills

Manage Versions

ASecurity

Keep cross-repo changes version-compatible through development, releases, and consumer pins. Load when repos must remain compatible to merge: unreleased dependencies, proto/REST contract changes, or breaking published symbols.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
developmentgobashsqldockergitapisecurity

Works with

  • cli
  • api

Security analysis

A92/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned November 7, 2026

npx -y skills add a-novel-kit/stack --skill manage-versions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Manage Versions?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Manage Versions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/a-novel-kit-manage-versions/badge)](https://www.skillsdirectory.com/skills/a-novel-kit-manage-versions)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: manage-versions
description: >
  Keep cross-repo changes version-compatible through development, releases, and consumer pins.
  Load when repos must remain compatible to merge: unreleased dependencies, proto/REST contract
  changes, or breaking published symbols.
---

# Cross-repo version management

Every repo in `a-novel` and `a-novel-kit` is independently versioned, and the repos depend on each
other (services pin `golib`, `jwt`, each other's `pkg/go`; deployments pin each other's Docker
images; clients pin published packages). When a change touches more than one repo, the versions
must line up before anything merges to production. This skill governs that.

Load it with whichever repo-kind skill applies (`write-go-kit` for a `kit/` change,
`write-go-service` for a service change), with `git-conventions` (the breaking-change commit
footer) and `implement-feature` (the layered-branch workflow these cross-repo plans extend).

---

## How things are versioned

- **Each repo is git-tag semver.** A release is a `vX.Y.Z` git tag pushed on a `master` commit; the
  next version comes from the commit history since the last tag (`fix:`/`chore:` → patch, `feat:` or
  an absorbable breaking change → minor). A `!` does **not** force a major here: a major is a planned
  `vX`-line initiative, never derived from a commit range — `prepare-release` owns that policy and the
  sizing. The tag
  push triggers the `release` workflow (`a-novel-kit/workflows/publish-actions/auto-release`),
  which cuts the GitHub Release and, for service repos, builds and publishes the Docker images /
  npm packages tagged with that version.
- **`go.mod` pins exact released tags.** `github.com/a-novel-kit/golib v0.20.31`, not a range, not
  a branch. **No `replace` directives** anywhere in this codebase — the dependency graph stays
  reproducible from `go.mod` alone.
- **Renovate keeps consumers current.** After a dependency releases, Renovate opens a PR in each
  consumer bumping the pin and running the configured post-upgrade tasks (`go mod tidy`, regenerate
  proto, …). A released version reaches a consumer's `master` only through Renovate, a feature
  branch waiting on it included, as
  [bring a release through Renovate](#bring-a-release-through-renovate) shows. Only a draft's
  pseudo-version pin, below, is ever written by hand. Production is the exception: a service or
  platform release pins its own images in infra, through infra's `pin-release.yaml`.
- **Docker-image pins** in CI / compose files follow the same rule: pin a concrete `vX.Y.Z` (or
  `${{ github.ref_name }}` for the repo's own image), never `latest` or a branch.

---

## Cutting a release

A release is cut **in CI**, not from a developer's working tree. An admin triggers the repo's
**release workflow** and picks a **release type** (patch / minor / major), or the release train does
(`coordinate-landing`). `release.yaml` then runs in three parts (a-novel/.github#414):

- **`plan`** runs the `release-core` action as a dry run, with no environment and no credential: it
  computes the version and checks the bump, the doc stamp and the tags. A rehearsal runs only this.
- **`cut`** runs `release-core` in the protected `release` environment as the release App, whose
  key only that environment holds. It bumps version files (workspace-aware), refreshes doc refs
  (`prepublish:doc` / `a-novel publish stamp`), commits, tags `vX.Y.Z`, pushes the commit and tags
  together, and creates the GitHub Release.
- **The tag publishes.** The root tag runs `release.yaml` again, and that run builds and attests the
  images, attests and publishes the npm tarballs, and dispatches infra's `pin-release.yaml`.

A repository that has not adopted the split yet releases in one job as the [Agent] bot; the
workflows repo's `docs/migrations/v1.57.0.md` is the adoption guide.

There is **no local release command**, and no per-repo `publish.sh` or `publish:*` pnpm script —
do not write or revive any of them. The bump is workspace-aware: `release-core` detects whether the
repo is a pnpm workspace (a `pnpm-workspace.yaml` is present) and runs the matching `pnpm version`
form — `--recursive` for workspaces, `--no-git-tag-version` for single-package repos. The only verb
under `a-novel publish` is `stamp`, the doc-version helper `prepublish:doc` calls (see
`use-a-novel-cli`).

**Who can cut a release is enforced server-side**, by rulesets and the environment that
a-novel/infra's GitHub root manages:

1. **The `master` ruleset** restricts who can push the version-bump commit.
2. **The `tags` ruleset** restricts who can create a tag. A tag is the release decision: it starts
   the publishing run, so only admins and the release bot may create one.
3. **The `release` environment** holds the release App's key and admits only the default branch,
   `release/v*` lines and `v*` tags. Every cut waits there for `core` or the repo's `owners-<class>`
   team; the person who dispatched may approve their own run. The first backport onto a new line
   waits there too, since only the release App may create the line.

These guard the release, never the registries. GitHub Packages lets every workflow run of a
repository write that repository's packages, whatever its ref, so a branch can publish an npm version
or push and move an image tag. A version proves its origin only through its release proof: an
attestation over its bytes, signed by the producer's `release.yaml` at the version's tag, and an
immutable Release of that tag from the release App. A branch run's attestation names its branch, and
only a release makes the tag and the Release. The auto-approver merges a first-party version only
with that proof (`write-github-actions`), and infra deploys an image only with it (`write-infra`).

A `v*` tag is only ever created by a CI dispatch — the release or release-train job, running as the
release App. Nobody pushes one by hand, maintainers included. Repo admins can bypass every rule
here, but that power exists to repair a broken state, never to cut a release. GitHub starts no
workflow when more than three tags arrive in one push, and the cut pushes the root tag with one tag
per Go sub-module, so a repository keeps fewer than three sub-modules.

**Releases are human-triggered but bot-run.** A human starts the release workflow; the agent never
decides to publish (or merge, or push to master) on its own, and a non-interactive token must lack
those rights server-side.

---

## Hotfixing a released line

A bug sometimes must be fixed on an already-released line **without** shipping whatever has since
landed on the default branch. That is a **hotfix**. The fix lands on the default branch first,
through its normal PR. A **backport** then carries it onto the released line, and merging the
backport cuts the patch. The default branch is never rewritten and never receives a backport. A
hotfix is a single-repo path; a bug spanning several repos is a fast **Epic** (see
`coordinate-landing`).

**Nothing rolls a release back.** Infra refuses a pin that lowers a version, so a bad release is
fixed forward, and the hotfix is the fast path:

- **When the default branch holds nothing unreleased** since the deployed release, skip the
  hotfix: a normal patch release ships the fix with one review less.
- **A hotfix deploys only on the deployed line or a newer one.** A patch to an older line than the
  one infra pins reads as a lowered version, and infra refuses it.
- **A pending newer release swallows a hotfix's pin.** `pin-release.yaml` keeps one pin pull request
  per repository at the newest release it saw. While a newer release's pin is still open, the hotfix
  folds into it and never deploys alone: close that pull request first.

**Vocabulary** (single-repo hotfix — freeze it here):

- **baseline** — the latest live tag on the target `X.Y` line (e.g. `v1.4.2` for line `1.4`).
- **release line** — `release/vX.Y`, created from the baseline the first time the line needs a fix.
  It holds released code plus backports only, and its head is always its latest patch. The
  `release-lines` ruleset protects it like the default branch: a PR, one approval, squash, and
  master's required checks. Only admins and the release bot write to it directly.
- **backport** — a PR into a release line carrying a fix in the form that line's code needs, titled
  `<fix subject> [vX.Y]`. Its checks run against the released code, so a fix that leans on unreleased
  code fails before it ships.
- **cut** — merging a backport runs `release-core-hotfix` behind the protected `release` environment,
  as the release App. It bumps the patch, then pushes the bump commit to the line together with the
  `vX.Y.(Z+1)` tag. It creates the Release with `--latest=false`, so a patch to an old line never
  becomes "Latest". The tag runs the line's own publishing jobs, so the patch reaches production,
  except on a line cut before its repository adopted the release split.
- **retract** — for a Go module, a `retract vX.Y.Z` directive (added in a later release) marks a bad
  version so `go get` skips it. The tag stays (immutable); `retract` is the downstream "don't use this".

**Ship a hotfix.**

1. Land the fix on the default branch through its normal PR.
2. Dispatch the repo's **`hotfix.yaml`** from the UI. Only admins may run it, and `dry_run` defaults
   on. Set the `X.Y` **line** and **`fix_refs`**: the fix's default-branch commit SHAs, oldest first.
   Every SHA must be on the default branch, or the run fails before creating anything. Each fix
   becomes its own backport PR. An empty `fix_refs` only prepares the line, for a fix the default
   branch does not need: branch from `release/vX.Y` and open that backport by hand.
3. A fix that does not apply cleanly opens nothing; a typical cause is a rename on the default branch
   that the line lacks. The escalation ticket carries the commands to write the line's form of the fix
   by hand: cherry-pick, resolve the conflicts against the released code, and open the PR into
   `release/vX.Y`.
4. A clean backport, whose patch-id equals its fix's on the default branch, is opened by the
   Dependencies App, approved by the agent and merged once its checks pass: it carries the fix's
   review. A backport that git fitted to different context opens as the agent: review and merge
   it. The merge cuts the patch once a `release` environment reviewer approves. Backports merged
   together cut one patch.

A backport never receives a required check that was added after its line's tag, so an admin merges
past that check. Nothing reminds anyone of a backport left open, so merge it once it is approved. A
hotfix is deliberately **exempt from `AGENT_KILL_SWITCH`**, so the emergency patch path stays open even when
normal automation is halted. The admin check and the `release` environment gate it instead.

**Security hotfix (embargoed).** When the fix can't be public before release:

1. Work it under a **draft GHSA advisory** in a **temporary private fork**. A private fork inherits
   none of the org secrets or runners, so there is **no CI there** and nothing leaks.
2. Coordinate the disclosure.
3. Land the fix and its backport back to back, cut the patch, and publish the advisory at the same
   time.
4. `retract` the vulnerable version(s) in the follow-up.

The default branch carries the fix before the patch exists, so no later release can re-ship the
hole. Outward policy (contact, disclosure window) lives in `SECURITY.md`; the mechanics live here.

---

## Working a cross-repo change on a branch

You're changing repo **A** (the dependency — e.g. `golib`) and repo **B** (the consumer — e.g. a
service) together, and B's branch needs A's _unreleased_ code. Don't add a `replace`; pin B to A's
commit SHA, which Go records as a **pseudo-version**:

```bash
# In B, on B's branch, after pushing A's branch:
go get github.com/a-novel-kit/golib@<commit-sha-on-A's-branch>
go mod tidy
# go.mod now reads e.g.  github.com/a-novel-kit/golib v0.20.32-0.20260512140000-abcdef123456
```

- Pin to an **explicit commit SHA**, not `@<branch-name>` — a branch ref resolves to whatever the
  tip is _at the moment you run `go get`_ and can silently drift; an SHA is stable. (`go get @main`
  also produces a pseudo-version, just a less reproducible one.)
- Each time you push more commits to A's branch, re-run `go get …@<new-sha>` + `go mod tidy` in B
  so B builds against the latest A.
- B's CI goes **red on a pseudo-version pin** where the org enforces "released versions only", as it
  should: that is the signal B is not yet mergeable. See the ordering rule.
- **Only a draft pins a pseudo-version.** A ready PR never pins an internal library to a commit,
  since a reviewer may approve and merge it. Before marking B ready, ask the developer to cut A's
  release, bring it to B's `master` through Renovate, and drop the pseudo-version commit as B
  rebases. Do not mark B ready on the pseudo-version and ask afterwards.

---

## The merge order — dependency first, always

A pseudo-version points at a commit on a branch. Whether that commit survives on `master` depends on
the merge strategy — a merge commit keeps it, a squash- or rebase-merge does not, and once the
branch is deleted GitHub serves the orphaned commit only for a while. Either way, a PR that _merges_
with a pseudo-version pin bakes a non-release into production: unreproducible from tags, invisible
to Renovate, and a landmine once the source commit is gone. **A pseudo-version pin is safe only
during in-flight development, never at merge time.** So:

**Hard rule: the dependency PR merges and releases before the consumer PR merges, and the release
reaches the consumer's `master` through Renovate first.**

1. **Merge A's PR** (the dependency change) to A's `master`. The release workflow tags `vX.Y.Z`
   and publishes it.
2. **Bring the release to B's `master`** through Renovate, as the next section describes.
3. **Rebase B's branch on `master`** and drop its pseudo-version commit. B's diff now holds only the
   feature, and its CI goes green on the released version.
4. **Merge B's PR.** Now production is fully on released versions.

### Bring a release through Renovate

A feature branch never pins a release itself. Renovate merges bumps on every consumer all day, so a
pin commit in a feature branch conflicts with each one that touches the same lines. It also asks the
feature's reviewer to acknowledge a dependency change, with `allow-dependency-change`, and mixes a
version bump into a feature diff.

1. **Dispatch Renovate** in each consumer once the release exists:
   `gh workflow run renovate.yaml --repo <org>/<consumer>`. Wait for its bump PR.
2. **Let a green bump PR land.** Its auto-merge queues it.
3. **Fix a red bump PR on its own branch.** When the release needs a code migration, such as a
   visual change a consumer's tests pin, push only that migration onto Renovate's branch, right
   after its last rebase. The push withholds the auto-approval, so the PR needs a maintainer's
   approval. Keep everything else for the feature branch.
4. **Rebase the feature branch on `master`** once every bump has merged. It carries the leftovers:
   the feature, without a pin or a lockfile change. A rebase conflict with the migration means the
   feature repeated it; keep `master`'s side.

It takes longer than a pin in the feature branch, but nothing conflicts, the feature needs no
dependency label, and each review reads one kind of change.

For a pnpm consumer, a fresh first-party release installs at once: every workspace exempts the
`@a-novel` and `@a-novel-kit` scopes from pnpm's `minimumReleaseAge`, since the release proof stands
in for the wait. A fresh third-party release sits inside that age (one day by default). Exempt it by
exact version in `pnpm-workspace.yaml` `minimumReleaseAgeExclude`, one entry per pinned package. Bump
in two steps, because pnpm also rejects the outgoing pin when it is still too fresh:

1. Add the new versions next to the old ones, then run `pnpm install`.
2. Remove the old entries, then pass `pnpm install --frozen-lockfile`.

Never hand-merge `pnpm-lock.yaml` during a rebase. At each conflicting commit, take the upstream
lockfile (`git checkout --ours`), then run `pnpm install`. Exempt only the versions pnpm names, and
only while installing. Restore the commit's workspace file, and pass `--frozen-lockfile` before
continuing. The policy relaxation never reaches a commit.

If B's change is itself a dependency of a repo C, repeat: A → release → Renovate bumps B → B
releases → Renovate bumps C → C merges. Stacked dependency chains merge bottom-up.

**Write the order down** so it can't get lost — in B's PR description, and/or as `TaskCreate`
entries:

```
1. golib#NNN  (adds X)                  → merge → release v0.20.32
2. service-foo#MMM  (uses X)            → Renovate bumps golib to v0.20.32, rebase, then merge
3. (if needed) golib#PPP  (removes old X) → after #2 ships → release v0.21.0
```

> Edge case — A is `golib`, which has no separate "release PR" and releases on tag: dispatch A's
> release workflow once its PR merges. Don't merge B until that tag exists and B is pinned to it. If
> you cannot dispatch it, hand off: "golib#NNN is merged; needs a release tag before
> service-foo#MMM can land: dispatch Renovate there once `vX.Y.Z` is out."

---

## Breaking changes are staged — never broken in one shot

A breaking change to anything other repos depend on — a `golib` / graduated-package public symbol,
a service's `pkg/go` API, a proto message, a REST endpoint, a DB schema another reader relies on —
is **never** a single PR that flips old → new. Stage it across releases, so there is always a window
where both old and new work.

### Step 0 — Grep the whole workspace for consumers _first_

Breaking changes are fine when genuinely needed — a better API, a security fix that required a
signature change, an obsolete helper a newer dependency now covers. The discipline this skill
enforces is **knowing the blast radius before you start**, so the consumer-migration step isn't a
scramble.

**Before** writing the additive PR, enumerate every consumer of the symbol you're about to change.
That grep:

- Tells you the **blast radius** — one consumer is a different change than ten, and the survey costs
  the same either way.
- Seeds the **consumer-migration list** in step 2 below, tracked explicitly so no consumer is
  forgotten and fails silently at step 3 (removal).
- Surfaces the case where the "shared" symbol has **zero in-org consumers**, and lets you decide what
  that means for _this particular symbol_. For an internal / not-yet-public helper (a fresh `golib`
  sub-package no service imports yet, a service's private `pkg/go` symbol), zero consumers usually
  means the additive-then-deprecate dance is over-engineering and the symbol can be removed directly.
  For a **graduated / community-facing package** (`a-novel-kit/jwt` and any future graduate — see
  `write-go-kit`'s graduation rules), the workspace grep does **not** see external pkg.go.dev
  consumers, so staged removal still applies at zero in-org hits: the discipline is the public API
  contract, not the consumer count. Similarly, a single in-org consumer of a `golib` helper may
  signal it belongs inlined into that consumer (`write-go-kit`'s "should this go in golib?" bar) —
  a `golib`-specific rule, since graduated packages stay staged regardless.

Grep commands from the workspace root (the directory holding `app/` and `kit/`), by symbol kind:

```bash
# A Go function / type / constant — name only, captures all import paths.
grep -rn "SymbolName" app/ kit/ --include='*.go'

# A proto message or RPC.
grep -rn "MessageName" app/ kit/ --include='*.proto' --include='*.go'

# A REST endpoint path or query parameter.
grep -rn 'apiPath' app/ kit/

# A renamed type — survey BOTH old and new names so consumer files using either show up.
grep -rn "HttpConfig\|HTTPConfig" app/ kit/ --include='*.go'

# A field on an exported struct (don't forget composite-literal usage).
grep -rn "\.FieldName\|FieldName:" app/ kit/ --include='*.go'
```

Search both `app/` (services) and `kit/` (other shared libraries) — a graduated-package change rips
through `jwt` consumers as easily as service consumers. Capture the file list as a checklist in the
dependency PR's description, under a heading like "Consumer migration follow-ups", so it travels
with the PR through review.

Skipping this step has a specific failure mode: the dependency PR merges and releases, and weeks
later someone finds a consumer nobody had on the radar — usually when it fails to compile after the
removal step. Catch it now, not then.

### The staged path itself

1. **Add the new path, non-breaking.** New function / field / endpoint / column alongside the old,
   which keeps working unchanged. Release this (a `feat:`, _not_ a breaking change — nothing broke
   yet). Mark the old path `// Deprecated: use NewThing` so consumers and pkg.go.dev see it.
2. **Migrate every consumer to the new path.** One PR per consumer (drawn from the step-0 list),
   each rebased on the Renovate bump that brings the release from step 1 (per the merge-order
   rule), each releasing once green. Tick consumers off the list as their PRs land; an empty list means step 3 is safe.
3. **Remove the old path.** Once _every_ consumer is off it, a follow-up PR in the dependency deletes
   the old symbol/field/endpoint/column. _This_ is the breaking change, now safe because nothing uses
   the old path. Release it (a major bump for a graduated package / `pkg/go`; for a service's
   internal-only contract, the version policy still applies).

The major-version bump in step 3 is **signalling** — it tells consumers "the old path is gone". It
does not license skipping steps 1–2: a major bump that strands consumers is still a broken release.

This shape recurs everywhere; the layer skills carry the specifics:

- **Go public APIs** (`golib`, graduated packages, `pkg/go`): add → `// Deprecated:` → migrate →
  remove. Never change a function signature in place; add a new function.
- **Proto** (`write-proto`): never reuse a field number, never change a field's type, never remove
  a field that's still consumed — add new fields/messages/RPCs; mark old ones `// Deprecated`;
  remove only after every gRPC consumer is updated. `buf breaking` enforces a lot of this.
- **REST API** (`write-openapi`): add new endpoints/parameters/fields; keep old ones working;
  document the deprecation; remove only after clients have migrated. Optional request params and
  additive response fields are non-breaking; removing or repurposing either is breaking.
- **DB migrations** (`write-sql`): additive first (new nullable column, new table, new index) — and
  never modify a `.up.sql` that has merged to `master`; a "change" is a new migration. A
  destructive migration (drop column, narrow a type) ships only after the code that read the old
  shape is gone.

Use the `BREAKING CHANGE:` commit footer (`git-conventions`) only on step 3 — the actual removal —
not on step 1.

---

## Quick checklist

When a change spans repos:

- [ ] Identify the dependency direction (who imports/consumes whom) and order the work bottom-up.
- [ ] On the consumer branch, pin to the dependency's **commit SHA** while its PR is a draft
      (`go get module@<sha>`, then `go mod tidy`) — never a `replace`, never `@branch`. Before
      marking it ready, ask the developer to cut the dependency's release, then rebase on
      Renovate's bump to it.
- [ ] Breaking a shared contract? Stage it (add → deprecate → migrate → remove); the current PR is
      the _additive_ step only.
- [ ] For any breaking change to a public symbol, **`grep -rn` the whole workspace** (`app/` _and_
      `kit/`) for consumers before writing the dependency PR, and paste the file list into the PR
      description under "Consumer migration follow-ups".
- [ ] Write the merge order in the PR description and/or `TaskCreate`.
- [ ] Merge the dependency PR → wait for its release tag → dispatch Renovate in the consumer and
      land its bump, with only a migration pushed onto it when red → rebase the consumer branch on
      `master`, without a pin of its own → confirm CI green → merge the consumer. Repeat up the
      chain.
- [ ] Never merge a PR whose `go.mod` (or Docker-image pin) points at a pseudo-version, a branch,
      or `latest`.

---

## When a cross-repo landing fails — the runbook

The saga's recovery _mechanics_ (freeze, roll-forward, rollback, the release train) live in
`coordinate-landing`. This is the **version-side** decision tree: what a stuck or bad cross-repo
landing means for the pins and releases, and how to unwind it. A published release is **immutable**,
so cross-repo recovery is almost always **roll forward** — a new release that supersedes, rather
than rewritten history.

| Symptom                                                 | What happened                                                                          | Do this                                                                                                                                                                                                                                                                                               |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Consumer PR red on **"released versions only"**         | Its `go.mod` still pins a pseudo-version (a dependency commit SHA), not a released tag | Wait for the dependency to release, land Renovate's bump to `vX.Y.Z`, rebase the consumer, push. Never merge on a pseudo-version.                                                                                                                                                                     |
| **Dependency released, consumer stuck** in the Epic set | Publish-before-rollout half-done: dep shipped, consumer not bumped/merged              | Dispatch Renovate in the consumer and rebase it on the bump; the merge-gate lands it with the set. The dep's release is immutable: roll forward, don't roll back.                                                                                                                                     |
| An Epic **partially landed**                            | INV-1 violated — a member left the queue                                               | The sweep freezes the survivors and rolls a landable stray forward within grace. If it can't land, fix it, or (last resort) `epic-rollback` the landed subset. See `coordinate-landing`.                                                                                                              |
| A **bad version shipped** (released, then found broken) | A release is immutable — you can't un-publish                                          | Roll **forward**: cut a patch that reverts the change, or `epic-rollback` the Epic (it lands compensating reverts and _re-releases_). Never delete a published tag a consumer may already pin.                                                                                                        |
| A **tag without its GitHub Release**                    | The cut pushed the tag, then `gh release create` failed (GitHub answered HTTP 500)     | Cut the next patch from the same `master`. No workflow publishes a Release for an existing tag, and production deploys an image only with an immutable Release from the release App, so the tag's images never deploy. The patch's generated notes cover both versions (genai v0.10.0, then v0.10.1). |
| The **release train left a repo un-released**           | A dispatch/approval failed mid-train                                                   | Re-dispatch the train — it resumes from live tags and re-cuts only the unshipped. A parked run (release-env approval) is pending; approve it, then re-dispatch to record the receipt.                                                                                                                 |
| A **contract-step removal broke a consumer**            | A consumer was missed in the expand→contract migration                                 | Its build is red against the new dep release. Fast-follow: migrate the missed consumer and release it; until then it pins the _previous_ dep release.                                                                                                                                                 |

`epic-rollback` is the one sanctioned "undo," and even it works by landing **new** compensating
commits and then re-releasing — never by rewriting a tag.

## Common pitfalls

- **`replace` directive for a local/branch dependency.** Pin to a commit SHA instead, then rebase on
  Renovate's bump to the released tag before merging.
- **Merging a consumer on a pseudo-version pin.** The pin must be a released `vX.Y.Z` at merge time;
  the red "released versions only" check is telling you the truth.
- **Merging the consumer before the dependency is released.** Dependency PR → release → Renovate
  bumps the consumer → the consumer rebases and merges, in that order.
- **One PR that flips a public API / proto / endpoint / column old → new.** Stage it, so there is
  always a both-work window.
- **`BREAKING CHANGE:` footer on the additive step.** It goes on the removal step (step 3).
- **A major bump used as a shortcut.** It signals "old path removed"; it does not excuse skipping
  the consumer migration.
- **A release pinned in a feature branch.** It conflicts with every Renovate merge on the same
  lines and asks the feature's reviewer for a dependency label. Dispatch Renovate and rebase on its
  bump instead, as [bring a release through Renovate](#bring-a-release-through-renovate) shows.
- **Forgetting the chain.** A consumer that is itself a dependency continues the dance one level up;
  write the whole order down.
- **Skipping the step-0 consumer grep.** A consumer found after step 3 shipped is a broken build, or
  worse a runtime hole, downstream.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.