Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 1 addition & 21 deletions .github/workflows/external-links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ jobs:
timeout-minutes: 20
env:
GH_TOKEN: ${{ github.token }}
ISSUE_TITLE: "Dead external links (weekly check)"
steps:
- uses: actions/checkout@v7

Expand All @@ -46,23 +45,4 @@ jobs:
PY

- name: Open, update or close the tracking issue
run: |
set -euo pipefail
python3 - <<'PY'
import json, os
from scripts.check_external_links import render_issue_body, report_needs_issue
report = json.load(open(os.environ["RUNNER_TEMP"] + "/report.json"))
open(os.environ["RUNNER_TEMP"] + "/body.md", "w").write(render_issue_body(report))
open(os.environ["RUNNER_TEMP"] + "/report_needs_issue", "w").write("1" if report_needs_issue(report) else "0")
PY
existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$ISSUE_TITLE\" in:title" --json number,title -q ".[] | select(.title == \"$ISSUE_TITLE\") | .number" | head -n 1)"
report_needs_issue="$(cat "$RUNNER_TEMP/report_needs_issue")"
if [ "$report_needs_issue" = "1" ]; then
if [ -n "$existing" ]; then
gh issue edit "$existing" --repo "$GITHUB_REPOSITORY" --body-file "$RUNNER_TEMP/body.md"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$ISSUE_TITLE" --label documentation --body-file "$RUNNER_TEMP/body.md"
fi
elif [ -n "$existing" ]; then
gh issue close "$existing" --repo "$GITHUB_REPOSITORY" --reason completed --comment "The weekly check found no broken links or stale ignore entries."
fi
run: python3 scripts/update_weekly_issue.py external-links "$RUNNER_TEMP/report.json"
22 changes: 1 addition & 21 deletions .github/workflows/traction-live.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ jobs:
timeout-minutes: 10
env:
GH_TOKEN: ${{ github.token }}
ISSUE_TITLE: "Traction figures out of date (weekly check)"
steps:
- uses: actions/checkout@v7

Expand All @@ -42,23 +41,4 @@ jobs:
exit 0

- name: Open, update or close the tracking issue
run: |
set -euo pipefail
status="$(cat "$RUNNER_TEMP/status")"
existing="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "\"$ISSUE_TITLE\" in:title" --json number,title -q ".[] | select(.title == \"$ISSUE_TITLE\") | .number" | head -n 1)"
if [ "$status" = "1" ]; then
{
echo "The weekly \`check_traction.py --live\` run found figures that differ from GitHub or from each other:"
echo
sed 's/^ - /- /' "$RUNNER_TEMP/err.txt" | tail -n +2
echo
echo "Update the figures in README.md, ROADMAP.md and METRICS.md together, then run \`python3 scripts/check_traction.py --live\`. This issue closes itself on the next weekly run once they match."
} > "$RUNNER_TEMP/body.md"
if [ -n "$existing" ]; then
gh issue edit "$existing" --repo "$GITHUB_REPOSITORY" --body-file "$RUNNER_TEMP/body.md"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$ISSUE_TITLE" --label documentation --body-file "$RUNNER_TEMP/body.md"
fi
elif [ "$status" = "0" ] && [ -n "$existing" ]; then
gh issue close "$existing" --repo "$GITHUB_REPOSITORY" --reason completed --comment "The weekly check found every traction figure matching GitHub."
fi
run: python3 scripts/update_weekly_issue.py traction "$RUNNER_TEMP/status" "$RUNNER_TEMP/err.txt"
139 changes: 139 additions & 0 deletions scripts/update_weekly_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Update the tracking issue for a weekly Fair Code report."""

from __future__ import annotations

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


REPORTS = {
"external-links": {
"title": "Dead external links (weekly check)",
"close_comment": "The weekly check found no broken links or stale ignore entries.",
},
"traction": {
"title": "Traction figures out of date (weekly check)",
"close_comment": "The weekly check found every traction figure matching GitHub.",
},
}


def choose_action(report_state: bool | None, existing_issue: int | None) -> str:
"""Return create/edit/close/none for a report state and matching open issue."""
if report_state is None:
return "none"
if report_state:
return "edit" if existing_issue is not None else "create"
return "close" if existing_issue is not None else "none"


def traction_issue_body(stderr: str) -> str:
"""Build the existing traction report body from check_traction stderr."""
lines = [
"The weekly `check_traction.py --live` run found figures that differ from GitHub or from each other:",
"",
]
for line in stderr.splitlines()[1:]:
lines.append(f"- {line[4:]}" if line.startswith(" - ") else line)
lines += [
"",
"Update the figures in README.md, ROADMAP.md and METRICS.md together, then run `python3 scripts/check_traction.py --live`. "
"This issue closes itself on the next weekly run once they match.",
]
return "\n".join(lines) + "\n"


def external_links_issue_body(report: dict) -> str:
"""Use the external-link report's tested renderer, including its stale count."""
if not __package__:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
report_module = importlib.import_module("scripts.check_external_links")
return report_module.render_issue_body(report)


def external_links_report_needs_issue(report: dict) -> bool:
"""Use the external-link report's tested issue-state rule, including stale ignores."""
if not __package__:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
report_module = importlib.import_module("scripts.check_external_links")
return report_module.report_needs_issue(report)


def update_issue(
repo: str,
kind: str,
report_state: bool | None,
body: str = "",
runner=subprocess.run,
) -> str:
"""Apply the report's open/edit/close decision through the GitHub CLI."""
if report_state is None:
return "none"

config = REPORTS[kind]
existing = runner(
["gh", "issue", "list", "--repo", repo, "--state", "open", "--search",
f'"{config["title"]}" in:title', "--json", "number,title"],
check=True,
capture_output=True,
text=True,
)
matches = [issue["number"] for issue in json.loads(existing.stdout)
if issue["title"] == config["title"]]
issue_number = matches[0] if matches else None
action = choose_action(report_state, issue_number)

if action in {"create", "edit"}:
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False) as file:
file.write(body)
body_path = Path(file.name)
try:
if action == "create":
command = ["gh", "issue", "create", "--repo", repo, "--title", config["title"],
"--label", "documentation", "--body-file", str(body_path)]
else:
command = ["gh", "issue", "edit", str(issue_number), "--repo", repo,
"--body-file", str(body_path)]
runner(command, check=True)
finally:
body_path.unlink(missing_ok=True)
elif action == "close":
runner(
["gh", "issue", "close", str(issue_number), "--repo", repo,
"--reason", "completed", "--comment", config["close_comment"]],
check=True,
)
return action


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("kind", choices=REPORTS)
parser.add_argument("report", type=Path, help="external-links JSON report or traction status file")
parser.add_argument("details", type=Path, nargs="?", help="traction stderr file")
args = parser.parse_args(argv)
repo = os.environ["GITHUB_REPOSITORY"]

if args.kind == "external-links":
report = json.loads(args.report.read_text(encoding="utf-8"))
state = external_links_report_needs_issue(report)
body = external_links_issue_body(report)
else:
if args.details is None:
parser.error("traction requires a status file and stderr file")
status = args.report.read_text(encoding="utf-8").strip()
state = True if status == "1" else False if status == "0" else None
body = traction_issue_body(args.details.read_text(encoding="utf-8")) if state else ""

update_issue(repo, args.kind, state, body)
return 0


if __name__ == "__main__":
raise SystemExit(main())
94 changes: 94 additions & 0 deletions tests/test_weekly_issue_updates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import importlib
import json
from pathlib import Path
from types import SimpleNamespace

import pytest

script = importlib.import_module("scripts.update_weekly_issue")


@pytest.mark.parametrize(
("report_state", "existing_issue", "expected"),
[
(True, None, "create"),
(True, 42, "edit"),
(False, None, "none"),
(False, 42, "close"),
(None, None, "none"),
(None, 42, "none"),
],
)
def test_report_state_selects_create_edit_close_or_noop(report_state, existing_issue, expected):
assert script.choose_action(report_state, existing_issue) == expected


def test_traction_issue_body_preserves_actionable_differences_and_instructions():
body = script.traction_issue_body(
"Traction figures are out of step:\n"
" - issues_closed: METRICS.md (latest weekly row, 2026-W43) says 28, GitHub says 29\n"
" - stars: README=100, GitHub=101\n"
)

assert "- issues_closed: METRICS.md (latest weekly row, 2026-W43) says 28, GitHub says 29" in body
assert "- stars: README=100, GitHub=101" in body
assert "Update the figures in README.md, ROADMAP.md and METRICS.md together" in body


def test_external_links_issue_body_reuses_the_report_renderer(monkeypatch):
report_module = importlib.import_module("scripts.check_external_links")
monkeypatch.setattr(report_module, "render_issue_body", lambda report: f"stale={len(report['stale_ignored'])}", raising=False)
monkeypatch.setattr(report_module, "report_needs_issue", lambda report: bool(report["stale_ignored"]), raising=False)

assert script.external_links_issue_body({"stale_ignored": ["one"]}) == "stale=1"
assert script.external_links_report_needs_issue({"stale_ignored": ["one"]}) is True


def test_external_links_integration_keeps_issue_open_for_stale_ignores():
report = {
"checked": 1,
"ok": [],
"broken": [],
"unknown": [],
"ignored": [],
"stale_ignored": [{"url": "https://example.org/retired", "reason": "no longer referenced"}],
}

body = script.external_links_issue_body(report)

assert "1 stale ignore-list entry" in body
assert "https://example.org/retired" not in body
assert script.external_links_report_needs_issue(report) is True
assert script.external_links_report_needs_issue({**report, "stale_ignored": []}) is False


@pytest.mark.parametrize(
("state", "issues", "expected_action", "expected_command"),
[
(True, [], "create", "create"),
(True, [{"number": 42, "title": "Traction figures out of date (weekly check)"}], "edit", "edit"),
(False, [], "none", None),
(False, [{"number": 42, "title": "Traction figures out of date (weekly check)"}], "close", "close"),
(None, [], "none", None),
],
)
def test_update_issue_runs_only_the_selected_github_operation(
state, issues, expected_action, expected_command
):
calls = []

def runner(command, **kwargs):
calls.append(command)
if command[:3] == ["gh", "issue", "list"]:
return SimpleNamespace(stdout=json.dumps(issues))
if "--body-file" in command:
body_path = command[command.index("--body-file") + 1]
assert Path(body_path).read_text(encoding="utf-8") == "report body"
return SimpleNamespace(stdout="")

action = script.update_issue("owner/repo", "traction", state, "report body", runner=runner)

assert action == expected_action
if expected_command:
assert calls[-1][2] == expected_command
assert len(calls) == (0 if state is None else 1 + (expected_command is not None))
Loading