Skip to content

fix(ci): pin checkout SHA + PEP 440 versions#225

Merged
NikolayS merged 1 commit intomainfrom
fix/release-workflow-prerelease
May 7, 2026
Merged

fix(ci): pin checkout SHA + PEP 440 versions#225
NikolayS merged 1 commit intomainfrom
fix/release-workflow-prerelease

Conversation

@NikolayS
Copy link
Copy Markdown
Owner

@NikolayS NikolayS commented May 7, 2026

Summary

Two real follow-ups to #222 surfaced by review:

High — Go publish TOCTOU (and the same class in TS)

release-go.yml's validate and publish jobs both actions/checkout@v4 with ref: main. Between validate finishing and publish running after the go-release environment approval, main can advance — and the post-approval publish builds a different SHA than the one validate verified. Same pattern in release-typescript.yml (validate + publish, gated by the npm environment).

Fix: pin every actions/checkout@v4 in all three release workflows to ref: ${{ github.sha }} so a single workflow_dispatch invocation always operates on one commit. The Python workflow's build job is also pinned for consistency, even though publish-{testpypi,pypi} download the validated artifact rather than re-checkout.

Medium — Python regex rejects PEP 440 canonical, accepts PyPI-invalid

release-python.yml:48 regex ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?\$ (pre-fix) silently misclassifies versions:

Input What it is Old regex New validator
0.2.0rc1 PEP 440 canonical reject accept
0.2.0.dev0 PEP 440 canonical reject accept
0.2.0-rc.1 semver, not PEP 440 (normalises to 0.2.0rc1) accept reject
0.2.0-dev PyPI-invalid (normalises to 0.2.0.dev0) accept reject

Replaced with packaging.version.Version(...) that requires the input to be in PEP 440 canonical form. This forces operators to type exactly the string that ends up in pyproject.toml and on PyPI — no silent normalisation surprise where the dispatch input disagrees with what gets published.

Validation

  • actionlint clean on all three workflows.
  • Validator behaviour confirmed locally:
    0.2.0rc1     → OK (canonical)
    0.2.0.dev0   → OK (canonical)
    0.2.0-rc.1   → rejected (not canonical; normalises to 0.2.0rc1)
    0.2.0-dev    → rejected (not canonical; normalises to 0.2.0.dev0)
    

Test plan

  • CI green
  • After merge, dispatch `Release Python` with version=`0.2.0rc1` and dry_run=true, confirm validator accepts and the artifact builds + uploads
  • After chore: stamp v0.2.0-rc.1 #223 merges and `v0.2.0-rc.1` is tagged, dispatch all three release workflows in dry-run mode

Related

🤖 Generated with Claude Code

Two follow-ups to #222:

- release-go.yml validate + publish, release-typescript.yml validate +
  publish, and release-python.yml build all `actions/checkout@v4` with
  `ref: main`. Between validate and post-approval publish, main can
  advance and publish would build a different SHA than validate verified.
  Pin every checkout to `${{ github.sha }}` so a single dispatch
  invocation always operates on one commit.

- The Python release workflow's regex
  `^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$` rejects PEP 440
  canonical prereleases (`0.2.0rc1`, `0.2.0.dev0`) while still accepting
  PyPI-invalid forms like `0.2.0-dev`. Replaced with packaging.version.Version
  that requires the input to be in PEP 440 canonical form (so `0.2.0-rc.1`
  and `0.2.0-dev` are also rejected, since they normalise to something
  the operator did not type and would silently disagree with pyproject.toml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@NikolayS NikolayS mentioned this pull request May 7, 2026
@NikolayS NikolayS merged commit 1671634 into main May 7, 2026
12 checks passed
@NikolayS NikolayS deleted the fix/release-workflow-prerelease branch May 7, 2026 03:51
NikolayS added a commit that referenced this pull request May 7, 2026
Surfaced by the v0.2.0-rc.1 dry-run: npm refuses to publish a prerelease
without an explicit dist-tag, because otherwise the prerelease would
silently inherit the `latest` tag and `npm install pgque` would resolve
to it for every user.

Resolve the dist-tag from the version's prerelease identifier:

  0.2.0          -> latest
  0.2.0-rc.1     -> rc
  0.2.0-dev.0    -> dev
  0.2.0-beta.3   -> beta
  0.2.0-alpha.1  -> alpha
  0.2.0-anything -> next   (catch-all)

Apply in both the validate-stage `npm publish --dry-run --provenance`
and the gated `npm publish --provenance` step. Caught by the post-#225
dry-run that ran end-to-end for the first time.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant