Registry Publishing¶
OpenMed publishes the openmed wheel and source distribution from the tag-driven .github/workflows/publish.yml workflow. The same workflow validates and publishes the openmed npm package for browsers and Node.js. Both packages must match the release tag before either registry upload begins. A guarded manual dispatch exists only to recover an existing immutable tag after a workflow-only failure.
The current PyPI path uses the project-scoped PYPI_API_TOKEN GitHub secret. The npm path uses the short-lived NPM_ACCESS_TOKEN secret from the GitHub npm environment and publishes with Sigstore provenance.
PyPI Trusted Publishing is the preferred future path, but it must not be used until the PyPI openmed project has a trusted publisher that exactly matches this repository, workflow file, and GitHub environment.
PyPI workflow contract¶
The only PyPI publishing workflow is .github/workflows/publish.yml.
- It runs automatically from
pushevents forv*tags. - A maintainer may manually dispatch it with an existing
vX.Y.Ztag to recover a failed tag run. The workflow checks out that exact tag, verifies the checked-out commit against the tag, and repeats every build and verification gate before publishing. - It does not run from
pull_requestor forked pull request events. - The reusable provenance job in
.github/workflows/provenance.ymlbuilds and checks the distributions, generates SLSA provenance, and verifies the attestations before upload. - The publish job downloads those verified distributions, uses
pypa/gh-action-pypi-publish, and grants onlycontents: read. - The publish job attaches the
pypiGitHub environment so it can read the environment-scopedPYPI_API_TOKENsecret. That environment must not have reviewer, wait-timer, or branch-policy gates that block tagged releases. - The publish action is configured with
password: ${{ secrets.PYPI_API_TOKEN }}andattestations: false. - PyPI-native PEP 740 attestations are disabled while token upload is active, because the PyPA action supports those attestations only with Trusted Publishing. The repository-level SLSA provenance artifact is still generated and verified before upload.
- The evidence job runs after the publish job, so it cannot gate the PyPI upload on GitHub OIDC or Sigstore availability. Signing is best effort, but evidence that is produced must verify against the
publish.ymlworkflow identity and triggering workflow revision before it is attached to the release, or the evidence job fails. The attachedrelease-source.jsonseparately records the exact immutable tag commit that was checked out, including during a recovery dispatch frommaster. - Conventional commits determine the minimum safe version bump. The tag may intentionally select a larger minor or major version, but it must be newer than the previous tag, meet or exceed that minimum, and exactly match both package manifests.
Do not add a second PyPI publishing workflow. Do not add hatch publish or Twine upload commands back to release CI.
npm workflow contract¶
The JavaScript package source lives in js/openmedkit-web, but it is published under the existing unscoped npm name openmed.
js/openmedkit-web/package.json,openmed/__about__.py, and thev*tag must contain the same semantic version.- The
npm-verifyjob uses Node.js 24, installs only from the committed lockfile, rejects any npm audit finding, builds both ESM and CommonJS distributions, typechecks the public API, runs the Web runtime tests, and inspects the package tarball. - PyPI and npm publication both depend on the Python provenance job and the npm verification job.
- The
npm-publishjob attaches thenpmGitHub environment, grantscontents: readandid-token: write, and reads only the environment-scopedNPM_ACCESS_TOKENsecret. - The publish job builds before exposing the token, then
npm publish --ignore-scripts --access public --provenanceuploads the package without running lifecycle hooks in the credential-bearing step. Provenance links the package to the tag workflow and source commit. - The release SBOM job starts only after both PyPI and npm publication succeed.
Do not publish @openmed/openmedkit-web; the public package name is openmed. Do not add another npm publishing workflow or place a plaintext npm token in an .npmrc file.
v1.8.0 Incident Lessons¶
On 2026-07-09, the first v1.8.0 PyPI publish failed because the release workflow used pypa/gh-action-pypi-publish without a password. In that mode, the action falls back to Trusted Publishing. PyPI rejected the GitHub OIDC exchange with invalid-publisher because the openmed project did not have a trusted publisher matching this repository, publish.yml, and the pypi environment.
Two follow-up checks matter:
PYPI_API_TOKENis currently an environment secret on the GitHubpypienvironment, not a repository secret. The publish job must keepenvironment: pypiwhile token upload is active.- GitHub OIDC attestation retrieval can fail independently of PyPI upload. SLSA provenance should be attempted and verified when GitHub's identity-token service is healthy, but transient attestation failures must not hide the separate PyPI credential contract.
The regression tests in tests/unit/test_publish_workflow_version.py and tests/unit/release/test_provenance_workflow.py are the local guardrails for this contract. Update them in the same change as any PyPI release workflow change.
PyPI project setup¶
To migrate back to Trusted Publishing, configure the trusted publisher on the PyPI openmed project first:
- Open the PyPI project settings for
openmed. - Add a GitHub trusted publisher with these values:
- Owner:
maziyarpanahi - Repository name:
openmed - Workflow name:
publish.yml - Environment name:
pypi - Ensure the GitHub
pypienvironment exists and is not blocking tagged releases with approval, wait-timer, or branch-policy gates. - Remove the
passwordinput from the publish action, grant the publish jobid-token: write, and setattestations: true.
If PyPI reports an invalid publisher during release, check those fields first. The workflow filename and optional environment name must match exactly.
Release checklist¶
Before pushing a version tag:
grep -R "pypa/gh-action-pypi-publish" .github/workflows/*.yml
if grep -R "hatch publish" .github/workflows/*.yml; then
echo "Legacy Hatch publishing is still present"
exit 1
fi
.venv/bin/python -m pytest tests/ -q
The first command should identify exactly one workflow. The second command should find nothing. The test command must pass before the release tag is pushed.
After the tagged publish succeeds, verify the PyPI release page lists the uploaded wheel and source distribution. For the repository-level SLSA provenance check, see SLSA Build Provenance.
If an immutable tag run fails before either registry upload, fix and merge the workflow guard first, then recover the existing tag through the same production workflow:
Do not delete, move, or recreate the tag. Do not use the recovery dispatch when either registry already contains the version without first determining whether the other publish can be safely resumed.
Token Handling¶
Keep PYPI_API_TOKEN project-scoped and rotate it if there is any evidence of exposure. Once the trusted publisher is configured and one tagged publish succeeds through the tokenless path, retire the token path:
- Delete
PYPI_API_TOKENfrom repository secrets and from thepypienvironment secrets, if present. - Remove local
.pypircor shell-profile entries that were only used for OpenMed package uploads. - Do not recreate a broad PyPI token for CI. If an emergency manual upload is ever required, create a short-lived project-scoped token outside the normal CI path and revoke it immediately after use.
Keep NPM_ACCESS_TOKEN scoped to the npm openmed package with publish access, store it only in the GitHub npm environment, and rotate it before its 90-day expiry. The package already exists, so npm Trusted Publishing can replace the token without a bootstrap release. When migrating, configure npm for owner maziyarpanahi, repository openmed, workflow publish.yml, and environment npm; then remove NODE_AUTH_TOKEN from the publish step and delete the secret.