diff --git a/.github/scripts/detect-build-matrix.py b/.github/scripts/detect-build-matrix.py index b0b6a7ff..14f6285d 100755 --- a/.github/scripts/detect-build-matrix.py +++ b/.github/scripts/detect-build-matrix.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # file: .github/scripts/detect-build-matrix.py -# version: 1.1.0 +# version: 1.1.1 # guid: a1b2c3d4-e5f6-7890-1234-56789abcdef0 +# last-edited: 2026-10-05 """Detect build matrix requirements for the repository. + This script analyzes the repository to determine what technologies are used and generates appropriate build matrices for GitHub Actions. """ @@ -74,7 +76,8 @@ def detect_build_requirements(): # Check for Python projects if any( - os.path.exists(f) for f in ["pyproject.toml", "requirements.txt", "setup.py"] + os.path.exists(f) + for f in ["pyproject.toml", "requirements.txt", "setup.py"] ) or check_file_exists("*.py"): print("Python project detected") flags["has_python"] = True @@ -103,9 +106,7 @@ def detect_build_requirements(): # Read package.json to determine if this is actually a frontend project try: with open("package.json") as f: - import json as json_lib - - pkg_data = json_lib.load(f) + pkg_data = json.load(f) # Check for frontend indicators frontend_indicators = [ @@ -154,7 +155,9 @@ def detect_build_requirements(): **pkg_data.get("devDependencies", {}), } - has_frontend_deps = any(indicator in all_deps for indicator in frontend_indicators) + has_frontend_deps = any( + indicator in all_deps for indicator in frontend_indicators + ) has_build_scripts = any( script in build_scripts for script in script_indicators @@ -197,7 +200,9 @@ def detect_build_requirements(): print("Docker project detected") flags["has_docker"] = True # Use docker-detect.py script for detailed Docker configuration - success, output = run_command("python3 .github/scripts/docker-detect.py") + success, _output = run_command( + "python3 .github/scripts/docker-detect.py" + ) if success: print("Docker detection script completed successfully") # The docker-detect.py script will set its own outputs @@ -215,7 +220,9 @@ def detect_build_requirements(): } # Check for protobuf - if any(os.path.exists(f) for f in ["buf.yaml", "buf.gen.yaml"]) or check_file_exists("*.proto"): + if any( + os.path.exists(f) for f in ["buf.yaml", "buf.gen.yaml"] + ) or check_file_exists("*.proto"): print("Protobuf project detected") flags["protobuf_needed"] = True diff --git a/.github/scripts/detect_languages.py b/.github/scripts/detect_languages.py index 39d4dff5..d375bbb2 100755 --- a/.github/scripts/detect_languages.py +++ b/.github/scripts/detect_languages.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # file: .github/scripts/detect_languages.py -# version: 1.2.0 +# version: 1.2.1 # guid: 4f6c9d88-2d4b-4a1e-9c61-3e0b2b9a7f11 +# last-edited: 2026-10-05 """Detect project languages and emit key=value lines for GitHub Actions outputs. Enhanced to respect `.github/workflow-config.yaml` for language versions, @@ -51,7 +52,11 @@ def load_build_config() -> dict[str, Any]: continue # Detect leaving build section (new top-level key) - if in_build and not line.startswith(" ") and not stripped.startswith("build:"): + if ( + in_build + and not line.startswith(" ") + and not stripped.startswith("build:") + ): # Reached a new top-level key; stop parsing build break @@ -77,7 +82,9 @@ def load_build_config() -> dict[str, Any]: key, remainder = key_match.groups() if remainder == "": # list start or empty value - current_list_key = key if key.endswith("s") else None # heuristic + current_list_key = ( + key if key.endswith("s") else None + ) # heuristic if current_list_key: build.setdefault(current_list_key, []) continue @@ -98,6 +105,7 @@ def load_build_config() -> dict[str, Any]: def exists_any(*paths: str) -> bool: + """Return True if any of the given paths exists.""" return any(os.path.exists(p) for p in paths) @@ -109,7 +117,9 @@ def exists_any(*paths: str) -> bool: else False ) has_frontend = exists_any("package.json", "yarn.lock", "pnpm-lock.yaml") -has_docker = exists_any("Dockerfile", "docker-compose.yml", "docker-compose.yaml") +has_docker = exists_any( + "Dockerfile", "docker-compose.yml", "docker-compose.yaml" +) has_rust = exists_any("Cargo.toml") protobuf_needed = ( (build_cfg.get("enable_protobuf") is True) @@ -130,8 +140,9 @@ def exists_any(*paths: str) -> bool: def build_matrix( - kind: str, versions: list[str], os_list: list[str], version_key: str + versions: list[str], os_list: list[str], version_key: str ) -> dict[str, Any]: + """Build a versions x OS include-matrix; the first entry is primary.""" include: list[dict[str, Any]] = [] if not versions or not os_list: return {"include": include} @@ -163,15 +174,17 @@ def build_matrix( platforms = build_cfg.get("platforms") or ["linux/amd64", "linux/arm64"] go_matrix = ( - build_matrix("go", go_versions, operating_systems, "go-version") if has_go else {"include": []} + build_matrix(go_versions, operating_systems, "go-version") + if has_go + else {"include": []} ) python_matrix = ( - build_matrix("python", python_versions, operating_systems, "python-version") + build_matrix(python_versions, operating_systems, "python-version") if has_python else {"include": []} ) frontend_matrix = ( - build_matrix("frontend", node_versions, operating_systems, "node-version") + build_matrix(node_versions, operating_systems, "node-version") if has_frontend else {"include": []} ) @@ -188,7 +201,8 @@ def build_matrix( ) -def emit(key: str, value): +def emit(key: str, value: object) -> None: + """Print a key=value line for GITHUB_OUTPUT.""" print(f"{key}={value}") diff --git a/.github/scripts/determine-security-languages.py b/.github/scripts/determine-security-languages.py index 7a00d909..6d5489f4 100755 --- a/.github/scripts/determine-security-languages.py +++ b/.github/scripts/determine-security-languages.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # file: .github/scripts/determine-security-languages.py -# version: 1.1.0 +# version: 1.1.1 # guid: a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d +# last-edited: 2026-10-05 """Determine which languages need security scanning based on file changes. @@ -16,8 +17,10 @@ def get_env_bool(key: str, default: bool = False) -> bool: - """Safely get a boolean environment variable.""" - value = os.environ.get(key, "").lower() + """Safely get a boolean environment variable; unset or empty -> default.""" + value = os.environ.get(key, "").strip().lower() + if not value: + return default return value in ("true", "1", "yes", "on") @@ -53,7 +56,9 @@ def determine_security_languages() -> dict[str, any]: else: matrix = {"language": languages_with_changes} has_languages = True - print(f"Languages for security scanning: {', '.join(languages_with_changes)}") + print( + f"Languages for security scanning: {', '.join(languages_with_changes)}" + ) result = { "matrix": matrix, @@ -92,9 +97,13 @@ def main() -> None: # Write outputs for GitHub Actions write_github_output("matrix", json.dumps(result["matrix"])) - write_github_output("has-languages", str(result["has_languages"]).lower()) + write_github_output( + "has-languages", str(result["has_languages"]).lower() + ) write_github_output("language-count", str(result["language_count"])) - write_github_output("languages", json.dumps(result["languages"])) # Output as JSON array + write_github_output( + "languages", json.dumps(result["languages"]) + ) # Output as JSON array print("āœ… Security language determination completed successfully") diff --git a/.github/scripts/docker-detect.py b/.github/scripts/docker-detect.py index 9517304d..c99416d3 100755 --- a/.github/scripts/docker-detect.py +++ b/.github/scripts/docker-detect.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # file: .github/scripts/docker-detect.py -# version: 1.0.1 +# version: 1.0.2 # guid: d1e2f3g4-h5i6-j7k8-l9m0-n1o2p3q4r5s6 +# last-edited: 2026-10-05 """Docker configuration detection script for matrix build system. + Detects Docker files, configurations, and determines build strategy. """ @@ -63,10 +65,8 @@ def check_docker_compose(): def should_build_docker(event_name, ref): """Determine if Docker image should be built and pushed.""" return ( - (event_name == "push" and ref == "refs/heads/main") - or event_name == "release" - or event_name == "workflow_dispatch" - ) + event_name == "push" and ref == "refs/heads/main" + ) or event_name in {"release", "workflow_dispatch"} def generate_docker_matrix(): @@ -76,7 +76,7 @@ def generate_docker_matrix(): return {"include": []} # Basic multi-platform matrix - matrix = { + return { "include": [ { "platform": "linux/amd64", @@ -93,8 +93,6 @@ def generate_docker_matrix(): ] } - return matrix - def set_github_output(key, value): """Set GitHub Actions output.""" diff --git a/.github/scripts/docker-security.py b/.github/scripts/docker-security.py index 0a4e2800..74e6b555 100755 --- a/.github/scripts/docker-security.py +++ b/.github/scripts/docker-security.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # file: .github/scripts/docker-security.py -# version: 1.0.0 +# version: 1.0.1 # guid: e2f3g4h5-i6j7-k8l9-m0n1-o2p3q4r5s6t7 +# last-edited: 2026-10-05 """Docker security scanning utilities for matrix build system. + Handles SBOM generation, image testing, and security reporting. """ @@ -11,6 +13,7 @@ import subprocess import sys + def run_command(cmd, capture_output=True, check=True): """Run a shell command and return the result.""" try: @@ -31,18 +34,19 @@ def run_command(cmd, capture_output=True, check=True): except Exception as e: return False, "", str(e) + def generate_sbom(image_ref, output_file="sbom.spdx.json"): """Generate Software Bill of Materials.""" print(f"Generating SBOM for {image_ref}...") # Try syft first (if available), fallback to docker sbom cmd = f"syft {image_ref} -o spdx-json > {output_file}" - success, stdout, stderr = run_command(cmd, check=False) + success, _stdout, stderr = run_command(cmd, check=False) if not success: # Fallback to docker sbom if syft is not available cmd = f"docker sbom {image_ref} --output {output_file}" - success, stdout, stderr = run_command(cmd, check=False) + success, _stdout, stderr = run_command(cmd, check=False) if not success: print(f"SBOM generation failed: {stderr}") @@ -51,6 +55,7 @@ def generate_sbom(image_ref, output_file="sbom.spdx.json"): print(f"SBOM generated: {output_file}") return True, output_file + def test_image_functionality(image_ref): """Test Docker image basic functionality.""" print(f"Testing image functionality: {image_ref}") @@ -65,7 +70,11 @@ def test_image_functionality(image_ref): { "name": "Container Startup", "passed": success, - "message": ("Container starts successfully" if success else f"Failed: {stderr}"), + "message": ( + "Container starts successfully" + if success + else f"Failed: {stderr}" + ), } ) @@ -78,7 +87,9 @@ def test_image_functionality(image_ref): "name": "Application Files", "passed": success, "message": ( - "Application files present" if success else "Application structure unknown" + "Application files present" + if success + else "Application structure unknown" ), } ) @@ -87,19 +98,24 @@ def test_image_functionality(image_ref): print("Checking health configuration...") cmd = f'docker inspect {image_ref} --format="{{{{.Config.Healthcheck}}}}"' success, stdout, stderr = run_command(cmd, check=False) - has_healthcheck = success and stdout != "none" and stdout != "" and stdout.strip() != "" + has_healthcheck = ( + success and stdout not in {"none", ""} and stdout.strip() != "" + ) tests.append( { "name": "Health Check", "passed": has_healthcheck, "message": ( - "Health check configured" if has_healthcheck else "No health check configured" + "Health check configured" + if has_healthcheck + else "No health check configured" ), } ) return tests + def validate_compose_files(compose_files): """Validate docker-compose files.""" results = [] @@ -107,18 +123,21 @@ def validate_compose_files(compose_files): for compose_file in compose_files: print(f"Validating {compose_file}...") cmd = f"docker-compose -f {compose_file} config" - success, stdout, stderr = run_command(cmd, check=False) + success, _stdout, stderr = run_command(cmd, check=False) results.append( { "file": compose_file, "valid": success, - "message": ("Valid configuration" if success else f"Invalid: {stderr}"), + "message": ( + "Valid configuration" if success else f"Invalid: {stderr}" + ), } ) return results + def generate_security_summary(scan_results, test_results, compose_results=None): """Generate security and testing summary for GitHub.""" summary = [] @@ -158,11 +177,14 @@ def generate_security_summary(scan_results, test_results, compose_results=None): summary.append("## 🐳 Docker Compose Validation") for result in compose_results: status = "āœ…" if result["valid"] else "āŒ" - summary.append(f"- **{result['file']}**: {status} {result['message']}") + summary.append( + f"- **{result['file']}**: {status} {result['message']}" + ) summary.append("") return "\n".join(summary) + def main(): """Main entry point.""" if len(sys.argv) < 2: @@ -181,7 +203,7 @@ def main(): print("Error: image reference required") sys.exit(1) image_ref = sys.argv[2] - success, output_file = generate_sbom(image_ref) + success, _output_file = generate_sbom(image_ref) sys.exit(0 if success else 1) elif command == "test-image": @@ -220,5 +242,6 @@ def main(): print(f"Error executing {command}: {e}") sys.exit(1) + if __name__ == "__main__": main() diff --git a/.github/scripts/generate-security-summary.py b/.github/scripts/generate-security-summary.py index 772ea1b0..c3f36978 100755 --- a/.github/scripts/generate-security-summary.py +++ b/.github/scripts/generate-security-summary.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # file: .github/scripts/generate-security-summary.py -# version: 1.0.0 +# version: 1.0.1 # guid: sec-sum-gen-2025-12-07 +# last-edited: 2026-10-05 """Generate security scan summary for GitHub Actions. @@ -12,8 +13,8 @@ from __future__ import annotations import os -import sys from pathlib import Path +import sys def main() -> int: @@ -37,7 +38,9 @@ def main() -> int: warning_required = any(status == "failure" for status in results.values()) summary_lines.append("") if warning_required: - summary_lines.append("āš ļø **Security issues detected. Please review the scan results.**") + summary_lines.append( + "āš ļø **Security issues detected. Please review the scan results.**" + ) else: summary_lines.append("āœ… **All security scans passed.**") diff --git a/.github/scripts/monitor-rollout.py b/.github/scripts/monitor-rollout.py index 89fd07f6..0c8f1da1 100755 --- a/.github/scripts/monitor-rollout.py +++ b/.github/scripts/monitor-rollout.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # file: .github/scripts/monitor-rollout.py -# version: 1.1.1 +# version: 1.1.2 # guid: 7f1a3b2c-4d5e-6f7a-8b9c-0d1e2f3a4b5c +# last-edited: 2026-10-05 -"""Monitor rollout across target repositories: +"""Monitor rollout across target repositories. - Reads target repos from .github/repositories.txt or TARGET_REPOS env (space/comma-separated) - For each repo, fetches recent GitHub Actions runs and summarizes key workflows: @@ -19,7 +20,7 @@ Auth: - Uses JF_CI_GH_PAT or GITHUB_TOKEN from environment -""" # noqa: D415 +""" import argparse import datetime as dt @@ -29,13 +30,16 @@ from urllib import error, request API_BASE = os.environ.get("GITHUB_API_URL", "https://api.github.com") +HTTP_OK = 200 def get_token() -> str | None: + """Return the GitHub token from JF_CI_GH_PAT or GITHUB_TOKEN, if set.""" return os.environ.get("JF_CI_GH_PAT") or os.environ.get("GITHUB_TOKEN") def gh_headers() -> dict[str, str]: + """Build GitHub REST API request headers, with auth when a token is set.""" headers = { "Accept": "application/vnd.github+json", "User-Agent": "ghcommon-monitor-rollout", @@ -47,9 +51,13 @@ def gh_headers() -> dict[str, str]: def http_get(url: str) -> tuple[int, dict]: - req = request.Request(url, headers=gh_headers()) + """GET a GitHub API URL; return (status, decoded JSON or error dict).""" + if not url.startswith(("https://", "http://")): + return 0, {"error": f"refusing non-HTTP(S) URL: {url}"} + # Scheme checked above, so S310's file:/custom-scheme concern is handled. + req = request.Request(url, headers=gh_headers()) # noqa: S310 try: - with request.urlopen(req, timeout=30) as resp: + with request.urlopen(req, timeout=30) as resp: # noqa: S310 status = resp.getcode() data = resp.read() try: @@ -67,10 +75,12 @@ def http_get(url: str) -> tuple[int, dict]: def now_utc() -> dt.datetime: + """Return the current time as an aware UTC datetime.""" return dt.datetime.now(dt.timezone.utc) def parse_iso8601(s: str) -> dt.datetime | None: + """Parse an ISO 8601 timestamp (trailing Z allowed); None if invalid.""" try: return dt.datetime.fromisoformat(s.replace("Z", "+00:00")) except Exception: @@ -78,20 +88,23 @@ def parse_iso8601(s: str) -> dt.datetime | None: def load_target_repos(explicit: str | None) -> list[str]: + """Resolve target repos from --repos, TARGET_REPOS or repositories.txt.""" if explicit: - parts = [p.strip() for p in explicit.replace(",", " ").split() if p.strip()] - return parts + return [ + p.strip() for p in explicit.replace(",", " ").split() if p.strip() + ] env_repos = os.environ.get("TARGET_REPOS") if env_repos: - parts = [p.strip() for p in env_repos.replace(",", " ").split() if p.strip()] - return parts + return [ + p.strip() for p in env_repos.replace(",", " ").split() if p.strip() + ] # Fallback to repositories.txt repos_file = os.path.join(os.getcwd(), ".github", "repositories.txt") repos: list[str] = [] if os.path.exists(repos_file): with open(repos_file, encoding="utf-8") as f: - for line in f: - line = line.strip() + for raw_line in f: + line = raw_line.strip() if not line or line.startswith("#"): continue # Support "repo" shorthand by defaulting owner to jdfalk @@ -103,9 +116,10 @@ def load_target_repos(explicit: str | None) -> list[str]: def list_recent_runs(repo: str, per_page: int) -> list[dict]: + """Return the most recent workflow runs for a repo ([] on API error).""" url = f"{API_BASE}/repos/{repo}/actions/runs?per_page={per_page}" status, payload = http_get(url) - if status != 200: + if status != HTTP_OK: return [] return payload.get("workflow_runs", []) or [] @@ -113,16 +127,24 @@ def list_recent_runs(repo: str, per_page: int) -> list[dict]: def pick_latest( runs: list[dict], name_contains: list[str], since_cutoff: dt.datetime ) -> dict | None: + """Return the newest run whose name contains every term, since cutoff.""" name_lc = [n.lower() for n in name_contains] for run in runs: n = (run.get("name") or "").lower() - created_at = parse_iso8601(run.get("created_at") or run.get("run_started_at") or "") - if all(term in n for term in name_lc) and created_at and created_at >= since_cutoff: + created_at = parse_iso8601( + run.get("created_at") or run.get("run_started_at") or "" + ) + if ( + all(term in n for term in name_lc) + and created_at + and created_at >= since_cutoff + ): return run return None def summarize_repo(repo: str, per_page: int, since_hours: int) -> dict: + """Summarize the latest security, release and sync runs for a repo.""" runs = list_recent_runs(repo, per_page) cutoff = now_utc() - dt.timedelta(hours=since_hours) sec = pick_latest(runs, ["security"], cutoff) @@ -138,7 +160,7 @@ def status_of(run: dict | None) -> str: # conclusion may be None while in progress return run.get("conclusion") or run.get("status") or "unknown" - result = { + return { "repo": repo, "security": { "status": status_of(sec), @@ -153,7 +175,6 @@ def status_of(run: dict | None) -> str: "url": sync.get("html_url") if sync else None, }, } - return result def write_step_summary(md: str) -> None: @@ -173,6 +194,7 @@ def write_step_summary(md: str) -> None: def build_markdown(results: list[dict]) -> str: + """Render the rollout results as a Markdown summary.""" lines = ["# Rollout Verification Summary", ""] ok_all = True for r in results: @@ -212,8 +234,11 @@ def badge(st: str) -> str: return "\n".join(lines) -def main(): - parser = argparse.ArgumentParser(description="Monitor rollout across target repositories") +def main() -> int: + """Summarize rollout status for every target repo; return the exit code.""" + parser = argparse.ArgumentParser( + description="Monitor rollout across target repositories" + ) parser.add_argument("--per-page", type=int, default=10) parser.add_argument("--since-hours", type=int, default=72) parser.add_argument("--repos", type=str, default="") @@ -224,12 +249,16 @@ def main(): print("No target repositories found.") return 1 if not get_token(): - print("Warning: No token provided (JF_CI_GH_PAT/GITHUB_TOKEN). You may hit rate limits.") + print( + "Warning: No token provided (JF_CI_GH_PAT/GITHUB_TOKEN). You may hit rate limits." + ) results: list[dict] = [] for repo in repos: try: - results.append(summarize_repo(repo, args.per_page, args.since_hours)) + results.append( + summarize_repo(repo, args.per_page, args.since_hours) + ) except Exception as e: results.append( { @@ -252,13 +281,17 @@ def ok_status(st: str) -> bool: return st in ("success", "completed") all_ok = all( - ok_status(r["security"]["status"]) and ok_status(r["release"]["status"]) # type: ignore[index] + ok_status(r["security"]["status"]) + and ok_status(r["release"]["status"]) # type: ignore[index] for r in results ) with open(github_output, "a", encoding="utf-8") as f: f.write(f"overall={'green' if all_ok else 'needs_attention'}\n") - except Exception: - pass + except Exception as e: + print( + f"::warning::could not write overall status to GITHUB_OUTPUT: {e}", + file=sys.stderr, + ) return 0 diff --git a/.github/scripts/parse_config_json.py b/.github/scripts/parse_config_json.py index 3336dfb1..ff5224b3 100755 --- a/.github/scripts/parse_config_json.py +++ b/.github/scripts/parse_config_json.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # file: .github/scripts/parse_config_json.py -# version: 1.0.0 +# version: 1.0.1 # guid: 2a1b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d +# last-edited: 2026-10-05 import json import os @@ -9,6 +10,7 @@ def main(): + """Parse CONFIG_JSON from the environment and emit its fields.""" config_json = os.environ.get("CONFIG_JSON", "{}") try: config = json.loads(config_json) diff --git a/.github/scripts/run_buf_generate.py b/.github/scripts/run_buf_generate.py index c4244bea..59eae752 100755 --- a/.github/scripts/run_buf_generate.py +++ b/.github/scripts/run_buf_generate.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # file: .github/scripts/run_buf_generate.py -# version: 1.0.0 +# version: 1.0.1 # guid: 8c5b2d4e-3f7a-45d1-9e2f-1b6c7d8e9f01 +# last-edited: 2026-10-05 """Run buf generate with basic safety checks and clearer logging.""" from __future__ import annotations @@ -12,6 +13,7 @@ def main() -> int: + """Run `buf generate` when buf.gen.yaml exists; return the exit code.""" if not os.path.exists("buf.gen.yaml"): print("No buf.gen.yaml found, skipping generation") return 0 diff --git a/.github/scripts/sync-auto-commit.py b/.github/scripts/sync-auto-commit.py index 1aefc35f..875b04ce 100755 --- a/.github/scripts/sync-auto-commit.py +++ b/.github/scripts/sync-auto-commit.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-auto-commit.py -# version: 1.0.0 +# version: 1.0.1 # guid: f7a8b9c0-d1e2-3f4a-5b6c-7d8e9f0a1b2c +# last-edited: 2026-10-05 """Auto-commit script for workflow modernization. + Creates conventional commits for workflow changes. """ @@ -12,7 +14,9 @@ def run_command(cmd, capture_output=True): """Run a shell command.""" - result = subprocess.run(cmd, check=False, shell=True, capture_output=capture_output, text=True) + result = subprocess.run( + cmd, check=False, shell=True, capture_output=capture_output, text=True + ) if result.returncode != 0 and capture_output: print(f"Error running command: {cmd}") print(f"Error: {result.stderr}") diff --git a/.github/scripts/sync-complete-workflow-modernizer.py b/.github/scripts/sync-complete-workflow-modernizer.py index 35f5112b..0be19b1f 100755 --- a/.github/scripts/sync-complete-workflow-modernizer.py +++ b/.github/scripts/sync-complete-workflow-modernizer.py @@ -1,51 +1,66 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-complete-workflow-modernizer.py -# version: 1.0.0 +# version: 1.0.1 # guid: a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6 +# last-edited: 2026-10-05 """Complete workflow modernization script. + Converts ALL workflows to use Python scripts and removes inline bash. """ import os +from pathlib import Path import re import subprocess import sys -from pathlib import Path class WorkflowModernizer: + """Rewrite workflow files to call Python scripts instead of inline bash.""" + def __init__(self, workflow_dir): + """Track the workflow directory and its sibling scripts directory.""" self.workflow_dir = Path(workflow_dir) self.scripts_dir = Path(workflow_dir).parent / "scripts" self.modernized_count = 0 def run_command(self, cmd): """Run shell command.""" - result = subprocess.run(cmd, check=False, shell=True, capture_output=True, text=True) + result = subprocess.run( + cmd, check=False, shell=True, capture_output=True, text=True + ) return result.returncode == 0, result.stdout, result.stderr def extract_inline_scripts(self, content): """Extract inline scripts from workflow and convert to Python calls.""" # Find all run: | blocks - run_blocks = re.findall(r"(\s+)run: \|\s*\n((?:\1 .*\n?)*)", content, re.MULTILINE) + run_blocks = re.findall( + r"(\s+)run: \|\s*\n((?:\1 .*\n?)*)", content, re.MULTILINE + ) script_replacements = [] for indent, script_content in run_blocks: # Clean up the script content lines = script_content.split("\n") cleaned_lines = [ - (line[len(indent) + 2 :] if line.startswith(indent + " ") else line.strip()) + ( + line[len(indent) + 2 :] + if line.startswith(indent + " ") + else line.strip() + ) for line in lines if line.strip() ] if cleaned_lines: - script_replacements.append((indent, script_content, cleaned_lines)) + script_replacements.append( + (indent, script_content, cleaned_lines) + ) return script_replacements - def modernize_workflow_content(self, content, workflow_name): + def modernize_workflow_content(self, content): """Modernize workflow content to use Python scripts.""" modernized = content @@ -84,7 +99,9 @@ def modernize_workflow_content(self, content, workflow_name): ] for pattern, replacement in replacements: - modernized = re.sub(pattern, replacement, modernized, flags=re.MULTILINE | re.DOTALL) + modernized = re.sub( + pattern, replacement, modernized, flags=re.MULTILINE | re.DOTALL + ) # Replace GitHub context variables with environment variables for security security_replacements = [ @@ -98,7 +115,9 @@ def modernize_workflow_content(self, content, workflow_name): modernized = re.sub(pattern, replacement, modernized) # Add environment variables at the job level if not present - if "env:" not in modernized and any("${{ env." in modernized for _ in [None]): + if "env:" not in modernized and any( + "${{ env." in modernized for _ in [None] + ): # Find the first job and add env section job_pattern = r"(jobs:\s*\n\s+\w+:\s*\n)" modernized = re.sub( @@ -120,8 +139,7 @@ def modernize_workflow(self, workflow_path): print(f" āœ“ {workflow_path.name} already modernized") return False - workflow_name = workflow_path.stem - modernized_content = self.modernize_workflow_content(content, workflow_name) + modernized_content = self.modernize_workflow_content(content) # Only write if there were changes if modernized_content != content: @@ -254,7 +272,8 @@ def main(): script_path = self.scripts_dir / script_name if not script_path.exists(): script_path.write_text(script_content) - os.chmod(script_path, 0o755) + # Generated scripts are executables invoked by workflows. + os.chmod(script_path, 0o755) # noqa: S103 print(f"Created missing script: {script_name}") @@ -279,10 +298,14 @@ def main(): # Auto-commit if there were changes if modernizer.modernized_count > 0: - print(f"\nšŸš€ Auto-committing {modernizer.modernized_count} modernized workflows...") + print( + f"\nšŸš€ Auto-committing {modernizer.modernized_count} modernized workflows..." + ) # Add files - subprocess.run(["git", "add", "."], check=False, cwd=os.path.dirname(workflow_dir)) + subprocess.run( + ["git", "add", "."], check=False, cwd=os.path.dirname(workflow_dir) + ) # Commit commit_msg = f"""feat(workflows): complete workflow modernization phase 2 @@ -305,7 +328,9 @@ def main(): print("āœ“ Changes committed successfully") # Push changes - result = subprocess.run(["git", "push"], check=False, cwd=os.path.dirname(workflow_dir)) + result = subprocess.run( + ["git", "push"], check=False, cwd=os.path.dirname(workflow_dir) + ) if result.returncode == 0: print("āœ“ Changes pushed successfully") else: diff --git a/.github/scripts/sync-determine-target-repos.py b/.github/scripts/sync-determine-target-repos.py index a5393d91..57fa9367 100755 --- a/.github/scripts/sync-determine-target-repos.py +++ b/.github/scripts/sync-determine-target-repos.py @@ -1,15 +1,17 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-determine-target-repos.py -# version: 1.0.1 +# version: 1.0.2 # guid: a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d +# last-edited: 2026-10-05 """Determine target repositories for sync operations. + Reads from repositories.txt and outputs repository names. """ import os -import sys from pathlib import Path +import sys def get_target_repos(): @@ -23,8 +25,8 @@ def get_target_repos(): repos = [] try: with open(repo_file) as f: - for line in f: - line = line.strip() + for raw_line in f: + line = raw_line.strip() if line and not line.startswith("#"): # Extract repo name from owner/repo format repo_name = line.split("/")[-1] if "/" in line else line diff --git a/.github/scripts/sync-dispatch-events.py b/.github/scripts/sync-dispatch-events.py index 254872a5..399974f5 100755 --- a/.github/scripts/sync-dispatch-events.py +++ b/.github/scripts/sync-dispatch-events.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-dispatch-events.py -# version: 1.1.1 +# version: 1.1.2 # guid: b2c3d4e5-f6a7-8b9c-0d1e-2f3a4b5c6d7e +# last-edited: 2026-10-05 """Dispatch repository events to target repositories for synchronization.""" import contextlib import json import os +from pathlib import Path import subprocess import sys -from pathlib import Path def get_target_repos(): @@ -24,8 +25,8 @@ def get_target_repos(): repos = [] try: with open(repo_file) as f: - for line in f: - line = line.strip() + for raw_line in f: + line = raw_line.strip() if line and not line.startswith("#"): # Keep full owner/repo format for API calls repos.append(line) @@ -75,11 +76,13 @@ def dispatch_event(repo, event_type, client_payload): ] try: - result = subprocess.run(curl_cmd, check=False, capture_output=True, text=True, timeout=30) + result = subprocess.run( + curl_cmd, check=False, capture_output=True, text=True, timeout=30 + ) if result.returncode == 0: status = (result.stdout or "").strip() - if status == "204" or status == "200": + if status in {"204", "200"}: print(f"āœ… Dispatched '{payload.get('event_type')}' to {repo}") return True print( @@ -121,7 +124,9 @@ def main(): print(f"Dispatching '{event_type}' events to target repositories...") print(f"Source: {source_repo}@{source_sha}") with contextlib.suppress(Exception): - print(f"Client payload: {json.dumps(client_payload, separators=(',', ':'))}") + print( + f"Client payload: {json.dumps(client_payload, separators=(',', ':'))}" + ) target_repos = get_target_repos() @@ -140,7 +145,9 @@ def main(): print(f"āœ… Successfully dispatched to {successful} repositories") if failed > 0: - print(f"āŒ Failed to dispatch to {failed} repositories", file=sys.stderr) + print( + f"āŒ Failed to dispatch to {failed} repositories", file=sys.stderr + ) sys.exit(1) print("All repository dispatch events sent successfully") diff --git a/.github/scripts/sync-generate-summary.py b/.github/scripts/sync-generate-summary.py index dbb0b756..9929760c 100755 --- a/.github/scripts/sync-generate-summary.py +++ b/.github/scripts/sync-generate-summary.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-generate-summary.py -# version: 1.0.0 +# version: 1.0.1 # guid: c3d4e5f6-a7b8-9c0d-1e2f-3a4b5c6d7e8f +# last-edited: 2026-10-05 """Generate synchronization summary for GitHub Actions step summary.""" +from datetime import datetime import os import sys -from datetime import datetime def generate_summary(): diff --git a/.github/scripts/sync-receiver-check-changes.py b/.github/scripts/sync-receiver-check-changes.py index 842facb0..ccb616bc 100755 --- a/.github/scripts/sync-receiver-check-changes.py +++ b/.github/scripts/sync-receiver-check-changes.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-receiver-check-changes.py -# version: 1.0.0 +# version: 1.0.1 # guid: a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d +# last-edited: 2026-10-05 """Check if there are changes after sync operation. + Usage: sync-receiver-check-changes.py """ diff --git a/.github/scripts/sync-receiver-commit-push.py b/.github/scripts/sync-receiver-commit-push.py index 6f14583a..c04fffcf 100755 --- a/.github/scripts/sync-receiver-commit-push.py +++ b/.github/scripts/sync-receiver-commit-push.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-receiver-commit-push.py -# version: 1.0.0 +# version: 1.0.1 # guid: e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b +# last-edited: 2026-10-05 """Commit and push synchronized changes.""" +from datetime import datetime import subprocess import sys -from datetime import datetime def run_command(cmd, description): @@ -65,8 +66,12 @@ def main(): print("šŸ“‹ Changes detected, proceeding with commit and push...") # Configure git user (in case not set in CI) - run_command('git config user.name "GitHub Actions"', "Setting git user name") - run_command('git config user.email "actions@github.com"', "Setting git user email") + run_command( + 'git config user.name "GitHub Actions"', "Setting git user name" + ) + run_command( + 'git config user.email "actions@github.com"', "Setting git user email" + ) # Add all changes if not run_command("git add .", "Adding all changes"): @@ -77,7 +82,9 @@ def main(): commit_message = f"chore(sync): synchronize from ghcommon ({timestamp})" # Commit changes - if not run_command(f'git commit -m "{commit_message}"', "Committing changes"): + if not run_command( + f'git commit -m "{commit_message}"', "Committing changes" + ): sys.exit(1) # Push changes diff --git a/.github/scripts/sync-receiver-generate-summary.py b/.github/scripts/sync-receiver-generate-summary.py index 614c4d4c..506c9183 100755 --- a/.github/scripts/sync-receiver-generate-summary.py +++ b/.github/scripts/sync-receiver-generate-summary.py @@ -1,13 +1,14 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-receiver-generate-summary.py -# version: 1.0.1 +# version: 1.0.2 # guid: f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c +# last-edited: 2026-10-05 """Generate synchronization summary for sync receiver workflow.""" +from datetime import datetime import os import subprocess -from datetime import datetime def get_sync_changes(): @@ -56,7 +57,10 @@ def get_changed_files_summary(): for _status, file_path in changes: if ".github/workflows/" in file_path: categories["workflows"].append(file_path) - elif ".github/instructions/" in file_path or "copilot-instructions.md" in file_path: + elif ( + ".github/instructions/" in file_path + or "copilot-instructions.md" in file_path + ): categories["instructions"].append(file_path) elif ".github/scripts/" in file_path or "/scripts/" in file_path: categories["scripts"].append(file_path) @@ -95,7 +99,7 @@ def generate_summary(): changes_summary = get_changed_files_summary() # Generate summary markdown - summary = f"""# šŸ“„ Repository Sync Received + return f"""# šŸ“„ Repository Sync Received ## Overview - **Source Repository**: {source_repo} @@ -124,8 +128,6 @@ def generate_summary(): *This repository is automatically synchronized with the central ghcommon repository.* """ - return summary - def write_to_step_summary(content): """Write content to GitHub Actions step summary.""" diff --git a/.github/scripts/sync-receiver-sync-files.py b/.github/scripts/sync-receiver-sync-files.py index 1bc4d987..123768aa 100755 --- a/.github/scripts/sync-receiver-sync-files.py +++ b/.github/scripts/sync-receiver-sync-files.py @@ -2,19 +2,21 @@ # file: .github/scripts/sync-receiver-sync-files.py #!/usr/bin/env python3 # file: .github/scripts/sync-receiver-sync-files.py -# version: 3.0.0 +# version: 3.0.1 # guid: 8d9e1f2a-3b4c-5d6e-7f8a-9b0c1d2e3f4a +# last-edited: 2026-10-05 """Sync receiver script for copying files from ghcommon to target repositories. + This script performs the actual file copying operations based on sync_type. Now reads workflow-config.yaml to determine what files to sync. """ +from pathlib import Path import shutil import stat import subprocess import sys -from pathlib import Path import yaml @@ -141,7 +143,9 @@ def sync_workflows(): else: print(f"āŒ Failed to copy workflow {workflow_name}") - print(f"šŸ“Š Workflows sync: {success_count}/{len(workflow_files)} files copied") + print( + f"šŸ“Š Workflows sync: {success_count}/{len(workflow_files)} files copied" + ) def sync_instructions(): @@ -160,14 +164,18 @@ def sync_instructions(): print( f"ā„¹ļø Copying main copilot-instructions.md: ghcommon-source/{copilot_instructions} -> {copilot_instructions}" ) - if copy_file_safe(f"ghcommon-source/{copilot_instructions}", copilot_instructions): + if copy_file_safe( + f"ghcommon-source/{copilot_instructions}", copilot_instructions + ): print("āœ… Successfully copied main copilot-instructions.md") else: print("āŒ Failed to copy main copilot-instructions.md") # Copy instructions directory if configured instructions_dir = ".github/instructions/" - if instructions_dir in sync_paths and not is_file_excluded(instructions_dir, exclude_files): + if instructions_dir in sync_paths and not is_file_excluded( + instructions_dir, exclude_files + ): src_dir = Path("ghcommon-source/.github/instructions") if src_dir.exists(): instruction_files = list(src_dir.glob("*")) @@ -182,9 +190,13 @@ def sync_instructions(): ): success_count += 1 else: - print(f"āŒ Failed to copy instruction file {instruction_file.name}") + print( + f"āŒ Failed to copy instruction file {instruction_file.name}" + ) - print(f"āœ… Copied {success_count}/{len(instruction_files)} instruction files") + print( + f"āœ… Copied {success_count}/{len(instruction_files)} instruction files" + ) else: print(f"āš ļø Source not found for instruction files: {src_dir}/*") @@ -218,8 +230,12 @@ def sync_prompts(): print( f"ā„¹ļø Copying prompt file {prompt_file.name}: {prompt_file} -> .github/prompts/" ) - if copy_file_safe(str(prompt_file), f".github/prompts/{prompt_file.name}"): - print(f"āœ… Successfully copied prompt file {prompt_file.name}") + if copy_file_safe( + str(prompt_file), f".github/prompts/{prompt_file.name}" + ): + print( + f"āœ… Successfully copied prompt file {prompt_file.name}" + ) else: print(f"āŒ Failed to copy prompt file {prompt_file.name}") else: @@ -257,13 +273,17 @@ def sync_scripts(): } # Copy GitHub scripts individually based on configuration - github_scripts_configured = any(path.startswith(".github/scripts/") for path in sync_paths) + github_scripts_configured = any( + path.startswith(".github/scripts/") for path in sync_paths + ) if github_scripts_configured: src_dir = Path("ghcommon-source/.github/scripts") if src_dir.exists(): script_files = [ - f for f in src_dir.glob("*") if f.is_file() and f.name not in excluded_scripts + f + for f in src_dir.glob("*") + if f.is_file() and f.name not in excluded_scripts ] print(f"šŸ“‹ Copying {len(script_files)} GitHub scripts...") @@ -274,10 +294,16 @@ def sync_scripts(): if copy_file_safe(str(script_file), script_path): success_count += 1 else: - print(f"āŒ Failed to copy GitHub script {script_file.name}") + print( + f"āŒ Failed to copy GitHub script {script_file.name}" + ) excluded_count = len( - [f for f in src_dir.glob("*") if f.is_file() and f.name in excluded_scripts] + [ + f + for f in src_dir.glob("*") + if f.is_file() and f.name in excluded_scripts + ] ) print( f"āœ… Copied {success_count}/{len(script_files)} GitHub scripts ({excluded_count} excluded)" @@ -313,7 +339,9 @@ def sync_linters(): if src_dir.exists(): linter_files = list(src_dir.glob("*")) print(f"šŸ“‹ Copying {len(linter_files)} linter files...") - copy_directory_safe("ghcommon-source/.github/linters", ".github/linters") + copy_directory_safe( + "ghcommon-source/.github/linters", ".github/linters" + ) print("āœ… Linter files copied") else: print(f"āš ļø Source not found for linter files: {src_dir}/*") @@ -334,7 +362,9 @@ def sync_labels(): label_files = ["labels.json", "labels.md"] for label_file in label_files: - if label_file in sync_paths and not is_file_excluded(label_file, exclude_files): + if label_file in sync_paths and not is_file_excluded( + label_file, exclude_files + ): total_files += 1 print(f"ā„¹ļø Copying {label_file}: ghcommon-source/{label_file} -> .") if copy_file_safe(f"ghcommon-source/{label_file}", label_file): @@ -345,7 +375,9 @@ def sync_labels(): # Copy GitHub labels sync script if configured labels_script = "scripts/sync-github-labels.py" - if labels_script in sync_paths and not is_file_excluded(labels_script, exclude_files): + if labels_script in sync_paths and not is_file_excluded( + labels_script, exclude_files + ): total_files += 1 print( f"ā„¹ļø Copying GitHub labels sync script: ghcommon-source/{labels_script} -> {labels_script}" @@ -385,7 +417,10 @@ def sync_other_files(): other_files = [ path for path in sync_paths - if not any(path.startswith(pattern) or path == pattern for pattern in handled_patterns) + if not any( + path.startswith(pattern) or path == pattern + for pattern in handled_patterns + ) ] if not other_files: @@ -411,7 +446,9 @@ def sync_other_files(): else: print(f"āŒ Failed to copy {file_path}") - print(f"šŸ“Š Other files sync: {success_count}/{len(other_files)} files copied") + print( + f"šŸ“Š Other files sync: {success_count}/{len(other_files)} files copied" + ) def main(): diff --git a/.github/scripts/sync-release-build-artifacts.py b/.github/scripts/sync-release-build-artifacts.py index 80437401..fcfcba5a 100755 --- a/.github/scripts/sync-release-build-artifacts.py +++ b/.github/scripts/sync-release-build-artifacts.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-build-artifacts.py -# version: 1.0.0 +# version: 1.0.1 # guid: f2a3b4c5-d6e7-8f9a-0b1c-2d3e4f5a6b7c +# last-edited: 2026-10-05 -"""Build Artifacts Script +"""Build Artifacts Script. Handles building release artifacts for different programming languages. Replaces embedded bash build scripts with reliable Python-based build logic. @@ -11,9 +12,10 @@ import json import os +from pathlib import Path import subprocess import sys -from pathlib import Path +from typing import Optional def log(message: str, level: str = "INFO") -> None: @@ -21,10 +23,14 @@ def log(message: str, level: str = "INFO") -> None: print(f"[{level}] {message}") -def run_command(cmd: list, cwd: str = None, env: dict = None) -> tuple: +def run_command( + cmd: list, cwd: Optional[str] = None, env: Optional[dict] = None +) -> tuple: """Run a command and return (success, stdout, stderr).""" try: - result = subprocess.run(cmd, cwd=cwd, env=env, capture_output=True, text=True, check=False) + result = subprocess.run( + cmd, cwd=cwd, env=env, capture_output=True, text=True, check=False + ) return result.returncode == 0, result.stdout, result.stderr except Exception as e: return False, "", str(e) @@ -85,7 +91,7 @@ def build_rust_artifacts() -> bool: continue # Build for target - build_success, stdout, stderr = run_command( + build_success, _stdout, stderr = run_command( ["cargo", "build", "--release", "--target", target] ) @@ -174,7 +180,7 @@ def build_go_artifacts() -> bool: env["GOARCH"] = goarch env["CGO_ENABLED"] = "0" - build_success, stdout, stderr = run_command( + build_success, _stdout, stderr = run_command( ["go", "build", "-o", f"releases/{binary_name}", "."], env=env ) @@ -224,7 +230,7 @@ def build_python_artifacts() -> bool: releases_dir.mkdir(exist_ok=True) # Build wheel - build_success, stdout, stderr = run_command( + build_success, _stdout, stderr = run_command( ["python", "-m", "build", "--wheel", "--outdir", "releases"] ) @@ -252,7 +258,9 @@ def build_javascript_artifacts() -> bool: scripts = package_data.get("scripts", {}) if "build" in scripts: - build_success, stdout, stderr = run_command(["npm", "run", "build"]) + build_success, _stdout, stderr = run_command( + ["npm", "run", "build"] + ) if build_success: log("Successfully built JavaScript project") return True @@ -268,7 +276,11 @@ def build_javascript_artifacts() -> bool: def main(): """Main execution function.""" - language = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("LANGUAGE", "unknown") + language = ( + sys.argv[1] + if len(sys.argv) > 1 + else os.environ.get("LANGUAGE", "unknown") + ) log(f"Building artifacts for language: {language}") diff --git a/.github/scripts/sync-release-create-package-json.py b/.github/scripts/sync-release-create-package-json.py index b70c0314..0eddd0a8 100755 --- a/.github/scripts/sync-release-create-package-json.py +++ b/.github/scripts/sync-release-create-package-json.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-create-package-json.py -# version: 1.0.0 +# version: 1.0.1 # guid: f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c +# last-edited: 2026-10-05 """Create package.json for semantic-release based on language type. + Usage: sync-release-create-package-json.py """ diff --git a/.github/scripts/sync-release-create-semantic-config.py b/.github/scripts/sync-release-create-semantic-config.py index 800d9559..26b889d7 100755 --- a/.github/scripts/sync-release-create-semantic-config.py +++ b/.github/scripts/sync-release-create-semantic-config.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-create-semantic-config.py -# version: 1.0.0 +# version: 1.0.1 # guid: e5f6a7b8-c9d0-1e2f-3a4b-5c6d7e8f9a0b +# last-edited: 2026-10-05 """Create semantic-release configuration based on language type. + Usage: sync-release-create-semantic-config.py """ @@ -105,7 +107,9 @@ def get_python_config(): [ [ "@semantic-release/exec", - {"prepareCmd": "python scripts/update_version.py ${nextRelease.version}"}, + { + "prepareCmd": "python scripts/update_version.py ${nextRelease.version}" + }, ], ["@semantic-release/changelog", {"changelogFile": "CHANGELOG.md"}], [ diff --git a/.github/scripts/sync-release-detect-language.py b/.github/scripts/sync-release-detect-language.py index 98d5a4f8..6f9a4628 100755 --- a/.github/scripts/sync-release-detect-language.py +++ b/.github/scripts/sync-release-detect-language.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-detect-language.py -# version: 1.2.0 +# version: 1.2.1 # guid: a7b8c9d0-e1f2-3a4b-5c6d-7e8f9a0b1c2d +# last-edited: 2026-10-05 """Detect programming languages and determine if release should be triggered. + Usage: sync-release-detect-language.py [force_language] """ import json import os +from pathlib import Path import subprocess import sys -from pathlib import Path def check_file_exists(filename): @@ -112,7 +114,9 @@ def set_github_output(name, value): def main(): """Main entry point.""" # Prefer argv if provided; fall back to FORCE_LANGUAGE env for workflow_dispatch compatibility - force_language = sys.argv[1] if len(sys.argv) > 1 else os.getenv("FORCE_LANGUAGE") + force_language = ( + sys.argv[1] if len(sys.argv) > 1 else os.getenv("FORCE_LANGUAGE") + ) if force_language: force_language = force_language.strip().lower() @@ -140,8 +144,12 @@ def main(): "javascript", "python", ] - language = next((lang for lang in priority if languages.get(lang)), "unknown") - print(f"Detected languages: {', '.join([k for k, v in languages.items() if v]) or 'none'}") + language = next( + (lang for lang in priority if languages.get(lang)), "unknown" + ) + print( + f"Detected languages: {', '.join([k for k, v in languages.items() if v]) or 'none'}" + ) # Determine if release should happen should_rel = should_release() @@ -151,18 +159,24 @@ def main(): set_github_output("language", language) set_github_output("should-release", "true" if should_rel else "false") # New multi-language outputs - set_github_output("languages", ",".join([k for k, v in languages.items() if v])) + set_github_output( + "languages", ",".join([k for k, v in languages.items() if v]) + ) set_github_output("languages_json", json.dumps(languages)) # Ordered matrix array (TS before GO to allow codegen, then others) ordered = [ - lang for lang in ["typescript", "go", "rust", "javascript", "python"] if languages.get(lang) + lang + for lang in ["typescript", "go", "rust", "javascript", "python"] + if languages.get(lang) ] set_github_output("languages_matrix", json.dumps({"language": ordered})) # Print summary print("\nLanguage Detection Summary:") print(f" Primary: {language}") - print(f" All: {', '.join([k for k, v in languages.items() if v]) or 'none'}") + print( + f" All: {', '.join([k for k, v in languages.items() if v]) or 'none'}" + ) print(f" Matrix JSON: {json.dumps(languages)}") print(f" Should Release: {should_rel}") diff --git a/.github/scripts/sync-release-determine-version.py b/.github/scripts/sync-release-determine-version.py index 6ca07469..e45eccde 100755 --- a/.github/scripts/sync-release-determine-version.py +++ b/.github/scripts/sync-release-determine-version.py @@ -1,17 +1,19 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-determine-version.py -# version: 1.0.0 +# version: 1.0.1 # guid: f6a7b8c9-d0e1-2f3a-4b5c-6d7e8f9a0b1c +# last-edited: 2026-10-05 """Determine version for release based on manual input or semantic-release analysis. + Usage: sync-release-determine-version.py """ import json +from pathlib import Path import re import subprocess import sys -from pathlib import Path def get_current_version_rust(): @@ -24,8 +26,8 @@ def get_current_version_rust(): if match: return match.group(1) raise ValueError("Could not find version in Cargo.toml") - except FileNotFoundError: - raise ValueError("Cargo.toml not found") + except FileNotFoundError as e: + raise ValueError("Cargo.toml not found") from e def get_current_version_go(): @@ -123,7 +125,9 @@ def run_semantic_release_dry_run(github_token): output = result.stdout + result.stderr # Look for version in output - version_match = re.search(r"The next release version is ([0-9.]+)", output) + version_match = re.search( + r"The next release version is ([0-9.]+)", output + ) if version_match: return version_match.group(1) @@ -180,12 +184,16 @@ def main(): # Manual release print(f"Manual release type specified: {manual_release_type}") - new_version = increment_version(current_version, manual_release_type) + new_version = increment_version( + current_version, manual_release_type + ) set_github_output("version", new_version) set_github_output("tag", f"v{new_version}") set_github_output("should-release", "true") - set_github_output("changelog", f"Manual {manual_release_type} release") + set_github_output( + "changelog", f"Manual {manual_release_type} release" + ) print(f"Manual release version: {new_version}") diff --git a/.github/scripts/sync-release-handle-manual-release.py b/.github/scripts/sync-release-handle-manual-release.py index 500492b0..df6f1ef1 100755 --- a/.github/scripts/sync-release-handle-manual-release.py +++ b/.github/scripts/sync-release-handle-manual-release.py @@ -1,17 +1,20 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-handle-manual-release.py -# version: 1.0.0 +# version: 1.0.1 # guid: b8c9d0e1-f2a3-4b5c-6d7e-8f9a0b1c2d3e +# last-edited: 2026-10-05 """Handle manual release version calculation. + Usage: sync-release-handle-manual-release.py """ +import json import os +from pathlib import Path import re import subprocess import sys -from pathlib import Path def get_current_version_rust(): @@ -79,8 +82,6 @@ def get_current_version_js_ts(): if not package_json.exists(): return "0.0.0" - import json - try: data = json.loads(package_json.read_text()) return data.get("version", "0.0.0") @@ -129,7 +130,7 @@ def increment_version(current_version, release_type): return f"{major}.{minor}.{patch}" except ValueError as e: - raise ValueError(f"Failed to increment version: {e}") + raise ValueError(f"Failed to increment version: {e}") from e def set_github_output(name, value): @@ -144,7 +145,9 @@ def main(): """Main entry point.""" if len(sys.argv) != 3: print("Error: Both release_type and language parameters required") - print("Usage: sync-release-handle-manual-release.py ") + print( + "Usage: sync-release-handle-manual-release.py " + ) sys.exit(1) release_type = sys.argv[1] diff --git a/.github/scripts/sync-release-run-semantic-release.py b/.github/scripts/sync-release-run-semantic-release.py index 5cc604a8..06ed0eb7 100755 --- a/.github/scripts/sync-release-run-semantic-release.py +++ b/.github/scripts/sync-release-run-semantic-release.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-run-semantic-release.py -# version: 1.0.0 +# version: 1.0.1 # guid: b3c4d5e6-f7a8-b9c0-d1e2-f3a4b5c6d7e8 +# last-edited: 2026-10-05 """Run semantic-release with proper environment and configuration.""" @@ -14,7 +15,9 @@ def main(): """Run semantic-release.""" # Ensure npm dependencies are installed print("Installing npm dependencies...") - result = subprocess.run(["npm", "install"], check=False, capture_output=True, text=True) + result = subprocess.run( + ["npm", "install"], check=False, capture_output=True, text=True + ) if result.returncode != 0: print(f"Failed to install npm dependencies: {result.stderr}") sys.exit(1) diff --git a/.github/scripts/sync-release-upload-artifacts.py b/.github/scripts/sync-release-upload-artifacts.py index 5ab21f51..eb09b935 100755 --- a/.github/scripts/sync-release-upload-artifacts.py +++ b/.github/scripts/sync-release-upload-artifacts.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-release-upload-artifacts.py -# version: 1.0.0 +# version: 1.0.1 # guid: c4d5e6f7-a8b9-c0d1-e2f3-a4b5c6d7e8f9 +# last-edited: 2026-10-05 """Upload release artifacts to GitHub release.""" import os +from pathlib import Path import subprocess import sys -from pathlib import Path def upload_artifacts(release_id, artifacts_dir): @@ -41,7 +42,9 @@ def upload_artifacts(release_id, artifacts_dir): os.environ.get("GITHUB_REPOSITORY", ""), ] - result = subprocess.run(cmd, check=False, capture_output=True, text=True) + result = subprocess.run( + cmd, check=False, capture_output=True, text=True + ) if result.returncode == 0: print(f" āœ“ Uploaded {artifact.name}") else: @@ -51,7 +54,9 @@ def upload_artifacts(release_id, artifacts_dir): def main(): """Main entry point.""" if len(sys.argv) < 3: - print("Usage: sync-release-upload-artifacts.py ") + print( + "Usage: sync-release-upload-artifacts.py " + ) sys.exit(1) release_id = sys.argv[1] diff --git a/.github/scripts/sync-workflow-modernizer.py b/.github/scripts/sync-workflow-modernizer.py index 31f40c45..e4274fae 100755 --- a/.github/scripts/sync-workflow-modernizer.py +++ b/.github/scripts/sync-workflow-modernizer.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 # file: .github/scripts/sync-workflow-modernizer.py -# version: 1.0.1 +# version: 1.0.2 # guid: e1f2a3b4-c5d6-7e8f-9a0b-1c2d3e4f5a6b +# last-edited: 2026-10-05 -"""Workflow Modernization Script +"""Workflow Modernization Script. This script modernizes GitHub Actions workflow files by replacing embedded bash scripts with external Python scripts for better reliability and maintainability. @@ -18,16 +19,17 @@ all language-specific workflows while maintaining the security improvements. """ -import os +from datetime import datetime, timezone +from pathlib import Path import re import sys -from pathlib import Path class WorkflowModernizer: """Modernizes GitHub Actions workflow files by replacing bash with Python scripts.""" def __init__(self, workflows_dir: str = ".github/workflows"): + """Locate the workflows and scripts directories.""" self.workflows_dir = Path(workflows_dir) self.scripts_dir = Path(".github/scripts") @@ -94,7 +96,9 @@ def modernize_workflow(self, language: str) -> bool: # Check if pattern exists if re.search(pattern, content, flags): - print(f" āœ… Replacing {script_type} bash script with Python") + print( + f" āœ… Replacing {script_type} bash script with Python" + ) content = re.sub(pattern, replacement, content, flags=flags) else: print(f" ā„¹ļø No {script_type} bash script found to replace") @@ -167,11 +171,13 @@ def validate_scripts_exist(self) -> bool: print("āœ… All required Python scripts are available") return True - def generate_modernization_report(self, updated_workflows: list[str]) -> str: + def generate_modernization_report( + self, updated_workflows: list[str] + ) -> str: """Generate a report of the modernization results.""" report = [ "# Workflow Modernization Report", - f"Generated: {os.popen('date').read().strip()}", + f"Generated: {datetime.now(timezone.utc).strftime('%a %b %d %H:%M:%S UTC %Y')}", "", "## Summary", f"- Total workflows processed: {len(self.language_workflows)}", @@ -185,7 +191,9 @@ def generate_modernization_report(self, updated_workflows: list[str]) -> str: workflow_file = self.language_workflows[language] report.append(f"- āœ… {language}: {workflow_file}") - unchanged_workflows = set(self.language_workflows.keys()) - set(updated_workflows) + unchanged_workflows = set(self.language_workflows.keys()) - set( + updated_workflows + ) if unchanged_workflows: report.append("") report.append("## Unchanged Workflows") diff --git a/.github/workflows/reusable-ci.yml b/.github/workflows/reusable-ci.yml index e6f6ab99..2d94752b 100644 --- a/.github/workflows/reusable-ci.yml +++ b/.github/workflows/reusable-ci.yml @@ -1,5 +1,5 @@ # file: .github/workflows/reusable-ci.yml -# version: 1.16.0 +# version: 1.16.1 # guid: reusable-ci-2025-09-24-core-workflow # last-edited: 2026-10-05 @@ -494,7 +494,7 @@ jobs: run: | set -euo pipefail python -m pip install --upgrade pip - python -m pip install ruff + python -m pip install ruff==0.16.10 # pinned: an unpinned ruff adds findings on its own release schedule if find .github/scripts -maxdepth 1 -name "*.py" | head -n 1 | grep -q .; then ruff check .github/scripts ruff format --check .github/scripts diff --git a/changelog.d/20261005-workflow-scripts-ruff-clean.md b/changelog.d/20261005-workflow-scripts-ruff-clean.md new file mode 100644 index 00000000..b0b72844 --- /dev/null +++ b/changelog.d/20261005-workflow-scripts-ruff-clean.md @@ -0,0 +1,19 @@ +### Fixed + +#### `Workflow Scripts` passes: `.github/scripts` is ruff-clean and ruff is pinned + +The job runs `ruff check` and `ruff format --check` over `.github/scripts` and +had 411 findings, so it failed on every run that reached it. That was every +manual dispatch, and since `force-all` (#369) also the weekly schedule. + +- `ruff.toml` gives `.github/scripts/**` the same per-file ignores as + `scripts/**` (`T20`, `S602`/`S603`/`S607`) plus `RUF001` (status emoji) and + `PLR2004` (argv and semver-part counts). +- The remaining findings are fixed in code: docstrings, loop-variable + shadowing, `raise ... from`, a scheme check before `urlopen`, a logged + warning in place of `except: pass`, hoisted imports, `os.popen('date')` + replaced by `datetime`, and unused parameters removed. `get_env_bool` in + `determine-security-languages.py` now honors its `default` argument, which it + used to ignore. +- The job installs `ruff==0.16.10` rather than the latest release, so a new + ruff release can no longer fail it overnight. diff --git a/ruff.toml b/ruff.toml index 513b871c..2741c651 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,7 @@ # file: ruff.toml -# version: 1.2.0 +# version: 1.3.0 # guid: f1a2b3c4-d5e6-789f-a012-456789abcdef -# last-edited: 2026-07-19 +# last-edited: 2026-10-05 # Ruff configuration following Google Python Style Guide # https://google.github.io/styleguide/pyguide.html @@ -72,6 +72,11 @@ ignore = [ "test_*.py" = ["S101", "S106", "D100", "D101", "D102", "D103", "D107", "PLR2004", "ARG001", "ARG005", "PT009", "PLC0415", "RUF001"] "**/tests/**/*.py" = ["S101", "S106", "D100", "D101", "D102", "D103", "D107", "PLR2004", "ARG001", "ARG005", "PT009", "PLC0415", "RUF001"] "scripts/**/*.py" = ["T20", "S602", "S603", "S607"] +# Workflow scripts are CLIs run by Actions: stdout is their interface (T20), +# they shell out to git/gh by name on a runner PATH (S60x), and they print the +# same status emoji the tests assert on (RUF001), and their only "magic" +# numbers are argv-length and semver-part counts (PLR2004). +".github/scripts/**/*.py" = ["T20", "S602", "S603", "S607", "RUF001", "PLR2004"] "**/migrations/**/*.py" = ["D100", "D103"] [lint.isort]