Files
psxrecomp/tools/generate_ci.py
Alex Vanderveen d371d74421 tools: generate_ci -- the release workflow for an existing project, and nothing else
sh psxrecomp/tools/generate_ci.sh            # in the project
    powershell -File psxrecomp\tools\generate_ci.ps1
    ... [root] [--check] [--force] [--dry-run] [--zip-prefix X] [--title X]

Project Studio already knows how to emit the setup-host release workflow
(emit_ci_workflow, behind `apply` and `git install-ci`), but reaching it
meant a migration plan or a commit-and-push. This is the single-purpose
face of that operation: it resolves the template from the project's own
psxrecomp submodule, takes the zip prefix from
scripts/package_setup_release.sh so CI uploads the zips the packager
built, writes .github/workflows/release.yml, and stops.

--check reports missing / placeholder / stale (step names the template
has and the file lacks) with exit 1, so a fleet can be audited without
writing anything; only --force overwrites. The .sh and .ps1 are thin
launchers over the one Python implementation.

Verified read-only against two ports: ApeEscapeRecomp reports stale
(four emitter/ccache steps missing), TombaRecomp reports current.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0173EhspvAdRQc9DZdDr97Hg
2026-09-05 18:53:24 -04:00

116 lines
4.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""generate_ci.py -- write the setup-host release workflow into an EXISTING
PSX project. Nothing else: no commit, no push, no migration plan.
python3 psxrecomp/tools/generate_ci.py # cwd is the project
python3 psxrecomp/tools/generate_ci.py ~/src/MyGameRecomp
python3 psxrecomp/tools/generate_ci.py --check # stale? (exit 1)
python3 psxrecomp/tools/generate_ci.py --force # overwrite
This is the thin, single-purpose face of Project Studio's emit_ci_workflow
operation (tools/new_project_layout/project_studio/ops.py), which is also
what `migrate_project.py apply` and `git install-ci` run. The template is
docs/ci/templates/setup-release.yml from the project's own psxrecomp
submodule when it has one, so the workflow matches the framework the project
pins; this checkout is the fallback.
Where the values come from:
ZIP_PREFIX scripts/package_setup_release.sh --zip-prefix <x> (the
packager's prefix, so the zips CI uploads are the zips it
built), else derived from the project name, else --zip-prefix
GAME_TITLE CMakeLists.txt / game.toml window_title, else --title
An existing, filled release.yml is not overwritten silently: its step names
are compared with the template's, and a step the template defines that the
file lacks means the file predates a framework change (reported as stale).
--force overwrites; a customised workflow is replaced wholesale, so diff it.
"""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
HERE = pathlib.Path(__file__).resolve().parent
sys.path.insert(0, str(HERE / "new_project_layout"))
WORKFLOW_REL = pathlib.Path(".github") / "workflows" / "release.yml"
def zip_prefix_from_packager(root: pathlib.Path) -> str:
try:
text = (root / "scripts" / "package_setup_release.sh").read_text(
encoding="utf-8", errors="replace")
except OSError:
return ""
m = re.search(r"--zip-prefix\s+([^\s\\'\"]+)", text)
return m.group(1) if m else ""
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0],
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="\n".join(__doc__.split("\n\n")[1:]))
ap.add_argument("root", nargs="?", default=".", help="project root (default: cwd)")
ap.add_argument("--zip-prefix", default="", help="override the release zip prefix")
ap.add_argument("--title", default="", help="override the game title in release names")
ap.add_argument("--force", action="store_true", help="overwrite an existing release.yml")
ap.add_argument("--check", action="store_true",
help="report whether release.yml is present and current; exit 1 if not")
ap.add_argument("--dry-run", action="store_true", help="resolve and report, write nothing")
args = ap.parse_args()
root = pathlib.Path(args.root).resolve()
if not (root / "CMakeLists.txt").is_file():
print(f"generate_ci: {root} has no CMakeLists.txt -- not a project root", file=sys.stderr)
return 2
try:
from project_studio.models import MigrateOptions
from project_studio.ops import op_emit_ci_workflow, _ci_steps_missing_from
from project_studio.paths import ci_setup_release_template
except ImportError as exc:
print(f"generate_ci: cannot import Project Studio from {HERE / 'new_project_layout'}: {exc}",
file=sys.stderr)
return 2
template = ci_setup_release_template(root)
if template is None:
print("generate_ci: no docs/ci/templates/setup-release.yml found (neither the "
"project's psxrecomp submodule nor this checkout)", file=sys.stderr)
return 2
dst = root / WORKFLOW_REL
if args.check:
if not dst.is_file():
print(f"{WORKFLOW_REL}: missing")
return 1
text = dst.read_text(encoding="utf-8", errors="replace")
if "YOUR_ZIP_PREFIX" in text or "YOUR_GAME_TITLE" in text:
print(f"{WORKFLOW_REL}: still has YOUR_* placeholders")
return 1
stale = _ci_steps_missing_from(dst, template)
if stale:
print(f"{WORKFLOW_REL}: stale -- missing step(s): {', '.join(stale)}")
return 1
print(f"{WORKFLOW_REL}: present and current")
return 0
options = MigrateOptions(
zip_prefix=args.zip_prefix or zip_prefix_from_packager(root) or None,
window_title=args.title or None,
force=args.force,
dry_run=args.dry_run,
)
result = op_emit_ci_workflow(root, options)
print(f"template: {template}")
print(f"zip prefix: {options.zip_prefix or '(derived from the project name)'}")
print(result.message)
return 0 if result.ok else 1
if __name__ == "__main__":
raise SystemExit(main())