#!/usr/bin/env bash set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" SEMVER="$SCRIPT_DIR/scripts/semver-ci" RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' pass=0 fail=0 assert_eq() { local desc="$1" expected="$2" actual="$3" if [[ "$expected" == "$actual" ]]; then echo -e "${GREEN}PASS${NC}: $desc" ((pass++)) || true else echo -e "${RED}FAIL${NC}: $desc" echo " expected: $expected" echo " actual: $actual" ((fail++)) || true fi } assert_match() { local desc="$1" pattern="$2" actual="$3" if [[ "$actual" =~ $pattern ]]; then echo -e "${GREEN}PASS${NC}: $desc" ((pass++)) || true else echo -e "${RED}FAIL${NC}: $desc" echo " pattern: $pattern" echo " actual: $actual" ((fail++)) || true fi } assert_no_match() { local desc="$1" pattern="$2" actual="$3" if [[ ! "$actual" =~ $pattern ]]; then echo -e "${GREEN}PASS${NC}: $desc" ((pass++)) || true else echo -e "${RED}FAIL${NC}: $desc" echo " should not match: $pattern" echo " actual: $actual" ((fail++)) || true fi } assert_exit() { local desc="$1" expected="$2" actual="$3" if [[ "$expected" -eq "$actual" ]]; then echo -e "${GREEN}PASS${NC}: $desc" ((pass++)) || true else echo -e "${RED}FAIL${NC}: $desc" echo " expected exit: $expected" echo " actual exit: $actual" ((fail++)) || true fi } make_repo() { local dir="$1" mkdir -p "$dir" ( cd "$dir" git init -q -b main git config user.email "t@t" git config user.name "t" git commit --allow-empty -q -m "init" ) } commit_msg() { local dir="$1" msg="$2" (cd "$dir" && git commit --allow-empty -q -m "$msg") } tag_at() { local dir="$1" tag="$2" (cd "$dir" && git tag "$tag") } run_in() { local dir="$1"; shift (cd "$dir" && "$SEMVER" "$@" 2>&1) } echo "=== semver-ci tests ===" echo # --- Help --- out=$("$SEMVER" --help 2>&1) rc=$? assert_exit "help exits 0" 0 "$rc" assert_match "help mentions BUILD" "BUILD" "$out" assert_match "help mentions VERSION.md" "VERSION.md" "$out" # --- Outside git repo --- tmp=$(mktemp -d) out=$(cd "$tmp" && "$SEMVER" --dryrun 2>&1) rc=$? assert_exit "outside git repo exits 1" 1 "$rc" assert_match "outside git repo error" "not inside a git repository" "$out" rm -rf "$tmp" # --- Fresh repo, no triggers (main branch) --- tmp=$(mktemp -d) make_repo "$tmp" out=$(run_in "$tmp" --dryrun --build 42) assert_match "fresh repo: version 0.0.0.42" "New version: 0\.0\.0\.42" "$out" assert_match "fresh repo: bump none" "Version bump: none" "$out" assert_match "fresh repo: triggers none" "Triggers found: none" "$out" assert_match "fresh repo: last release none" "Last release: " "$out" assert_match "fresh repo: branch main detected" "Branch: main \(main-branch detected\)" "$out" rm -rf "$tmp" # --- NEW_PATCH triggers patch bump --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'fix: stuff\n\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --build 10) assert_match "NEW_PATCH: version 0.0.1.10" "New version: 0\.0\.1\.10" "$out" assert_match "NEW_PATCH: bump PATCH" "Version bump: PATCH" "$out" assert_match "NEW_PATCH: trigger counted" "NEW_PATCH \(1\)" "$out" rm -rf "$tmp" # --- NEW_MINOR triggers minor bump and resets patch --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'feat: thing\n\nNEW_MINOR' out=$(run_in "$tmp" --dryrun --build 10) assert_match "NEW_MINOR: version 0.1.0.10" "New version: 0\.1\.0\.10" "$out" assert_match "NEW_MINOR: bump MINOR" "Version bump: MINOR" "$out" rm -rf "$tmp" # --- NEW_MAJOR triggers major bump and resets minor/patch --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'feat!: breaking\n\nNEW_MAJOR' out=$(run_in "$tmp" --dryrun --build 10) assert_match "NEW_MAJOR: version 1.0.0.10" "New version: 1\.0\.0\.10" "$out" assert_match "NEW_MAJOR: bump MAJOR" "Version bump: MAJOR" "$out" rm -rf "$tmp" # --- Precedence: MAJOR > MINOR > PATCH --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'mixed\n\nNEW_MINOR\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --build 1) assert_match "minor beats patch" "Version bump: MINOR" "$out" rm -rf "$tmp" tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'big\n\nNEW_MAJOR\nNEW_MINOR\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --build 1) assert_match "major beats minor and patch" "Version bump: MAJOR" "$out" rm -rf "$tmp" # --- Inline mentions do NOT trigger --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" "refactor: mention NEW_PATCH inline but not alone" out=$(run_in "$tmp" --dryrun --build 1) assert_match "inline NEW_PATCH ignored: bump none" "Version bump: none" "$out" assert_match "inline NEW_PATCH ignored: triggers none" "Triggers found: none" "$out" rm -rf "$tmp" # --- Whitespace-tolerant triggers --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'spaced\n\n NEW_PATCH ' out=$(run_in "$tmp" --dryrun --build 1) assert_match "whitespace-padded trigger counts" "Version bump: PATCH" "$out" rm -rf "$tmp" # --- Base from prior tag: PATCH bump --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'seed\n\nNEW_MINOR' tag_at "$tmp" "v1.2.3.50" commit_msg "$tmp" $'hotfix\n\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --build 88) assert_match "prior v1.2.3.50 + NEW_PATCH = 1.2.4.88" "New version: 1\.2\.4\.88" "$out" assert_match "prior tag detected" "Last release: v1\.2\.3\.50" "$out" rm -rf "$tmp" # --- MAJOR bump resets MINOR/PATCH from prior tag --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" "seed" tag_at "$tmp" "v3.5.7.99" commit_msg "$tmp" $'breaking\n\nNEW_MAJOR' out=$(run_in "$tmp" --dryrun --build 100) assert_match "prior v3.5.7.99 + NEW_MAJOR = 4.0.0.100" "New version: 4\.0\.0\.100" "$out" rm -rf "$tmp" # --- No triggers, prior tag: M/m/p unchanged --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" "seed" tag_at "$tmp" "v2.4.8.60" commit_msg "$tmp" "chore: tidy" out=$(run_in "$tmp" --dryrun --build 77) assert_match "no trigger: reuses M/m/p" "New version: 2\.4\.8\.77" "$out" assert_match "no trigger: bump none" "Version bump: none" "$out" rm -rf "$tmp" # --- Tag selection: latest by -V --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" "c1"; tag_at "$tmp" "v1.0.0.1" commit_msg "$tmp" "c2"; tag_at "$tmp" "v10.2.0.5" # 10 > 2 numerically commit_msg "$tmp" "c3"; tag_at "$tmp" "v2.9.9.9" commit_msg "$tmp" $'next\n\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --build 1) assert_match "sort -V picks v10.2.0.5 as latest" "Last release: v10\.2\.0\.5" "$out" assert_match "bump from v10.2.0.5" "New version: 10\.2\.1\.1" "$out" rm -rf "$tmp" # --- --base overrides detection --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" "c1"; tag_at "$tmp" "v5.0.0.1" commit_msg "$tmp" "c2"; tag_at "$tmp" "v6.0.0.2" commit_msg "$tmp" $'n\n\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --build 9 --base v5.0.0.1) assert_match "--base honoured" "Last release: v5\.0\.0\.1" "$out" assert_match "--base affects bump base" "New version: 5\.0\.1\.9" "$out" rm -rf "$tmp" # --- --base to nonexistent tag errors --- tmp=$(mktemp -d) make_repo "$tmp" out=$(run_in "$tmp" --dryrun --base v99.99.99.99 2>&1) rc=$? assert_exit "--base nonexistent exits 1" 1 "$rc" assert_match "--base nonexistent error" "does not exist" "$out" rm -rf "$tmp" # --- --build validation --- tmp=$(mktemp -d) make_repo "$tmp" out=$(run_in "$tmp" --dryrun --build abc 2>&1) rc=$? assert_exit "--build non-numeric exits 1" 1 "$rc" assert_match "--build non-numeric error" "non-negative integer" "$out" rm -rf "$tmp" # --- Env-based BUILD --- tmp=$(mktemp -d) make_repo "$tmp" out=$(cd "$tmp" && GITHUB_RUN_NUMBER=555 "$SEMVER" --dryrun 2>&1) assert_match "GITHUB_RUN_NUMBER used for BUILD" "New version: 0\.0\.0\.555" "$out" out=$(cd "$tmp" && GITEA_RUN_NUMBER=777 "$SEMVER" --dryrun 2>&1) assert_match "GITEA_RUN_NUMBER used for BUILD" "New version: 0\.0\.0\.777" "$out" out=$(cd "$tmp" && GITHUB_RUN_NUMBER=100 GITEA_RUN_NUMBER=200 "$SEMVER" --dryrun 2>&1) assert_match "GITHUB_RUN_NUMBER preferred over GITEA_RUN_NUMBER" "New version: 0\.0\.0\.100" "$out" rm -rf "$tmp" # --- Branch build: non-main --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" "seed"; tag_at "$tmp" "v1.2.3.10" (cd "$tmp" && git checkout -q -b feature/login) commit_msg "$tmp" $'wip\n\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --build 77) assert_match "non-main version has branch+sha suffix" "New version: 1\.2\.4\.77-feature-login-[0-9a-f]{7}" "$out" assert_match "non-main shows non-main label" "Branch: feature/login \(non-main\)" "$out" rm -rf "$tmp" # --- --branch override --- tmp=$(mktemp -d) make_repo "$tmp" out=$(run_in "$tmp" --dryrun --build 5 --branch bugfix/oops) assert_match "--branch override labelled non-main" "Branch: bugfix/oops \(non-main\)" "$out" assert_match "--branch override in version" "New version: 0\.0\.0\.5-bugfix-oops-[0-9a-f]{7}" "$out" rm -rf "$tmp" # --- --main-branch override --- tmp=$(mktemp -d) make_repo "$tmp" (cd "$tmp" && git checkout -q -b trunk) out=$(run_in "$tmp" --dryrun --build 1 --main-branch trunk) assert_match "--main-branch override treats trunk as main" "Branch: trunk \(main-branch detected\)" "$out" assert_no_match "--main-branch override: no branch suffix" "\-trunk\-" "$out" rm -rf "$tmp" # --- Non-main skips tag/push/release in dryrun --- tmp=$(mktemp -d) make_repo "$tmp" (cd "$tmp" && git checkout -q -b feature/x) commit_msg "$tmp" $'wip\n\nNEW_PATCH' out=$(run_in "$tmp" --dryrun --release --build 1) assert_match "non-main with --release: dryrun skip message" "Would skip tag/push/release on non-main branch" "$out" assert_no_match "non-main with --release: no create tag line" "Would create tag" "$out" rm -rf "$tmp" # --- Real mode: version file written --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'feat\n\nNEW_MINOR' out=$(run_in "$tmp" --build 42) rc=$? assert_exit "real-mode exits 0" 0 "$rc" assert_eq "stdout is just the version" "0.1.0.42" "$out" assert_match "VERSION.md created" "0.1.0.42" "$(cat "$tmp/VERSION.md")" assert_match "VERSION.md has Major line" "Major: 0" "$(cat "$tmp/VERSION.md")" assert_match "VERSION.md has Build line" "Build: 42" "$(cat "$tmp/VERSION.md")" assert_match "VERSION.md has Tag line" "Tag: v0.1.0.42" "$(cat "$tmp/VERSION.md")" rm -rf "$tmp" # --- Real mode: non-main VERSION.md shows branch suffix and no tag --- tmp=$(mktemp -d) make_repo "$tmp" (cd "$tmp" && git checkout -q -b dev/x) commit_msg "$tmp" $'wip\n\nNEW_PATCH' out=$(run_in "$tmp" --build 7) assert_match "non-main stdout has suffix" "0\.0\.1\.7-dev-x-[0-9a-f]{7}" "$out" assert_match "non-main VERSION.md Tag is dash" "Tag: -" "$(cat "$tmp/VERSION.md")" assert_match "non-main VERSION.md branch preserved" "Branch: dev/x" "$(cat "$tmp/VERSION.md")" rm -rf "$tmp" # --- --no-version-file skips file write --- tmp=$(mktemp -d) make_repo "$tmp" run_in "$tmp" --build 1 --no-version-file >/dev/null if [[ ! -e "$tmp/VERSION.md" ]]; then echo -e "${GREEN}PASS${NC}: --no-version-file skips write" ((pass++)) || true else echo -e "${RED}FAIL${NC}: --no-version-file skips write" ((fail++)) || true fi rm -rf "$tmp" # --- Real mode: --tag creates the tag on main --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'feat\n\nNEW_PATCH' run_in "$tmp" --tag --build 3 >/dev/null tag_present=$(cd "$tmp" && git tag -l "v0.0.1.3") assert_eq "--tag on main creates tag" "v0.0.1.3" "$tag_present" rm -rf "$tmp" # --- Real mode: --tag is skipped on non-main --- tmp=$(mktemp -d) make_repo "$tmp" (cd "$tmp" && git checkout -q -b feature/a) commit_msg "$tmp" $'w\n\nNEW_PATCH' run_in "$tmp" --tag --build 3 >/dev/null 2>&1 tag_present=$(cd "$tmp" && git tag -l "v0.0.1.3") assert_eq "--tag on non-main does NOT create tag" "" "$tag_present" rm -rf "$tmp" # --- Tag conflict on main errors --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'feat\n\nNEW_PATCH' tag_at "$tmp" "v0.0.1.3" out=$(run_in "$tmp" --tag --build 3 2>&1) rc=$? assert_exit "tag conflict exits 1" 1 "$rc" assert_match "tag conflict error message" "tag already exists" "$out" rm -rf "$tmp" # --- GITHUB_OUTPUT is populated --- tmp=$(mktemp -d) make_repo "$tmp" commit_msg "$tmp" $'feat\n\nNEW_MINOR' gh_out="$tmp/.outputs" : > "$gh_out" (cd "$tmp" && GITHUB_OUTPUT="$gh_out" "$SEMVER" --build 9 >/dev/null) content="$(cat "$gh_out")" assert_match "GITHUB_OUTPUT has version" "version=0\.1\.0\.9" "$content" assert_match "GITHUB_OUTPUT has tag" "tag=v0\.1\.0\.9" "$content" assert_match "GITHUB_OUTPUT has released=true" "released=true" "$content" assert_match "GITHUB_OUTPUT has branch=main" "branch=main" "$content" assert_match "GITHUB_OUTPUT has is_main=true" "is_main=true" "$content" rm -rf "$tmp" # --- GITHUB_OUTPUT on non-main: tag empty, is_main false --- tmp=$(mktemp -d) make_repo "$tmp" (cd "$tmp" && git checkout -q -b feature/z) commit_msg "$tmp" $'w\n\nNEW_PATCH' gh_out="$tmp/.outputs" : > "$gh_out" (cd "$tmp" && GITHUB_OUTPUT="$gh_out" "$SEMVER" --build 4 >/dev/null) content="$(cat "$gh_out")" assert_match "non-main GITHUB_OUTPUT: is_main=false" "is_main=false" "$content" assert_match "non-main GITHUB_OUTPUT: tag empty" "^tag=$" "$(printf '%s\n' "$content" | grep '^tag=')" rm -rf "$tmp" # --- Summary --- echo total=$((pass + fail)) echo -e "Results: ${GREEN}${pass}${NC}/${total} passed" if [[ $fail -gt 0 ]]; then echo -e "${RED}${fail} test(s) failed${NC}" exit 1 fi