From 9bc795c331bd653b0c483c0713b2ae983728d120 Mon Sep 17 00:00:00 2001 From: lmoresi Date: Wed, 2 Sep 2026 13:29:58 -0700 Subject: [PATCH] Do not reserve a second DOI for a note already being asked about I dropped this guard when I rewrote the step in #43. It had been added in #42 for the queue design, and the rewrite replaced the whole step without carrying it across. `unreserved()` reads main, and a reserved record only reaches main when a request is MERGED. So between opening a request and merging it the note still looks unreserved, and the next run of the reminder creates a second figshare draft, reserves a second DOI, and opens a second request -- abandoning the first draft. That is the duplicate-mint hazard the whole flow exists to prevent. It is already wrong on the push trigger, where two metadata pushes before a merge are enough. It would be worse on a schedule, which is what prompted looking: a weekly run would mint a fresh DOI for every waiting request, every week. So an OPEN request counts as having asked, and the reminder subtracts those before it reserves anything. The test asserts the guard rather than its wording, since this is the second time the mechanism has been rewritten around it. Underworld development team with AI support from Claude Code --- .github/workflows/deposit-ready.yml | 23 +++++++++++++++++++++-- tests/test_migration.py | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deposit-ready.yml b/.github/workflows/deposit-ready.yml index d192f39..4ecd551 100644 --- a/.github/workflows/deposit-ready.yml +++ b/.github/workflows/deposit-ready.yml @@ -31,13 +31,32 @@ jobs: - name: Which notes have no record at all id: pending + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + # A request that is already OPEN counts as asked. `unreserved()` + # reads main, and a reserved record only reaches main when a request + # is MERGED -- so without this, every run reserves a second DOI for a + # note whose request is still waiting, and abandons the draft behind + # it. That is the duplicate-mint hazard the whole flow exists to + # prevent, so the guard has to be here and not in the report. + gh pr list --state open --limit 100 --json title \ + --jq '.[] | select(.title | startswith("Deposit: ")) | + .title | sub("^Deposit: "; "")' \ + | tr -d ' ' | sed '/^$/d' > .already-asked || true + echo "already asked: $(tr '\n' ' ' < .already-asked)" SLUGS=$(pixi run -q python3 -c " import sys; sys.path.insert(0, 'scripts'); import deposit - print(','.join(deposit.unreserved())) + asked = set() + try: + asked = {l.strip() for l in open('.already-asked') if l.strip()} + except FileNotFoundError: + pass + print(','.join(s for s in deposit.unreserved() if s not in asked)) ") + rm -f .already-asked echo "slugs=${SLUGS}" >> "$GITHUB_OUTPUT" - echo "no record yet: ${SLUGS:-none}" + echo "to ask about: ${SLUGS:-none}" # ONE pull request per note, each touching only that note's own # metadata.yml. The queue file this replaced was appended to by every diff --git a/tests/test_migration.py b/tests/test_migration.py index f60d95c..84108e7 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -2349,3 +2349,26 @@ def candidates(slug, built): ["years-of-citcom-ellipsis-and-underworld"] long_slug = "a" * 60 assert candidates(long_slug, {"a" * cap}) == ["a" * cap] + + +def test_an_open_request_is_not_asked_for_twice(): + """`unreserved()` reads main, and a reserved record only reaches main when + a request is MERGED. + + So without a guard on what is already open, every run of the reminder + reserves a SECOND DOI for a note whose request is still waiting, and + abandons the draft behind it -- the exact duplicate-mint hazard the flow + exists to prevent. It bites hardest on a schedule, where the reminder runs + whether or not anything changed. + """ + ready = (ROOT / ".github" / "workflows" / "deposit-ready.yml").read_text( + encoding="utf-8") + config = "\n".join(l for l in ready.splitlines() + if not l.lstrip().startswith("#")) + ask_step = config.split("id: pending")[1].split("- name:")[0] + assert "gh pr list --state open" in ask_step, \ + "the reminder must look at what is already open before reserving" + assert 'startswith("Deposit: ")' in ask_step, \ + "it must match the requests it opens itself" + assert "if s not in asked" in ask_step, \ + "and it must subtract them from what it asks about"