diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c0bcf56 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,77 @@ +name: Publish PyPI package + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + publish: + if: ${{ !github.event.release.prerelease }} + runs-on: ubuntu-22.04 + timeout-minutes: 15 + environment: + name: pypi + url: https://pypi.org/p/grid-sdk + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + ref: ${{ github.event.release.tag_name }} + fetch-depth: 0 + fetch-tags: true + persist-credentials: false + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + cache: pip + - name: Verify release identity + shell: bash + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" + module_version="$(python - <<'PY' + import ast + from pathlib import Path + + tree = ast.parse(Path("src/grid_sdk/__init__.py").read_text()) + for node in tree.body: + if not isinstance(node, ast.Assign): + continue + if any(isinstance(target, ast.Name) and target.id == "__version__" for target in node.targets): + print(ast.literal_eval(node.value)) + break + else: + raise SystemExit("__version__ assignment not found") + PY + )" + test "$module_version" = "$version" || { + echo "module version $module_version does not match project version $version" >&2 + exit 1 + } + test "$RELEASE_TAG" = "v$version" || { + echo "release tag $RELEASE_TAG does not match package version $version" >&2 + exit 1 + } + test "$(git rev-parse HEAD)" = "$(git rev-list -n 1 "$RELEASE_TAG")" + - name: Build and inspect distributions + run: | + python -m pip install build==1.6.0 twine==7.0.0 + python -m build + python -m twine check dist/* + - name: Test built wheel + shell: bash + run: | + wheel="$(find dist -maxdepth 1 -name '*.whl' -print -quit)" + test -n "$wheel" + python -m pip install "$wheel" pytest pytest-asyncio respx httpx + pytest tests/ -v + python -c 'import grid_sdk; print(grid_sdk.__version__)' + - name: Publish with PyPI trusted publishing + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1fc1f2..78f010a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,15 +6,19 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: pytest: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 + timeout-minutes: 10 strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: ${{ matrix.python-version }} cache: pip diff --git a/AGENTS.md b/AGENTS.md index 35739c6..366b17c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,8 +41,8 @@ subclasses the official `openai` client, pre-points it at the Grid, reads the ke environment, and adds Grid-specific conveniences (`online_models()`, and `client.grid` for the native SYNCHRONOUS media endpoints: `/v1/images/generations` (video / img2img / ControlNet / LoRAs / styles) and `/v1/videos/generations` — the call returns the finished result, no -submit/poll). The retired horde `/api/v2` async queue is NOT used. Published to PyPI as -`grid-sdk`; importable as `grid_sdk`. Co-canonical peer of the JS SDK `../grid-sdk-js` +submit/poll). The retired horde `/api/v2` async queue is NOT used. Its PyPI package name is +`grid-sdk`; it is importable as `grid_sdk`. Co-canonical peer of the JS SDK `../grid-sdk-js` (npm `grid-ai`) — keep the two SDKs' surfaces aligned. ## Ownership @@ -53,6 +53,9 @@ submit/poll). The retired horde `/api/v2` async queue is NOT used. Published to - **`pyproject.toml`** — setuptools build, `src/` layout, deps, pytest config. - **`.github/workflows/secret-scan.yml`, `.gitleaks.toml`, and `.gitleaksignore`** — checksum-verified complete-history secret scanning with exact historical fingerprints only. +- **`.github/workflows/release.yml`** — release-published, tag-bound PyPI + trusted publishing. It uses GitHub OIDC and the protected `pypi` + environment; registry tokens do not belong in GitHub secrets. - `dist/`, `.venv/`, `*.egg-info/` — build/vendored artifacts; do not edit or document. ## Local Contracts @@ -67,6 +70,9 @@ submit/poll). The retired horde `/api/v2` async queue is NOT used. Published to `DEFAULT_BASE_URL`. `AIPG`/`AsyncAIPG` are aliases of `Grid`/`AsyncGrid` and must stay so. - **Keep `__version__` (in `__init__.py`) in sync with `version` in `pyproject.toml`.** - Package metadata, source SPDX headers, and `LICENSE` all use MIT. +- **Release identity:** publish only a non-prerelease GitHub Release whose + `v` tag exactly matches `pyproject.toml` and `__version__`. The + release workflow tests the built wheel before OIDC publication. ## Work Guidance diff --git a/README.md b/README.md index 0ccb913..391f24a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ Open-model, OpenAI-compatible inference across community-operated GPUs. +> **Release status:** `grid-sdk` is staged for its initial PyPI publication. +> The install command below becomes available when the first verified release +> is published; until then, use the OpenAI SDK with the Grid base URL shown below. + The Grid API speaks the OpenAI protocol, so this SDK is a thin layer over the official `openai` package: it points at the Grid, reads your key from the environment, and adds Grid-specific conveniences. Everything you know from the diff --git a/pyproject.toml b/pyproject.toml index e4a5109..b5502fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=68.0", "wheel"] +requires = ["setuptools==82.0.1", "wheel==0.48.0"] build-backend = "setuptools.build_meta" [project] @@ -8,7 +8,8 @@ version = "0.1.1" description = "Python SDK for AI Power Grid open-model, OpenAI-compatible inference." readme = "README.md" requires-python = ">=3.9" -license = { text = "MIT" } +license = "MIT" +license-files = ["LICENSE"] authors = [{ name = "AI Power Grid" }] keywords = ["ai", "llm", "inference", "openai", "decentralized", "aipg"] dependencies = [