Release automation for Crystal shards, driven by Conventional Commits
  • Crystal 97.1%
  • Just 1.8%
  • Dockerfile 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-31 17:09:12 +07:00
.forgejo/workflows chore(ci): add prune-image job 2026-10-28 16:01:31 +07:00
spec feat: add npm adapter for package.json (#3) 2026-10-29 07:12:56 +00:00
src feat: add npm adapter for package.json (#3) 2026-10-29 07:12:56 +00:00
.gitignore feat: first draft 2026-10-27 17:12:32 +07:00
CHANGELOG.md chore(release): v0.5.0 2026-10-30 08:18:37 +07:00
Containerfile fix: use Alpine image for runtime stage to reduce image size 2026-10-28 12:48:35 +07:00
justfile chore: add unreachable to tests 2026-10-31 17:09:12 +07:00
lefthook.yml feat: first draft 2026-10-27 17:12:32 +07:00
LICENSE feat: first draft 2026-10-27 17:12:32 +07:00
README.md feat: add npm adapter for package.json (#3) 2026-10-29 07:12:56 +00:00
renovate.json feat: first draft 2026-10-27 17:12:32 +07:00
shard.lock feat: first draft 2026-10-27 17:12:32 +07:00
shard.yml chore(release): v0.5.0 2026-10-30 08:18:37 +07:00

conventional-release

Release automation driven by Conventional Commits.

Currently supporting only:

  • Node.js/TypeScript projects (npm, via package.json)
  • Crystal shards (crystal)
  • projects keeping up-to-date version in ./VERSION (version-file)

but comes with a pluggable backend that allows adding support for various other runtimes.

Install

Requires git binary (>= v2.54.0) to be installed and available in PATH.

Binary install

Requires Crystal >= v1.22.0:

shards build --release conventional-release
install -m 755 bin/conventional-release ~/.local/bin/

Run as OCI image

Also available as OCI image for e.g. running with Docker or Podman:

podman run --rm -v $(pwd):/data forge.l3x.in/alex/conventional-release:latest

Note

Depending on your setup you might need to expose config and credentials used by git push, like SSH config, HTTPS tokens, etc

Usage

conventional-release              # dry run first is a good idea:
conventional-release --dry-run

Flags:

  • --dry-run (print planned diffs and git commands, change nothing)
  • --lang NAME (force the project type instead of auto-detection)
  • --pre STAGE and --final (prerelease control, see Release policies)

Run it from the root of the checkout being released (same model as git commit); the supported project type (currently: Crystal, via shard.yml) is auto-detected.

What it does, in order:

  1. Derives the next version from commits since the last v* tag: breaking change → major, feat: → minor, anything else → patch.
  2. Rewrites version: in shard.yml (aborting if it had drifted from the last tag).
  3. Rewrites the ## [Unreleased] section of CHANGELOG.md with the new version and categorized commits (Added / Fixed / Changed), and bumps the compare-link definitions at the bottom.
  4. Commits, tags v<version> and pushes branch + tag, which triggers the tag-driven release pipeline of the target repository.

Warning

git push commands are passed the --no-verify flag, if you need to run tests before pushing consider having them as script dependencies. For example:

# Release a new version if checks are green
release: checks
  conventional-release

Release policies

Versions are validated and ordered per SemVer 2.0.0 (MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]); the last v* tag is the source of truth, and a manifest that has drifted from it aborts the release.

Release bumps

  • breaking change (feat!:/fix!: or a BREAKING CHANGE: footer) → major
  • feat: → minor
  • anything else → patch
  • during 0.x: breaking changes bump the minor version instead of the major one. 0.x signals an unstable API where majors carry no meaning (0.3.1 + breaking → 0.4.0).

Prereleases

  • Without flags, a running prerelease is simply advanced, ignoring the commits: v1.2.0-rc.1 → v1.2.0-rc.2.
  • --pre STAGE starts a prerelease series from a release. The commit- derived bump is applied first, then -STAGE.1 is appended: with a feat since v1.2.3, --pre rc → v1.3.0-rc.1. On an existing prerelease it switches stage (v1.3.0-alpha.3 + --pre beta → v1.3.0-beta.1); passing the current stage increments its number instead (v1.3.0-beta.2 + --pre beta → v1.3.0-beta.3).
  • --final promotes a prerelease to a release by dropping the suffix: v1.2.0-rc.2 → v1.2.0. The version bump already happened when the series was started with --pre, so commits are not consulted again.

Execution output example

🔍 Detected project type: crystal
$ git describe --tags --abbrev=0 --match v*
$ git log v0.3.1..HEAD --format=%s%x00%b%x1e
🚀 Releasing v0.3.2 (v0.3.1 → v0.3.2, patch bump)
📝 shard.yml
📝 CHANGELOG.md
$ git add shard.yml CHANGELOG.md
$ git commit -m chore(release): v0.3.2
$ git tag v0.3.2
$ git push --no-verify origin HEAD
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 8 threads
Compressing objects: 100% (7/7), done.
Writing objects: 100% (7/7), 1.25 KiB | 1.25 MiB/s, done.
Total 7 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
To ssh://forge.l3x.in:2222/alex/conventional-release.git
   5b27fcc..0828c51  HEAD -> master
$ git push --no-verify origin v0.3.2
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To ssh://forge.l3x.in:2222/alex/conventional-release.git
 * [new tag]         v0.3.2 -> v0.3.2
✅ Released v0.3.2 🎉

Development

Requires:

just         # list all available commands
setup        # install dev dependencies (Ameba) and build the ameba launcher
just checks  # run validations

A lefthook pre-push hook runs specs and ameba (lefthook install to activate).

Roadmap

  • add more adapters to support other popular runtimes like e.g. Go, Python, etc.