diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..25f26c3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,96 @@ +name: Release + +# Publishing is a tag push: +# +# git tag v0.2.0 && git push origin v0.2.0 +# +# No token anywhere. PyPI Trusted Publishing mints a short-lived credential from +# this workflow's OIDC identity, so there is no long-lived PYPI_API_KEY to leak, +# rotate, or paste into a shell. The previous scripts/release.sh read one out of +# ~/.pixeltable/config.toml. +# +# One-time setup on PyPI (Manage project -> Publishing -> Add a pending publisher): +# owner: pixeltable repository: pixelmemory +# workflow: release.yml environment: pypi + +on: + push: + tags: ["v*"] + workflow_dispatch: + inputs: + dry_run: + description: "Build and check only; do not publish" + type: boolean + default: true + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v5 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Version in pyproject.toml must match the tag + if: startsWith(github.ref, 'refs/tags/v') + run: | + tag="${GITHUB_REF_NAME#v}" + pyproject=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2) + echo "tag=$tag pyproject=$pyproject" + if [ "$tag" != "$pyproject" ]; then + echo "::error::Tag v$tag does not match pyproject.toml version $pyproject." + exit 1 + fi + + - run: uv build + + - name: Check the artifacts + run: | + uv tool run twine check dist/* + ls -la dist/ + + # A release that cannot be installed is worse than no release. This is the + # same check that caught the 0.1.x breakage: install the actual artifact + # against the newest pixeltable our bound allows, then build an agent. + - name: Install the built wheel and smoke-test it + env: + PIXELTABLE_HOME: ${{ runner.temp }}/pxt-release + run: | + uv venv /tmp/verify --python 3.12 + VIRTUAL_ENV=/tmp/verify uv pip install dist/*.whl + VIRTUAL_ENV=/tmp/verify uv pip list | grep -E "pixelmemory|pixeltable" + /tmp/verify/bin/python -c " + from pixelmemory import Memory + from pixelmemory.context import Text + m = Memory([Text(id='body', embed=False)], namespace='release_check') + assert m.table.columns() == ['body'], m.table.columns() + print('built wheel installs and builds a memory') + " + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + if: startsWith(github.ref, 'refs/tags/v') && !inputs.dry_run + runs-on: ubuntu-latest + # The environment gives you a required-reviewer gate if you want one: + # Settings -> Environments -> pypi -> Required reviewers. + environment: + name: pypi + url: https://pypi.org/p/pixelmemory + permissions: + id-token: write # mints the OIDC token Trusted Publishing exchanges + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..01a6828 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,45 @@ +# Releasing pixelmemory + +```bash +# 1. bump the version +# pyproject.toml -> version = "X.Y.Z" +uv lock # keep uv.lock in step; CI runs `uv sync --locked` and will fail otherwise + +# 2. land it on main through a PR, then +git tag vX.Y.Z && git push origin vX.Y.Z +``` + +The tag push runs `.github/workflows/release.yml`, which builds the sdist and +wheel, checks the tag matches `pyproject.toml`, installs the built wheel into a +clean environment and exercises it, then publishes to PyPI. + +## There is no PyPI token + +Publishing uses PyPI Trusted Publishing. PyPI mints a short-lived credential +from the workflow's OIDC identity, so there is no long-lived secret to leak, +rotate, or paste into a shell. + +One-time setup, on PyPI under **Manage project -> Publishing -> Add a pending publisher**: + +| field | value | +|---|---| +| owner | `pixeltable` | +| repository | `pixelmemory` | +| workflow | `release.yml` | +| environment | `pypi` | + +To require a human approval before each publish, add a required reviewer to the +`pypi` environment under **Settings -> Environments**. + +## Dry run + +Run the workflow manually from the Actions tab with `dry_run` left checked. It +builds and verifies the artifact without publishing anything. + +## Why the release script is gone + +`scripts/release.sh` read a long-lived `PYPI_API_KEY` out of +`~/.pixeltable/config.toml`, required a git remote named `home` that a normal +clone does not have, and called `poetry build` after the project had moved to +uv. It also published without ever installing what it built, which is how +0.1.x shipped a package that could not import against a current Pixeltable. diff --git a/scripts/release.sh b/scripts/release.sh deleted file mode 100644 index 0b6ac88..0000000 --- a/scripts/release.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash -e - -# pixelmemory release script - -SCRIPT_DIR="$(dirname "$0")" -PROJECT_ROOT="$(readlink -f "$SCRIPT_DIR/..")" - -echo -e "\n============= Running pixelmemory release script.\n" - -echo "Project root: $PROJECT_ROOT" -cd "$PROJECT_ROOT" - -if [[ ! $(gh --version) ]]; then - echo "You must have Github commandline utilities installed to run this script. See: https://cli.github.com/" - exit 1 -fi - -if [ "$(git remote get-url home)" != 'https://github.com/pixeltable/pixelmemory' ]; then - echo "Unexpected home repo: $(git remote get-url home)" - exit 1 -fi - -git diff # Needed to ensure correct behavior of subsequent `git diff-index` call - -if [[ -n "$(git diff-index HEAD)" || -n "$(git diff-files)" || -n "$(git ls-files --exclude-standard --others)" ]]; then - echo "The release script must be run from a clean git repo." - exit 1 -fi - -if [ -z "$PYPI_API_KEY" ]; then - echo "PYPI_API_KEY not defined in environment; searching in ~/.pixeltable/config.toml." - PYPI_API_KEY=$( - python -c "import toml,sys; y = toml.load(sys.stdin); print(y['pypi']['api_key'])" < ~/.pixeltable/config.toml - ) -fi - -git fetch home -git checkout home/main - -echo -n "Enter version number for release: " -read VERSION - -echo "" -echo "This will publish version v$VERSION to PyPI. Enter to confirm; Ctrl-C to abort." -read - -git tag "v$VERSION" -git push home "v$VERSION" - -echo "v$VERSION tag created and pushed to home repo." -echo "Enter to proceed; Ctrl-C to abort." -read - -git tag -d release -git push --delete home release -git tag release -git push home release - -poetry build -poetry publish --username __token__ --password "$PYPI_API_KEY" - -echo "Creating release on github ..." -gh release create "v$VERSION" --generate-notes --repo pixeltable/pixelmemory -open "https://github.com/pixeltable/pixelmemory/releases/tag/v$VERSION"