Releasing and publishing
Where the pipeline stops today
OpenSDL has never been released. No Git tag exists, no GitHub Release exists, and no distribution
has been uploaded to any package index. The version in every pyproject.toml is 0.1.0a0, and the
only way to obtain the packages is to build them from a checkout.
Two things exist, and both stop at a built artifact:
| What it does | What it does not do | |
|---|---|---|
.agents/skills/release/run.sh VERSION |
Synchronizes the version across the 22 workspace distributions, the generated dependency floors and CITATION.cff; relocks; runs make test lint example; builds every wheel and sdist into dist/ |
Nothing leaves the machine. No commit, tag, signature, SBOM or upload. |
.github/workflows/release.yml ("Build distribution candidates", manual trigger only) |
Runs the same gate on a clean runner and uploads dist/ as a GitHub Actions artifact |
The artifact expires with the run and is visible only to people who can see the repository. No tag, release, signature, SBOM or upload. |
That boundary is deliberate: a package-index name is claimed by its first upload, and this is an alpha whose contracts are not stable. Building candidates is reversible; publishing is not.
What publishing would require
None of this is configured. Each step below is something the repository owner has to do deliberately, in this order.
1. Decide the naming surface, and understand what it costs
Publishing means claiming 22 project names on the index, all under the opensdl- prefix:
opensdl-adapter-grid-optimizer opensdl-domain-materials opensdl-runtime
opensdl-adapter-human-task opensdl-domain-physics opensdl-schemas
opensdl-adapter-local-compute opensdl-operators opensdl-sdk
opensdl-adapter-simulated-lab opensdl-policy opensdl-simulation
opensdl-api opensdl-provenance opensdl-storage
opensdl-capabilities opensdl-cli opensdl-twin
opensdl-controller opensdl-core opensdl-workflows
opensdl-domain-chemistry
As of 2026-08-10 all 22 are unregistered on PyPI, and so is the bare name opensdl — which is
not a distribution here but is the top-level import package that opensdl-sdk installs. Anyone
may register it.
What is irreversible, and what is not:
- A name is claimed by the first upload. Nothing reserves it beforehand except a pending trusted publisher (step 3), which reserves it only for that one GitHub workflow.
- A version and its filenames can never be re-uploaded, even after the release or the whole
project is deleted. A wrong
0.1.0a1is corrected by publishing0.1.0a2, never by fixing0.1.0a1in place. Plan the first version number as if it cannot be taken back, because it cannot. - Yanking is the reversible half. A yanked release stays installable by exact pin and stops being selected by resolvers. Use it for a bad release; deletion is not the tool.
- Reclaiming a name someone else holds is a discretionary process (PEP 541), measured in months and not guaranteed. Releasing a name you hold, by deleting the project, exposes it to that process from the other side.
- Uploading to TestPyPI claims nothing on PyPI. It is a separate index with separate accounts, and it is the right place to rehearse the mechanics.
Publishing fewer distributions is a legitimate answer, and the dependency graph already suggests
where the line falls. opensdl-cli and opensdl-sdk reach 14 of the 22 as required dependencies,
so those 14 have to be published together or not at all. The other eight — the four reference
adapters, the three domain packs, and opensdl-simulation — are reached only through a manifest at
runtime, so they can be held back. Publishing them anyway claims their names, which is a reason to
publish them and a reason not to.
2. Meet the preconditions the current repository does not meet
CHANGELOG.mdcarries a## 0.1.0a0 — 2026-08-02heading for a version that was never tagged or published, and everything since sits under## Unreleased. A release needs a heading that names something someone can install.- No release notes or migration guidance exist, and compatibility and versioning states that no contract is stable between releases. Publishing makes that statement load-bearing for strangers.
- No SBOM is generated and no artifact is signed by any current process.
CITATION.cffcarriesversion: 0.1.0a0anddate-released: 2026-08-02.scripts/release.pyrewrites both; check the result rather than assuming it.
3. Configure a trusted publisher on the index — before the project exists
PyPI accepts a pending publisher for a project that has not been created yet, which is what makes the first upload possible without an API token. In the PyPI account settings, under publishing, add a pending publisher for each project name with:
- owner
fl-sean03, repositoryOpenSDL; - workflow filename
release.yml(or whatever file holds the job in step 5); - environment name
pypi.
This is 22 entries. It reserves each name for that workflow alone and grants nothing else.
4. Create the GitHub environment
Create an environment named pypi in the repository settings with required reviewers, so that a
dispatch of the workflow pauses for a human approval before any upload. Without the environment the
job in step 5 fails closed, which is the desired failure.
5. Add the publish job
Adding this job is the act of enabling publishing. It lives here rather than in
.github/workflows/ so that it cannot run by accident. Append it to
.github/workflows/release.yml, pin the action to a digest the same way every other action in this
repository is pinned, and read what you are pinning:
publish:
# Manual dispatch only, gated on the `pypi` environment's reviewers.
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/opensdl-core/
permissions:
id-token: write # mints the OIDC token the trusted publisher verifies
steps:
- uses: actions/download-artifact@<pin-a-digest> # v7.0.1
with:
name: python-distributions
path: dist/
- uses: pypa/gh-action-pypi-publish@<pin-a-digest>
# No password: the trusted publisher configured in step 3 authenticates this run.
# Recent versions attach PEP 740 attestations to trusted-publisher uploads; confirm that
# for the version you pin rather than assuming it.
Rehearse it against TestPyPI first by adding repository-url: https://test.pypi.org/legacy/ and a
pending publisher on that index. A rehearsal there proves the OIDC exchange and the metadata; it
proves nothing about the name on PyPI.
6. Tag
Nothing in this repository creates a tag, and no step above needs one. If a release should be citable, create it after a successful build and before publishing:
git tag -s v0.1.0a1 -m "OpenSDL 0.1.0a1"
git push origin v0.1.0a1
A tag is the one reversible step here — it can be moved or deleted while nobody has fetched it. The upload in step 5 cannot. Tag first, verify, then publish.
Release checklist
Once the above exists, a release is:
.agents/skills/release/run.sh VERSIONon a clean worktree with an emptydist/;- review the version, lockfile and citation diff, and the contents of every wheel and sdist;
- write the
CHANGELOG.mdheading, release notes, and migration guidance; - commit, tag, push;
- dispatch Build distribution candidates against the tag and approve the
pypienvironment.