From c4d84af4ec6c00c74a0589c64f8064da479ac18e Mon Sep 17 00:00:00 2001 From: dxnnystyles Date: Thu, 29 Oct 2026 07:04:53 -0700 Subject: [PATCH] test: cover weekly issue update helpers --- .github/workflows/external-links.yml | 22 +---- .github/workflows/traction-live.yml | 22 +---- scripts/update_weekly_issue.py | 139 +++++++++++++++++++++++++++ tests/test_weekly_issue_updates.py | 94 ++++++++++++++++++ 4 files changed, 235 insertions(+), 42 deletions(-) create mode 100644 scripts/update_weekly_issue.py create mode 100644 tests/test_weekly_issue_updates.py diff --git a/.github/workflows/external-links.yml b/.github/workflows/external-links.yml index cc1f38b..f32d7ae 100644 --- a/.github/workflows/external-links.yml +++ b/.github/workflows/external-links.yml @@ -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 @@ -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" diff --git a/.github/workflows/traction-live.yml b/.github/workflows/traction-live.yml index ad470be..77215fb 100644 --- a/.github/workflows/traction-live.yml +++ b/.github/workflows/traction-live.yml @@ -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 @@ -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" diff --git a/scripts/update_weekly_issue.py b/scripts/update_weekly_issue.py new file mode 100644 index 0000000..a9399ee --- /dev/null +++ b/scripts/update_weekly_issue.py @@ -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()) diff --git a/tests/test_weekly_issue_updates.py b/tests/test_weekly_issue_updates.py new file mode 100644 index 0000000..333c01e --- /dev/null +++ b/tests/test_weekly_issue_updates.py @@ -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))