Skip to content

ci(publish): add PyPI publish pipeline via Trusted Publishing (OIDC)#4

Merged
alphacrack merged 1 commit into
developmentfrom
feat/pypi-publish-pipeline
Apr 19, 2026
Merged

ci(publish): add PyPI publish pipeline via Trusted Publishing (OIDC)#4
alphacrack merged 1 commit into
developmentfrom
feat/pypi-publish-pipeline

Conversation

@alphacrack

Copy link
Copy Markdown
Owner

Summary

Adds a PyPI publish pipeline that ships python_maithili via Trusted Publishing (OIDC) — no long-lived API tokens stored in GitHub.

How it fires

Trigger Target Purpose
release.published (non-prerelease) PyPI (environment pypi) Production release
release.published (pre-release) TestPyPI (environment testpypi) Staging
workflow_dispatch with target=testpypi TestPyPI On-demand staging
workflow_dispatch with target=build-only (no network) Credential-free dry run

Safety rails

  • Every trigger runs the same build job 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.
  • The production publish-pypi job additionally verifies the GitHub Release tag matches __version__ in the package and refuses to publish on mismatch.
  • pypi environment is designed with a required-reviewer + tag-only deploy-branch rule (documented in docs/RELEASE.md).

Why Trusted Publishing (not API token)

  • No secret to rotate or leak.
  • PyPI binds trust to (owner, repo, workflow filename, environment) — a fork or different workflow can't publish on your behalf.
  • The pypi environment can gate production publishes with required reviewers.

One-time setup (documented in docs/RELEASE.md)

  1. Create GitHub Environments testpypi and pypi in repo Settings.
  2. Register Trusted Publisher on PyPI (project exists as python-maithili at v0.2.0).
  3. Register pending Trusted Publisher on TestPyPI (project doesn't exist there yet; pending publishers auto-convert on first successful publish).

Local verification before PR

I ran the full build locally on the current source:

$ python -m build
Successfully built python_maithili-0.3.0.tar.gz and python_maithili-0.3.0-py3-none-any.whl

$ twine check --strict dist/*
Checking dist/python_maithili-0.3.0-py3-none-any.whl: PASSED
Checking dist/python_maithili-0.3.0.tar.gz:           PASSED

$ pip install dist/*.whl  (into clean venv)
$ python -m maithili_dsl --version      → python_maithili 0.3.0
$ python_maithili examples/hello.dmai   → हम मैथिली में कोड कऽ रहल छी।
$ echo "import os" > /tmp/evil.dmai && python -m maithili_dsl /tmp/evil.dmai
  ⚠️ त्रुटि: आयात अनुमति नहि अछि: 'os'  → exit 5 (sandbox holding)

$ pytest -q  (against the installed wheel)
  142 passed in 0.46s

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 build succeeds on the current source
  • twine check --strict dist/* PASSED for both sdist and wheel
  • Wheel installs in a clean venv; both entry points (python -m maithili_dsl, python_maithili) resolve
  • pytest (142 tests) passes against the installed wheel
  • Security sandbox still blocks import os when running against the installed wheel (exit code 5)
  • publish.yml parses as valid YAML with expected job + environment + permission structure

The build-only workflow_dispatch path 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

  • Trusted Publishing keys are scoped to (alphacrack, python-maithili-dsl, publish.yml, pypi|testpypi). A PR from a fork cannot trigger the publish job because it can't access the pypi / testpypi environments.
  • id-token: write is only granted to the two publish jobs, not to the build job.
  • The contents: read top-level permission minimizes attack surface.
  • Tag / version verification prevents a situation where a release tag v0.3.1 accidentally publishes 0.3.0 bits from an older commit.

Breaking Changes

None — this is purely additive.

Post-merge plan

After this merges to development:

  1. I'll open a release PR development → main so the workflow is available on the default branch (some OIDC consumers are strict about this).
  2. You set up the Trusted Publishers on PyPI + TestPyPI and create the two GitHub Environments (docs/RELEASE.md walks through it).
  3. We trigger workflow_dispatch → testpypi to verify end-to-end.
  4. You cut a GitHub Release for v0.3.0 to publish to real PyPI.

🤖 Generated with Claude Code

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>
@alphacrack alphacrack merged commit 92c44e9 into development Apr 19, 2026
8 checks passed
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