Skip to content
Back to skills

Pooled Release

ASecurity

Cut fewer, batched releases (a 'release train') instead of one per merge — by triggering semantic-release on demand or on a cadence rather than on every push to main. Use when releasing on every merge is too noisy, when you want one larger readable changelog per release, to add a manual 'cut a release' button (workflow_dispatch) or a scheduled/weekly release, to set up prerelease channels (next/beta → promote to stable), or when deciding whether you need a release branch.

  • 3 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 5, 2026
ai-agentsgogit

Works with

  • cli

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned September 5, 2026

npx -y skills add stealth-factory/skills --skill pooled-release --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pooled Release?

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

Security grade badge for Pooled Release
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/stealth-factory-pooled-release/badge)](https://www.skillsdirectory.com/skills/stealth-factory-pooled-release)

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: pooled-release
description: "Cut fewer, batched releases (a 'release train') instead of one per merge — by triggering semantic-release on demand or on a cadence rather than on every push to main. Use when releasing on every merge is too noisy, when you want one larger readable changelog per release, to add a manual 'cut a release' button (workflow_dispatch) or a scheduled/weekly release, to set up prerelease channels (next/beta → promote to stable), or when deciding whether you need a release branch."
metadata:
  author: stealth-factory
  co-author: wiiiimm
  version: "1.2.1"
---

# Pooled releases (release trains)

Release on every merge is great for a fast-moving lib, but sometimes you want
**fewer, fatter releases** — one readable changelog per cut, not a version bump per
PR. For the manual and scheduled models it does that **without changing how you
develop** — it only changes the release **trigger** (the prerelease-channel model in
Model 3 does shift where you target work; see there). It builds directly on
[`semantic-release-automation`](../semantic-release-automation/SKILL.md) — same
plugin pipeline, same Conventional Commits; read that first.

## The key idea: decouple *merge* from *release*

Trunk-based development still wants frequent small merges to `main` — that's
unchanged, so **dev velocity and PR flow don't change**. The only thing you move is
**when a release is published**:

| | Trigger | Result |
| --- | --- | --- |
| **Per-merge (default)** | `on: push: [main]` | a release every qualifying merge — many small ones |
| **Pooled (this skill)** | `workflow_dispatch` and/or `schedule` | semantic-release batches **all** commits since the last tag into **one** release |

semantic-release already does the batching — it always releases "everything since the
last tag." Pooling just means you tag **less often**.

A pooled run produces the **same outputs** as a per-merge one (just batched), and
those outputs are independent — CHANGELOG, GitHub Release, and npm publish are each
opt-in. A pooled **app** usually wants CHANGELOG + GitHub Release but not npm; see
[`semantic-release-automation` → Outputs are à la carte](../semantic-release-automation/SKILL.md#outputs-are-à-la-carte--pick-any-subset).
The GitHub Release is also what a deploy gate keys on (below).

> **For a visual, human-readable walkthrough** — trigger diagrams, a merges-to-releases
> timeline, and the "why each config delta" table — see
> [`reference/pooled-vs-unpooled.md`](./reference/pooled-vs-unpooled.md).

## Pick a model

Use [`templates/release-on-demand.yml`](./templates/release-on-demand.yml) for the
first two; it ships with `workflow_dispatch` on and `schedule` commented.

1. **Manual button — `workflow_dispatch`** *(recommended default).* Cut a release
   when you decide it's worth one (Actions tab → Run workflow). Simplest; no surprise
   releases; no release branch.
2. **Cadence — `schedule`.** Uncomment the `cron` for an automatic train (e.g. weekly).
   Good when you want predictable, regular cuts. (Combine with the button if you like.)
3. **Prerelease channels — `next` (and/or `beta`).** A *related* pattern, not strictly
   pooling: push work to a `next` branch for **continuous** `x.y.z-next.N` prereleases,
   then cut a batched **stable** release by promoting `next` → `main` **when you
   choose** (that promotion is the "pooling"). Notes:
   - This model is **push-triggered on the prerelease branch**, so it pairs with the
     parent skill's push-triggered `release.yml` (`on: push: [main, next]`) — **not**
     the on-demand workflow above (which would defeat the "continuous" part). The
     `branches: [main, next]` setting itself lives in the **semantic-release config**,
     not the workflow — see
     [`templates/releaserc.prerelease-channels.json`](./templates/releaserc.prerelease-channels.json).
   - Channels are **independent** — a `next` and a `beta` channel don't flow into each
     other (no auto-promotion between them), so **pick one** unless you genuinely need
     two parallel pre-release lines. The template ships just `next`; add a
     `{ "name": "beta", "prerelease": true }` entry only if you do.
   - Promote with a normal **merge or fast-forward** of `next` into `main` — semantic-
     release cuts the stable release once the commits are reachable from `main`, either way.
   - **Known quirk:** the template runs `@semantic-release/changelog` + `git` on the
     `next` branch too, so each `x.y.z-next.N` prerelease commits its own CHANGELOG
     entries; the stable cut then regenerates notes for the same commits. If the
     duplicated prerelease sections bother you, scope the `changelog`/`git` plugins to
     stable only (e.g. via a per-branch config) or accept the noise on `next`.

## Release branches — usually don't

A long-lived `release/x.y` branch is real trunk-based practice but only pays off when
you must **stabilise/QA a release while trunk keeps moving**, or **support multiple
live versions** at once. It costs you hotfix cherry-picking and snapshot maintenance.
For a single-version app/CLI, **skip it** — pooling via trigger (above) gives you the
batched releases without the branch overhead.

## What changes vs the per-merge setup

Start from `semantic-release-automation`'s `release.yml` and:

- **Replace the trigger** — drop `on: push: [main]`; add `workflow_dispatch` (and/or
  `schedule`).
- **Drop the no-loop guard** — with no `on: push`, the `chore(release): …` commit
  semantic-release pushes can't re-trigger the workflow, so the
  `if: !startsWith(... 'chore(release):')` guard is unnecessary.
- **Everything else is identical** — plugin pipeline, `fetch-depth: 0`,
  `$GITHUB_SERVER_URL` repo URL, tokens, npm publish.

## Gotchas

- **`schedule` only runs once the workflow file is on the default branch** — merge it
  to `main` before expecting cron to fire; `cron` is UTC.
- **The branch guard must special-case `schedule`.** On scheduled runs
  `github.event.repository` is empty, so a bare
  `if: github.ref_name == github.event.repository.default_branch` evaluates to
  `main == ''` → false and **silently skips every cron run**. The template's guard
  short-circuits with `github.event_name == 'schedule' ||` (safe — `schedule` only
  fires on the default branch); keep that clause if you adapt the guard.
- **`fetch-depth: 0` still required** (full history + tags for the batched analysis).
- **`concurrency: { group: release }`** so a manual run and a scheduled run can't
  collide on the tag push.
- **`workflow_dispatch` lets the runner pick any branch** — the Actions UI branch
  dropdown (or `gh workflow run --ref <branch>`) means a manual run could analyze a
  feature/prerelease branch instead of `main`. The template guards this with a
  job-level `if: github.event_name == 'schedule' || github.ref_name == github.event.repository.default_branch`;
  keep it. (`semantic-release-automation`'s push-triggered `release.yml` carries the
  same guard on its `workflow_dispatch` path, so the two templates stay consistent —
  a manual dispatch on either can only cut a release from the default branch.)
- **Big gaps → big changelogs.** That's the point, but communicate the cadence so
  contributors know when their merged work actually ships.
- **Pooling does NOT gate production deploys — wire that separately.** If a platform
  (Vercel/Netlify) auto-deploys `main` on every push, it keeps shipping prod on every
  *merge*, defeating the point of batching. Gate it with
  [`production-release-gating`](../production-release-gating/SKILL.md) so prod ships on
  the **train**, not on merges. The most portable gate is its **Promotion Branch**
  model: `main` → staging on every merge, promote `production` to the released commit
  to ship prod — no platform-specific script.
- **Don't add `on: push` to the on-demand workflow without re-adding the loop guard.**
  With only `workflow_dispatch`/`schedule`, the `chore(release): …` commit can't
  re-trigger it (no guard needed). If you later add `on: push` (e.g. for hotfixes),
  re-add `if: !startsWith(github.event.head_commit.message, 'chore(release):')` or it
  will loop.

## Verify

- `npx semantic-release --dry-run` on `main` prints the **batched** next version and
  the full accumulated release notes — without publishing.
- Manual: Actions tab → the workflow → **Run workflow**. Scheduled: confirm the cron
  in the default-branch copy of the file.

## See also

- [`reference/pooled-vs-unpooled.md`](./reference/pooled-vs-unpooled.md) — visual
  walkthrough (diagrams, timeline, config-delta table) for humans skimming the design.
- [`semantic-release-automation`](../semantic-release-automation/SKILL.md) — the
  pipeline this reuses; pooling only swaps the trigger.
- [`conventional-commits`](../conventional-commits/SKILL.md) — what the batched
  changelog is built from.
- [`production-release-gating`](../production-release-gating/SKILL.md) — deploy on the
  pooled release rather than on every merge.

## Sources

- semantic-release branches/channels config (prerelease `next`/`beta`):
  <https://semantic-release.gitbook.io/semantic-release/usage/configuration#branches>
- Trunk-based development & release strategies: <https://trunkbaseddevelopment.com/>,
  Atlassian's TBD guide, and the semantic-release TBD discussion (#2041). The
  decouple-merge-from-release framing and "release branches only when you must
  stabilise/QA or support multiple live versions" come from those.

Files in this skill

  • SKILL.md9.4 KB
  • reference/pooled-vs-unpooled.md5.5 KB
  • templates/release-on-demand.yml2.7 KB
  • templates/releaserc.prerelease-channels.json480 B

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.