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>
This commit is contained in:
184
docs/semver-ci.md
Normal file
184
docs/semver-ci.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user