Repository navigation
Add a shared check that no workflow leaves a release as a draft - #93
Merged
Merged
Conversation
Deleting a tag does not delete its release -- it reverts it to a draft. The release keeps its body and assets and stops being visible, and the state outlives the tag: laranail/validation still holds v1.0.0 and v1.0.1 as drafts from tags withdrawn on 2026-08-24, and neither tag exists on the remote any more. `gh release edit` leaves that flag exactly as it finds it, so an edit after a tag is deleted and re-pushed republishes nothing on a run that still goes green. Nothing else reports it: the tag resolves, composer installs, and verify-tag-currency.sh next door passes, because none of them reads the release. It was found by reading one. The check takes the caller's path the way verify-action-pins.py does, so the reusable hygiene workflow can point it at another repository, and it needs no network and no token. Most repositories pass by having no `gh release edit` at all, because softprops/action-gh-release publishes an existing draft on its own. So "found nothing" is the normal result, and a matcher that silently stopped matching would report the whole estate clean. The script runs five built-in controls on every invocation and refuses to report anything when one misbehaves; the workflow-file floor covers a glob that goes empty. The test sabotages the pattern in a copy of the script and asserts the controls catch it.
Neither file is touched by this branch's own change. The repository tracks no lock file, so CI resolves fresh on every run: it got Rector 2.8.4 and PHPStan 2.3.5 while the local vendor directory held 2.6.7 and 2.2.17, and AddArrowFunctionReturnTypeRector in the newer Rector wants `fn (): never` on an arrow function whose body only throws. So the static-analysis job went red on code that has been on main since before this branch, and the last run on main predates the Rector release. A `composer update -W` locally reproduces both findings exactly; this is what Rector itself writes. It lands here rather than in its own pull request because the branch cannot go green without it, and the family's convention is one pull request per repository rather than a trickle.
imanimanyara
added a commit
to laranail/.github
that referenced
this pull request
Nov 8, 2026
`pull_request` with no `types` defaults to opened, synchronize and reopened, and the job skips a draft. So a pull request opened as a draft never gets this check: marking it ready re-triggers nothing, the skipped run stands, and the pull request can merge with its only job never having run. Measured on this very pull request, which is a draft deliberately -- it must not merge before laranail/package-tools#93. Marking it ready left `audit script tests` showing "skipping" from the draft run. Adding `ready_for_review` is additive: every event that triggered the workflow before still does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Deleting a tag does not delete its GitHub release — it reverts it to a draft. The release keeps its body and its assets and simply stops being visible, and the state outlives the tag:
laranail/validationstill holdsv1.0.0andv1.0.1as drafts from tags withdrawn on 2026-08-24, neither of which exists on the remote any more.gh release editleaves that flag exactly as it finds it, so an edit after a tag is deleted and re-pushed — how this family moves a pre-stable tag wherevergit push --forceis unavailable — republishes nothing on a run that still goes green: correct tag, invisible release.Nothing else reports that state. The tag resolves,
composerinstalls, andverify-tag-currency.shnext door passes, because none of them reads the release. It was found by reading one, inlaranail/validation-js, whose release had to be published by hand.Measured 2026-11-08 across the 286 workflow files in
laranail/packages: four hand-rolled call sites in three repositories, every one with green runs behind it. All four are fixed in their own repositories (password-history#21, password-tools#22, validation#42, validation-js#35); this is the shared check that keeps the next one from landing.What this adds
scripts/verify-release-publish.py, alongsideverify-action-pins.pyand following it exactly:__file__silently scanned the wrong repository once it stopped being a local copy.The way this check could lie, and what stops it
Most repositories are clean by having no
gh release editat all —softprops/action-gh-releasepublishes an existing draft unless itsdraftinput is true, "omit it to publish after upload" in its own words. So found nothing is the normal pass, and a matcher that silently stopped matching would report the whole estate clean while reading nothing.So the script drives its matcher against five built-in controls on every invocation — an edit with no flag must fail, one with the flag must pass, a continuation must join, prose about the command must not match, and another
gh releasesubcommand must not match — and refuses to report anything at all if one misbehaves, naming the matcher rather than the repository. The workflow-file floor covers the other direction, a glob that goes empty.tests/Feature/VerifyReleasePublishScriptTest.phpsabotages the pattern in a copy of the script and asserts the controls catch it, so the control mechanism is itself tested rather than assumed.Proof
Run against each of the four repositories' unfixed
main, not against synthetic samples:main)password-historyrelease.ymlpassword-toolsrelease.ymlvalidationrelease.ymlandrelease-benchmark.ymlvalidation-jsrelease.ymlAnd across all 70 package checkouts with the fix branches in place: 4 repositories hold hand-rolled edits, all clean, 0 failing. That is a second method agreeing with the original grep about which call sites exist.
Gates
composer test1735 passed, 3 skipped (4558 assertions) ·laranail-pint --testpassed ·phpstan analyseno errors ·rector process --dry-runno changes. The new test is 10 passing (24 assertions), four of which assert exit 1 on a specific defect.Not in this PR
The reusable
hygiene.ymlinlaranail/.githubis where the 18 callers would inherit this with no change of their own. That wiring must land after this, since the job checkslaranail/package-tools@mainout and runs the script from it — a hygiene job referencing a script that is not onmainyet fails for every caller. It is opened as a draft againstlaranail/.githubfor that reason.