Files
jcoffey-dev 76d90360a7
ci / fork-checks (pull_request) Skipped
ci / build (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 10m26s
CI: skip the build on branches that only change docs
A README change took a full Rust build. A new changes job compares the branch with where it left main, and the build is skipped when only top-level Markdown or docs/ changed. fork-checks still runs, and the report still posts success, so Gitea's github job doesn't sit waiting.

Markdown elsewhere doesn't count, because vendor crates compile their READMEs in. main always builds: a push there cancels the run before it, so a docs merge that skipped its build could leave the previous merge unchecked.
2026-11-02 18:09:11 -07:00

72 lines
2.1 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Coffey Labs LLC
# SPDX-License-Identifier: AGPL-3.0-only
"""
Say whether a branch changes only documentation, so CI can skip its build.
tools/fork/docs-only.py # prints docs_only=true or docs_only=false
Under GitHub Actions the answer also goes to $GITHUB_OUTPUT.
The whole branch is compared with where it left main, not just its latest
push. A newer push cancels the run for the older one, so a documentation commit
on top of a code commit must still build.
Only top-level *.md files and docs/ count. Markdown elsewhere can be compiled
in: vendor/calcard and vendor/sieve-rs include their READMEs as doc comments.
main always builds. A push to main cancels the run before it, so a documentation
merge skipping its build would leave the merge before it unchecked. When there's
any doubt, the answer is false: a needless build only costs time.
"""
import os
import re
import subprocess
import sys
BASE = "origin/main"
MAIN_REF = "refs/heads/main"
DOCS = re.compile(r"(?:[^/]+\.md|docs/.+)", re.S)
def is_docs_only(paths):
return bool(paths) and all(DOCS.fullmatch(p) for p in paths)
def changed_paths(base=BASE, cwd=None):
"""The branch's files, or None if git can't say. A rename lists both ends."""
result = subprocess.run(
["git", "diff", "--name-only", "--no-renames", "-z", f"{base}...HEAD"],
cwd=cwd,
capture_output=True,
text=True,
)
if result.returncode != 0:
print(result.stderr, file=sys.stderr)
return None
return [p for p in result.stdout.split("\0") if p]
def answer(ref, base=BASE, cwd=None):
if ref == MAIN_REF:
return False
paths = changed_paths(base, cwd)
if paths is None:
return False
for path in paths:
print(f"changed: {path}")
return is_docs_only(paths)
def main():
line = f"docs_only={str(answer(os.environ.get('GITHUB_REF', ''))).lower()}"
print(line)
output = os.environ.get("GITHUB_OUTPUT")
if output:
with open(output, "a", encoding="utf-8") as f:
f.write(line + "\n")
if __name__ == "__main__":
main()