Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}
Expand All @@ -31,14 +33,40 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
cache: pip
- run: python -m pip install --upgrade pip build twine
- run: python -m build
- run: python -m twine check dist/*
- name: Smoke-test the wheel in a clean environment
- name: Upload distributions for cross-version smoke tests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: python-package-distributions
path: dist/
if-no-files-found: error
retention-days: 7

wheel-smoke:
needs: package
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ matrix.python-version }}
- name: Download built distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-package-distributions
path: dist/
- name: Smoke-test the wheel outside the checkout
shell: bash
run: |
python -m venv "$RUNNER_TEMP/wheel-smoke"
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Release distributions

on:
push:
tags:
- "v*"
workflow_dispatch:

permissions:
contents: read

jobs:
build-and-attest:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
- name: Install the project and release tooling
run: python -m pip install ".[dev]"
- name: Require the tag to match the package version
if: github.ref_type == 'tag'
env:
RELEASE_TAG: ${{ github.ref_name }}
run: python -c "import os, unified_llm; expected = f'v{unified_llm.__version__}'; assert os.environ['RELEASE_TAG'] == expected, f'tag must be {expected}'"
- name: Verify the exact release source
run: |
python -m ruff check .
python -m ruff format --check .
python -m mypy unified_llm tests examples
python -m pytest --cov=unified_llm --cov-report=term-missing
- name: Build and validate distributions
run: |
python -m build
python -m twine check dist/*
- name: Generate SLSA provenance
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Store distributions
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: python-package-distributions
path: dist/
if-no-files-found: error
retention-days: 30

publish-to-pypi:
if: github.event_name == 'push' && github.ref_type == 'tag'
needs: build-and-attest
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/unified-llm
permissions:
id-token: write
steps:
- name: Download attested distributions
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-package-distributions
path: dist/
- name: Publish through PyPI Trusted Publishing
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: dist/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project will be documented here. The project follows
- Content-free sync/async attempt observation and inspectable provider health snapshots.
- Cross-request transient-failure cooldown that deprioritizes unhealthy routes without removing last-resort fallback.
- A canonical support-ticket triage reference consumer and deterministic public-API contract fixture.
- Cross-version installed-wheel smoke tests, supply-chain contract tests, and an attested PyPI Trusted Publishing workflow.

### Changed

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ python -m build
python -m twine check dist/*
```

Tests use fakes and `httpx.MockTransport`; they do not need credentials and do not call external APIs. CI runs lint, type checking, tests, package build, metadata checks, and a wheel smoke test. See [.github/workflows/ci.yml](.github/workflows/ci.yml).
Tests use fakes and `httpx.MockTransport`; they do not need credentials and do not call external APIs. CI runs lint, type checking, tests, package build, metadata checks, and installed-wheel smoke tests on every supported Python version. See [.github/workflows/ci.yml](.github/workflows/ci.yml).

## Architecture

Expand Down Expand Up @@ -222,11 +222,11 @@ See [SUPPORT.md](SUPPORT.md) for the information to include and the prerelease s
The repository is a coherent release candidate for local evaluation. Public package publication remains blocked on:

1. owner-authorized live endpoint smoke tests;
2. owner authorization and configuration for PyPI trusted publishing/signing.
2. owner configuration and approval of the prepared PyPI Trusted Publishing environment.

The package name was not present on PyPI when checked on 2026-07-28; re-check immediately before publishing. No package has been published or production infrastructure changed by this work.

See [docs/PRODUCTIZATION.md](docs/PRODUCTIZATION.md) for the evidence, remaining work, and acceptance criteria.
See [docs/PRODUCTIZATION.md](docs/PRODUCTIZATION.md) for the evidence and remaining gates, and [docs/RELEASING.md](docs/RELEASING.md) for the attested, tokenless release process.

## Contributing

Expand Down
41 changes: 41 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Release process

No package has been published from this repository yet. This process makes releases deliberate, reviewable, and recoverable; merging to `main` never uploads a package.

## One-time owner setup

1. Confirm that the `unified-llm` PyPI project name is available or controlled by Samsarix LLC.
2. In PyPI, register a pending GitHub Trusted Publisher for owner `Deathcharge`, repository `unified-llm`, workflow `release.yml`, and environment `pypi`.
3. Create the GitHub `pypi` environment and require manual approval. Restrict deployment to protected version tags when repository policy permits it.
4. Protect `main` and release tags, require the CI checks, and review any change to `.github/workflows/release.yml` as a credential-equivalent security change. Release builds intentionally do not restore dependency caches.
5. Ensure at least two Samsarix-controlled recovery methods exist for the PyPI and GitHub owner accounts.

Do not add a long-lived PyPI token to repository secrets. The workflow requests a short-lived OIDC credential only inside the `pypi` environment.

## Candidate gate

Before tagging:

1. update `pyproject.toml`, `unified_llm.__version__`, and `CHANGELOG.md` to the same version;
2. verify the exact commit with Ruff, mypy, pytest/coverage, build, Twine, and the installed-wheel smoke test;
3. record any live endpoint conformance evidence described in `docs/CONSUMER_CONTRACT.md`, or explicitly defer publication;
4. run the release workflow manually from the candidate commit and verify its build provenance and downloadable artifacts; manual runs never publish;
5. review the source distribution and wheel contents, dependency metadata, license, owner/support identity, and artifact digests.

## Publication

Create and push an annotated tag exactly matching `v` plus the package version, for example `v0.1.0`. The release workflow rejects a mismatched tag, rebuilds the source distribution and wheel, checks their metadata, creates GitHub-hosted SLSA provenance, and pauses at the protected `pypi` environment. An owner must inspect the run and approve that deployment before PyPI receives anything.

The official PyPA publishing action uses Trusted Publishing and uploads PyPI attestations by default. After approval, verify the project page, both distributions, their hashes and attestations, and installation in a clean supported Python environment. Then create the GitHub release from the same tag and attach or link the verification record.

## Rollback and incident response

PyPI release files and versions are immutable. Do not overwrite or reuse a version. If a release is faulty:

1. yank the affected version on PyPI with a concise reason;
2. publish a corrected higher patch version through the same reviewed workflow;
3. mark the GitHub release and changelog clearly;
4. if provenance or publisher identity is suspect, disable the PyPI trusted publisher and GitHub environment before investigating;
5. follow `SECURITY.md` for any confidentiality or integrity issue.

Existing users can remain pinned to the last verified artifact while the correction is prepared.
39 changes: 39 additions & 0 deletions tests/test_release_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import re
from pathlib import Path

import unified_llm

ROOT = Path(__file__).parents[1]
WORKFLOWS = ROOT / ".github" / "workflows"


def test_package_version_is_consistent() -> None:
pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'^version = "([^"]+)"$', pyproject, flags=re.MULTILINE)
assert match is not None
assert match.group(1) == unified_llm.__version__
assert f"## {unified_llm.__version__} -" in (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")


def test_all_third_party_workflow_actions_are_commit_pinned() -> None:
for workflow in WORKFLOWS.glob("*.yml"):
source = workflow.read_text(encoding="utf-8")
actions = re.findall(r"^\s*-?\s*uses:\s*([^\s#]+)", source, flags=re.MULTILINE)
assert actions, f"{workflow.name} must contain at least one action"
for action in actions:
assert re.fullmatch(r"[^@]+@[0-9a-f]{40}", action), f"mutable action reference in {workflow.name}: {action}"


def test_release_workflow_uses_tokenless_approved_publication() -> None:
source = (WORKFLOWS / "release.yml").read_text(encoding="utf-8")

assert "github.event_name == 'push' && github.ref_type == 'tag'" in source
assert re.search(r"environment:\s+name: pypi", source)
assert "id-token: write" in source
assert "pypa/gh-action-pypi-publish@" in source
assert "actions/attest@" in source
assert "python -m pytest --cov=unified_llm" in source
assert "password:" not in source
assert "secrets." not in source
Loading