Update version_branch.yml

This commit is contained in:
2025-12-23 18:13:31 -06:00
parent 17b8d5f254
commit 85ca93927b

View File

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