github-scan-actions
Scan GitHub Actions workflows across repos and summarize uses: references.
Metadata
- Author: ropean, Claude Sonnet (Anthropic)
- Version: 1.0.0
Code
python
#!/usr/bin/env python3
"""
@title github-scan-actions
@description Scan GitHub Actions workflows across repos and summarize `uses:` references.
@author ropean, Claude Sonnet (Anthropic)
@version 1.0.0
@example
python github-scan-actions.py [git_dir] [-o FILE] [-l]
Arguments:
git_dir Root directory containing your git repos (default: platform-dependent)
-o / --output Save JSON result to this file
-l / --list Output a flat sorted list of unique action names only (no counts/repos)
"""
import json
import re
import sys
import argparse
from collections import defaultdict
from pathlib import Path
SCRIPTS_DIR = Path(__file__).resolve().parent
# ════════════════════════════════════════════════════════════
# Platform defaults
# ════════════════════════════════════════════════════════════
PLATFORM_DEFAULTS = {
"win32": r"D:\Git",
"darwin": "~/Git",
"linux": "~/Git", # includes WSL
}
def default_git_dir() -> Path:
raw = PLATFORM_DEFAULTS.get(sys.platform, "~/Git")
return Path(raw).expanduser()
# ════════════════════════════════════════════════════════════
# Core logic
# ════════════════════════════════════════════════════════════
USES_RE = re.compile(r"^\s*(?:-\s*)?uses:\s+(.+)$", re.MULTILINE)
def scan_workflows(git_dir: Path) -> dict:
"""
Scan all .github/workflows/*.yml and *.yaml files under git_dir.
Returns a dict shaped like:
{
"action@version": {"count": int, "repos": ["repo/workflow.yml", ...]},
...
}
"""
summary: dict[str, dict] = defaultdict(lambda: {"count": 0, "repos": []})
workflow_files = sorted({
*git_dir.glob("*/.github/workflows/*.yml"),
*git_dir.glob("*/.github/workflows/*.yaml"),
})
if not workflow_files:
print(f"[warn] No workflow files found under: {git_dir}", file=sys.stderr)
return {}
for wf_path in workflow_files:
try:
label = str(wf_path.relative_to(git_dir))
except ValueError:
label = str(wf_path)
try:
text = wf_path.read_text(encoding="utf-8", errors="replace")
except OSError as e:
print(f"[warn] Cannot read {wf_path}: {e}", file=sys.stderr)
continue
for match in USES_RE.finditer(text):
action = match.group(1).strip().split("#")[0].strip()
if not action:
continue
summary[action]["count"] += 1
summary[action]["repos"].append(label)
return dict(sorted(summary.items()))
def build_report(git_dir: Path, summary: dict) -> dict:
from datetime import datetime, timezone
return {
"scanned_at": datetime.now(timezone.utc).isoformat(),
"git_dir": str(git_dir),
"total_unique_actions": len(summary),
"total_uses": sum(v["count"] for v in summary.values()),
"actions": summary,
}
# ════════════════════════════════════════════════════════════
# CLI
# ════════════════════════════════════════════════════════════
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Scan GitHub Actions `uses:` across all repos in a directory."
)
parser.add_argument(
"git_dir",
nargs="?",
default=None,
help=f"Root directory of your git repos (default: {default_git_dir()})",
)
parser.add_argument(
"--output", "-o",
metavar="FILE",
default=None,
help=f"Save JSON output to this file (default when omitted: print to stdout only)",
)
parser.add_argument(
"--list", "-l",
action="store_true",
help="Output a flat sorted list of unique action names only (no counts/repos)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
git_dir = Path(args.git_dir).expanduser() if args.git_dir else default_git_dir()
if not git_dir.is_dir():
print(f"[error] Directory not found: {git_dir}", file=sys.stderr)
sys.exit(1)
print(f"Scanning: {git_dir} …", file=sys.stderr)
summary = scan_workflows(git_dir)
if args.list:
json_text = json.dumps(sorted(summary.keys()), indent=2, ensure_ascii=False)
print(f"[info] {len(summary)} unique actions.", file=sys.stderr)
else:
report = build_report(git_dir, summary)
json_text = json.dumps(report, indent=2, ensure_ascii=False)
workflow_count = len({r for v in summary.values() for r in v["repos"]})
print(
f"[info] {report['total_unique_actions']} unique actions, "
f"{report['total_uses']} total uses across {workflow_count} workflow files.",
file=sys.stderr,
)
print(json_text)
if args.output:
out_path = Path(args.output)
out_path.write_text(json_text, encoding="utf-8")
print(f"[saved] {out_path}", file=sys.stderr)
if __name__ == "__main__":
main()File Information
- Filename:
github-scan-actions.py - Category: python
- Language: PYTHON