diff --git a/.github/workflows/version_branch.yml b/.github/workflows/version_branch.yml index 77c3849..7721da6 100644 --- a/.github/workflows/version_branch.yml +++ b/.github/workflows/version_branch.yml @@ -1,10 +1,10 @@ -name: Create version branch and bump versions +name: Create version branch and bump versions (src and docs only) on: workflow_dispatch: inputs: new_version: - description: "New version in format NN.NN.NN (example 01.03.00)" + description: "New version in format NN.NN.NN (example 03.01.00)" required: true base_branch: description: "Base branch to branch from" @@ -47,22 +47,21 @@ jobs: shell: bash run: | set -Eeuo pipefail - trap 'echo "[FATAL] Validation error at line $LINENO" >&2' ERR + trap 'echo "[FATAL] Validation error at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR echo "[INFO] Inputs received:" echo " NEW_VERSION=${NEW_VERSION}" echo " BASE_BRANCH=${BASE_BRANCH}" echo " BRANCH_PREFIX=${BRANCH_PREFIX}" + echo " COMMIT_CHANGES=${COMMIT_CHANGES}" - [[ -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 - } + [[ -n "${NEW_VERSION}" ]] || { echo "[ERROR] new_version missing" >&2; exit 2; } + [[ "${NEW_VERSION}" =~ ^[0-9]{2}\.[0-9]{2}\.[0-9]{2}$ ]] || { echo "[ERROR] Invalid version format: ${NEW_VERSION}" >&2; exit 2; } - git show-ref --verify --quiet "refs/remotes/origin/${BASE_BRANCH}" || { - echo "[ERROR] Base branch does not exist on origin: ${BASE_BRANCH}" - git branch -a + git ls-remote --exit-code --heads origin "${BASE_BRANCH}" >/dev/null 2>&1 || { + echo "[ERROR] Base branch does not exist on origin: ${BASE_BRANCH}" >&2 + echo "[INFO] Remote branches:" + git ls-remote --heads origin | awk '{sub("refs/heads/","",$2); print $2}' exit 2 } @@ -72,6 +71,8 @@ jobs: shell: bash run: | set -Eeuo pipefail + trap 'echo "[FATAL] Git identity step failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR + git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" echo "[INFO] Git identity configured" @@ -80,78 +81,198 @@ jobs: shell: bash run: | set -Eeuo pipefail - trap 'echo "[FATAL] Branch creation failed at line $LINENO" >&2' ERR + trap 'echo "[FATAL] Branch creation failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR BRANCH_NAME="${BRANCH_PREFIX}${NEW_VERSION}" - echo "[INFO] Creating branch ${BRANCH_NAME}" + echo "[INFO] Creating branch: ${BRANCH_NAME} from origin/${BASE_BRANCH}" 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}" + echo "[ERROR] Branch already exists on origin: ${BRANCH_NAME}" >&2 exit 2 fi git checkout -B "${BRANCH_NAME}" "origin/${BASE_BRANCH}" + echo "BRANCH_NAME=${BRANCH_NAME}" >> "$GITHUB_ENV" - - name: Bump versions in headers and XML + - name: Preflight discovery (src and docs only) shell: bash run: | set -Eeuo pipefail - trap 'echo "[FATAL] Version bump failed at line $LINENO" >&2' ERR + trap 'echo "[FATAL] Preflight failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR + + TARGET_DIRS=("src" "docs") + + echo "[INFO] Confirming target directories exist" + FOUND_ANY=0 + for d in "${TARGET_DIRS[@]}"; do + if [[ -d "${d}" ]]; then + echo "[INFO] Found directory: ${d}" + FOUND_ANY=1 + else + echo "[WARN] Missing directory: ${d}" + fi + done + + if [[ "${FOUND_ANY}" -ne 1 ]]; then + echo "[ERROR] Neither ./src nor ./docs exists in this checkout" >&2 + exit 2 + fi + + echo "[INFO] Searching for VERSION: lines under src and docs" + HIT_VERSION=0 + for d in "${TARGET_DIRS[@]}"; do + [[ -d "${d}" ]] || continue + COUNT=$(grep -RIn --exclude-dir=.git "^[[:space:]]*VERSION[[:space:]]*:" "${d}" | wc -l || true) + echo "[INFO] VERSION: hits in ${d}: ${COUNT}" + HIT_VERSION=$((HIT_VERSION + COUNT)) + done + + echo "[INFO] Searching for XML tags under src and docs" + HIT_XML=0 + for d in "${TARGET_DIRS[@]}"; do + [[ -d "${d}" ]] || continue + COUNT=$(grep -RIn --exclude-dir=.git " hits in ${d}: ${COUNT}" + HIT_XML=$((HIT_XML + COUNT)) + done + + echo "[INFO] Total VERSION: hits: ${HIT_VERSION}" + echo "[INFO] Total hits: ${HIT_XML}" + + - name: Bump versions in headers and XML (src and docs only) + shell: bash + run: | + set -Eeuo pipefail + trap 'echo "[FATAL] Version bump failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR python3 - <<'PY' + import os import re from pathlib import Path - import os + from collections import defaultdict + + new_version = os.environ.get("NEW_VERSION", "").strip() + if not new_version: + raise SystemExit("[FATAL] NEW_VERSION env var missing") - new_version = os.environ["NEW_VERSION"] root = Path(".").resolve() + targets = [root / "src", root / "docs"] - 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*)()") + header_re = re.compile(r"(?m)^(\s*VERSION\s*:\s*)(\S+)(\s*)$") + xml_re = re.compile(r"(?is)()([^<]*?)()") + skip_ext = {".json", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".pdf", ".zip", ".7z", ".tar", ".gz", ".woff", ".woff2", ".ttf", ".otf", ".mp3", ".mp4"} + skip_dirs = {".git", "node_modules", "vendor", ".venv", "dist", "build"} + + counters = defaultdict(int) 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 + def should_skip(p: Path) -> bool: + if p.suffix.lower() in skip_ext: + counters["skipped_by_ext"] += 1 + return True + parts = {x.lower() for x in p.parts} + if any(d in parts for d in skip_dirs): + counters["skipped_by_dir"] += 1 + return True + return False - 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) + existing_targets = [t for t in targets if t.exists() and t.is_dir()] + if not existing_targets: + raise SystemExit("[ERROR] Neither ./src nor ./docs exists in this checkout") - if text != original: - p.write_text(text, encoding="utf-8") - updated.append(str(p)) + print("[INFO] Scanning directories:") + for t in existing_targets: + print(f" - {t}") + + for base in existing_targets: + for p in base.rglob("*"): + if not p.is_file(): + continue + if should_skip(p): + continue + + try: + text = p.read_text(encoding="utf-8") + except UnicodeDecodeError: + counters["skipped_non_utf8"] += 1 + continue + except Exception as e: + counters["skipped_read_error"] += 1 + print(f"[WARN] Read error: {p} :: {e}") + continue + + original = text + + text, n1 = header_re.subn(r"\\1" + new_version + r"\\3", text) + if n1: + counters["header_replacements"] += n1 + + if p.suffix.lower() == ".xml": + text2, n2 = xml_re.subn(r"\\1" + new_version + r"\\3", text) + text = text2 + if n2: + counters["xml_replacements"] += n2 + + if text != original: + try: + p.write_text(text, encoding="utf-8") + updated.append(str(p)) + except Exception as e: + raise SystemExit(f"[FATAL] Write failed: {p} :: {e}") + + print("[INFO] Scan summary") + for k in sorted(counters.keys()): + print(f" {k}: {counters[k]}") + + print(f"[INFO] Updated files: {len(updated)}") + for f in updated[:200]: + print(f" [UPDATED] {f}") + if len(updated) > 200: + print(f" [INFO] (truncated) +{len(updated) - 200} more") 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}") + print("[ERROR] No files updated within src and docs") + print("[DIAG] Confirm these exist in src or docs:") + print(" - A line containing: VERSION: ") + print(" - An XML tag: ...") + raise SystemExit(1) PY + - name: Show git status + shell: bash + run: | + set -Eeuo pipefail + trap 'echo "[FATAL] git status failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR + git status --porcelain=v1 + - name: Commit changes if: ${{ env.COMMIT_CHANGES == 'true' }} shell: bash run: | set -Eeuo pipefail - git status --porcelain - git add -A + trap 'echo "[FATAL] Commit failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR + + if [[ -z "$(git status --porcelain=v1)" ]]; then + echo "[INFO] No changes detected. Skipping commit and push." + exit 0 + fi + + git add src docs 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}" + trap 'echo "[FATAL] Push failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR + + git push --set-upstream origin "${BRANCH_NAME}" + + - name: Output branch name + shell: bash + run: | + set -Eeuo pipefail + echo "[INFO] Created branch: ${BRANCH_NAME}"