← Files Plugin AutopilotARCHIVED FILE

scripts/action_entrypoint.py

12.3 KB · Oct 2, 2026 · 00:33 UTC

↓ Download file

#!/usr/bin/env python3
"""GitHub Action entrypoint for ChatGPT & Codex Plugin Autopilot."""
from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path

# Prevent bytecode generation during action execution
sys.dont_write_bytecode = True

ROOT = Path(os.environ.get("ACTION_PATH") or Path(__file__).resolve().parents[1])
SCRIPTS_DIR = ROOT / "skills/chatgpt-codex-plugin-autopilot/scripts"


def write_output(name: str, value: str | int | bool) -> None:
    """Write an output key-value pair to GITHUB_OUTPUT."""
    output_file = os.environ.get("GITHUB_OUTPUT")
    if not output_file:
        return
    text = str(value)
    if "\n" in text:
        delimiter = "EOF_AUTOPILOT"
        line = f"{name}<<{delimiter}\n{text}\n{delimiter}\n"
    else:
        line = f"{name}={text}\n"
    with open(output_file, "a", encoding="utf-8") as fh:
        fh.write(line)


def write_summary(markdown_text: str) -> None:
    """Append formatted markdown text to GITHUB_STEP_SUMMARY."""
    summary_file = os.environ.get("GITHUB_STEP_SUMMARY")
    if not summary_file:
        return
    with open(summary_file, "a", encoding="utf-8") as fh:
        fh.write(markdown_text.rstrip() + "\n\n")


def run_analyzer(target_dir: Path) -> dict:
    """Run analyze_repo.py on the target directory."""
    analyzer = SCRIPTS_DIR / "analyze_repo.py"
    cmd = [sys.executable, str(analyzer), str(target_dir), "--json"]
    proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT))
    try:
        data = json.loads(proc.stdout)
        if "ok" not in data:
            data["ok"] = bool(proc.returncode == 0 and not data.get("errors"))
        return data
    except Exception as exc:
        return {
            "ok": False,
            "errors": [f"analyzer execution failed: {proc.stderr or proc.stdout or exc}"],
            "skills": [],
            "candidates": [],
        }


def run_validator(target_dir: Path) -> dict:
    """Run validation on the target directory, using self_check if available or validate_plugin."""
    self_check = target_dir / "scripts/self_check.py"
    if self_check.is_file():
        cmd = [sys.executable, str(self_check), "--json"]
        proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(target_dir))
        try:
            return json.loads(proc.stdout)
        except Exception as exc:
            return {"ok": False, "errors": [f"self_check execution failed: {proc.stderr or proc.stdout or exc}"]}

    validator = SCRIPTS_DIR / "validate_plugin.py"
    cmd = [sys.executable, str(validator), str(target_dir), "--json"]
    proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT))
    try:
        data = json.loads(proc.stdout)
        if proc.returncode != 0 and "ok" not in data:
            data["ok"] = False
        return data
    except Exception as exc:
        return {
            "ok": False,
            "errors": [f"validator execution failed: {proc.stderr or proc.stdout or exc}"],
            "warnings": [],
            "skills": [],
        }


def run_packager(target_dir: Path, output_dir: Path) -> dict:
    """Package the target directory into a verified release ZIP."""
    output_dir.mkdir(parents=True, exist_ok=True)
    custom_build_script = target_dir / "scripts/build_release.py"
    if custom_build_script.is_file():
        cmd = [sys.executable, str(custom_build_script), "--out-dir", str(output_dir), "--json"]
        proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(target_dir))
        try:
            return json.loads(proc.stdout)
        except Exception as exc:
            return {"ok": False, "errors": [f"build_release failed: {proc.stderr or proc.stdout or exc}"]}

    manifest_path = target_dir / ".codex-plugin/plugin.json"
    if not manifest_path.is_file():
        return {"ok": False, "errors": [f"missing manifest at {manifest_path}"]}
    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
        name = manifest.get("name", "plugin")
        version = manifest.get("version", "0.1.0")
    except Exception as exc:
        return {"ok": False, "errors": [f"failed to read manifest: {exc}"]}

    archive_path = output_dir / f"{name}-{version}.zip"
    packager = SCRIPTS_DIR / "package_plugin.py"
    cmd = [sys.executable, str(packager), str(target_dir), str(archive_path), "--json"]
    proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT))
    try:
        result = json.loads(proc.stdout)
        if result.get("ok"):
            result["archive"] = result.get("outputPath", str(archive_path))
        return result
    except Exception as exc:
        return {"ok": False, "errors": [f"package_plugin failed: {proc.stderr or proc.stdout or exc}"]}


def format_summary_table(headers: list[str], rows: list[list[str]]) -> str:
    """Format a GitHub Flavored Markdown table."""
    col_widths = [len(h) for h in headers]
    for row in rows:
        for i, cell in enumerate(row):
            col_widths[i] = max(col_widths[i], len(cell))
    header_line = "| " + " | ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) + " |"
    sep_line = "| " + " | ".join("-" * col_widths[i] for i in range(len(headers))) + " |"
    data_lines = [
        "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(row)) + " |"
        for row in rows
    ]
    return "\n".join([header_line, sep_line] + data_lines)


def render_markdown_summary(mode: str, report: dict, target_dir: Path, arch_str: str) -> str:
    """Generate a rich GitHub Step Summary."""
    ok = report.get("ok", False)
    status_emoji = "✅" if ok else "❌"
    status_text = "PASSED" if ok else "FAILED"

    lines = [
        f"## {status_emoji} ChatGPT / Codex Plugin Autopilot Summary",
        "",
        f"- **Mode:** `{mode}`",
        f"- **Target Directory:** `{target_dir}`",
        f"- **Status:** **{status_text}**",
        f"- **Architecture:** `{arch_str}`",
    ]

    skills = report.get("skills", [])
    if isinstance(skills, list):
        lines.append(f"- **Skills Count:** {len(skills)}")
    elif isinstance(skills, int):
        lines.append(f"- **Skills Count:** {skills}")

    if "archive" in report:
        lines.append(f"- **Release Archive:** `{report['archive']}`")
        if "sha256" in report:
            lines.append(f"- **SHA-256 Checksum:** `{report['sha256']}`")
        if "bytes" in report:
            kb = round(report["bytes"] / 1024, 2)
            lines.append(f"- **Archive Size:** {kb} KB ({report['bytes']} bytes)")

    lines.append("")

    # Errors section
    errors = report.get("errors", [])
    if errors:
        lines.extend([
            "### ❌ Errors Encountered",
            "",
        ])
        for err in errors:
            lines.append(f"- {err}")
        lines.append("")

    # Warnings section
    warnings = report.get("warnings", [])
    if warnings:
        lines.extend([
            "### ⚠️ Warnings",
            "",
        ])
        for warn in warnings:
            lines.append(f"- {warn}")
        lines.append("")

    # Candidates table (from analyzer)
    candidates = report.get("candidates", [])
    if candidates:
        lines.extend([
            "### 🔍 Discovered Workflow Candidates",
            "",
        ])
        headers = ["Kind", "Slug", "Path", "Target Action"]
        rows = [
            [str(c.get("kind", "")), str(c.get("slug", "")), str(c.get("path", "")), str(c.get("recommendedTarget", ""))]
            for c in candidates[:25]
        ]
        lines.append(format_summary_table(headers, rows))
        if len(candidates) > 25:
            lines.append(f"\n*(and {len(candidates) - 25} more candidates)*")
        lines.append("")

    # Skills table (from validator)
    if isinstance(skills, list) and skills and isinstance(skills[0], dict):
        lines.extend([
            "### 📦 Included Skills",
            "",
        ])
        headers = ["Skill Name", "Path", "Description"]
        rows = []
        for s in skills:
            name = s.get("name", "")
            path = s.get("path", "")
            desc = s.get("description", "")[:60] + "..." if len(s.get("description", "")) > 60 else s.get("description", "")
            rows.append([name, path, desc])
        lines.append(format_summary_table(headers, rows))
        lines.append("")

    lines.extend([
        "---",
        "*Powered by [ChatGPT & Codex Plugin Autopilot](https://github.com/imMamdouhaboammar/chatgpt-codex-plugin-autopilot)*",
    ])
    return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="GitHub Action Entrypoint for Plugin Autopilot")
    parser.add_argument("--path", default=os.environ.get("INPUT_PATH", "."))
    parser.add_argument("--action", default=os.environ.get("INPUT_ACTION", "auto"))
    parser.add_argument("--output-dir", default=os.environ.get("INPUT_OUTPUT_DIR", "dist"))
    parser.add_argument("--fail-on-error", default=os.environ.get("INPUT_FAIL_ON_ERROR", "true"))
    parser.add_argument("--summary", default=os.environ.get("INPUT_SUMMARY", "true"))
    args = parser.parse_args(argv)

    target_dir = Path(args.path).expanduser().resolve()
    output_dir = Path(args.output_dir).expanduser().resolve()
    fail_on_error = str(args.fail_on_error).lower() in ("true", "1", "yes")
    gen_summary = str(args.summary).lower() in ("true", "1", "yes")
    action_mode = args.action.lower().strip()

    if not target_dir.exists():
        err_msg = f"Target directory does not exist: {target_dir}"
        print(f"Error: {err_msg}", file=sys.stderr)
        write_output("ok", "false")
        if gen_summary:
            write_summary(render_markdown_summary(action_mode, {"ok": False, "errors": [err_msg]}, target_dir, "unknown"))
        return 1 if fail_on_error else 0

    has_plugin_manifest = (target_dir / ".codex-plugin/plugin.json").is_file()

    report: dict = {}
    actual_mode = action_mode

    if action_mode == "auto":
        if has_plugin_manifest:
            actual_mode = "package"
        else:
            actual_mode = "analyze"

    if actual_mode == "analyze":
        report = run_analyzer(target_dir)
    elif actual_mode == "validate":
        report = run_validator(target_dir)
    elif actual_mode == "package":
        # Validate first
        val_report = run_validator(target_dir)
        if not val_report.get("ok"):
            report = val_report
        else:
            pkg_report = run_packager(target_dir, output_dir)
            report = {**val_report, **pkg_report}
            if not pkg_report.get("ok"):
                report["ok"] = False
    elif actual_mode == "build-directory-pack":
        packer = SCRIPTS_DIR / "build_directory_pack.py"
        listing_path = target_dir / "submission/listing.json"
        cmd = [sys.executable, str(packer), str(target_dir), "--json"]
        if listing_path.is_file():
            cmd.extend(["--listing", str(listing_path)])
        proc = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT))
        try:
            report = json.loads(proc.stdout)
        except Exception as exc:
            report = {"ok": False, "errors": [f"build_directory_pack failed: {proc.stderr or proc.stdout or exc}"]}
    else:
        report = {"ok": False, "errors": [f"Unknown action: {action_mode}"]}

    ok = report.get("ok", False)
    arch_raw = report.get("architecture", "unknown")
    if isinstance(arch_raw, dict):
        architecture = arch_raw.get("recommended", "skills-only")
    elif isinstance(arch_raw, str):
        architecture = arch_raw
    else:
        architecture = "unknown"

    skills = report.get("skills", [])
    skills_count = len(skills) if isinstance(skills, list) else int(skills) if isinstance(skills, int) else 0

    write_output("ok", "true" if ok else "false")
    write_output("architecture", architecture)
    write_output("skills_count", skills_count)
    if "archive" in report:
        write_output("plugin_path", report["archive"])
    elif "outputPath" in report:
        write_output("plugin_path", report["outputPath"])
    if "sha256" in report:
        write_output("sha256", report["sha256"])

    summary_md = render_markdown_summary(actual_mode, report, target_dir, architecture)
    if gen_summary:
        write_summary(summary_md)

    print(json.dumps(report, indent=2, sort_keys=True))

    if not ok and fail_on_error:
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 9a64645948a7771e9f6627c9624bb90f9609709b5c98f8e16cb56fccd628ba75