Skip to content
Menu

Documentation

Publishing packages to PyPI

Sandloom publishes five distributions through Release (PyPI): sandloom, sandloom-identity, sandloom-tenancy, sandloom-billing and sandloom-admin. Versions are independent. A manual run can select one package or all for a coordinated release. A per-package tag selects only that package. The current published alpha is 0.7.0a6 for all five packages.

Authentication and approval

The publish job uses PyPI Trusted Publishing with GitHub OIDC. No permanent PyPI API token, username or password is stored in GitHub. Configure each PyPI project's Trusted Publisher with these exact public values. A project that doesn't exist yet needs a pending publisher, added on PyPI under your account before the first release:

Field Value
Owner Sandloom-dev
Repository sandloom
Workflow filename release.yml
Environment pypi

The GitHub pypi environment requires approval by Maryarty, including runs started by the owner. Administrator bypass is disabled. The owner may approve their own run because there is only one maintainer. Allowed deployment refs are main and the four tag patterns sandloom-v*, sandloom-identity-v*, sandloom-tenancy-v* and sandloom-billing-v*.

The plan step also refuses publication from a feature branch or a tag whose commit is not reachable from origin/main. A tag must exactly match the selected package's version in its pyproject.toml. Merging a PR never publishes packages.

Build and validation

  1. The plan validates the event, ref, package selection and versions.
  2. Full framework CI runs, including the compatibility matrix and heavy scenarios.
  3. The package workflow builds every wheel and standalone source archive, checks licenses, compares direct and rebuilt wheels, enforces import layering and tests clean framework-only and standard installations.
  4. It preserves those exact validated archives and their SHA-256 digests, then runs strict Twine metadata/description checks. The preparation job selects the requested packages without rebuilding them.
  5. After owner approval, a separate publish job downloads the reviewed files, verifies local hashes and checks for matching files already on PyPI. Only missing files are uploaded. This job performs no source checkout, build or dependency installation and is the only job with id-token: write.
  6. PyPI publication uses signed attestations. The final verification waits for the release to become visible and compares every filename and SHA-256 digest against the reviewed manifest.

Artifacts are retained for seven days. A differing existing file fails closed; the workflow never replaces an immutable PyPI file or silently accepts a hash mismatch. After an interrupted upload, a rerun reuses identical published files and uploads only the missing ones. A publication/verification failure must be investigated before choosing a new version.

GitHub Releases

After PyPI publication and hash verification succeed, a separate job creates an initially private draft GitHub Release, uploads the same reviewed archives, SHA256SUMS and the source manifest, verifies their hashes, then publishes it. Release notes come from the selected version's changelog section, captured before owner approval. Only this job receives contents: write; it has no PyPI token or OIDC permission and does no checkout, build or installation.

A coordinated all release requires matching package versions and creates v<version> (for example v0.7.0a6). A single-package release creates <package>-v<version>. Alpha, beta and release candidates are marked as GitHub prereleases. The tag must point to the manifest's exact source commit. Reruns can fill missing assets in a draft but never overwrite a tag or a conflicting asset. A matching published release is verified without changes. A dry run never creates a tag, draft, GitHub Release or PyPI upload.

GitHub can briefly delay a newly created draft's visibility in REST reads. The publisher retries that read up to six times, with two seconds between attempts; it never repeats the create or upload command to handle this delay. If the job still fails, inspect the draft and rerun only the github-release job. Matching drafts and assets are resumed, while conflicting hashes or tags still stop publication. A successful PyPI upload does not need to be repeated.

Manual release

For a coordinated release, run this from the repository root:

python tools/packaging/sync_release.py --version 0.7.0a6 --write
uv lock
uv lock --project benchmarks
uv run python tools/checks/check_benchmark_dependencies.py --write

The command above prepares the 0.7.0a6 release. The canonical current framework version is packages/sandloom/pyproject.toml. The helper updates all five package versions, compatible dependency floors, example manifests, scaffold requirements, CLI help and current documentation. It discovers package pins in source documentation; prose that describes the current release carries an inline <!-- sandloom-release --> marker. Changelogs, ADRs and the dependency security audit retain historical versions. Move unreleased changes to the new changelog section and review the lock diffs; these commands must not upgrade unrelated dependencies.

python tools/packaging/sync_release.py checks without writing. This guard runs in required CI, before package validation and before a website build. It uses checked-in metadata, so builds need no PyPI request to determine a version. Module packages may still release independently: without --version, their recorded versions are retained and references use their own package metadata. A release PR describes its candidate version; deploy the site after the packages are published and verified. Merging alone neither publishes nor deploys.

After the owner merges the PR, open Actions → Release (PyPI) → Run workflow:

  • Select main and one package, or all for the coordinated 0.7.0a6 release.
  • Leave dry_run checked for validation and artifacts without an upload.
  • For publication, uncheck dry_run. The workflow still validates first and stops at the pypi environment for owner approval before obtaining credentials.
  • Review the commit, validation results and artifact manifest before approving.

A dry run can validate a feature branch and never enters the publishing environment. A real publication from a feature branch is refused.

Tag release

A per-package tag on a reviewed main commit requests that package's release:

git tag sandloom-v0.7.0a6
git push origin sandloom-v0.7.0a6

The other package prefixes are sandloom-identity-v, sandloom-tenancy-v and sandloom-billing-v. sandloom-admin releases through a coordinated manual run; its tag and single-package choice need an entry in release.yml. Release a framework version before module versions that require it, or use one coordinated manual run. Each tag-triggered upload still requires the owner's environment approval.

Package metadata and licenses

Short descriptions come from description and long descriptions from readme in each package's pyproject.toml. PyPI release metadata is immutable: changing an already-published description requires a new release. Validate the rendered metadata with twine check --strict before uploading.

Each package declares license = "Apache-2.0" and license-files = ["LICENSE", "NOTICE"]. The build hook rejects package copies that differ from the canonical root files. Standalone sdists and wheels retain those files and third-party notices. See packages/README.md for build details.

Testing source changes in an application

Use sandloom new --framework-source path --framework-path <checkout> and uv sync to test local source changes. The path must name the repository root containing packages/. These editable paths are local dependencies, not portable Docker or CI inputs. --framework-source git is disabled; use published packages or a complete matching wheel set for portable deployments.