Shared release-cut toolkit: conventional-commits + changelog.d/ fragments + manifest-vs-tag CI + Forgejo reusable workflows.
  • Go 85.1%
  • Shell 14.5%
  • Python 0.3%
  • Dockerfile 0.1%
Find a file
release-bot 02fae6a352 chore: post-cut bookkeeping for v0.63.1 [skip ci]
Combined bake-reset (release-toolkit#173) + auto-re-pin self-workflows to v0.63.1 (release-toolkit#283). [skip ci] suppresses the push trigger so no CI run fires (release-toolkit#202). Manifest committed + pushed first (release-toolkit#179 ordering: work-loss-safe by position).

Tracker: frankenbit/release-toolkit#283
2026-09-07 23:21:08 +00:00
.forgejo chore: post-cut bookkeeping for v0.63.1 [skip ci] 2026-09-07 23:21:08 +00:00
.gitea/workflows chore: post-cut bookkeeping for v0.63.1 [skip ci] 2026-09-07 23:21:08 +00:00
changelog.d chore(release): prepare v0.63.1 2026-09-07 23:15:39 +00:00
cmd/rt fix(pre-push): do not predeclare fragment contexts 2026-09-08 00:42:23 +02:00
composite/bootstrap-rt docs: scope minisign public key to reusable consumers 2026-08-31 00:58:58 +02:00
docs chore(fragment-check): split coverage and density contexts 2026-09-08 00:42:23 +02:00
examples chore(readme): point the adopter pin at the newest servable release (#1378) 2026-09-07 21:59:00 +00:00
internal fix(decide): branch the pending-cut message on the evidence it already carries (#1457) 2026-09-07 22:22:35 +02:00
scripts fix(record-landing-review): bound pagination loops (#1469) 2026-09-08 01:07:35 +02:00
testdata/workflow-schema feat(ci): ask the Forgejo engine whether it can parse the workflows (#769) 2026-08-20 03:00:56 +02:00
tests fix(record-landing-review): bound pagination loops (#1469) 2026-09-08 01:07:35 +02:00
.gitignore chore(scaffold): CI (go-ci) + goreleaser + composite-action bootstrap (#502) 2026-07-26 22:10:19 +02:00
.goreleaser.yaml security: authenticate release checksum manifests 2026-08-28 22:29:28 +02:00
.register-allowlist chore(register-allowlist): add docs/retro/ — historical-record grain matches docs/adr/ 2026-07-29 20:54:22 +02:00
.release-toolkit-manifest.json chore(manifest): post-cut bookkeeping for v0.63.1 2026-09-07 23:21:08 +00:00
action.yml security: authenticate release checksum manifests 2026-08-28 22:29:28 +02:00
AGENTS.md feat(review): guard submissions against moved heads 2026-09-06 21:17:51 +02:00
CHANGELOG.md chore(release): prepare v0.63.1 2026-09-07 23:15:39 +00:00
docker-entrypoint.sh feat(action): docker action, digest-pinned — the verified half of #794 2026-08-21 15:44:17 +02:00
Dockerfile docs(#933): exclude the Dockerfile from its own re-derivation grep 2026-08-26 17:40:10 +02:00
go.mod feat(config): implement Loader + Config against #505 interface + C1 schema (#527) 2026-07-26 22:10:19 +02:00
go.sum feat(config): implement Loader + Config against #505 interface + C1 schema (#527) 2026-07-26 22:10:19 +02:00
LICENSE scaffold: initial repo structure + ADR-0001 + LICENSE + CHANGELOG 2026-06-24 09:09:47 +02:00
minisign.pub security(docs): publish the minisign public key adopters verify with (#1062) 2026-08-30 00:47:10 +02:00
README.md chore(readme): point the adopter pin at the newest servable release (#1378) 2026-09-07 21:59:00 +00:00
release-toolkit.yml port update-doc-version-refs.sh to rt prep §8d (rt#720) (#754) 2026-08-19 20:45:41 +02:00
VERSION chore(release): prepare v0.63.1 2026-09-07 23:15:39 +00:00

release-toolkit

A shared, Forgejo-native release pipeline you reference instead of copy-pasting release machinery into every repository. Add one config file and a few workflow stubs, and your project gets a consistent, operator-gated release flow — cut → draft → publish → deploy — with an auto-maintained CHANGELOG and a CI check that catches version-number drift before it ships.

Built for any project on Forgejo Actions that wants a real release pipeline without building (or re-building) one — it already runs its own releases and tmux-tell's (that repository is private, so its releases are not anonymously readable — release-toolkit's own, linked above, are).

Positioning

release-toolkit is developed on a self-hosted Forgejo — source, releases, issues and CI all at git.frankenbit.de, which is publicly reachable. This repository is public and anonymously readable: source, issues, tags and releases are all inspectable at the canonical source, without an account. Projects that should not be public are private on the same host, per repository.

⚠️ Not every git.frankenbit.de link in these docs resolves for you. A link into a private repository returns 404 anonymously, which is indistinguishable from a deleted one. Where that matters, it is called out at the link.

There is also a public mirror, and its releases lag far behind its code. gitea.com/FrankenBit/release-toolkit is a push mirror: commits, branches and tags are pushed outward from git.frankenbit.de on an 8-hour interval. It replaces the Codeberg mirror, which was retired in #793 because Codeberg does not accept code that is largely LLM-generated, and this project is.

🔴 A push mirror carries git data only. A Forgejo release is an API object, not git data, so no release crosses by mirroring — each one has to be published there separately. That is the durable fact; the counts below are a snapshot.

2026-09-04   source tags 107   release objects 89   asset-bearing 39   assets 81
             mirror tags 107   release objects 39   asset-bearing 39   assets 81
             -> 68 tags have no mirrored release object
             -> all 68 lack an asset-bearing source release
             -> 50 have a source zero-asset release object; 18 have no source object

A backfill run put those 39 there (#1087), and a second identical run changed nothing — same release ids, same asset ids, same sizes. So the mirror is not merely populated; the mechanism that populates it is repeatable. The 68 remaining tags are not one missing-source-release population: 50 have a source release object with zero assets, while 18 have no source release object at all. There is no asset-bearing source release to mirror for any of those 68.

A git tag and a release object are independent on this mirror. git checkout <tag> can succeed with no binary behind it, and nothing on either end reports a problem: the tag resolves, the release page is simply absent.

🔴 This warning deliberately names no example version, and that is the second lesson rather than a stylistic choice. It named v0.55.0 once and v0.61.1 once; both were backfilled afterwards, so on both occasions the warning ended up demonstrating a version that WORKS. A reader checks the example, finds it fine, and concludes the caveat is historical — twice, from opposite directions, in one week. The mechanism is permanent; any example of it is a temporary fact about a particular afternoon. rt readme-pin-check (#1345) now enforces the version this page recommends against what the mirror can serve, so the invariant has a gate instead of a paragraph.

⚠️ Check that a release exists for YOUR tag rather than inferring it from the page having releases on it. Every asset-bearing release is now mirrored, but the invariant above is unchanged: nothing crosses by mirroring, so a future release reaches this mirror only when something publishes it there.

⚠️ A cut never originates on a mirror. Releases are cut here and pushed outward, one-directionally. The mirror runs no CI of its own — our workflows live in .forgejo/workflows, which Gitea Actions does not read — and a mirror that cut its own release would push a manifest commit that the next sync overwrites.

It targets Forgejo Actions as its CI runtime — the release flow is a set of reusable Forgejo Actions workflows you pin by tag on the uses: line. Because Codeberg is Forgejo, the same workflows run there wherever an Actions runner is available.

🔑 Those are two different questions and this README used to answer them with one sentence. Where this project lives is a fact about the maintainers' infrastructure. Where the workflows run is a fact about your repository — Forgejo, Gitea or Codeberg, whichever you host on. The second is unaffected by the first.

A companion demo — tic-tac-toe — ran the full toolkit end-to-end on Codeberg on 2026-07-05: fresh-repo bootstrap through the full cut-and-publish flow to a live v0.1.0 release.

⚠️ That is a dated demonstration, not a live reference, and the release it produced is gone. The Codeberg original was retired with the rest of the Codeberg surface in #793. A tic-tac-toe repository survives on the gitea.com mirror and is publicly readable, but it carries 0 tags and 0 releases (measured 2026-09-07) — so there is no v0.1.0 object to look at, and no pointer to it is given here rather than one that resolves to nothing. No CI arm in this repository exercises a Codeberg target. It is evidence that the flow worked on that date; nothing here re-checks it.

Runtime compatibility — what the workflows run on, distinct from where the project lives:

  • Forgejo Actions — the authored target. Tested on Forgejo 11.x (the maintainers' self-host); not tested on 10.x or older, where the workflow parser may reject syntax the toolkit uses.
  • Gitea Actions — compatible in principle (Forgejo forked from Gitea and shares the API surface), not exercised in CI. Feedback welcome via issue.
  • Codebergdemonstrated on 2026-07-05, not continuously tested: the companion demo above ran the full cut cycle on Codeberg's shared codeberg-small-lazy runner. No CI arm here exercises a Codeberg target, so treat this as the strongest evidence in this list below Forgejo Actions — stronger than Gitea's "in principle", weaker than a tested target.

Status

Pin @v0.63.0. That is the version to put on your uses: line — the newest one the public mirror can actually serve, which is the only thing a uses: reference can consume. The full example is below.

⚠️ That is the only version on this page, and it is deliberate. The canonical newest release and the newest one the mirror can serve are different facts, and either can be ahead: mirroring is a separate manual step, so the mirror trails the source by any amount — they were four versions apart on the morning of 2026-09-06. While it trails, uses: must name a version the mirror can serve, and the failure if it does not is the quiet kind: the git tag resolves there either way, so an unmirrored reference is accepted, the checkout succeeds, and fetch-rt then finds no asset — your first failure is at bootstrap, with a green reference behind you.

🔑 Why the pin above survives and a "latest version" line does not: only one of them is machine-KEPT. rt readme-pin-check grades the @vX.Y.Z pins on this page against what the mirror can serve, and --fix sets them from the same computation — so the pin above cannot go stale without a gate saying so on the next PR. ⚠️ A version written in prose has no such gate, is true when written, goes false the moment the mirror lags, and cannot tell a reader how old it is; a hand-kept one on this page went stale in public twice. The verb grades pins, not paragraphs, and it does not run when you read this. For the live answer, the mirror's releases cannot be stale.

🔴 AND THE RULE IS ABOUT CURRENCY CLAIMS, NOT ABOUT VERSION NUMBERS. A number naming a moment in the past cannot go false — a retirement, an earliest-supported tag, a dated observation. A number a reader would ACT on can, and that is the one a gate has to keep. The test, so it is not a matter of taste: does it carry its own date, or name a version already superseded? Then it is historical. Would a reader put it in their own file? Then it is a currency claim. ⚠️ Read as "no numbers" this rule deletes true sentences: the integration guide carries dozens of descriptive versions and exactly ONE of them claimed currency. 📌 Worked examples live in internal/readmepin/readme_status_test.go, beside the arm that enforces this — they are numbers, and this section is the one place they may not be.

Pre-1.0 and actively developed: the architecture has settled — recent releases cut cleanly — and a pre-1.0 readiness sweep is underway (real-world adoption to surface friction + a public-surface audit) before committing to 1.0's backwards-compatibility guarantees.

What pre-1.0 means for you: the toolkit is stable in practice — it cuts its own releases and drives tmux-tell's (private repository — not anonymously readable). Config keys and workflow inputs may still shift between minor versions (0.x0.(x+1)), but each such change is called out explicitly in the CHANGELOG. Pin to a tag your forge can reach — the version named at the top of this section.

What pinning @v1.x will commit to — and what it deliberately won't — is named surface-by-surface in docs/VERSIONING.md.

What it does

  • Cuts releases from a single config. You dispatch (or push) a release; the toolkit computes the SemVer bump, transitions your CHANGELOG.md (moves [Unreleased] into the new version section), bumps your version files, and opens a release-prep PR. You review and merge to fire the cut.

  • Separates release-note content from bump signals.

    • Conventional commits (feat: / fix: / feat!: / …) are read straight from git log to determine the version bump. A subject alone is not copied into adopter-facing notes.
    • changelog.d/<id>.<kind>.md fragments are hand-written release-note prose for changes that need an entry beyond the bump signal.

    Use a conventional commit for the bump, and add a fragment when the change needs release-note content. The toolkit merges explicit note sources without duplicating section headings.

  • Catches release-prep mistakes in CI. A reusable check runs on every PR and flags the usual foot-guns: version numbers out of sync across files, a missing ## [Unreleased] heading, or a manifest left behind the latest tag.

  • Keeps your tag list tidy. The pre-release (-rc.N) tags a cut creates are pruned automatically once the release ships, so they don't accumulate (v0.12.0+).

  • Runs the workflow YAML and its scripts at one version. When you pin the toolkit at a tag, the reusable workflow and the scripts it calls are always that same version — no drift between the two (v0.13.0+).

  • Reviews every release before it is cut. The flow is multi-stage — dispatch → prep PR → cut → publish → deploy — and the operator gate is the prep PR: the version bump, changelog and manifest are reviewed and merged before anything is tagged. The cut itself publishes automatically. Adopters who want a manual publish step can set publish_mode: 'draft'.

Bump signals

The toolkit decides the SemVer bump from up to four sources, and the highest bump wins — so a single feat!: or BREAKING CHANGE anywhere forces a major:

Source How you set it Drives
Conventional-commit subject feat: / fix: / feat!: (or BREAKING CHANGE:) in commit messages minor / patch / major, auto-detected from git log
Fragment body a BREAKING CHANGE marker in a changelog.d/ fragment (or a removed-kind fragment) major
Workflow override bump_override: major|minor|patch dispatch input forces that level explicitly
Rolling-PR label bump/major / bump/minor / bump/patch on the prep PR (Forgejo exclusive-label group — picking one auto-removes the others) that level

Set nothing and the bump is inferred from your commits and fragments. Set an override and it takes precedence. Either way, the resolved bump is shown in the prep PR before you merge.

Quick start (consumer adoption)

Four steps — full guide in docs/integration.md.

Runner requirements: toolkit-owned gates use the measured go runner image, which carries Go, golangci-lint, bats, shellcheck, jq, curl, and the Python yq wrapper; the optional LikeC4 build uses playwright with Graphviz. The reusable workflows still accept a consumer-selected runs_on label and retain a fail-loud install fallback for non-standard images. Details in integration.md § Runner requirements.

Fresh repos need zero manual setup — the toolkit assumes a virtual v0.0.0 anchor and walks from your initial commit. First cut is v0.0.1 (fix: only), v0.1.0 (any feat:), or mode=noop if only chore: / docs: / refactor: commits exist (add a feat: / fix:, or drop a bump/* label to force the cut). Details in integration.md § Bootstrapping a new consumer repo.

  1. Add release-toolkit.yml at your repo root:

    version: 1
    release_type: node          # or go, python, multi
    version_files:
      - package.json
      - VERSION
    changelog: CHANGELOG.md
    tag_format: 'v{version}'
    
  2. Seed the fragment directory: add changelog.d/.keep.

  3. Add a release workflow referencing the reusable (pin the tag):

    # .forgejo/workflows/release.yml
    name: release
    on:
      push:
        branches: [main]
        paths-ignore: ['.release-toolkit-manifest.json']   # don't re-fire the cut on its own manifest commit
      workflow_dispatch:           # retained as a fast-path for manual/emergency cuts
        inputs:
          bump_override: { type: string, default: auto }
    
    jobs:
      release:
        # ABSOLUTE URL, because the toolkit is not on your forge. A bare
        # `frankenbit/release-toolkit/...` reference resolves against YOUR OWN
        # instance and 404s unless you host a copy of the toolkit there.
        #
        # THE PATH BELOW DEPENDS ON YOUR FORGE, not on ours. `.forgejo/workflows/`
        # is for Forgejo and Codeberg. On GITEA, use `.gitea/workflows/` instead.
        # Both paths exist in the mirror and both resolve, so the wrong one does
        # NOT 404. The caller registers, validates, and the job never runs — no
        # error, no red, no log line. See integration.md, "Your forge decides".
        uses: https://gitea.com/FrankenBit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.63.0
        with:
          runs_on:       go            # your Forgejo runner label
          bump_override: ${{ inputs.bump_override }}
          # publish_mode omitted — defaults to 'immediate' (the cut publishes itself).
          # Adopters who want a manual publish step can add `publish_mode: 'draft'`,
          # which creates the release as a draft for you to click Publish.
        secrets: inherit               # REQUIRED — the one adopter measured without it has a release PR that has never received any CI. Also enables the release-bot path. See integration.md.
    

    On a push to main the toolkit opens or updates a rolling release-prep PR; merging that PR fires the cut. (workflow_dispatch stays available for a manual cut.)

    Token setup is optional. The default needs no token — after each cut the toolkit opens a small manifest PR for you to merge. To make cuts fully autonomous on a protected main, see token setup in integration.md.

  4. Document your commit + fragment conventions for contributors — templates in docs/conventions.md, plus the skim-scan fragment style guide in docs/fragment-style.md for the shape a good fragment takes (contributors point their own PR reviewers at this).

Why this exists

Every project reinvents release engineering: the version-bump logic, the CHANGELOG dance, the tag-vs-manifest drift checks, the "did I forget to publish?" gates. release-toolkit extracts that into shared, versioned building blocks you reference by tag — so a new project starts with a complete, working release pipeline on day one instead of copy-pasting and maintaining the machinery itself.

The design is release-please-inspired — manifest-vs-tag CI, configurable version-file handling, and composable release-note sources — reworked natively for Forgejo, where release-please (a GitHub product) isn't a clean drop-in. That Forgejo-native fit is the niche. It's pre-1.0 and deliberately scoped: the shared release-please architecture, not its full language and monorepo breadth (see Status).

Architecture and design

  • Architecture overview — how the toolkit is built at a glance: the reusable workflows, the cut lifecycle, and the design tenets (each links to its decision record).
  • Consumer integration guide — the full adoption walkthrough.
  • Conventions — commit-subject and fragment formats.
  • Fragment style guide — the skim-scan shape a good fragment takes, plus the length-warn CI signal that surfaces essay-length drift at PR-review time.
  • Operations — running and troubleshooting the flow.
  • Architecture decisions — the design rationale: the multi-stage operator gate (ADR-0003), push-triggered prep (ADR-0004), and the release-note source model (ADR-0011; ADR-0002 is its historical predecessor, with ADR-0006 recording the RNA boundary).

License

MIT — see LICENSE.