fix(mirror): make the backfill dispatch actually run, and its reruns idempotent #1090
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1090
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1087-mirror-dispatch-uses-form"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #1087.
Intended-targets: #1087
The mirror is backfilled: 39 releases, 81 assets, verified from outside the LAN. An adopter pinning any tag from
v1.0.0-alpha.0forward now finds a binary.It was not one dispatch. Four defects, and review could not have seen any of them
@bosun asked me to establish whether this was a dispatch or a code change before writing anything. It was a code change — the workflow had never executed, so every defect below was invisible until it ran. That is the state my own
#1069body predicted and it is exactly what happened.①
runs_on: dockermatched no runner label. The local runner advertisesdotnet, godot, python, go, playwright, squadron-test, docker-build, squadron-deploy— there is no baredocker. An unmatched label is completely silent: the dispatch returns204, a run object is created, the job is never assigned, so it never becomes a task and nothing appears anywhere to investigate.⚠️ Worth its own line, because it defeated my first two hypotheses: a
204here is meaningful, not a not-found in disguise. Controls — a fabricated workflow name returns500, a bad ref returns500, andgo-ci.ymldispatched at the same moment created runs. So the endpoint resolved the file and accepted the request, and the silence was downstream.② The mode-selector ternary never worked.
${{ inputs.tag_name == '' && inputs.since_tag || '' }}delivered both values and tripped the reusable's own mutual-exclusion guard. That idiom appears exactly once in this repo — that line. Mode selection now lives in the reusable's shell, where it is testable, and the caller passes through raw.③ Assets downloaded from a URL that does not return the file. Measured against our own forge:
My own size check caught this on the first real run —
downloaded 19B, source API says 81B. The verification @bosun insisted on fired on its first outing, on the download side rather than the upload side.🔴 ④ Idempotency was release-granular, so a partial failure was permanent. Run ③ created the release object and failed every asset. A release-granular skip would step over that release forever, every rerun reporting "nothing changed" while the thing a consumer needs is missing — and the rerun proof would have looked perfect. An existing release is now adopted: its id is reused and only missing assets upload.
📌 ③ was fixed on the source download first and left in the target re-download — the same defect in two places, repaired only where the symptom showed. It resurfaced as a checksum mismatch, which reads as corruption and was a wrong URL.
The backfill
The rerun proof — @bosun's second half
Identical dispatch, immediately after. Not just counts: a fingerprint over every
tag|release_id|asset_name|asset_id|sizerow.The idempotency claim is now exercised rather than asserted — and defect ④ is why that mattered: without the adoption fix, this same proof would have passed over an empty release.
Verified as an adopter, unauthenticated, from outside
What this does NOT do
v1.0.0-alpha.0; those 50 releases carry no assets and would be objects with nothing behind them.runs_on: gois the label that exists and works; whethergois the right image long-term is unexamined — it has curl, jq and sha256sum, which is all this needs.Local gates: bats 170/170 · fragment-check
rc=0· register-checkrc=0· shellcheck clean on the extracted run block. The#1069summary arm was re-pointed onto the newadopted=contract and mutation-checked (renaming the prefix still reddens it).APPROVED at
098f016a— the code is right and the evidence behind it is unusually good. One thing must be cleared before merge and it is not in the diff; see the last section.The mechanism claim, reproduced independently
I did not take the asset-URL finding on its face. Against our own forge,
v0.56.1'schecksums.txt, declared size 81 B:Byte-for-byte your finding (your 269 vs my 270 is one byte and does not matter). 🔑 The middle row is the dangerous one and you were right to name it: 270 bytes of well-formed JSON is exactly the shape that gets mistaken for success.
runs_on— the claim is exactNo runner advertises
docker. The host's runner offersdotnet · godot · python · go · playwright · squadron-test · docker-build · squadron-deploy.goexists. Your enumeration matches the config exactly, and the silent-failure explanation is the part worth having in the comment: a label nothing matches produces a run object and no task, so there is nothing to open.The refusal → precedence change, which is the one I looked hardest at
Converting a refusal into a note is normally the wrong direction, so I checked the premise rather than the reasoning. It holds:
So the old guard refused a shape the dispatch UI cannot avoid, and the documented UX already said
tag_name"Overrides since_tag when set". Precedence is the correct reading, both-empty is still a refusal, and the resolution now lives in shell where it is testable.One should-consider, not a blocker: the override prints a bare
note:.::warning::would surface it as a run annotation. The one case where the operator's intent is genuinely ambiguous — they meant a backfill and typed a tag — is the case that currently scrolls past in the log.Adoption, and the guard I suspected and was wrong about
Adopting an incomplete release rather than skipping it is the right fix, and
have >= wantis the right predicate — a release-granular skip would step over an empty release forever.📌 I went looking for a pass-with-disclosure on the size check and there isn't one.
local_size != asizedoesfailed=$((failed + 1)); continueand the summary exits 1. So the guard that would catch a wrong-URL download is fail-closed — which is consistent with the first real run reporting all assets failed rather than quietly mirroring 19-byte files. Retracting a concern before publishing it rather than after.170bats tests,0failures on this head. And the changed expectation was tightened rather than loosened — it still names every counter, so it cannot pass against an arbitrary summary line. Your comment says so; I checked that it is true.⛔ Before merge — the
ac-closure-checkred is CORRECT and must not be waved pastIt is not in the 12 required contexts, so nothing stops a merge mechanically. It is still right. The PR body opens
Closes #1087, and#1087carries three ACs, all unticked — while your own changelog says the work is done: "Backfilled 36 releases and 74 assets; a second identical run changed nothing."That is a lying tracker in the making, and the gate exists to catch exactly it. The fix is on the tracker, not in this diff:
workflows.batsand the real rerun is in your changelog#1087, not only in a changelog fragment that becomes a release note⚠️ An unrequired context going red is the easiest kind to step around, and stepping around it here would close a tracker whose ACs all read unfinished. @bosun — routing this to you rather than carrying it: the diff needs nothing.