Skip to content

Check that no caller can leave a release sitting as a draft - #23

Open
imanimanyara wants to merge 2 commits into
mainfrom
ci/hygiene-release-publish
Open

imanimanyara wants to merge 2 commits into
mainfrom
ci/hygiene-release-publish

Conversation

@imanimanyara

Copy link
Copy Markdown
Member

Draft on purpose, and it must not be marked ready until laranail/package-tools#93 is merged. The job runs checks/scripts/verify-release-publish.py out of laranail/package-tools@main. That script is not on main yet — gh api repos/laranail/package-tools/contents/scripts/verify-release-publish.py answers 404 as of opening this — so merging this first would fail the hygiene run of all 18 callers at once with "No such file or directory".

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/validation still holds v1.0.0 and v1.0.1 as drafts from tags withdrawn on 2026-08-24, neither of which 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 — how this family moves a pre-stable tag wherever git push --force is unavailable — republishes nothing on a run that still goes green: correct tag, invisible release.

Nothing else reports that state. The tag resolves, composer installs, and the tag-currency job next door passes, because none of them reads the release. It was found by reading one, in laranail/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, all with green runs behind them. Each is fixed in its own pull request; this is the job that stops the next one.

What this adds

One input (run-release-publish, default true) and one job, release-publish, placed after action-pins because it is the other no-network, caller-path check. It mirrors it line for line, including the path argument — action-pins' own comment records that deriving the target from __file__ silently scanned the wrong repository once the script stopped being a local copy. Unlike the other three jobs it passes no token, because it is a text scan.

The header comment gains a paragraph on why this check is the odd one out: most callers pass it by finding nothing, since they create and update their release with softprops/action-gh-release, which publishes an existing draft on its own ("omit it to publish after upload", in its own input description). A check whose clean answer is an empty result can lie by simply stopping matching, which is why that script drives its own matcher against built-in controls on every invocation and refuses to report anything when one misbehaves.

What each of the 18 callers will see

Measured against the local checkouts on 2026-11-08:

  • 17 pass with "no hand-rolled release edit": ai-compliance artisan-ui barua captcha chrono confetti console db-console db-console-webui db-tools emojis package-management package-scaffolder pdf-toolkit polyglot sis-wrapper toolkit.
  • validation fails until validation#42 merges. It is a caller and holds two of the four call sites, so expect that one red hygiene run, or merge validation#42 first. Verified by running the script against its main: exit 1, both release.yml and release-benchmark.yml.

password-history, password-tools and validation-js are not hygiene callers, which is why each also carries its own Pest guard. Those stay — they cover repositories this workflow does not reach.

Verification

  • The edited file parses: read back with symfony/yaml, it has the 6 inputs and 4 jobs, and the new job's three steps with no env.
  • verify-release-publish.py run against this repository: 8 workflow files, no hand-rolled release edit, exit 0.
  • The same script against laranail/validation@main: exit 1, 2 findings — so the job will do something, rather than passing everything.
  • This repository's own gates, locally: shellcheck scripts/*.sh scripts/tests/*.sh clean, and scripts/tests/*.test.sh 13 passed / 0 failed. They are what audit-tests.yml runs, and it skips draft pull requests (if: github.event.pull_request.draft != true), so this draft shows no checks by design — marking it ready runs that one job.
  • vcs-orgs-presence/bin/health-audit.sh --offline: 0 failures, 18 warnings, all pre-existing (missing client checkouts, and the live-API checks --offline skips). It reads no workflow files at all — grep -cE 'workflows|hygiene' over it returns 0 — so it confirms the tree is unchanged rather than confirming anything about this diff.

Noted, not fixed

The README's "Reusable workflows" list names tests.yml, static-analysis.yml, security.yml and release.yml, and omits hygiene.yml, keepalive.yml and audit-tests.yml. Pre-existing and a separate concern.

Deleting a tag does not delete its GitHub 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,
neither of which 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: correct tag, invisible release. Nothing else
reports it -- the tag resolves, composer installs, and the
tag-currency job next door passes, because none of them reads the
release. It was found by reading one, in laranail/validation-js,
whose release had to be published by hand.

The check is laranail/package-tools' scripts/verify-release-publish.py
and it follows verify-action-pins.py exactly, including the path
argument that makes it point at the caller rather than at
package-tools. It needs no token, because it is a text scan.

Most callers pass it by finding nothing: they use
softprops/action-gh-release, which publishes an existing draft on its
own. That is why the script carries its own matcher controls, and why
the header comment now says so -- a check whose clean answer is an
empty result can lie by simply stopping matching.
@imanimanyara
imanimanyara marked this pull request as ready for review November 8, 2026 05:11
Copilot AI balanced review requested due to automatic review settings November 8, 2026 05:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

`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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants