8000 ci: consumer-test the wheels, before tagging and after publishing by adityamukho · Pull Request #10 · project-minigraf/minigraf-python · GitHub
[go: up one dir, main page]

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
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ on:
required: false
type: string
default: ''
version:
description: 'Version stamped into the wheels, PEP 440 (release.yml passes the release version)'
required: false
type: string
default: '0.0.0.dev0'

jobs:
test:
Expand Down Expand Up @@ -47,3 +52,46 @@ jobs:
pip install pytest
bash build.sh test
shell: bash

# Builds the wheels as the release publishes them.
package:
name: Package
uses: ./.github/workflows/package.yml
with:
ref: ${{ inputs.ref }}
version: ${{ inputs.version || '0.0.0.dev0' }}

# Installs the built wheel into a new virtual environment, without PyPI, and
# runs the README quick start and the consumer contract.
consumer:
name: Consumer test (${{ matrix.os }})
needs: package
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, windows-latest]
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- uses: actions/download-artifact@v4
with:
pattern: dist-wheel-*
path: dist
merge-multiple: true
- name: Run consumer test
shell: bash
run: bash consumer-test/run.sh local dist ${{ inputs.version || '0.0.0.dev0' }}

readme-sync:
name: README quick start matches the consumer test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- run: python3 -I consumer-test/readme_sync.py README.md consumer-test/quickstart.py
99 changes: 99 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
name: Package

# Builds the wheels exactly as the release publishes them, stamped with
# `version` (PEP 440), and uploads each as dist-wheel-<os>. Linux wheels are
# manylinux_2_34. (`--zig --compatibility manylinux2014` would lower the floor
# to glibc 2.17, but maturin then builds a wheel with no Python bindings and no
# error; see the glibc-floor issue.)

on:
workflow_call:
inputs:
ref:
required: false
type: string
default: ''
version:
required: true
type: string

jobs:
wheel:
name: Build wheel (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
build_args: --release --manylinux manylinux_2_34 --out dist
rust_targets: ''
- os: ubuntu-24.04-arm
build_args: --release --manylinux manylinux_2_34 --out dist
rust_targets: ''
- os: macos-14
build_args: --release --target universal2-apple-darwin --out dist
rust_targets: x86_64-apple-darwin
- os: windows-latest
build_args: --release --out dist
rust_targets: ''
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_targets }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install maturin
run: pip install "maturin>=1.5"

- name: Stamp version into pyproject.toml
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
python3 -c "
import os, re, pathlib
p = pathlib.Path('pyproject.toml')
p.write_text(re.sub(r'^version = .*', 'version = \"%s\"' % os.environ['VERSION'], p.read_text(), flags=re.MULTILINE))
"
grep '^version' pyproject.toml

- name: Build wheel
shell: bash
run: bash build.sh build ${{ matrix.build_args }}

- name: Check the wheel
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
ls -l dist
WHEEL=$(ls dist/*.whl)
[ "$(echo "$WHEEL" | wc -l)" -eq 1 ] || { echo "::error::expected one wheel"; exit 1; }
case "$(basename "$WHEEL")" in
"minigraf-$VERSION-"*) ;;
*) echo "::error::$(basename "$WHEEL") is not version $VERSION"; exit 1 ;;
esac
# Both the generated bindings and the native library: maturin can
# build a wheel without the bindings and report success.
FILES=$(python3 -m zipfile -l "$WHEEL" | awk '{print $1}')
echo "$FILES"
grep -qx 'minigraf/minigraf_ffi/minigraf_ffi.py' <<<"$FILES" \
|| { echo "::error::the wheel has no generated bindings (minigraf_ffi.py)"; exit 1; }
grep -qE '^minigraf/minigraf_ffi/(lib)?minigraf_ffi\.(so|dylib|dll)$' <<<"$FILES" \
|| { echo "::error::the wheel has no native library"; exit 1; }

- uses: actions/upload-artifact@v4
with:
name: dist-wheel-${{ matrix.os }}
path: dist/*.whl
if-no-files-found: error
85 changes: 22 additions & 63 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ jobs:
uses: ./.github/workflows/ci.yml
with:
ref: ${{ needs.prepare.outputs.sha }}
# The wheels are built and consumer-tested here at the release version;
# publish uploads exactly these files.
version: ${{ needs.prepare.outputs.semver }}

tag:
name: Push tag and update main
Expand Down Expand Up @@ -114,69 +117,9 @@ jobs:
git push origin --delete "release/$TAG" \
|| echo "::warning::Could not delete branch release/$TAG"

build-wheels:
name: Build wheel (${{ matrix.os }})
needs: [prepare, tag]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
build_args: --release --manylinux manylinux_2_34 --out dist
rust_targets: ''
- os: ubuntu-24.04-arm
build_args: --release --manylinux manylinux_2_34 --out dist
rust_targets: ''
- os: macos-14
build_args: --release --target universal2-apple-darwin --out dist
rust_targets: x86_64-apple-darwin
- os: windows-latest
build_args: --release --out dist
rust_targets: ''

steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.prepare.outputs.tag }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_targets }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install maturin
run: pip install "maturin>=1.5"

- name: Stamp version into pyproject.toml
shell: bash
run: |
TAG="${{ needs.prepare.outputs.tag }}"
VERSION="${TAG#v}"
python3 -c "
import re, pathlib
p = pathlib.Path('pyproject.toml')
p.write_text(re.sub(r'^version = .*', 'version = \"${VERSION}\"', p.read_text(), flags=re.MULTILINE))
"

- name: Build wheel
run: bash build.sh build ${{ matrix.build_args }}
shell: bash

- name: Upload wheel
uses: actions/upload-artifact@v4
with:
name: python-wheel-${{ matrix.os }}
path: dist/*.whl

publish:
name: Publish to PyPI
needs: [prepare, build-wheels, tag]
needs: [prepare, test, tag]
runs-on: ubuntu-latest
environment:
name: pypi
Expand All @@ -188,13 +131,29 @@ jobs:
- name: Download all wheels
uses: actions/download-artifact@v4
with:
pattern: python-wheel-*
pattern: dist-wheel-*
path: dist
merge-multiple: true

- name: Check the tested wheels
run: |
ls -l dist
[ "$(ls dist/*.whl | wc -l)" -eq 4 ] || { echo "::error::expected 4 wheels"; exit 1; }

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/


# Installs the published version from PyPI and runs the consumer test; on
# failure the run fails and a release-broken issue is opened.
verify-published:
name: Verify the published wheels
needs: [prepare, publish]
permissions:
contents: read
issues: write
uses: ./.github/workflows/verify-published.yml
with:
version: ${{ needs.prepare.outputs.semver }}
ref: ${{ needs.prepare.outputs.tag }}
77 changes: 77 additions & 0 deletions .github/workflows/verify-published.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Verify published

# Installs minigraf <version> from PyPI into a new virtual environment, as a
# user would, and runs the consumer test on every platform. A failure means a broken release is
# public: the run fails and an issue labelled release-broken is opened (or
# commented on, if one is open for this version).

on:
workflow_call:
inputs:
version:
required: true
type: string
ref:
description: 'Ref whose consumer-test/ to run (release.yml passes the release tag)'
required: false
type: string
default: ''
workflow_dispatch:
inputs:
version:
description: 'Published version (e.g. 2.0.9)'
required: true
type: string
ref:
description: 'Ref whose consumer-test/ to run (default: the branch dispatched from)'
required: false
type: string
default: ''

permissions:
contents: read
issues: write

jobs:
verify:
name: Consumer test from PyPI (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm, macos-14, windows-latest]
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Run consumer test
shell: bash
run: bash consumer-test/run.sh registry ${{ inputs.version }}

report:
name: Report broken release
needs: verify
if: failure()
runs-on: ubuntu-latest
steps:
- name: Open or update the release-broken issue
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
VERSION: ${{ inputs.version }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
gh label create release-broken --repo "$REPO" --color B60205 \
--description "A published release does not install or run" --force
TITLE="v$VERSION published but unusable: consumer test failed (PyPI)"
BODY="The consumer test failed against minigraf $VERSION installed from PyPI. Run: $RUN_URL"
NUMBER=$(gh issue list --repo "$REPO" --label release-broken --state open \
--search "\"v$VERSION published\" in:title" --json number -q '.[0].number')
if [ -n "$NUMBER" ]; then
gh issue comment "$NUMBER" --repo "$REPO" --body "$BODY"
else
gh issue create --repo "$REPO" --title "$TITLE" --label bug --label release-broken --body "$BODY"
fi
39 changes: 39 additions & 0 deletions consumer-test/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""The consumer contract every binding checks (spec §4): in memory,
file-backed across a reopen, and MiniGrafError for invalid Datalog.

Usage: python contract.py <empty directory>
"""

import json
import os
import sys

from minigraf import MiniGrafDb, MiniGrafError

QUERY = "(query [:find ?n :where [?e :name ?n]])"


def names(db):
return [row[0] for row in json.loads(db.execute(QUERY))["results"]]


mem = MiniGrafDb.open_in_memory()
mem.execute('(transact [[:alice :name "Alice"]])')
assert names(mem) == ["Alice"], "query in memory returns Alice"
print("ok: in memory")

try:
mem.execute("(query [:find")
except MiniGrafError:
print("ok: invalid Datalog raises MiniGrafError")
else:
sys.exit("invalid Datalog did not raise MiniGrafError")

path = os.path.join(sys.argv[1], "test.graph")
db = MiniGrafDb.open(path)
db.execute('(transact [[:bob :name "Bob"]])')
db.checkpoint()
del db # releases the file lock; the binding has no close()
again = MiniGrafDb.open(path)
assert names(again) == ["Bob"], "query after reopen returns Bob"
print("ok: file-backed survives reopen")
Loading
Loading
0