Update version_branch.yml

This commit is contained in:
2025-12-18 18:17:33 -06:00
parent 07752aedac
commit 757cc0d611

View File

@@ -1,4 +1,31 @@
#
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# FILE INFORMATION
# DEFGROUP: GitHub.Workflow
# INGROUP: Versioning.Branching
# REPO: https://github.com/mokoconsulting-tech/MokoStandards
# PATH: /.github/workflows/version_branch.yml
# VERSION: 01.00.00
# BRIEF: Create a version branch and align versions across governed files
# NOTE: Enterprise gates: policy checks, collision defense, manifest targeting, audit summary
name: Create version branch and bump versions name: Create version branch and bump versions
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -9,10 +36,10 @@ on:
description: "Base branch to branch from" description: "Base branch to branch from"
required: false required: false
default: "dev" default: "dev"
branch_prefix: type: choice
description: "Prefix for the new version branch" options:
required: false - "dev"
default: "dev/" - "main"
commit_changes: commit_changes:
description: "Commit and push changes" description: "Commit and push changes"
required: false required: false
@@ -36,7 +63,7 @@ jobs:
env: env:
NEW_VERSION: ${{ github.event.inputs.new_version }} NEW_VERSION: ${{ github.event.inputs.new_version }}
BASE_BRANCH: ${{ github.event.inputs.base_branch }} BASE_BRANCH: ${{ github.event.inputs.base_branch }}
BRANCH_PREFIX: ${{ github.event.inputs.branch_prefix }} BRANCH_PREFIX: dev/
COMMIT_CHANGES: ${{ github.event.inputs.commit_changes }} COMMIT_CHANGES: ${{ github.event.inputs.commit_changes }}
steps: steps:
@@ -59,7 +86,12 @@ jobs:
echo " COMMIT_CHANGES=${COMMIT_CHANGES}" echo " COMMIT_CHANGES=${COMMIT_CHANGES}"
[[ -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 [[ "${BASE_BRANCH}" != "dev" && "${BASE_BRANCH}" != "main" ]]; then
echo "[ERROR] base_branch must be dev or main" >&2
exit 2
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
@@ -70,6 +102,47 @@ jobs:
echo "[INFO] Input validation passed" echo "[INFO] Input validation passed"
- name: Enterprise policy gate (required files)
shell: bash
run: |
set -Eeuo pipefail
trap 'echo "[FATAL] Policy gate failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR
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
if [[ -f ".github/CODEOWNERS" ]] && [[ ! -s ".github/CODEOWNERS" ]]; then
echo "[ERROR] .github/CODEOWNERS exists but is empty" >&2
exit 2
fi
echo "[INFO] Policy gate passed"
- name: Configure git identity - name: Configure git identity
shell: bash shell: bash
run: | run: |
@@ -80,6 +153,26 @@ jobs:
git config user.email "github-actions[bot]@users.noreply.github.com" git config user.email "github-actions[bot]@users.noreply.github.com"
echo "[INFO] Git identity configured" echo "[INFO] Git identity configured"
- name: Branch namespace collision defense
shell: bash
run: |
set -Eeuo pipefail
trap 'echo "[FATAL] Collision defense failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR
# Git cannot create refs like dev/03.02.00 if a ref named dev already exists.
# This is a known enterprise failure mode. We fail fast with a deterministic diagnostic.
PREFIX_TOP="${BRANCH_PREFIX%%/*}"
if git ls-remote --exit-code --heads origin "${PREFIX_TOP}" >/dev/null 2>&1; then
echo "[ERROR] Branch namespace collision detected" >&2
echo "[ERROR] A branch named '${PREFIX_TOP}' exists on origin, so '${BRANCH_PREFIX}<version>' cannot be created." >&2
echo "[ERROR] Remediation options:" >&2
echo " - Change BRANCH_PREFIX to a non colliding namespace (example: version/dev/)" >&2
echo " - Rename the existing '${PREFIX_TOP}' branch (organizational policy permitting)" >&2
exit 2
fi
echo "[INFO] No namespace collision detected for BRANCH_PREFIX=${BRANCH_PREFIX}"
- name: Create and push version branch - name: Create and push version branch
shell: bash shell: bash
run: | run: |
@@ -102,25 +195,11 @@ jobs:
echo "[INFO] Pushing new branch to origin" echo "[INFO] Pushing new branch to origin"
git push --set-upstream origin "${BRANCH_NAME}" git push --set-upstream origin "${BRANCH_NAME}"
- name: Ensure CHANGELOG.md has an H2 immediately after TODO block (repo creation) - name: Ensure CHANGELOG.md has a release entry and a VERSION line
shell: bash shell: bash
run: | run: |
set -Eeuo pipefail set -Eeuo pipefail
trap 'echo "[FATAL] CHANGELOG initialization failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR trap 'echo "[FATAL] CHANGELOG enforcement failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR
if [[ ! -f "CHANGELOG.md" ]]; then
echo "[INFO] CHANGELOG.md missing. Pulling baseline from MokoDefaults/generic-git (main)."
BASE_URL="https://raw.githubusercontent.com/mokoconsulting-tech/MokoDefaults/main/generic-git/CHANGELOG.md"
if ! curl -fsSL "${BASE_URL}" -o CHANGELOG.md; then
echo "[FATAL] Unable to fetch baseline CHANGELOG.md from: ${BASE_URL}" >&2
echo "[FATAL] Validate repository visibility and path: MokoDefaults/main/generic-git/CHANGELOG.md" >&2
exit 2
fi
echo "[INFO] Baseline CHANGELOG.md retrieved"
fi
python3 - <<'PY' python3 - <<'PY'
import os import os
@@ -128,19 +207,25 @@ jobs:
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
new_version = os.environ.get('NEW_VERSION', '').strip() or '00.00.00' nl = chr(10)
cr = chr(13)
new_version = (os.environ.get('NEW_VERSION') or '').strip() or '00.00.00'
p = Path('CHANGELOG.md') p = Path('CHANGELOG.md')
if not p.exists():
raise SystemExit('[FATAL] CHANGELOG.md missing')
text = p.read_text(encoding='utf-8', errors='replace').splitlines(True) text = p.read_text(encoding='utf-8', errors='replace').splitlines(True)
todo_re = re.compile(r'^\s*##\s*(?:\[\s*TODO\s*\]|TODO)\s*$', re.IGNORECASE) todo_re = re.compile(r'^[ ]*##[ ]*(?:\[[ ]*TODO[ ]*\]|TODO)[ ]*$', re.IGNORECASE)
h2_re = re.compile(r'^##\s+') bullet_re = re.compile(r'^[ ]*[-*+][ ]+')
bullet_re = re.compile(r'^\s*[-*+]\s+') blank_re = re.compile(r'^[ ]*$')
blank_re = re.compile(r'^\s*$') unreleased_re = re.compile(r'^[ ]*##[ ]*(?:\[[ ]*UNRELEASED[ ]*\]|UNRELEASED)[ ]*$', re.IGNORECASE)
idx = None idx = None
for i, line in enumerate(text): for i, line in enumerate(text):
clean = line.lstrip('\ufeff').rstrip('\n').rstrip('\r') clean = line.lstrip(chr(65279)).rstrip(nl).rstrip(cr)
if todo_re.match(clean): if todo_re.match(clean):
idx = i idx = i
break break
@@ -152,7 +237,7 @@ jobs:
j = idx + 1 j = idx + 1
saw_bullet = False saw_bullet = False
while j < len(text): while j < len(text):
line = text[j].rstrip('\n').rstrip('\r') line = text[j].rstrip(nl).rstrip(cr)
if bullet_re.match(line): if bullet_re.match(line):
saw_bullet = True saw_bullet = True
j += 1 j += 1
@@ -163,175 +248,300 @@ jobs:
break break
if not saw_bullet: if not saw_bullet:
print('[INFO] TODO section missing bullet list, inserting placeholder bullet') text.insert(idx + 1, '- Placeholder TODO item' + nl)
text.insert(idx + 1, '- Placeholder TODO item\n')
j = idx + 2 j = idx + 2
# UNRELEASED is for code going into the next release, distinct from TODO.
# Release behavior:
# - If an UNRELEASED H2 exists, convert it into this release version heading (preserving its bullets).
# - Then ensure a fresh UNRELEASED section exists immediately after TODO.
unreleased_re = re.compile(r'^\s*##\s*(?:\[\s*UNRELEASED\s*\]|UNRELEASED)\s*$', re.IGNORECASE)
stamp = datetime.now(timezone.utc).strftime('%Y-%m-%d') stamp = datetime.now(timezone.utc).strftime('%Y-%m-%d')
version_heading = "## [" + new_version + "] " + stamp + "\n" version_heading = '## [' + new_version + '] ' + stamp + nl
target_prefix = '## [' + new_version + '] '
if any(l.strip().startswith(target_prefix) for l in text):
print('[INFO] Version H2 already present. No action taken.')
raise SystemExit(0)
# 1) If UNRELEASED exists, replace it with the version heading
unreleased_idx = None unreleased_idx = None
for i, line in enumerate(text): for i, line in enumerate(text):
if unreleased_re.match(line.strip()): if unreleased_re.match(line.strip()):
unreleased_idx = i unreleased_idx = i
break break
def ensure_version_line(at_index: int) -> None:
k = at_index + 1
while k < len(text) and blank_re.match(text[k].rstrip(nl).rstrip(cr)):
k += 1
if k >= len(text) or not text[k].lstrip().startswith('- VERSION:'):
text.insert(at_index + 1, '- VERSION: ' + new_version + nl)
text.insert(at_index + 2, '- Version bump' + nl)
if unreleased_idx is not None: if unreleased_idx is not None:
# Avoid duplicate: if this version already exists anywhere, do nothing
target_prefix = "## [" + new_version + "] "
if any(l.strip().startswith(target_prefix) for l in text):
print('[INFO] Version H2 already present. No action taken.')
raise SystemExit(0)
text[unreleased_idx] = version_heading text[unreleased_idx] = version_heading
print('[INFO] Replaced UNRELEASED H2 with version heading') ensure_version_line(unreleased_idx)
insert_unreleased = nl + '## [UNRELEASED]' + nl + '- Placeholder for next release' + nl + nl
# After converting UNRELEASED to a release, insert a new UNRELEASED section right after TODO block
insert_unreleased = chr(10) + "## [UNRELEASED]" + chr(10) + "- Placeholder for next release" + chr(10) + chr(10)
text.insert(j, insert_unreleased) text.insert(j, insert_unreleased)
p.write_text(''.join(text), encoding='utf-8') p.write_text(''.join(text), encoding='utf-8')
raise SystemExit(0) raise SystemExit(0)
# 2) No UNRELEASED section present: insert a new version section after TODO block insert = (
# Avoid duplicate nl +
target_prefix = "## [" + new_version + "] " '## [' + new_version + '] ' + stamp + nl +
if any(line.strip().startswith(target_prefix) for line in text): '- VERSION: ' + new_version + nl +
print('[INFO] Version H2 already present. No action taken.') '- Version bump' + nl
raise SystemExit(0) )
insert = chr(10) + "## [" + new_version + "] " + stamp + chr(10) + "- Version bump" + chr(10)
print('[INFO] Inserting version H2 after TODO block')
text.insert(j, insert) text.insert(j, insert)
p.write_text(''.join(text), encoding='utf-8') p.write_text(''.join(text), encoding='utf-8')
PY PY
- name: Preflight discovery (repo-wide excluding .github) - name: Preflight discovery (governed version markers outside .github)
shell: bash shell: bash
run: | run: |
set -Eeuo pipefail set -Eeuo pipefail
trap 'echo "[FATAL] Preflight failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR trap 'echo "[FATAL] Preflight failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR
echo "[INFO] Scanning all directories except .github" echo "[INFO] Scanning all directories except .github"
HIT_VERSION=0
HIT_XML=0
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)
HIT_VERSION=${COUNT} echo "[INFO] VERSION: hits (repo-wide): ${COUNT}"
echo "[INFO] VERSION: hits (repo-wide): ${HIT_VERSION}"
COUNT=$(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)
HIT_XML=${COUNT} echo "[INFO] <version> hits (repo-wide): ${COUNT2}"
echo "[INFO] <version> hits (repo-wide): ${HIT_XML}"
if [[ "${HIT_VERSION}" -eq 0 && "${HIT_XML}" -eq 0 ]]; then if [[ "${COUNT}" -eq 0 && "${COUNT2}" -eq 0 ]]; then
echo "[ERROR] No VERSION: (NN.NN.NN) or <version> tags found outside .github" >&2 echo "[ERROR] No VERSION: (NN.NN.NN) or <version> tags found outside .github" >&2
exit 2 exit 2
fi fi
- name: Bump versions in headers and XML (repo-wide excluding .github) - name: Bump versions and update manifest dates (targeted, excluding .github)
id: bump
shell: bash shell: bash
run: | run: |
set -Eeuo pipefail set -Eeuo pipefail
trap 'echo "[FATAL] Version bump failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR trap 'echo "[FATAL] Version bump failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR
python3 - <<'PY' python3 - <<'PY'
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
new_version = os.environ.get("NEW_VERSION", "").strip() new_version = (os.environ.get('NEW_VERSION') or '').strip()
if not new_version: if not new_version:
raise SystemExit("[FATAL] NEW_VERSION env var missing") raise SystemExit('[FATAL] NEW_VERSION env var missing')
root = Path(".").resolve() stamp = datetime.now(timezone.utc).strftime('%Y-%m-%d')
targets = [root] root = Path('.').resolve()
header_re = re.compile(r"(?im)(VERSION\s*:\s*)(\d{2}\.\d{2}\.\d{2})") header_re = re.compile(r'(?im)(VERSION[ ]*:[ ]*)([0-9]{2}[.][0-9]{2}[.][0-9]{2})')
xml_re = re.compile(r"(?is)(<version\s*>)([^<]*?)(</version\s*>)")
skip_ext = {".json", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".pdf", ".zip", ".7z", ".tar", ".gz", ".woff", ".woff2", ".ttf", ".otf", ".mp3", ".mp4"} # Joomla manifest targeting: only update XML files that look like extension manifests.
skip_dirs = {".git", ".github", "node_modules", "vendor", ".venv", "dist", "build"} manifest_marker_re = re.compile(r'(?is)<extension')
xml_version_re = re.compile(r'(?is)(<version[ ]*>)([^<]*?)(</version[ ]*>)')
xml_date_res = [
re.compile(r'(?is)(<creationDate[ ]*>)([^<]*?)(</creationDate[ ]*>)'),
re.compile(r'(?is)(<date[ ]*>)([^<]*?)(</date[ ]*>)'),
re.compile(r'(?is)(<releaseDate[ ]*>)([^<]*?)(</releaseDate[ ]*>)'),
]
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) counters = defaultdict(int)
updated = [] updated = []
updated_manifests = []
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
existing_targets = [t for t in targets if t.exists() and t.is_dir()] for p in root.rglob('*'):
if not existing_targets: if not p.is_file():
raise SystemExit("[ERROR] Repository root not found") continue
if should_skip(p):
continue
print(f"[INFO] Scanning repository (excluding: {', '.join(sorted(skip_dirs))})") try:
text = p.read_text(encoding='utf-8', errors='replace')
except Exception as e:
counters['skipped_read_error'] += 1
continue
for base in existing_targets: original = text
for p in base.rglob("*"):
if not p.is_file():
continue
if should_skip(p):
continue
try: # Header VERSION: bumps across governed files
text = p.read_text(encoding="utf-8", errors="replace") text, n1 = header_re.subn(lambda m: m.group(1) + new_version, text)
except Exception as e: if n1:
counters["skipped_read_error"] += 1 counters['header_replacements'] += n1
print(f"[WARN] Read error: {p} :: {e}")
continue
original = text # Targeted manifest updates only (avoid rewriting random XML)
if p.suffix.lower() == '.xml':
text, n1 = header_re.subn(lambda m: m.group(1) + new_version, text) if manifest_marker_re.search(text):
if n1: text2, n2 = xml_version_re.subn(lambda m: m.group(1) + new_version + m.group(3), text)
counters["header_replacements"] += n1
if p.suffix.lower() == ".xml":
text2, n2 = xml_re.subn(lambda m: m.group(1) + new_version + m.group(3), text)
text = text2 text = text2
if n2: if n2:
counters["xml_replacements"] += n2 counters['xml_version_replacements'] += n2
if text != original: for rx in xml_date_res:
try: text3, n3 = rx.subn(lambda m: m.group(1) + stamp + m.group(3), text)
p.write_text(text, encoding="utf-8") text = text3
updated.append(str(p)) if n3:
except Exception as e: counters['xml_date_replacements'] += n3
raise SystemExit(f"[FATAL] Write failed: {p} :: {e}")
print("[INFO] Scan summary") if text != original:
updated_manifests.append(str(p))
else:
counters['xml_skipped_non_manifest'] += 1
if text != original:
p.write_text(text, encoding='utf-8')
updated.append(str(p))
report = {
'new_version': new_version,
'stamp_utc': stamp,
'counters': dict(counters),
'updated_files': updated,
'updated_manifests': updated_manifests,
}
Path('.github').mkdir(parents=True, exist_ok=True)
Path('.github/version-bump-report.json').write_text(json.dumps(report, indent=2), encoding='utf-8')
print('[INFO] Scan summary')
for k in sorted(counters.keys()): for k in sorted(counters.keys()):
print(f" {k}: {counters[k]}") print(' ' + k + ': ' + str(counters[k]))
print(f"[INFO] Updated files: {len(updated)}") print('[INFO] Updated files: ' + str(len(updated)))
for f in updated[:200]: print('[INFO] Updated manifests: ' + str(len(updated_manifests)))
print(f" [UPDATED] {f}")
if len(updated) > 200:
print(f" [INFO] (truncated) +{len(updated) - 200} more")
if not updated: if not updated:
print("[ERROR] No files updated in repository (excluding .github)") raise SystemExit('[FATAL] No files updated (excluding .github)')
print("[DIAG] Confirm these exist outside .github:")
print(" - A line containing: VERSION: <value>")
print(" - An XML tag: <version>...</version>")
raise SystemExit(1)
PY PY
echo "report_path=.github/version-bump-report.json" >> "$GITHUB_OUTPUT"
- name: Post bump audit (version consistency)
shell: bash
run: |
set -Eeuo pipefail
trap 'echo "[FATAL] Audit failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR
python3 - <<'PY'
import os
import re
from pathlib import Path
new_version = (os.environ.get('NEW_VERSION') or '').strip()
if not new_version:
raise SystemExit('[FATAL] NEW_VERSION env var missing')
root = Path('.').resolve()
skip_dirs = {'.git', '.github', 'node_modules', 'vendor', '.venv', 'dist', 'build'}
header_re = re.compile(r'(?im)VERSION[ ]*:[ ]*([0-9]{2}[.][0-9]{2}[.][0-9]{2})')
xml_version_re = re.compile(r'(?is)<version[ ]*>([^<]*?)</version[ ]*>')
mismatches = []
for p in root.rglob('*'):
if not p.is_file():
continue
parts = {x.lower() for x in p.parts}
if any(d in parts for d in skip_dirs):
continue
if p.suffix.lower() in {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf', '.zip', '.7z', '.tar', '.gz', '.woff', '.woff2', '.ttf', '.otf', '.mp3', '.mp4', '.json'}:
continue
try:
text = p.read_text(encoding='utf-8', errors='replace')
except Exception:
continue
for m in header_re.finditer(text):
if m.group(1).strip() != new_version:
mismatches.append(str(p) + ' :: VERSION: ' + m.group(1).strip())
if p.suffix.lower() == '.xml' and '<extension' in text.lower():
for m in xml_version_re.finditer(text):
if m.group(1).strip() != new_version:
mismatches.append(str(p) + ' :: <version> ' + m.group(1).strip())
if mismatches:
print('[ERROR] Version consistency audit failed. Mismatches found:')
for x in mismatches[:200]:
print(' ' + x)
raise SystemExit(2)
print('[INFO] Version consistency audit passed')
PY
- name: Change scope allowlist (block unexpected edits)
shell: bash
run: |
set -Eeuo pipefail
trap 'echo "[FATAL] Change scope gate 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. Scope gate skipped."
exit 0
fi
echo "[INFO] Evaluating changed paths"
git diff --name-only > /tmp/changed_paths.txt
# Allowlist patterns for this workflow.
# Note: .github is excluded from staging later, but we still allow the bump report file.
allow_re='^(CHANGELOG\.md|src/.*\.xml|.*templateDetails\.xml|.*manifest.*\.xml|.*\.md|\.github/version-bump-report\.json)$'
bad=0
while IFS= read -r p; do
if [[ ! "${p}" =~ ${allow_re} ]]; then
echo "[ERROR] Unexpected file modified by version workflow: ${p}" >&2
bad=1
fi
done < /tmp/changed_paths.txt
if [[ "${bad}" -ne 0 ]]; then
echo "[FATAL] Scope gate failed. Update allowlist or adjust bump targeting." >&2
exit 2
fi
echo "[INFO] Scope gate passed"
- name: Publish audit trail to job summary
shell: bash
run: |
set -Eeuo pipefail
trap 'echo "[FATAL] Summary publish failed at line $LINENO" >&2; echo "[FATAL] Last command: $BASH_COMMAND" >&2' ERR
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 "- New branch: ${BRANCH_NAME}" >> "$GITHUB_STEP_SUMMARY"
echo "- Version: ${NEW_VERSION}" >> "$GITHUB_STEP_SUMMARY"
echo "- Commit changes: ${COMMIT_CHANGES}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [[ -f ".github/version-bump-report.json" ]]; then
echo "## Bump report" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "\`\`\`json" >> "$GITHUB_STEP_SUMMARY"
head -c 12000 ".github/version-bump-report.json" >> "$GITHUB_STEP_SUMMARY" || true
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Show git status - name: Show git status
shell: bash shell: bash
run: | run: |