diff --git a/.github/workflows/version_branch.yml b/.github/workflows/version_branch.yml
index 1e6c3f9..77c3849 100644
--- a/.github/workflows/version_branch.yml
+++ b/.github/workflows/version_branch.yml
@@ -4,224 +4,154 @@ on:
workflow_dispatch:
inputs:
new_version:
- description: "New version (for example: 01.02.00). Leave blank to auto-increment from highest version/* branch."
- required: false
- default: ""
+ description: "New version in format NN.NN.NN (example 01.03.00)"
+ required: true
base_branch:
- description: "Base branch to create version branch from"
+ description: "Base branch to branch from"
required: false
default: "main"
+ branch_prefix:
+ description: "Prefix for the new version branch"
+ required: false
+ default: "version/"
+ commit_changes:
+ description: "Commit and push changes"
+ required: false
+ default: "true"
+ type: choice
+ options:
+ - "true"
+ - "false"
permissions:
contents: write
jobs:
- create-version-branch:
- name: Create version branch and update versions
+ version-bump:
runs-on: ubuntu-latest
env:
+ NEW_VERSION: ${{ github.event.inputs.new_version }}
BASE_BRANCH: ${{ github.event.inputs.base_branch }}
+ BRANCH_PREFIX: ${{ github.event.inputs.branch_prefix }}
+ COMMIT_CHANGES: ${{ github.event.inputs.commit_changes }}
steps:
- - name: Check out repository
+ - name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ env.BASE_BRANCH }}
- - name: Determine new version (input or auto-increment)
- id: version
- env:
- INPUT_VERSION: ${{ github.event.inputs.new_version }}
+ - name: Validate inputs
+ shell: bash
run: |
- python << 'PY'
- import os
- import re
- import subprocess
+ set -Eeuo pipefail
+ trap 'echo "[FATAL] Validation error at line $LINENO" >&2' ERR
- input_version = os.environ.get("INPUT_VERSION", "").strip()
+ echo "[INFO] Inputs received:"
+ echo " NEW_VERSION=${NEW_VERSION}"
+ echo " BASE_BRANCH=${BASE_BRANCH}"
+ echo " BRANCH_PREFIX=${BRANCH_PREFIX}"
- if input_version:
- new_version = input_version
- else:
- completed = subprocess.run(
- ["git", "ls-remote", "--heads", "origin"],
- check=True,
- capture_output=True,
- text=True,
- )
+ [[ -n "${NEW_VERSION}" ]] || { echo "[ERROR] new_version missing"; exit 2; }
+ [[ "${NEW_VERSION}" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$ ]] || {
+ echo "[ERROR] Invalid version format: ${NEW_VERSION}"
+ exit 2
+ }
- pattern = re.compile(r"refs/heads/version/([0-9]+\.[0-9]+\.[0-9]+)$")
- versions = []
+ git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}" || {
+ echo "[ERROR] Base branch does not exist on origin: ${BASE_BRANCH}"
+ git branch -a
+ exit 2
+ }
- for line in completed.stdout.splitlines():
- parts = line.split()
- if len(parts) != 2:
- continue
- ref = parts[1]
- m = pattern.search(ref)
- if not m:
- continue
+ echo "[INFO] Input validation passed"
- v_str = m.group(1)
- try:
- major, minor, patch = map(int, v_str.split("."))
- except ValueError:
- continue
- versions.append((major, minor, patch))
-
- if versions:
- major, minor, patch = max(versions)
- patch += 1
- else:
- major, minor, patch = 1, 0, 0
-
- new_version = f"{major:02d}.{minor:02d}.{patch:02d}"
-
- print(f"Using version {new_version}")
-
- with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as fh:
- fh.write(f"new_version={new_version}\n")
- PY
-
- - name: Compute branch name
- id: branch
- env:
- NEW_VERSION: ${{ steps.version.outputs.new_version }}
- run: |
- SAFE_VERSION="${NEW_VERSION// /-}"
- BRANCH_NAME="version/${SAFE_VERSION}"
- echo "Using branch name: $BRANCH_NAME"
- echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
-
- - name: Create version branch from base
- run: |
- git checkout -b "${{ steps.branch.outputs.branch_name }}"
-
- - name: Bump version strings across repo
- env:
- NEW_VERSION: ${{ steps.version.outputs.new_version }}
- run: |
- echo "Updating version strings to ${NEW_VERSION} across repository"
-
- python << 'PY'
- import os
- import re
- from pathlib import Path
-
- new_version = os.environ["NEW_VERSION"]
-
- targets = [
- Path("src"),
- Path("docs"),
- ]
-
- patterns = [
- (re.compile(r"\(VERSION\s+[0-9]+\.[0-9]+\.[0-9]+\)"), lambda v: f"(VERSION {v})"),
- (re.compile(r"(VERSION[:\s]+)([0-9]+\.[0-9]+\.[0-9]+)"), lambda v: r"\1" + v),
- (re.compile(r"()([0-9]+\.[0-9]+\.[0-9]+)()"), lambda v: r"\1" + v + r"\3"),
- (re.compile(r'(]*\\bversion=")([0-9]+\.[0-9]+\.[0-9]+)(")'), lambda v: r"\1" + v + r"\3"),
- (re.compile(r'(\"version\"\s*:\s*\")(\d+\.\d+\.\d+)(\")'), lambda v: r"\1" + v + r"\3"),
- ]
-
- def update_file(path: Path) -> bool:
- try:
- text = path.read_text(encoding="utf-8")
- except UnicodeDecodeError:
- return False
-
- original = text
- for regex, repl in patterns:
- text = regex.sub(lambda m, r=repl: r(m), text)
-
- if text != original:
- path.write_text(text, encoding="utf-8")
- print(f"Updated version in {path}")
- return True
-
- return False
-
- for root in targets:
- if not root.exists():
- continue
-
- for path in root.rglob("*"):
- if not path.is_file():
- continue
-
- if path.suffix.lower() in {
- ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico",
- ".zip", ".pdf", ".tar", ".gz",
- }:
- continue
-
- update_file(path)
-
- repo_root = Path(".").resolve()
-
- def is_under_any_target(p: Path) -> bool:
- for t in targets:
- if not t.exists():
- continue
- try:
- p.resolve().relative_to(t.resolve())
- return True
- except ValueError:
- continue
- return False
-
- # Explicitly update README.md and CHANGELOG.md
- for fname in ["README.md", "CHANGELOG.md"]:
- p = Path(fname)
- if p.exists() and p.is_file():
- update_file(p)
-
- # Update remaining markdown files outside src/docs
- for path in repo_root.rglob("*.md"):
- if not path.is_file():
- continue
-
- if path.name.lower() in {"readme.md", "changelog.md"}:
- continue
-
- if is_under_any_target(path):
- continue
-
- update_file(path)
- PY
-
- - name: Update CHANGELOG using script
- env:
- NEW_VERSION: ${{ steps.version.outputs.new_version }}
- run: |
- if [ ! -f "scripts/update_changelog.sh" ]; then
- echo "scripts/update_changelog.sh not found. Failing version branch creation."
- exit 1
- fi
-
- chmod +x scripts/update_changelog.sh
- ./scripts/update_changelog.sh "${NEW_VERSION}"
-
- - name: Configure git user
+ - name: Configure git identity
+ shell: bash
run: |
+ set -Eeuo pipefail
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
+ echo "[INFO] Git identity configured"
- - name: Commit version bump
- env:
- NEW_VERSION: ${{ steps.version.outputs.new_version }}
+ - name: Create version branch
+ shell: bash
run: |
- git status
- if git diff --quiet; then
- echo "No changes detected after version bump. Skipping commit."
- exit 0
+ set -Eeuo pipefail
+ trap 'echo "[FATAL] Branch creation failed at line $LINENO" >&2' ERR
+
+ BRANCH_NAME="${BRANCH_PREFIX}${NEW_VERSION}"
+ echo "[INFO] Creating branch ${BRANCH_NAME}"
+
+ git fetch --all --tags --prune
+
+ if git ls-remote --exit-code --heads origin "${BRANCH_NAME}" >/dev/null 2>&1; then
+ echo "[ERROR] Branch already exists on origin: ${BRANCH_NAME}"
+ exit 2
fi
- git add -A
+ git checkout -B "${BRANCH_NAME}" "origin/${BASE_BRANCH}"
- git commit -m "chore: bump version to ${NEW_VERSION}"
-
- - name: Push version branch
+ - name: Bump versions in headers and XML
+ shell: bash
run: |
- git push -u origin "${{ steps.branch.outputs.branch_name }}"
+ set -Eeuo pipefail
+ trap 'echo "[FATAL] Version bump failed at line $LINENO" >&2' ERR
+
+ python3 - <<'PY'
+ import re
+ from pathlib import Path
+ import os
+
+ new_version = os.environ["NEW_VERSION"]
+ root = Path(".").resolve()
+
+ header_re = re.compile(r"(?m)^(\\s*VERSION\\s*:\\s*)(\\d{2}\\.\\d{2}\\.\\d{2})(\\s*)$")
+ xml_re = re.compile(r"(?is)()(\\s*\\d{2}\\.\\d{2}\\.\\d{2}\\s*)()")
+
+ updated = []
+
+ for p in root.rglob("*"):
+ if not p.is_file():
+ continue
+ if p.suffix.lower() == ".json":
+ continue
+ try:
+ text = p.read_text(encoding="utf-8")
+ except Exception:
+ continue
+
+ original = text
+ text = header_re.sub(r"\\1" + new_version + r"\\3", text)
+ if p.suffix.lower() == ".xml":
+ text = xml_re.sub(r"\\1" + new_version + r"\\3", text)
+
+ if text != original:
+ p.write_text(text, encoding="utf-8")
+ updated.append(str(p))
+
+ if not updated:
+ raise SystemExit("[ERROR] No files updated. Check headers and XML manifests.")
+
+ print(f"[INFO] Updated {len(updated)} files")
+ for f in updated:
+ print(f" - {f}")
+ PY
+
+ - name: Commit changes
+ if: ${{ env.COMMIT_CHANGES == 'true' }}
+ shell: bash
+ run: |
+ set -Eeuo pipefail
+ git status --porcelain
+ git add -A
+ git commit -m "chore(release): bump version to ${NEW_VERSION}"
+
+ - name: Push branch
+ if: ${{ env.COMMIT_CHANGES == 'true' }}
+ shell: bash
+ run: |
+ set -Eeuo pipefail
+ git push --set-upstream origin "${BRANCH_PREFIX}${NEW_VERSION}"