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>
185 lines
7.2 KiB
Markdown
185 lines
7.2 KiB
Markdown
# 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`](../specs/semver-ci.spec.md) and the reusable Gitea Actions workflow at [`../templates/gitea-workflow-version.yml`](../templates/gitea-workflow-version.yml).
|
|
|
|
## Quick start
|
|
|
|
1. Make the script available on your PATH.
|
|
```bash
|
|
ln -sf ~/dev/claude/small-scripts/scripts/semver-ci ~/sbin/semver-ci
|
|
```
|
|
2. Copy the workflow template into your project.
|
|
```bash
|
|
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/login` → `feature-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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```yaml
|
|
- name: Compute version & release
|
|
id: ver
|
|
run: semver-ci --release
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
```
|
|
|
|
Downstream steps can read the outputs:
|
|
|
|
```yaml
|
|
- 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`.
|