Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .github/checks-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ checks:
triggers: [".github/workflows/desktop_qualify_beta.yml", ".github/scripts/check-release-process-guards.py", "scripts/run-release-process-guards.sh", "backend/tests/unit/test_desktop_release_scripts.py", ".github/checks-manifest.yaml"]
lanes: ["local", "ci"]
reason: "SCA-174: qualification must check out the exact release tag inside its isolated source directory; the guard and its focused behavioral test run on every relevant diff"
- id: windows-release-sync-pr-lifecycle
command: ["python3", ".github/scripts/test_retire_superseded_windows_sync_prs.py"]
triggers: [".github/workflows/desktop_windows_release.yml", ".github/scripts/retire_superseded_windows_sync_prs.py", ".github/scripts/test_retire_superseded_windows_sync_prs.py", ".github/checks-manifest.yaml"]
lanes: ["local", "ci"]
reason: "#10727: a replacement Windows version-sync PR must retire only older same-repository sync PRs; nine superseded PRs accumulated when one-shot auto-merge could not pass branch protection"
- id: failure-class-retirement-author
command: ["python3", ".github/scripts/test_failure_class_retirement.py"]
triggers: [".github/scripts/failure_class_retirement.py", ".github/scripts/test_failure_class_retirement.py", ".github/workflows/repo-hygiene.yml", "scripts/failure-class", ".github/checks-manifest.yaml"]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"schema_version": 1,
"id": "FC-superseded-windows-sync-pr-lifecycle",
"violated_contract": "Only the newest unmerged Windows release version-sync PR remains actionable; once a replacement PR exists, older same-repository sync PRs must stop competing for review.",
"canonical_prevention": "The Windows release workflow confirms its current same-repository main-targeting sync PR, then retires only strictly older exact-semver release/windows-v* PRs without deleting release state or blocking publication.",
"canonical_prevention_artifact": [
".github/scripts/retire_superseded_windows_sync_prs.py",
".github/scripts/test_retire_superseded_windows_sync_prs.py"
],
"evidence_prs": [
10729
],
"scope_hints": [
".github/workflows/desktop_windows_release.yml",
".github/scripts/retire_superseded_windows_sync_prs.py"
],
"status": "open"
}
199 changes: 199 additions & 0 deletions .github/scripts/retire_superseded_windows_sync_prs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""Close Windows version-sync PRs superseded by a newer release."""

from __future__ import annotations

import argparse
import json
import os
import re
import subprocess
import sys
from collections.abc import Callable, Sequence
from dataclasses import dataclass
from typing import TextIO

WINDOWS_SYNC_BRANCH_RE = re.compile(r"^release/windows-v(\d+)\.(\d+)\.(\d+)$")
Version = tuple[int, int, int]
Runner = Callable[..., subprocess.CompletedProcess[str]]


@dataclass(frozen=True)
class SyncPullRequest:
number: int
branch: str
version: Version


def parse_version(value: str) -> Version:
match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", value)
if match is None:
raise argparse.ArgumentTypeError("version must use MAJOR.MINOR.PATCH")
return tuple(int(part) for part in match.groups())


def format_version(version: Version) -> str:
return ".".join(str(part) for part in version)


def decode_sync_pr(raw: object) -> SyncPullRequest | None:
if not isinstance(raw, dict):
return None
number = raw.get("number")
branch = raw.get("headRefName")
if (
not isinstance(number, int)
or isinstance(number, bool)
or not isinstance(branch, str)
or raw.get("baseRefName") != "main"
or raw.get("isCrossRepository") is not False
):
return None
match = WINDOWS_SYNC_BRANCH_RE.fullmatch(branch)
if match is None:
return None
return SyncPullRequest(
number=number,
branch=branch,
version=tuple(int(part) for part in match.groups()),
)


def select_superseded_prs(
raw_prs: object,
*,
current_pr: int,
current_version: Version,
) -> list[SyncPullRequest] | None:
if not isinstance(raw_prs, list):
return None

sync_prs = [decoded for raw in raw_prs if (decoded := decode_sync_pr(raw)) is not None]
current = next((pr for pr in sync_prs if pr.number == current_pr), None)
if current is None or current.version != current_version:
return None

return sorted(
(pr for pr in sync_prs if pr.number != current_pr and pr.version < current_version),
key=lambda pr: (pr.version, pr.number),
)


def retire_superseded_prs(
*,
repository: str,
current_pr: int,
current_version: Version,
runner: Runner = subprocess.run,
stdout: TextIO = sys.stdout,
stderr: TextIO = sys.stderr,
) -> int:
listing = runner(
[
"gh",
"pr",
"list",
"--repo",
repository,
"--base",
"main",
"--state",
"open",
"--limit",
"200",
"--json",
"number,headRefName,baseRefName,isCrossRepository",
],
check=False,
capture_output=True,
text=True,
)
if listing.returncode != 0:
print("Warning: could not list Windows sync PRs; cleanup skipped.", file=stderr)
return 0

try:
raw_prs = json.loads(listing.stdout)
except json.JSONDecodeError:
print("Warning: gh returned invalid Windows sync PR JSON; cleanup skipped.", file=stderr)
return 0

superseded = select_superseded_prs(
raw_prs,
current_pr=current_pr,
current_version=current_version,
)
if superseded is None:
print(
f"Warning: current Windows sync PR #{current_pr} was not confirmed; cleanup skipped.",
file=stderr,
)
return 0

current_version_text = format_version(current_version)
closed = 0
for pull_request in superseded:
comment = (
f"Superseded by Windows release sync PR #{current_pr} "
f"(v{current_version_text}). The release tag remains authoritative; "
"no tag, release, or branch was deleted."
)
result = runner(
[
"gh",
"pr",
"close",
str(pull_request.number),
"--repo",
repository,
"--comment",
comment,
],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
print(
f"Warning: could not close superseded Windows sync PR #{pull_request.number}.",
file=stderr,
)
continue
closed += 1
message = (
f"Closed superseded Windows sync PR #{pull_request.number} " f"(v{format_version(pull_request.version)})."
)
print(
message,
file=stdout,
)

print(f"Retired {closed} superseded Windows sync PR(s).", file=stdout)
return closed


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Close older same-repository Windows version-sync PRs.")
parser.add_argument(
"--repository",
default=os.environ.get("GITHUB_REPOSITORY"),
help="GitHub repository in owner/name form (defaults to GITHUB_REPOSITORY).",
)
parser.add_argument("--current-pr", type=int, required=True)
parser.add_argument("--current-version", type=parse_version, required=True)
args = parser.parse_args(argv)
if not args.repository:
parser.error("--repository or GITHUB_REPOSITORY is required")
if args.current_pr <= 0:
parser.error("--current-pr must be positive")

retire_superseded_prs(
repository=args.repository,
current_pr=args.current_pr,
current_version=args.current_version,
)
return 0


if __name__ == "__main__":
raise SystemExit(main())
164 changes: 164 additions & 0 deletions .github/scripts/test_retire_superseded_windows_sync_prs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""Tests for retiring superseded Windows release sync PRs."""

from __future__ import annotations

import importlib.util
import io
import json
import subprocess
import sys
import unittest
from pathlib import Path

_SPEC = importlib.util.spec_from_file_location(
"retire_superseded_windows_sync_prs",
Path(__file__).with_name("retire_superseded_windows_sync_prs.py"),
)
sync_cleanup = importlib.util.module_from_spec(_SPEC)
sys.modules[_SPEC.name] = sync_cleanup
_SPEC.loader.exec_module(sync_cleanup)


def pull_request(
number: int,
branch: str,
*,
base: str = "main",
cross_repository: bool = False,
) -> dict[str, object]:
return {
"number": number,
"headRefName": branch,
"baseRefName": base,
"isCrossRepository": cross_repository,
}


class FakeRunner:
def __init__(
self,
payload: object,
*,
list_returncode: int = 0,
close_failures: set[int] | None = None,
) -> None:
self.payload = payload
self.list_returncode = list_returncode
self.close_failures = close_failures or set()
self.calls: list[list[str]] = []

def __call__(self, args: list[str], **_: object) -> subprocess.CompletedProcess[str]:
self.calls.append(args)
if args[:3] == ["gh", "pr", "list"]:
return subprocess.CompletedProcess(
args,
self.list_returncode,
stdout=json.dumps(self.payload),
stderr="list failed" if self.list_returncode else "",
)
number = int(args[3])
return subprocess.CompletedProcess(
args,
1 if number in self.close_failures else 0,
stdout="",
stderr="close failed" if number in self.close_failures else "",
)

@property
def closed_numbers(self) -> list[int]:
return [int(args[3]) for args in self.calls if args[:3] == ["gh", "pr", "close"]]


class RetireSupersededWindowsSyncPRsTests(unittest.TestCase):
def test_closes_only_older_same_repository_main_sync_prs(self) -> None:
runner = FakeRunner(
[
pull_request(10723, "release/windows-v1.0.26"),
pull_request(10419, "release/windows-v1.0.3"),
pull_request(10718, "release/windows-v1.0.25"),
pull_request(10730, "release/windows-v1.0.27"),
pull_request(10684, "release/windows-v1.0.22", cross_repository=True),
pull_request(10653, "release/windows-v1.0.21", base="development"),
pull_request(10000, "release/windows-maintenance"),
]
)
stdout = io.StringIO()

closed = sync_cleanup.retire_superseded_prs(
repository="BasedHardware/Omi",
current_pr=10723,
current_version=(1, 0, 26),
runner=runner,
stdout=stdout,
stderr=io.StringIO(),
)

self.assertEqual(closed, 2)
self.assertEqual(runner.closed_numbers, [10419, 10718])
close_calls = [args for args in runner.calls if args[:3] == ["gh", "pr", "close"]]
self.assertTrue(all("--delete-branch" not in args for args in close_calls))
self.assertTrue(all("#10723" in args[args.index("--comment") + 1] for args in close_calls))
self.assertIn("Retired 2 superseded", stdout.getvalue())

def test_skips_cleanup_unless_the_current_pr_is_confirmed(self) -> None:
runner = FakeRunner([pull_request(10419, "release/windows-v1.0.3")])
stderr = io.StringIO()

closed = sync_cleanup.retire_superseded_prs(
repository="BasedHardware/Omi",
current_pr=10723,
current_version=(1, 0, 26),
runner=runner,
stdout=io.StringIO(),
stderr=stderr,
)

self.assertEqual(closed, 0)
self.assertEqual(runner.closed_numbers, [])
self.assertIn("current Windows sync PR #10723 was not confirmed", stderr.getvalue())

def test_close_failure_is_nonfatal_and_does_not_stop_cleanup(self) -> None:
runner = FakeRunner(
[
pull_request(10723, "release/windows-v1.0.26"),
pull_request(10419, "release/windows-v1.0.3"),
pull_request(10718, "release/windows-v1.0.25"),
],
close_failures={10419},
)
stderr = io.StringIO()

closed = sync_cleanup.retire_superseded_prs(
repository="BasedHardware/Omi",
current_pr=10723,
current_version=(1, 0, 26),
runner=runner,
stdout=io.StringIO(),
stderr=stderr,
)

self.assertEqual(closed, 1)
self.assertEqual(runner.closed_numbers, [10419, 10718])
self.assertIn("could not close superseded Windows sync PR #10419", stderr.getvalue())

def test_list_failure_is_nonfatal(self) -> None:
runner = FakeRunner([], list_returncode=1)
stderr = io.StringIO()

closed = sync_cleanup.retire_superseded_prs(
repository="BasedHardware/Omi",
current_pr=10723,
current_version=(1, 0, 26),
runner=runner,
stdout=io.StringIO(),
stderr=stderr,
)

self.assertEqual(closed, 0)
self.assertEqual(runner.closed_numbers, [])
self.assertIn("could not list Windows sync PRs", stderr.getvalue())


if __name__ == "__main__":
unittest.main()
Loading
Loading