ci(publish): add PyPI publish pipeline via Trusted Publishing (OIDC)#4
Merged
Merged
Conversation
Adds a two-stage publish workflow that ships python_maithili to
PyPI without storing long-lived API tokens in GitHub. Three
triggers, three behaviours:
- release.published (non-prerelease) -> publishes to production
PyPI (environment: pypi, with optional manual-approval gate)
- release.published (prerelease) OR workflow_dispatch target=testpypi
-> publishes to TestPyPI (environment: testpypi, staging/verification)
- workflow_dispatch target=build-only -> builds + twine-checks, no
network publish (credential-free dry run)
Every path runs the same build job first: python -m build, twine
check --strict, install the wheel in a clean venv, run the full
pytest suite against the installed wheel. Only if the wheel itself
passes the test suite does anything get uploaded. The production
job additionally verifies that the GitHub Release tag (v0.X.Y)
matches __version__ in the package and refuses to publish on
mismatch — preventing accidental version drift.
Trusted Publishing was chosen over API tokens because:
- No secrets to rotate or leak.
- Scoped per environment: the pypi environment can be gated with
required reviewers and tag-only deployment branches.
- PyPI binds the trust to (owner, repo, workflow filename,
environment), so a fork or a different workflow cannot publish
on your behalf even if it runs on this repo.
The one-time setup (register Trusted Publisher on PyPI and pending
publisher on TestPyPI, create pypi + testpypi GitHub Environments)
is documented in the new docs/RELEASE.md, along with the routine
release checklist, version-bump convention, and recovery procedure
for bad releases.
build + twine added to requirements-dev.txt so contributors can
reproduce the build locally before opening a release PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Adds a PyPI publish pipeline that ships
python_maithilivia Trusted Publishing (OIDC) — no long-lived API tokens stored in GitHub.How it fires
release.published(non-prerelease)pypi)release.published(pre-release)testpypi)workflow_dispatchwithtarget=testpypiworkflow_dispatchwithtarget=build-onlySafety rails
buildjob first:python -m build,twine check --strict, install the wheel in a fresh venv, then run the full pytest suite against the installed wheel. Nothing gets uploaded unless the wheel itself passes its own tests.publish-pypijob additionally verifies the GitHub Release tag matches__version__in the package and refuses to publish on mismatch.pypienvironment is designed with a required-reviewer + tag-only deploy-branch rule (documented indocs/RELEASE.md).Why Trusted Publishing (not API token)
(owner, repo, workflow filename, environment)— a fork or different workflow can't publish on your behalf.pypienvironment can gate production publishes with required reviewers.One-time setup (documented in
docs/RELEASE.md)testpypiandpypiin repo Settings.python-maithiliat v0.2.0).Local verification before PR
I ran the full build locally on the current source:
So the package builds, passes twine's metadata check, installs cleanly from a wheel, and the test suite passes against the installed wheel — exactly the invariants the workflow will enforce on CI.
Test Evidence
python -m buildsucceeds on the current sourcetwine check --strict dist/*PASSED for both sdist and wheelpython -m maithili_dsl,python_maithili) resolvepytest(142 tests) passes against the installed wheelimport oswhen running against the installed wheel (exit code 5)publish.ymlparses as valid YAML with expected job + environment + permission structureThe
build-onlyworkflow_dispatchpath will re-run all of the above on CI once this merges, and will be the easiest way to verify future builds without any credentials.Security Considerations
(alphacrack, python-maithili-dsl, publish.yml, pypi|testpypi). A PR from a fork cannot trigger the publish job because it can't access thepypi/testpypienvironments.id-token: writeis only granted to the two publish jobs, not to the build job.contents: readtop-level permission minimizes attack surface.v0.3.1accidentally publishes 0.3.0 bits from an older commit.Breaking Changes
None — this is purely additive.
Post-merge plan
After this merges to
development:development → mainso the workflow is available on the default branch (some OIDC consumers are strict about this).docs/RELEASE.mdwalks through it).workflow_dispatch → testpypito verify end-to-end.v0.3.0to publish to real PyPI.🤖 Generated with Claude Code