ci: fix releaser action

This commit is contained in:
George Cushen committed 2025-08-20 01:27:19 +01:00
1 parent a6dc896c98
commit 33cde7169c
1 file changed
+52 -6
+52 -6
View File
@@ -12,8 +12,8 @@ Features:
- Updates starter templates' go.mod to latest released module versions and bumps Hugo version in CI/Netlify config
Usage examples:
poetry run python scripts/release_modules.py --dry-run
poetry run python scripts/release_modules.py --yes --no-propagate
poetry run python scripts/release_modules.py --dry-run --log-level INFO
poetry run python scripts/release_modules.py --yes --no-propagate --log-level DEBUG
Notes:
- Tags must be created with the subdirectory path prefix per Go's submodule tagging rules.
@@ -30,6 +30,7 @@ import re
import subprocess
import sys
from dataclasses import dataclass, field
import logging
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
@@ -46,8 +47,15 @@ EXCLUDED_MODULE_DIRS = {"blox-bootstrap"}
def run_cmd(args: List[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess:
process = subprocess.run(args, cwd=str(cwd) if cwd else None, text=True, capture_output=True)
workdir = str(cwd) if cwd else None
logging.debug("run: %s [cwd=%s]", " ".join(args), workdir or str(REPO_ROOT))
process = subprocess.run(args, cwd=workdir, text=True, capture_output=True)
if process.stdout:
logging.debug("stdout:\n%s", process.stdout.strip())
if process.stderr:
logging.debug("stderr:\n%s", process.stderr.strip())
if check and process.returncode != 0:
logging.error("command failed (%s): %s", process.returncode, " ".join(args))
raise RuntimeError(f"Command failed: {' '.join(args)}\nSTDOUT:\n{process.stdout}\nSTDERR:\n{process.stderr}")
return process
@@ -141,12 +149,14 @@ def discover_modules() -> Dict[str, Module]:
major=major,
requires=requires,
)
logging.debug("discovered module: %s (dir=%s, major=%s, requires=%s)", module_path, rel_dir, major, sorted(list(requires)))
# Fill dependents
for mod in modules.values():
for req in list(mod.requires):
if req in modules:
modules[req].dependents.add(mod.module_path)
logging.info("discovered %d modules", len(modules))
return modules
@@ -157,6 +167,7 @@ def get_commits_since_tag(module: Module, tag: Optional[str]) -> List[str]:
else:
res = run_cmd(["git", "log", f"{tag}..HEAD", "--pretty=%s", "--", str(module.rel_dir)], cwd=REPO_ROOT)
msgs = [l.strip() for l in res.stdout.splitlines() if l.strip()]
logging.debug("%s commits since %s: %d", module.name, tag or "<none>", len(msgs))
return msgs
@@ -164,7 +175,9 @@ def has_changes_since_tag(module: Module, tag: Optional[str]) -> bool:
if not tag:
return True
res = run_cmd(["git", "diff", "--name-only", tag, "--", str(module.rel_dir)], cwd=REPO_ROOT)
return any(l.strip() for l in res.stdout.splitlines())
changed = any(l.strip() for l in res.stdout.splitlines())
logging.debug("changed since tag? %s: %s", module.name, changed)
return changed
def determine_bump_from_commits(commits: List[str]) -> str:
@@ -236,6 +249,7 @@ def write_blox_tailwind_theme_version(module: Module, version: str) -> List[Path
with data_file.open("w", encoding="utf-8") as f:
yaml.dump(data, f)
updated.append(data_file)
logging.info("bumped theme version in %s to %s", data_file, version)
return updated
@@ -257,11 +271,26 @@ def update_go_mod_require_version(go_mod_text: str, dep_module_path: str, new_ve
def stage_and_maybe_commit(paths: List[Path], message: str, commit: bool, push: bool) -> None:
if not paths:
return
str_paths = [str(p.relative_to(REPO_ROOT)) for p in paths]
str_paths: List[str] = []
for p in paths:
if p.is_absolute():
try:
rel = p.relative_to(REPO_ROOT)
str_paths.append(str(rel))
except Exception:
# Fallback to OS relpath if not within repo root
rel = os.path.relpath(str(p), str(REPO_ROOT))
str_paths.append(rel)
else:
# Use as-is relative to repo root since git cwd is REPO_ROOT
str_paths.append(str(p))
logging.info("git add: %s", ", ".join(str_paths))
run_cmd(["git", "add", *str_paths], cwd=REPO_ROOT)
if commit:
logging.info("git commit -m '%s'", message)
run_cmd(["git", "commit", "-m", message], cwd=REPO_ROOT)
if push:
logging.info("git push origin main")
run_cmd(["git", "push", "origin", "main"], cwd=REPO_ROOT)
@@ -269,7 +298,9 @@ def create_and_push_tag(module: Module, version: str, yes: bool) -> None:
tag = f"{module.rel_dir.as_posix()}/v{version}"
annotate_msg = f"{module.name}: v{version}"
if yes:
logging.info("create tag %s", tag)
run_cmd(["git", "tag", "-a", tag, "-m", annotate_msg], cwd=REPO_ROOT)
logging.info("push tag %s", tag)
run_cmd(["git", "push", "origin", tag], cwd=REPO_ROOT)
@@ -293,6 +324,7 @@ def topological_order(modules: Dict[str, Module]) -> List[Module]:
# If cycle (shouldn't happen), just return arbitrary order
if len(ordered) != len(modules):
return list(modules.values())
logging.info("release order: %s", ", ".join(m.module_path for m in ordered))
return ordered
@@ -320,6 +352,7 @@ def update_dependents_for_release(
if updated != original:
go_mod_file.write_text(updated, encoding="utf-8")
stage_and_maybe_commit([go_mod_file], f"chore({dep_mod.name}): require {released_module.name} v{released_version}", commit, push)
logging.info("updated dependent %s go.mod -> %s %s", dep_mod.name, released_module.name, released_version)
touched.append(dep_mod)
return touched
@@ -346,6 +379,7 @@ def update_starters(modules: Dict[str, Module], commit: bool, push: bool) -> Non
if new_text != text:
go_mod.write_text(new_text, encoding="utf-8")
updated_paths.append(go_mod)
logging.info("starter %s: updated go.mod module versions", starter_dir.name)
# bump hugo version in hugoblox.yaml
hb_yaml = starter_dir / "hugoblox.yaml"
@@ -359,6 +393,7 @@ def update_starters(modules: Dict[str, Module], commit: bool, push: bool) -> Non
with hb_yaml.open("w", encoding="utf-8") as f:
yaml.dump(data, f)
updated_paths.append(hb_yaml)
logging.info("starter %s: hugoblox.yaml hugo_version -> %s", starter_dir.name, latest_hugo)
# bump netlify.toml HUGO_VERSION
netlify = starter_dir / "netlify.toml"
@@ -370,6 +405,7 @@ def update_starters(modules: Dict[str, Module], commit: bool, push: bool) -> Non
env["HUGO_VERSION"] = latest_hugo
netlify.write_text(tomlkit.dumps(parsed), encoding="utf-8")
updated_paths.append(netlify)
logging.info("starter %s: netlify.toml HUGO_VERSION -> %s", starter_dir.name, latest_hugo)
# bump publish.yaml env WC_HUGO_VERSION
workflow = starter_dir / ".github" / "workflows" / "publish.yaml"
@@ -381,6 +417,7 @@ def update_starters(modules: Dict[str, Module], commit: bool, push: bool) -> Non
with workflow.open("w", encoding="utf-8") as f:
yaml.dump(wf, f)
updated_paths.append(workflow)
logging.info("starter %s: publish.yaml WC_HUGO_VERSION -> %s", starter_dir.name, latest_hugo)
if updated_paths:
stage_and_maybe_commit(updated_paths, "chore(starters): bump modules and Hugo", commit, push)
@@ -391,6 +428,7 @@ def ensure_clean_worktree() -> None:
dirty = [l for l in res.stdout.splitlines() if l.strip()]
if dirty:
raise RuntimeError("Working tree is dirty. Commit or stash changes before running with --yes. Use --dry-run to preview.")
logging.info("working tree is clean")
def main() -> None:
@@ -401,8 +439,13 @@ def main() -> None:
parser.add_argument("--no-propagate", dest="propagate", action="store_false", help="Do not bump dependents purely for dependency updates.")
parser.add_argument("--allow-major", action="store_true", help="Allow major version bumps when inferred from commits. By default majors are downgraded to minor to avoid Go module path changes.")
parser.add_argument("--print-plan", action="store_true", help="Print the planned releases as JSON.")
parser.add_argument("--log-level", default=os.getenv("LOG_LEVEL", "INFO"), help="Logging level: DEBUG, INFO, WARNING, ERROR")
args = parser.parse_args()
# Configure logging
level = getattr(logging, str(args.log_level).upper(), logging.INFO)
logging.basicConfig(level=level, format="%(asctime)s %(levelname)s %(message)s")
yes = bool(args.yes) and not bool(args.dry_run)
commit = yes
push = yes
@@ -437,6 +480,7 @@ def main() -> None:
"effective_bump": effective_bump,
}
will_release.add(m.module_path)
logging.info("plan: %s -> %s (bump=%s, effective=%s)", m.name, next_version, bump, effective_bump)
# If propagate is enabled, any dependent of a releasing module is considered for release later (due to go.mod update)
if args.propagate:
@@ -485,6 +529,7 @@ def main() -> None:
# Tag and push
create_and_push_tag(m, next_version, yes=True)
latest_versions[m.module_path] = next_version
logging.info("released %s v%s", m.name, next_version)
# Update dependents to require this new version (causes their go.mod to change)
touched_dependents = update_dependents_for_release(modules, m, next_version, commit=True, push=True)
@@ -506,13 +551,14 @@ def main() -> None:
# 3) Update Starters (excluding starters-bootstrap)
update_starters(modules, commit=True, push=True)
print("Release complete.")
logging.info("release complete")
if __name__ == "__main__":
try:
main()
except Exception as e:
logging.exception("release failed: %s", e)
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)