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
70 changes: 51 additions & 19 deletions .github/scripts/prune_renders.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,33 @@ def collect_paths_under(dir_path: str) -> list[str]:
return paths


def _is_git_data_404(exc: RuntimeError) -> bool:
message = str(exc)
return (
" → 404:" in message
and repo_path("/git/") in message
and (message.startswith("POST ") or message.startswith("PATCH "))
)


def delete_via_contents_api(paths_to_delete: list[str]) -> None:
for path in paths_to_delete:
current = request_api(
"GET", repo_path(f"/contents/{path}?ref={BRANCH}"), expect_404=True
)
if current is None:
continue
request_api(
"DELETE",
repo_path(f"/contents/{path}"),
{
"message": f"Prune stale render file {path}",
"branch": BRANCH,
"sha": current["sha"],
},
)


def commit_deletions(paths_to_delete: list[str]) -> None:
"""Build a new tree omitting the given paths and push a single commit.

Expand All @@ -115,27 +142,32 @@ def commit_deletions(paths_to_delete: list[str]) -> None:
# base_tree is load-bearing: omit it and the new tree contains ONLY the
# delete-stubs, which the API resolves to an empty tree → next commit
# wipes the branch. Test test_commit_deletions_payload_shape locks this.
new_tree = request_api(
"POST",
repo_path("/git/trees"),
{"base_tree": base_tree_sha, "tree": tree_entries},
)
try:
new_tree = request_api(
"POST",
repo_path("/git/trees"),
{"base_tree": base_tree_sha, "tree": tree_entries},
)

new_commit = request_api(
"POST",
repo_path("/git/commits"),
{
"message": f"Prune {len(paths_to_delete)} stale render file(s)",
"tree": new_tree["sha"],
"parents": [head_sha],
},
)
new_commit = request_api(
"POST",
repo_path("/git/commits"),
{
"message": f"Prune {len(paths_to_delete)} stale render file(s)",
"tree": new_tree["sha"],
"parents": [head_sha],
},
)

request_api(
"PATCH",
repo_path(f"/git/refs/heads/{BRANCH}"),
{"sha": new_commit["sha"], "force": False},
)
request_api(
"PATCH",
repo_path(f"/git/refs/heads/{BRANCH}"),
{"sha": new_commit["sha"], "force": False},
)
except RuntimeError as exc:
if not _is_git_data_404(exc):
raise
delete_via_contents_api(paths_to_delete)


def main() -> int:
Expand Down
60 changes: 60 additions & 0 deletions tests/github_scripts/test_prune_renders.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,66 @@ def test_commit_deletions_at_safety_limit_proceeds():
assert m.call_count == 5, "all 5 API calls must occur — no early return at the limit"


def test_commit_deletions_falls_back_to_contents_api_on_git_data_404():
calls = []

def fake_api(method, path, body=None, **kwargs):
calls.append((method, path, body, kwargs))
if (method, path) == ("GET", "/repos/owner/repo/git/refs/heads/renders"):
return {"object": {"sha": "head_sha"}}
if (method, path) == ("GET", "/repos/owner/repo/git/commits/head_sha"):
return {"tree": {"sha": "base_tree_sha"}}
if (method, path) == ("POST", "/repos/owner/repo/git/trees"):
raise RuntimeError(
'POST /repos/owner/repo/git/trees → 404: {"message":"Not Found"}'
)
if (method, path) == ("GET", "/repos/owner/repo/contents/issue-1/a.png?ref=renders"):
return {"sha": "sha-a"}
if (method, path) == ("GET", "/repos/owner/repo/contents/issue-1/b.png?ref=renders"):
return {"sha": "sha-b"}
if (method, path) == ("DELETE", "/repos/owner/repo/contents/issue-1/a.png"):
return {}
if (method, path) == ("DELETE", "/repos/owner/repo/contents/issue-1/b.png"):
return {}
raise AssertionError(f"unexpected API call: {(method, path, body, kwargs)}")

with patch.object(prune_renders, "request_api", side_effect=fake_api):
prune_renders.commit_deletions(["issue-1/a.png", "issue-1/b.png"])

assert calls[3] == (
"GET",
"/repos/owner/repo/contents/issue-1/a.png?ref=renders",
None,
{"expect_404": True},
)
assert calls[4] == (
"DELETE",
"/repos/owner/repo/contents/issue-1/a.png",
{
"message": "Prune stale render file issue-1/a.png",
"branch": "renders",
"sha": "sha-a",
},
{},
)
assert calls[5] == (
"GET",
"/repos/owner/repo/contents/issue-1/b.png?ref=renders",
None,
{"expect_404": True},
)
assert calls[6] == (
"DELETE",
"/repos/owner/repo/contents/issue-1/b.png",
{
"message": "Prune stale render file issue-1/b.png",
"branch": "renders",
"sha": "sha-b",
},
{},
)


def test_list_issue_dirs_ignores_non_issue_entries():
contents = [
{"type": "dir", "name": "issue-1", "path": "issue-1"},
Expand Down
Loading