From 17b8d5f25427e7b1890b266c8aece419edbb9dcf Mon Sep 17 00:00:00 2001
From: Jonathan Miller <230051081+jmiller-moko@users.noreply.github.com>
Date: Tue, 23 Dec 2025 18:13:00 -0600
Subject: [PATCH] Update version_branch.yml
---
.github/workflows/version_branch.yml | 965 +++++++++++++--------------
1 file changed, 482 insertions(+), 483 deletions(-)
diff --git a/.github/workflows/version_branch.yml b/.github/workflows/version_branch.yml
index 2c33cea..bcd2885 100644
--- a/.github/workflows/version_branch.yml
+++ b/.github/workflows/version_branch.yml
@@ -27,494 +27,493 @@
name: Create version branch and bump versions
on:
- workflow_dispatch:
- inputs:
- new_version:
- description: "New version in format NN.NN.NN (example 03.01.00)"
- required: true
- type: string
- version_text:
- description: "Optional version label text (example: LTS, RC1, hotfix)"
- required: false
- default: ""
- type: string
- report_only:
- description: "Report only mode (no branch creation, no file writes, report output only)"
- required: false
- default: "false"
- type: choice
- options:
- - "true"
- - "false"
- commit_changes:
- description: "Commit and push changes (forced to true when report_only=false)"
- required: false
- default: "true"
- type: choice
- options:
- - "true"
- - "false"
+ workflow_dispatch:
+ inputs:
+ new_version:
+ description: "New version in format NN.NN.NN (example 03.01.00)"
+ required: true
+ type: string
+ version_text:
+ description: "Optional version label text (example: LTS, RC1, hotfix)"
+ required: false
+ default: ""
+ type: string
+ report_only:
+ description: "Report only mode (no branch creation, no file writes, report output only)"
+ required: false
+ default: "false"
+ type: choice
+ options:
+ - "true"
+ - "false"
+ commit_changes:
+ description: "Commit and push changes (forced to true when report_only=false)"
+ required: false
+ default: "true"
+ type: choice
+ options:
+ - "true"
+ - "false"
concurrency:
- group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.inputs.new_version }}
- cancel-in-progress: false
+ group: ${{ github.workflow }}-${{ github.repository }}-${{ github.event.inputs.new_version }}
+ cancel-in-progress: false
permissions:
- contents: write
+ contents: write
defaults:
- run:
- shell: bash
+ run:
+ shell: bash
jobs:
- version-bump:
- name: Version branch and bump
- runs-on: ubuntu-latest
-
- env:
- NEW_VERSION: ${{ github.event.inputs.new_version }}
- VERSION_TEXT: ${{ github.event.inputs.version_text }}
- REPORT_ONLY: ${{ github.event.inputs.report_only }}
- COMMIT_CHANGES: ${{ github.event.inputs.commit_changes }}
- BASE_BRANCH: ${{ github.ref_name }}
- BRANCH_PREFIX: dev/
- ERROR_LOG: /tmp/version_branch_errors.log
- CI_HELPERS: /tmp/moko_ci_helpers.sh
- REPORT_PATH: ${{ runner.temp }}/version-bump-report.json
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- ref: ${{ github.ref_name }}
-
- - name: Init CI helpers
- run: |
- set -Eeuo pipefail
- : > "$ERROR_LOG"
-
- cat > "$CI_HELPERS" <<'SH'
- set -Eeuo pipefail
-
- moko_init() {
- local step_name="${1:-step}"
- export PS4='+ ['"${step_name}"':${BASH_SOURCE##*/}:${LINENO}] '
- set -x
- trap "moko_on_err '${step_name}' \"\$LINENO\" \"\$BASH_COMMAND\"" ERR
- }
-
- moko_on_err() {
- local step_name="$1"
- local line_no="$2"
- local last_cmd="$3"
-
- echo "[FATAL] ${step_name} failed at line ${line_no}" >&2
- echo "[FATAL] Last command: ${last_cmd}" >&2
-
- if [[ -n "${ERROR_LOG:-}" ]]; then
- echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | ${step_name} | line ${line_no} | ${last_cmd}" >> "$ERROR_LOG" || true
- fi
- }
-
- moko_bool() {
- local v="${1:-false}"
- [[ "${v}" == "true" ]]
- }
-
- moko_trim() {
- local s="${1:-}"
- s="${s#${s%%[![:space:]]*}}"
- s="${s%${s##*[![:space:]]}}"
- printf '%s' "$s"
- }
- SH
-
- chmod 0755 "$CI_HELPERS"
-
- - name: Validate inputs and policy locks
- run: |
- source "$CI_HELPERS"
- moko_init "Validate inputs and policy locks"
-
- VERSION_TEXT="$(moko_trim "${VERSION_TEXT}")"
-
- echo "[INFO] Inputs received:"
- echo " NEW_VERSION=${NEW_VERSION}"
- echo " VERSION_TEXT=${VERSION_TEXT}"
- echo " REPORT_ONLY=${REPORT_ONLY}"
- echo " COMMIT_CHANGES=${COMMIT_CHANGES}"
- echo " BASE_BRANCH=${BASE_BRANCH}"
- echo " BRANCH_PREFIX=${BRANCH_PREFIX}"
-
- [[ -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; }
-
- if [[ "${BRANCH_PREFIX}" != "dev/" ]]; then
- echo "[FATAL] BRANCH_PREFIX is locked by policy. Expected 'dev/' but got '${BRANCH_PREFIX}'." >&2
- exit 2
- fi
-
- if ! moko_bool "${REPORT_ONLY}" && [[ "${COMMIT_CHANGES}" != "true" ]]; then
- echo "[FATAL] commit_changes must be 'true' when report_only is 'false' to ensure the branch is auditable." >&2
- exit 2
- fi
-
- if [[ -n "${VERSION_TEXT}" ]]; then
- if [[ ! "${VERSION_TEXT}" =~ ^[A-Za-z0-9._-]{1,32}$ ]]; then
- echo "[FATAL] version_text must match ^[A-Za-z0-9._-]{1,32}$ when set." >&2
- exit 2
- fi
- fi
-
- 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:" >&2
- git ls-remote --heads origin | awk '{sub("refs/heads/","",$2); print $2}' >&2
- exit 2
- }
-
- echo "VERSION_TEXT=${VERSION_TEXT}" >> "$GITHUB_ENV"
-
- - name: Sanity check workflow file (no literal tabs or control chars)
- run: |
- source "$CI_HELPERS"
- moko_init "Sanity check workflow file"
-
- python3 - <<'PY'
- from pathlib import Path
-
- target = Path('.github/workflows/version_branch.yml')
- if not target.exists():
- raise SystemExit('[FATAL] Missing workflow file: .github/workflows/version_branch.yml')
-
- data = target.read_bytes()
-
- # Disallow literal tab (0x09) and other ASCII control characters except LF (0x0A) and CR (0x0D).
- # Report line numbers without printing the raw characters.
-
- def byte_to_line(blob: bytes, idx: int) -> int:
- return blob[:idx].count(b'
-') + 1
-
- bad = []
- for i, b in enumerate(data):
- if b == 0x09:
- bad.append(('TAB', i, b))
- elif b < 0x20 and b not in (0x0A, 0x0D):
- bad.append(('CTRL', i, b))
-
- if bad:
- print('[ERROR] Disallowed characters detected in workflow file:')
- for kind, off, val in bad[:200]:
- line_no = byte_to_line(data, off)
- if kind == 'TAB':
- print(f' line {line_no}: TAB_PRESENT')
- else:
- print(f' line {line_no}: CTRL_0x{val:02X}_PRESENT')
- raise SystemExit(2)
-
- print('[INFO] Sanity check passed')
- PY
-
- - name: Enterprise policy gate
- run: |
- source "$CI_HELPERS"
- moko_init "Enterprise policy gate"
-
- required=(
- "LICENSE.md"
- "CONTRIBUTING.md"
- "CODE_OF_CONDUCT.md"
- "SECURITY.md"
- "GOVERNANCE.md"
- "CHANGELOG.md"
- )
-
- missing=0
- for f in "${required[@]}"; do
- if [[ ! -f "${f}" ]]; then
- echo "[ERROR] Missing required file: ${f}" >&2
- missing=1
- continue
- fi
- if [[ ! -s "${f}" ]]; then
- echo "[ERROR] Required file is empty: ${f}" >&2
- missing=1
- continue
- fi
- done
-
- if [[ "${missing}" -ne 0 ]]; then
- echo "[FATAL] Policy gate failed. Add missing governance artifacts before versioning." >&2
- exit 2
- fi
-
- echo "[INFO] Policy gate passed"
-
- - name: Branch namespace collision defense
- run: |
- source "$CI_HELPERS"
- moko_init "Branch namespace collision defense"
-
- PREFIX_TOP="${BRANCH_PREFIX%%/*}"
- if git ls-remote --exit-code --heads origin "${PREFIX_TOP}" >/dev/null 2>&1; then
- echo "[FATAL] Branch namespace collision detected: '${PREFIX_TOP}' exists on origin." >&2
- exit 2
- fi
-
- - name: Configure git identity
- if: ${{ env.REPORT_ONLY != 'true' }}
- run: |
- source "$CI_HELPERS"
- moko_init "Configure git identity"
-
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
-
- - name: Create version branch (local)
- if: ${{ env.REPORT_ONLY != 'true' }}
- run: |
- source "$CI_HELPERS"
- moko_init "Create version branch (local)"
-
- BRANCH_NAME="${BRANCH_PREFIX}${NEW_VERSION}"
- echo "[INFO] Creating local 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 "[FATAL] 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: Enforce update feed files absent (update.xml, updates.xml)
- if: ${{ env.REPORT_ONLY != 'true' }}
- run: |
- source "$CI_HELPERS"
- moko_init "Enforce update feed deletion"
-
- git rm -f --ignore-unmatch update.xml updates.xml || true
- rm -f update.xml updates.xml || true
-
- if [[ -f update.xml || -f updates.xml ]]; then
- echo "[FATAL] update feed files still present after deletion attempt." >&2
- ls -la update.xml updates.xml 2>/dev/null || true
- exit 2
- fi
-
- - name: Preflight discovery (governed version markers outside .github)
- run: |
- source "$CI_HELPERS"
- moko_init "Preflight discovery"
-
- COUNT=$(grep -RIn --exclude-dir=.git --exclude-dir=.github -i -E "VERSION[[:space:]]*:[[:space:]]*[0-9]{2}[.][0-9]{2}[.][0-9]{2}" . | wc -l || true)
- COUNT2=$(grep -RIn --exclude-dir=.git --exclude-dir=.github " hits (repo wide): ${COUNT2}"
-
- if [[ "${COUNT}" -eq 0 && "${COUNT2}" -eq 0 ]]; then
- echo "[FATAL] No governed version markers found outside .github" >&2
- exit 2
- fi
-
- - name: Bump versions and update manifest dates (targeted, excluding .github)
- run: |
- source "$CI_HELPERS"
- moko_init "Version bump"
-
- python3 - <<'PY'
- import json
- import os
- import re
- from pathlib import Path
- from collections import defaultdict
- from datetime import datetime, timezone
-
- new_version = (os.environ.get("NEW_VERSION") or "").strip()
- version_text = (os.environ.get("VERSION_TEXT") or "").strip()
- report_only = (os.environ.get("REPORT_ONLY") or "").strip().lower() == "true"
- report_path = (os.environ.get("REPORT_PATH") or "").strip()
-
- stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d")
- root = Path(".").resolve()
-
- # Use escape sequences only. Do not introduce literal tab characters.
- header_re = re.compile(r"(?im)(VERSION[ ]*:[ ]*)([0-9]{2}[.][0-9]{2}[.][0-9]{2})")
- manifest_marker_re = re.compile(r"(?is))([^<]*?)()")
- xml_date_res = [
- re.compile(r"(?is)()([^<]*?)()"),
- re.compile(r"(?is)()([^<]*?)()"),
- 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", ".github", "node_modules", "vendor", ".venv", "dist", "build"}
-
- counters = defaultdict(int)
- updated_files = []
- updated_manifests = []
- would_update_files = []
- would_update_manifests = []
-
- exclude_root = {"update.xml", "updates.xml"}
-
- 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
-
- for p in root.rglob("*"):
- if not p.is_file():
- continue
- if should_skip(p):
- continue
-
- if p.parent == root and p.name.lower() in exclude_root:
- counters["skipped_release_artifacts"] += 1
- continue
-
- try:
- original = p.read_text(encoding="utf-8", errors="replace")
- except Exception:
- counters["skipped_read_error"] += 1
- continue
-
- text = original
-
- text, n1 = header_re.subn(lambda m: m.group(1) + new_version, text)
- if n1:
- counters["header_replacements"] += n1
-
- is_manifest = (p.suffix.lower() == ".xml" and manifest_marker_re.search(original) is not None)
- if is_manifest:
- text, n2 = xml_version_re.subn(lambda m: m.group(1) + new_version + m.group(3), text)
- if n2:
- counters["xml_version_replacements"] += n2
-
- for rx in xml_date_res:
- text, n3 = rx.subn(lambda m: m.group(1) + stamp + m.group(3), text)
- if n3:
- counters["xml_date_replacements"] += n3
-
- if text != original:
- would_update_files.append(str(p))
- if is_manifest:
- would_update_manifests.append(str(p))
-
- if not report_only:
- p.write_text(text, encoding="utf-8")
- updated_files.append(str(p))
- if is_manifest:
- updated_manifests.append(str(p))
-
- report = {
- "mode": "report_only" if report_only else "apply",
- "new_version": new_version,
- "version_text": version_text,
- "stamp_utc": stamp,
- "counters": dict(counters),
- "updated_files": updated_files,
- "updated_manifests": updated_manifests,
- "would_update_files": would_update_files,
- "would_update_manifests": would_update_manifests,
- }
-
- Path(report_path).write_text(json.dumps(report, indent=2), encoding="utf-8")
-
- print("[INFO] Report written to:", report_path)
- print("[INFO] Mode:", report["mode"])
- print("[INFO] Would update files:", len(would_update_files))
- print("[INFO] Would update manifests:", len(would_update_manifests))
- print("[INFO] Updated files:", len(updated_files))
- print("[INFO] Updated manifests:", len(updated_manifests))
- PY
-
- - name: Commit changes
- if: ${{ env.REPORT_ONLY != 'true' }}
- run: |
- source "$CI_HELPERS"
- moko_init "Commit changes"
-
- if [[ -z "$(git status --porcelain=v1)" ]]; then
- echo "[INFO] No changes detected. Skipping commit."
- exit 0
- fi
-
- git add -A
-
- MSG="chore(release): bump version to ${NEW_VERSION}"
- if [[ -n "${VERSION_TEXT}" ]]; then
- MSG="${MSG} (${VERSION_TEXT})"
- fi
-
- git commit -m "${MSG}"
-
- - name: Push branch
- if: ${{ env.REPORT_ONLY != 'true' }}
- run: |
- source "$CI_HELPERS"
- moko_init "Push branch"
-
- if [[ -z "${BRANCH_NAME:-}" ]]; then
- echo "[FATAL] BRANCH_NAME not set." >&2
- exit 2
- fi
-
- git push --set-upstream origin "${BRANCH_NAME}"
-
- - name: Publish audit trail
- if: always()
- run: |
- source "$CI_HELPERS"
- moko_init "Publish audit trail"
-
- echo "# Version branch run" >> "$GITHUB_STEP_SUMMARY"
- echo "" >> "$GITHUB_STEP_SUMMARY"
- echo "- Repository: $GITHUB_REPOSITORY" >> "$GITHUB_STEP_SUMMARY"
- echo "- Base branch: ${BASE_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
- echo "- Branch prefix: ${BRANCH_PREFIX}" >> "$GITHUB_STEP_SUMMARY"
- echo "- New version: ${NEW_VERSION}" >> "$GITHUB_STEP_SUMMARY"
- echo "- Version text: ${VERSION_TEXT}" >> "$GITHUB_STEP_SUMMARY"
- echo "- Report only: ${REPORT_ONLY}" >> "$GITHUB_STEP_SUMMARY"
- echo "- Commit changes: ${COMMIT_CHANGES}" >> "$GITHUB_STEP_SUMMARY"
- echo "- New branch: ${BRANCH_NAME:-}" >> "$GITHUB_STEP_SUMMARY"
- echo "" >> "$GITHUB_STEP_SUMMARY"
-
- echo "## Version bump report" >> "$GITHUB_STEP_SUMMARY"
- echo "" >> "$GITHUB_STEP_SUMMARY"
-
- if [[ -f "${REPORT_PATH}" ]]; then
- echo "\`\`\`json" >> "$GITHUB_STEP_SUMMARY"
- head -c 12000 "${REPORT_PATH}" >> "$GITHUB_STEP_SUMMARY" || true
- echo "" >> "$GITHUB_STEP_SUMMARY"
- echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
- else
- echo "Report file not found at: ${REPORT_PATH}" >> "$GITHUB_STEP_SUMMARY"
- fi
-
- echo "" >> "$GITHUB_STEP_SUMMARY"
- echo "## Error summary" >> "$GITHUB_STEP_SUMMARY"
- echo "" >> "$GITHUB_STEP_SUMMARY"
-
- if [[ -f "$ERROR_LOG" && -s "$ERROR_LOG" ]]; then
- echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
- tail -n 200 "$ERROR_LOG" >> "$GITHUB_STEP_SUMMARY" || true
- echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
- else
- echo "No errors recorded." >> "$GITHUB_STEP_SUMMARY"
- fi
+ version-bump:
+ name: Version branch and bump
+ runs-on: ubuntu-latest
+
+ env:
+ NEW_VERSION: ${{ github.event.inputs.new_version }}
+ VERSION_TEXT: ${{ github.event.inputs.version_text }}
+ REPORT_ONLY: ${{ github.event.inputs.report_only }}
+ COMMIT_CHANGES: ${{ github.event.inputs.commit_changes }}
+ BASE_BRANCH: ${{ github.ref_name }}
+ BRANCH_PREFIX: dev/
+ ERROR_LOG: /tmp/version_branch_errors.log
+ CI_HELPERS: /tmp/moko_ci_helpers.sh
+ REPORT_PATH: ${{ runner.temp }}/version-bump-report.json
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ github.ref_name }}
+
+ - name: Init CI helpers
+ run: |
+ set -Eeuo pipefail
+ : > "$ERROR_LOG"
+
+ cat > "$CI_HELPERS" <<'SH'
+ set -Eeuo pipefail
+
+ moko_init() {
+ local step_name="${1:-step}"
+ export PS4='+ ['"${step_name}"':${BASH_SOURCE##*/}:${LINENO}] '
+ set -x
+ trap "moko_on_err '${step_name}' \"\$LINENO\" \"\$BASH_COMMAND\"" ERR
+ }
+
+ moko_on_err() {
+ local step_name="$1"
+ local line_no="$2"
+ local last_cmd="$3"
+
+ echo "[FATAL] ${step_name} failed at line ${line_no}" >&2
+ echo "[FATAL] Last command: ${last_cmd}" >&2
+
+ if [[ -n "${ERROR_LOG:-}" ]]; then
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | ${step_name} | line ${line_no} | ${last_cmd}" >> "$ERROR_LOG" || true
+ fi
+ }
+
+ moko_bool() {
+ local v="${1:-false}"
+ [[ "${v}" == "true" ]]
+ }
+
+ moko_trim() {
+ local s="${1:-}"
+ s="${s#${s%%[![:space:]]*}}"
+ s="${s%${s##*[![:space:]]}}"
+ printf '%s' "$s"
+ }
+ SH
+
+ chmod 0755 "$CI_HELPERS"
+
+ - name: Validate inputs and policy locks
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Validate inputs and policy locks"
+
+ VERSION_TEXT="$(moko_trim "${VERSION_TEXT}")"
+
+ echo "[INFO] Inputs received:"
+ echo " NEW_VERSION=${NEW_VERSION}"
+ echo " VERSION_TEXT=${VERSION_TEXT}"
+ echo " REPORT_ONLY=${REPORT_ONLY}"
+ echo " COMMIT_CHANGES=${COMMIT_CHANGES}"
+ echo " BASE_BRANCH=${BASE_BRANCH}"
+ echo " BRANCH_PREFIX=${BRANCH_PREFIX}"
+
+ [[ -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; }
+
+ if [[ "${BRANCH_PREFIX}" != "dev/" ]]; then
+ echo "[FATAL] BRANCH_PREFIX is locked by policy. Expected 'dev/' but got '${BRANCH_PREFIX}'." >&2
+ exit 2
+ fi
+
+ if ! moko_bool "${REPORT_ONLY}" && [[ "${COMMIT_CHANGES}" != "true" ]]; then
+ echo "[FATAL] commit_changes must be 'true' when report_only is 'false' to ensure the branch is auditable." >&2
+ exit 2
+ fi
+
+ if [[ -n "${VERSION_TEXT}" ]]; then
+ if [[ ! "${VERSION_TEXT}" =~ ^[A-Za-z0-9._-]{1,32}$ ]]; then
+ echo "[FATAL] version_text must match ^[A-Za-z0-9._-]{1,32}$ when set." >&2
+ exit 2
+ fi
+ fi
+
+ 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:" >&2
+ git ls-remote --heads origin | awk '{sub("refs/heads/","",$2); print $2}' >&2
+ exit 2
+ }
+
+ echo "VERSION_TEXT=${VERSION_TEXT}" >> "$GITHUB_ENV"
+
+ - name: Sanity check workflow file (no literal tabs or control chars)
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Sanity check workflow file"
+
+ python3 - <<'PY'
+ from pathlib import Path
+
+ target = Path('.github/workflows/version_branch.yml')
+ if not target.exists():
+ raise SystemExit('[FATAL] Missing workflow file: .github/workflows/version_branch.yml')
+
+ data = target.read_bytes()
+
+ # Disallow literal tab (0x09) and other ASCII control characters except LF (0x0A) and CR (0x0D).
+ # Report line numbers without printing the raw characters.
+
+ def byte_to_line(blob: bytes, idx: int) -> int:
+ return blob[:idx].count(b'\n') + 1
+
+ bad = []
+ for i, b in enumerate(data):
+ if b == 0x09:
+ bad.append(('TAB', i, b))
+ elif b < 0x20 and b not in (0x0A, 0x0D):
+ bad.append(('CTRL', i, b))
+
+ if bad:
+ print('[ERROR] Disallowed characters detected in workflow file:')
+ for kind, off, val in bad[:200]:
+ line_no = byte_to_line(data, off)
+ if kind == 'TAB':
+ print(f' line {line_no}: TAB_PRESENT')
+ else:
+ print(f' line {line_no}: CTRL_0x{val:02X}_PRESENT')
+ raise SystemExit(2)
+
+ print('[INFO] Sanity check passed')
+ PY
+
+ - name: Enterprise policy gate
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Enterprise policy gate"
+
+ required=(
+ "LICENSE.md"
+ "CONTRIBUTING.md"
+ "CODE_OF_CONDUCT.md"
+ "SECURITY.md"
+ "GOVERNANCE.md"
+ "CHANGELOG.md"
+ )
+
+ missing=0
+ for f in "${required[@]}"; do
+ if [[ ! -f "${f}" ]]; then
+ echo "[ERROR] Missing required file: ${f}" >&2
+ missing=1
+ continue
+ fi
+ if [[ ! -s "${f}" ]]; then
+ echo "[ERROR] Required file is empty: ${f}" >&2
+ missing=1
+ continue
+ fi
+ done
+
+ if [[ "${missing}" -ne 0 ]]; then
+ echo "[FATAL] Policy gate failed. Add missing governance artifacts before versioning." >&2
+ exit 2
+ fi
+
+ echo "[INFO] Policy gate passed"
+
+ - name: Branch namespace collision defense
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Branch namespace collision defense"
+
+ PREFIX_TOP="${BRANCH_PREFIX%%/*}"
+ if git ls-remote --exit-code --heads origin "${PREFIX_TOP}" >/dev/null 2>&1; then
+ echo "[FATAL] Branch namespace collision detected: '${PREFIX_TOP}' exists on origin." >&2
+ exit 2
+ fi
+
+ - name: Configure git identity
+ if: ${{ env.REPORT_ONLY != 'true' }}
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Configure git identity"
+
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Create version branch (local)
+ if: ${{ env.REPORT_ONLY != 'true' }}
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Create version branch (local)"
+
+ BRANCH_NAME="${BRANCH_PREFIX}${NEW_VERSION}"
+ echo "[INFO] Creating local 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 "[FATAL] 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: Enforce update feed files absent (update.xml, updates.xml)
+ if: ${{ env.REPORT_ONLY != 'true' }}
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Enforce update feed deletion"
+
+ git rm -f --ignore-unmatch update.xml updates.xml || true
+ rm -f update.xml updates.xml || true
+
+ if [[ -f update.xml || -f updates.xml ]]; then
+ echo "[FATAL] update feed files still present after deletion attempt." >&2
+ ls -la update.xml updates.xml 2>/dev/null || true
+ exit 2
+ fi
+
+ - name: Preflight discovery (governed version markers outside .github)
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Preflight discovery"
+
+ COUNT=$(grep -RIn --exclude-dir=.git --exclude-dir=.github -i -E "VERSION[[:space:]]*:[[:space:]]*[0-9]{2}[.][0-9]{2}[.][0-9]{2}" . | wc -l || true)
+ COUNT2=$(grep -RIn --exclude-dir=.git --exclude-dir=.github " hits (repo wide): ${COUNT2}"
+
+ if [[ "${COUNT}" -eq 0 && "${COUNT2}" -eq 0 ]]; then
+ echo "[FATAL] No governed version markers found outside .github" >&2
+ exit 2
+ fi
+
+ - name: Bump versions and update manifest dates (targeted, excluding .github)
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Version bump"
+
+ python3 - <<'PY'
+ import json
+ import os
+ import re
+ from pathlib import Path
+ from collections import defaultdict
+ from datetime import datetime, timezone
+
+ new_version = (os.environ.get("NEW_VERSION") or "").strip()
+ version_text = (os.environ.get("VERSION_TEXT") or "").strip()
+ report_only = (os.environ.get("REPORT_ONLY") or "").strip().lower() == "true"
+ report_path = (os.environ.get("REPORT_PATH") or "").strip()
+
+ stamp = datetime.now(timezone.utc).strftime("%Y-%m-%d")
+ root = Path(".").resolve()
+
+ # Use escape sequences only. Do not introduce literal tab characters.
+ header_re = re.compile(r"(?im)(VERSION[ \t]*:[ \t]*)([0-9]{2}[.][0-9]{2}[.][0-9]{2})")
+ manifest_marker_re = re.compile(r"(?is))([^<]*?)()")
+ xml_date_res = [
+ re.compile(r"(?is)()([^<]*?)()"),
+ re.compile(r"(?is)()([^<]*?)()"),
+ 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", ".github", "node_modules", "vendor", ".venv", "dist", "build"}
+
+ counters = defaultdict(int)
+ updated_files = []
+ updated_manifests = []
+ would_update_files = []
+ would_update_manifests = []
+
+ exclude_root = {"update.xml", "updates.xml"}
+
+ 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
+
+ for p in root.rglob("*"):
+ if not p.is_file():
+ continue
+ if should_skip(p):
+ continue
+
+ if p.parent == root and p.name.lower() in exclude_root:
+ counters["skipped_release_artifacts"] += 1
+ continue
+
+ try:
+ original = p.read_text(encoding="utf-8", errors="replace")
+ except Exception:
+ counters["skipped_read_error"] += 1
+ continue
+
+ text = original
+
+ text, n1 = header_re.subn(lambda m: m.group(1) + new_version, text)
+ if n1:
+ counters["header_replacements"] += n1
+
+ is_manifest = (p.suffix.lower() == ".xml" and manifest_marker_re.search(original) is not None)
+ if is_manifest:
+ text, n2 = xml_version_re.subn(lambda m: m.group(1) + new_version + m.group(3), text)
+ if n2:
+ counters["xml_version_replacements"] += n2
+
+ for rx in xml_date_res:
+ text, n3 = rx.subn(lambda m: m.group(1) + stamp + m.group(3), text)
+ if n3:
+ counters["xml_date_replacements"] += n3
+
+ if text != original:
+ would_update_files.append(str(p))
+ if is_manifest:
+ would_update_manifests.append(str(p))
+
+ if not report_only:
+ p.write_text(text, encoding="utf-8")
+ updated_files.append(str(p))
+ if is_manifest:
+ updated_manifests.append(str(p))
+
+ report = {
+ "mode": "report_only" if report_only else "apply",
+ "new_version": new_version,
+ "version_text": version_text,
+ "stamp_utc": stamp,
+ "counters": dict(counters),
+ "updated_files": updated_files,
+ "updated_manifests": updated_manifests,
+ "would_update_files": would_update_files,
+ "would_update_manifests": would_update_manifests,
+ }
+
+ Path(report_path).write_text(json.dumps(report, indent=2), encoding="utf-8")
+
+ print("[INFO] Report written to:", report_path)
+ print("[INFO] Mode:", report["mode"])
+ print("[INFO] Would update files:", len(would_update_files))
+ print("[INFO] Would update manifests:", len(would_update_manifests))
+ print("[INFO] Updated files:", len(updated_files))
+ print("[INFO] Updated manifests:", len(updated_manifests))
+ PY
+
+ - name: Commit changes
+ if: ${{ env.REPORT_ONLY != 'true' }}
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Commit changes"
+
+ if [[ -z "$(git status --porcelain=v1)" ]]; then
+ echo "[INFO] No changes detected. Skipping commit."
+ exit 0
+ fi
+
+ git add -A
+
+ MSG="chore(release): bump version to ${NEW_VERSION}"
+ if [[ -n "${VERSION_TEXT}" ]]; then
+ MSG="${MSG} (${VERSION_TEXT})"
+ fi
+
+ git commit -m "${MSG}"
+
+ - name: Push branch
+ if: ${{ env.REPORT_ONLY != 'true' }}
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Push branch"
+
+ if [[ -z "${BRANCH_NAME:-}" ]]; then
+ echo "[FATAL] BRANCH_NAME not set." >&2
+ exit 2
+ fi
+
+ git push --set-upstream origin "${BRANCH_NAME}"
+
+ - name: Publish audit trail
+ if: always()
+ run: |
+ source "$CI_HELPERS"
+ moko_init "Publish audit trail"
+
+ echo "# Version branch run" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Repository: $GITHUB_REPOSITORY" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Base branch: ${BASE_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Branch prefix: ${BRANCH_PREFIX}" >> "$GITHUB_STEP_SUMMARY"
+ echo "- New version: ${NEW_VERSION}" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Version text: ${VERSION_TEXT}" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Report only: ${REPORT_ONLY}" >> "$GITHUB_STEP_SUMMARY"
+ echo "- Commit changes: ${COMMIT_CHANGES}" >> "$GITHUB_STEP_SUMMARY"
+ echo "- New branch: ${BRANCH_NAME:-}" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+
+ echo "## Version bump report" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+
+ if [[ -f "${REPORT_PATH}" ]]; then
+ echo "\`\`\`json" >> "$GITHUB_STEP_SUMMARY"
+ head -c 12000 "${REPORT_PATH}" >> "$GITHUB_STEP_SUMMARY" || true
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "Report file not found at: ${REPORT_PATH}" >> "$GITHUB_STEP_SUMMARY"
+ fi
+
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+ echo "## Error summary" >> "$GITHUB_STEP_SUMMARY"
+ echo "" >> "$GITHUB_STEP_SUMMARY"
+
+ if [[ -f "$ERROR_LOG" && -s "$ERROR_LOG" ]]; then
+ echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
+ tail -n 200 "$ERROR_LOG" >> "$GITHUB_STEP_SUMMARY" || true
+ echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
+ else
+ echo "No errors recorded." >> "$GITHUB_STEP_SUMMARY"
+ fi