diff --git a/scripts/check-issue-links.py b/scripts/check-issue-links.py index dbbe0e53..3045e3c6 100644 --- a/scripts/check-issue-links.py +++ b/scripts/check-issue-links.py @@ -1399,7 +1399,25 @@ def _mention_detail(refs: Refs, number: int) -> str: return "; ".join(clauses) -def _declined_detail(refs: Refs, number: int) -> str: +#: A release version an issue's own text names, e.g. `Release v0.3.46`. It is read +#: because it is the one fact an open issue carries about what finishes *it*: a PR's own +#: title never reaches `judge_issues`, so "was the declaring PR a bump?" cannot be answered +#: here at all, while "does this issue's completion name a published artifact?" can. +_RELEASE_VERSION = re.compile(r"\bv\d+\.\d+\.\d+\b") + + +def release_version(*texts: str | None) -> str | None: + """The release version this issue's own text names, or `None` when it names none.""" + for text in texts: + if not text: + continue + found = _RELEASE_VERSION.search(text) + if found: + return found.group(0) + return None + + +def _declined_detail(refs: Refs, number: int, release: str | None = None) -> str: """What to do about declarers that closed with no open one left. Two facts, one sentence each, because the remedies are opposite: a **merged** @@ -1435,6 +1453,19 @@ def _declined_detail(refs: Refs, number: int) -> str: "finish the work: do not close this issue on that merge, state here what " "it still leaves" ) + elif release is not None: + clauses.append( + f"{_numbers(refs.declared_merged)} declared `Closes #{number}` and {verb} " + f"merged, and what finishes this issue is the published `{release}`, not " + "that merge - a declaring pull request can move the version string while " + "nothing is released, so the merge is not the reading that settles it. " + f"`uv run --no-sync python3 scripts/check-release-published.py {release}` " + "is the reading: green across the platform matrix is what says the work " + "is done, and this issue may be " + "closed on that reading; anything else, or no answer at all, means it is " + "not done - do not close this issue on the merge, state here what it " + "still leaves" + ) else: clauses.append( f"{_numbers(refs.declared_merged)} declared `Closes #{number}` and {verb} " @@ -1490,7 +1521,9 @@ def judge_issues( ) continue if refs.declared_merged or refs.declared_abandoned: - detail = _declined_detail(refs, number) + detail = _declined_detail( + refs, number, release_version(issue.get("title"), issue.get("body")) + ) elif refs.mentioned_landed or refs.mentioned_abandoned or refs.mentioned_open: detail = _mention_detail(refs, number) else: diff --git a/tests/test_check_issue_links.py b/tests/test_check_issue_links.py index b17e2869..cdd775d7 100644 --- a/tests/test_check_issue_links.py +++ b/tests/test_check_issue_links.py @@ -2803,3 +2803,54 @@ def test_the_ledger_path_is_overridable(mod, monkeypatch): monkeypatch.setenv("EMRG_RANTS", "/tmp/from-env.jsonl") assert mod.rants_path() == Path("/tmp/from-env.jsonl") assert mod.rants_path("/tmp/wins.jsonl") == Path("/tmp/wins.jsonl") + + +# --- an issue finished by a published artifact, not by a merge --------------------- +# +# A release issue is the case where "a declaring PR merged" and "the work is done" come +# apart: the bump PR carries the closing keyword, so its merge closes the issue while +# nothing has been tagged or published. The row then offered "the reading that says the +# work is done" for work that had not begun. The fix reads the one fact the issue itself +# carries - the release version it names - and sends the reader to that artifact's own +# reading instead of inferring completion from the merge. + + +def test_a_release_issue_is_not_told_to_close_on_its_bump_merge(mod) -> None: + detail = mod._declined_detail(mod.Refs(declared_merged={2782}), 2781, "v0.3.46") + assert "scripts/check-release-published.py v0.3.46" in detail + assert "either close this issue with the reading that says the work is done" not in detail + + +def test_an_ordinary_issue_keeps_the_remedy_it_had(mod) -> None: + detail = mod._declined_detail(mod.Refs(declared_merged={2779}), 2778) + assert "either close this issue with the reading that says the work is done" in detail + assert "check-release-published" not in detail + + +def test_the_artifact_is_what_changes_the_sentence_not_the_branch(mod) -> None: + refs = mod.Refs(declared_merged={2782}) + withheld = mod._declined_detail(refs, 2781) + assert withheld == mod._declined_detail(refs, 2781, None) + assert withheld != mod._declined_detail(refs, 2781, "v0.3.46") + + +def test_the_release_version_is_read_only_where_it_is_one(mod) -> None: + assert mod.release_version("Release v0.3.46") == "v0.3.46" + assert mod.release_version("an ordinary title") is None + assert mod.release_version(None, "") is None + assert mod.release_version("0.3.46") is None + assert mod.release_version("xv0.3.46y") is None + + +def test_judge_issues_reads_the_version_off_the_issue_it_is_judging(mod) -> None: + issues = [ + {"number": 2781, "title": "Release v0.3.46", "created_at": "2026-11-09T17:35:22Z"}, + {"number": 2778, "title": "A change that needs no release", "created_at": "2026-11-09T15:49:24Z"}, + ] + refs = { + 2781: mod.Refs(declared_merged={2782}), + 2778: mod.Refs(declared_merged={2779}), + } + rows = {row.number: row for row in mod.judge_issues(issues, refs, {})} + assert "check-release-published.py v0.3.46" in rows[2781].detail + assert "check-release-published" not in rows[2778].detail diff --git a/tests/test_printed_commands_carry_the_python_runner.py b/tests/test_printed_commands_carry_the_python_runner.py index b86d63bb..c968c32c 100644 --- a/tests/test_printed_commands_carry_the_python_runner.py +++ b/tests/test_printed_commands_carry_the_python_runner.py @@ -73,10 +73,11 @@ what keeps the clause off prose: "update `VERSION_SOURCES` in `scripts/bump-version.py`" ends at the file name and is not a thing to run, while `scripts/check-vote-count.py ` is. -What the clause reads on this tree, measured 2026-10-24: **39** command-shaped mentions, in -eight tools - `bump-version.py` 2, `cast-vote.py` 8, `check-merge-freshness.py` 8, -`check-merge-landed.py` 1, `check-merge-sequence.py` 1, `check-release-published.py` 1, -`check-vote-count.py` 1, `review-queue.py` 17. This number has gone down as well as up, and +What the clause reads on this tree, measured 2026-11-10: **40** command-shaped mentions, in +nine tools - `bump-version.py` 2, `cast-vote.py` 8, `check-issue-links.py` 1, +`check-merge-freshness.py` 8, `check-merge-landed.py` 1, `check-merge-sequence.py` 1, +`check-release-published.py` 1, `check-vote-count.py` 1, `review-queue.py` 17. This number +has gone down as well as up, and it goes down the way it went up: a *printed remedy* is a printed command either way, so a remedy deleted together with the tool that printed it takes its mention with it. `review-queue.py` carried a second `abstain` branch - a head pushed below a window whose