From a9ae65781f9190218c5d9932c99e195071cb4ca5 Mon Sep 17 00:00:00 2001 From: daharoni Date: Mon, 11 May 2026 18:53:26 +0200 Subject: [PATCH] push_to_wiki: treat delete of an already-missing page as idempotent success MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before: a delete API call for a page that no longer exists on the wiki returned `error.code = "missingtitle"`, the script counted it as a generic error, and the workflow exited 1 — even though the post-state ("page not on wiki") matched the intent. This is a real-world case: pages may be manually deleted by a curator on the wiki, or be cleaned up by a previous sync that ran with a different base_ref. The recent dispatch with `base_ref=6348e17` hit this on the two phantom keys from the initial commit (`aharoni2019`, `cai2016`) — both already absent, both reported as "errors", workflow failed despite both real-target pages being created successfully. Make delete_page idempotent on `missingtitle`: return success with a sentinel `MISSING_PAGE` marker. Caller logs it as `(already absent)` and keeps a separate `already_missing` counter so it stays out of the deleted-count (the wiki didn't actually do anything) but doesn't trip the error path. Summary line includes the new counter only when it's non-zero, so output stays clean for the common case. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/push_to_wiki.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/scripts/push_to_wiki.py b/scripts/push_to_wiki.py index 6c14e9d..01e6524 100644 --- a/scripts/push_to_wiki.py +++ b/scripts/push_to_wiki.py @@ -22,6 +22,9 @@ re.escape(MARKER_START) + r".*?" + re.escape(MARKER_END), re.DOTALL, ) +# Sentinel returned by WikiClient.delete_page for the idempotent "already +# absent" case so the caller can distinguish it from a real delete. +MISSING_PAGE = "__already_missing__" TEMPLATE_PARAMS_RE = re.compile( r"\|([^=|]+)=([^|]*?)(?=\n\||\n\}\}|\}\})", re.DOTALL ) @@ -130,7 +133,17 @@ def edit_page(self, title, content, summary, csrf_token): return True, edit_info.get("result", "Unknown") def delete_page(self, title, reason, csrf_token): - """Delete a wiki page. Returns (success, result_or_error).""" + """Delete a wiki page. + + Idempotent: if the page is already absent (manual deletion, prior + sync, etc.) the call is treated as success rather than error — the + post-state matches the intent regardless of who removed the page. + + Returns (success, marker). On true success, marker is the page title + the wiki echoed back. For the already-absent case, marker is the + sentinel `MISSING_PAGE` so the caller can log it differently and + keep it out of the deleted-count. + """ resp = self.session.post( self.api_url, data={ @@ -143,6 +156,8 @@ def delete_page(self, title, reason, csrf_token): ) result = resp.json() if "error" in result: + if result["error"].get("code") == "missingtitle": + return True, MISSING_PAGE return False, result["error"]["info"] return True, result.get("delete", {}).get("title", title) @@ -282,19 +297,29 @@ def main(): # Delete pages for removed citations deleted = 0 + already_missing = 0 for entry in deleted_entries: title = entry["page_title"] success, result = client.delete_page( title, "citations-sync: removed from BibTeX", csrf_token ) if success: - deleted += 1 - print(f" - {title}") + if result == MISSING_PAGE: + # Page was already absent — idempotent no-op, not an error. + already_missing += 1 + print(f" - {title} (already absent)") + else: + deleted += 1 + print(f" - {title}") else: errors += 1 print(f" ! {title} (delete): {result}", file=sys.stderr) - print(f"\nDone: {created} created, {updated} updated, {skipped} unchanged, {deleted} deleted, {errors} errors") + missing_note = f", {already_missing} already absent" if already_missing else "" + print( + f"\nDone: {created} created, {updated} updated, {skipped} unchanged, " + f"{deleted} deleted{missing_note}, {errors} errors" + ) if errors: sys.exit(1)