# 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.` (or higher, depending on the trigger you used) and a release tagged `v0.0.1.`. ## 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` | `/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`.