Files
small-scripts/docs/semver-ci.md
Paul O'Reilly cde811fa0e Add semver-ci: commit-driven semantic versioning for Gitea Actions
Computes MAJOR.MINOR.PATCH.BUILD from NEW_MAJOR/NEW_MINOR/NEW_PATCH
whole-line tokens in commit messages since the last release tag.
Non-main branches get a BRANCH-SHORTSHA suffix and never release.
Maintains VERSION.md, writes $GITHUB_OUTPUT, and optionally creates
a Gitea release via API.

Includes spec, 67-assertion test, usage guide (docs/semver-ci.md),
and a drop-in Gitea Actions workflow template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 19:30:44 +12:00

7.2 KiB

semver-ci — usage guide

semver-ci computes the next semantic version for a repository from its commit history, maintains a VERSION.md file, and — on the main branch — creates a git tag and a Gitea release when MAJOR, MINOR, or PATCH actually changes.

Version format:

  • Main branch: MAJOR.MINOR.PATCH.BUILD (e.g. 1.4.2.317)
  • Other branches: MAJOR.MINOR.PATCH.BUILD-BRANCH-SHORTSHA (e.g. 1.4.2.317-feature-auth-a1b2c3d)

See also: the full spec at ../specs/semver-ci.spec.md and the reusable Gitea Actions workflow at ../templates/gitea-workflow-version.yml.

Quick start

  1. Make the script available on your PATH.
    ln -sf ~/dev/claude/small-scripts/scripts/semver-ci ~/sbin/semver-ci
    
  2. Copy the workflow template into your project.
    mkdir -p .gitea/workflows
    cp ~/dev/claude/small-scripts/templates/gitea-workflow-version.yml \
       .gitea/workflows/version.yml
    
  3. Configure a GITEA_TOKEN secret on the repo (Settings → Actions → Secrets) with permission to create releases.
  4. Push a commit whose message contains NEW_PATCH, NEW_MINOR, or NEW_MAJOR on a line of its own.

On the first run you will get 0.0.1.<build> (or higher, depending on the trigger you used) and a release tagged v0.0.1.<build>.

Commit-message conventions

Put exactly one of the trigger tokens on a line of its own — leading and trailing whitespace are allowed, but anything else on that line disqualifies it.

Token Effect Precedence
NEW_MAJOR MAJOR += 1, MINOR = 0, PATCH = 0, new release highest
NEW_MINOR MINOR += 1, PATCH = 0, new release middle
NEW_PATCH PATCH += 1, new release lowest

Examples — these do trigger:

feat: search endpoint

NEW_MINOR

Adds a faceted search API with …
fix: transient DB connection error

NEW_PATCH
   NEW_PATCH

These do not trigger (token is not alone on its line):

refactor: see NEW_PATCH notes for details
NEW_PATCH fixes a CVE

Multiple triggers across a range of commits collapse into a single bump: the highest-precedence token wins. You never get two bumps from a single build.

BUILD number

BUILD always increases and is never reset between releases. The source is chosen in this order (first hit wins):

  1. --build N command-line flag
  2. $GITHUB_RUN_NUMBER (set by Gitea Actions)
  3. $GITEA_RUN_NUMBER
  4. git rev-list --count HEAD (monotonic fallback for local runs)

Branch handling

  • On the main branch (main by default; override with --main-branch NAME), versions are M.m.p.B and --tag/--push/--release operate as documented.
  • On any other branch the version becomes M.m.p.B-BRANCH-SHORTSHA. --tag, --push, and --release become no-ops and log a stderr notice. VERSION.md is still refreshed.
  • / in a branch name is replaced by - in the suffix (so feature/loginfeature-login).
  • Override auto-detection with --branch NAME for local previews or custom CI setups.

VERSION.md

By default the script writes VERSION.md in the git repo root:

# Version

**1.4.2.317**

- Major: 1
- Minor: 4
- Patch: 2
- Build: 317
- Branch: main
- Commit: a1b2c3d
- Tag: v1.4.2.317
- Generated: 2026-04-17T09:12:33Z

Two common patterns for handling this file:

  • Treat as a build artifact (recommended). Add VERSION.md to .gitignore; let CI regenerate it each build and publish it as a release asset or workflow artifact. The template uses actions/upload-artifact.
  • Commit it manually. Run semver-ci --dryrun locally before pushing a release commit, then run semver-ci (without --dryrun) and commit VERSION.md with the same PR. Do not have CI auto-commit it — that creates a push loop.

Customise the path with --version-file PATH, or skip writing with --no-version-file.

Using locally

# Preview what the next build would be, without side effects
semver-ci --dryrun

# Emit just the version string (useful for scripting)
semver-ci --build 42 --no-version-file

# Produce a dev build with an explicit branch name
semver-ci --dryrun --branch feature/auth --build 77

Using in Gitea Actions

The supplied workflow template installs semver-ci from the small-scripts repo, runs it with --release, and uploads VERSION.md as an artifact. The relevant step:

- name: Compute version & release
  id: ver
  run: semver-ci --release
  env:
    GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

Downstream steps can read the outputs:

- name: Build container
  run: |
    docker build -t myapp:${{ steps.ver.outputs.version }} .
    docker push myapp:${{ steps.ver.outputs.version }}

Outputs emitted to $GITHUB_OUTPUT:

Name Example Notes
version 1.4.2.317 or 1.4.2.317-feature-x-abc1234 always set
tag v1.4.2.317 empty on non-main branches
released true / false true when M/m/p actually changed
branch main / feature/x raw branch name (no /- substitution)
is_main true / false convenience flag for if: conditions

Flag reference

Flag Default Purpose
-n, --dryrun off Preview; no side effects
--tag off Create the tag locally (main-only)
--push off Also push tag to origin (implies --tag, main-only)
--release off Also POST a Gitea release (implies --push, main-only, requires M/m/p change)
--build N env → git count Override BUILD
--base TAG auto Override last-release tag detection
--branch NAME env → git Override current-branch detection
--main-branch NAME main Which branch is considered "main"
--version-file PATH <repo>/VERSION.md Where to write the version file
--no-version-file off Skip writing the version file

Troubleshooting

"Cannot determine BUILD number" — you're not in CI and the repo has no commits yet, or git rev-list --count HEAD failed. Pass --build N explicitly.

Release not created even though I pushed NEW_PATCH — releases happen only on the main branch. Verify steps.ver.outputs.is_main == 'true' and released == 'true' in the workflow log, or re-run locally with --dryrun to see what semver-ci detected.

Tag already exists — someone else (or a previous run) already created the tag. If you rebuilt the same commit, the resulting BUILD will be the same and the tag will collide. Either bump BUILD (re-run the workflow — GITHUB_RUN_NUMBER will be higher) or delete the conflicting tag if it was mistaken.

Branch push created a release anyway — double-check --main-branch. If your main branch isn't named main, pass --main-branch master (or whatever) in the workflow step.

Gitea release API 401/403 — the GITEA_TOKEN secret is missing or lacks repo-write scope. Mint a new token in User Settings → Applications and grant write:repository.

sort -V picked the wrong tag — only tags matching ^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ are considered. Older tags in other formats are ignored, which is usually what you want. If you need to pin a specific base, use --base vX.Y.Z.B.