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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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
 62
 63
 64
 65
 66
 67
 68
 69
 70
 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
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