Skip to content

ci_map

mkdocs_terok.ci_map

Generate a Markdown map of GitHub workflows and jobs.

Parses .github/workflows/*.yml and .yaml files and produces a Markdown document with workflow summary and per-job detail tables.

generate_ci_map(workflows=None, *, workflows_dir=None)

Generate the Markdown CI map.

Parameters:

Name Type Description Default
workflows list[dict[str, object]] | None

Pre-loaded workflow data. If None, loads from disk.

None
workflows_dir Path | None

Override for the workflows directory.

None
Source code in src/mkdocs_terok/ci_map.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def generate_ci_map(
    workflows: list[dict[str, object]] | None = None,
    *,
    workflows_dir: Path | None = None,
) -> str:
    """Generate the Markdown CI map.

    Args:
        workflows: Pre-loaded workflow data. If ``None``, loads from disk.
        workflows_dir: Override for the workflows directory.
    """
    if workflows is None:
        workflows = load_workflows(workflows_dir)
    job_count = sum(len(workflow["jobs"]) for workflow in workflows)
    now = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")

    lines = [
        "# CI Workflow Map\n\n",
        f"*Generated: {now}*\n\n",
        f"**{len(workflows)} workflows** with **{job_count} jobs**\n\n",
        "## Workflows\n\n",
        "| Workflow | File | Triggers | Jobs |\n",
        "|---|---|---|---|\n",
    ]
    for workflow in workflows:
        lines.append(
            f"| `{workflow['name']}` | `{workflow['file_name']}` | "
            f"{workflow['triggers']} | {len(workflow['jobs'])} |\n"
        )

    lines.extend(
        [
            "\n## Jobs\n\n",
            "| Workflow | Job | Needs | Uploads | Downloads |\n",
            "|---|---|---|---|---|\n",
        ]
    )
    for workflow in workflows:
        for job in workflow["jobs"]:
            lines.append(
                f"| `{workflow['name']}` | `{job['name']}` | {_render(job['needs'])} | "
                f"{_render(job['uploads'])} | {_render(job['downloads'])} |\n"
            )

    lines.append("\n")
    return "".join(lines)

load_workflows(workflows_dir=None)

Load workflow and job facts from .github/workflows/*.yml.

Parameters:

Name Type Description Default
workflows_dir Path | None

Directory containing workflow YAML files. Defaults to .github/workflows/ relative to cwd.

None
Source code in src/mkdocs_terok/ci_map.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def load_workflows(workflows_dir: Path | None = None) -> list[dict[str, object]]:
    """Load workflow and job facts from ``.github/workflows/*.yml``.

    Args:
        workflows_dir: Directory containing workflow YAML files.
            Defaults to ``.github/workflows/`` relative to cwd.
    """
    if workflows_dir is None:
        workflows_dir = Path.cwd() / ".github" / "workflows"
    workflows: list[dict[str, object]] = []
    yml_files = list(workflows_dir.glob("*.yml")) + list(workflows_dir.glob("*.yaml"))
    for path in sorted(yml_files):
        data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
        if not isinstance(data, dict):
            continue

        jobs: list[dict[str, object]] = []
        jobs_section = data.get("jobs", {})
        if isinstance(jobs_section, dict):
            for job_id, job_data in jobs_section.items():
                if not isinstance(job_data, dict):
                    continue
                needs = job_data.get("needs", ())
                needs_tuple = (
                    (needs,)
                    if isinstance(needs, str)
                    else tuple(str(item) for item in needs)
                    if isinstance(needs, list)
                    else ()
                )
                jobs.append(
                    {
                        "name": str(job_data.get("name", job_id)),
                        "needs": needs_tuple,
                        "uploads": _artifact_names(
                            job_data.get("steps"), "actions/upload-artifact"
                        ),
                        "downloads": _artifact_names(
                            job_data.get("steps"), "actions/download-artifact"
                        ),
                    }
                )

        workflows.append(
            {
                "file_name": path.name,
                "name": str(data.get("name", path.stem)),
                "triggers": _trigger_summary(data),
                "jobs": jobs,
            }
        )
    return workflows