Skip to content

A column of sessions, and each one drives its own browsers (0.17.0) (… #28

A column of sessions, and each one drives its own browsers (0.17.0) (…

A column of sessions, and each one drives its own browsers (0.17.0) (… #28

Workflow file for this run

name: publish
# Publish `aihawk` from a tag via PyPI Trusted Publishing (OIDC), with no
# long-lived token anywhere. The trust lives in the `pypi` GitHub Environment,
# minted per run and valid for minutes. Only the `upload` job names that
# environment, and it declares `needs: gate`, so a red gate means the only job
# that can authenticate never starts.
#
# BEFORE THE FIRST RUN, on PyPI: the name is unregistered, so Your projects will
# not list it. Use Account -> Publishing -> Add a pending publisher with:
# PyPI Project Name: aihawk
# Owner: feder-cr
# Repository name: AIHawk
# Workflow name: publish.yml
# Environment name: pypi
# Until that exists the upload fails with `invalid-publisher`, which is the
# correct failure: nothing has said this workflow may speak for this project.
on:
push:
tags:
- "v*"
workflow_dispatch:
permissions:
contents: read
jobs:
already-published:
runs-on: ubuntu-latest
outputs:
present: ${{ steps.probe.outputs.present }}
version: ${{ steps.probe.outputs.version }}
steps:
- uses: actions/checkout@v4
- id: probe
shell: bash
# Asks the INDEX, not the ledger and not the tag: a re-pushed tag or a
# backfill is then a logged no-op rather than a red run, and the only
# answer that stops everything is one that is neither yes nor no.
run: |
VERSION="$(grep -m1 '^version = ' pyproject.toml | cut -d'"' -f2)"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
CODE=$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/aihawk/$VERSION/json")
case "$CODE" in
200) echo "present=yes" >> "$GITHUB_OUTPUT"
echo "::notice::aihawk $VERSION is already on the index. No-op, not a failure." ;;
404) echo "present=no" >> "$GITHUB_OUTPUT" ;;
*) echo "the index answered $CODE for $VERSION, which is neither present nor absent. Refusing rather than guessing." >&2
exit 1 ;;
esac
gate:
needs: already-published
if: needs.already-published.outputs.present != 'yes'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: the tag names the version being built
if: github.event_name == 'push'
shell: bash
run: |
TAG="${GITHUB_REF_NAME#v}"
HAVE="${{ needs.already-published.outputs.version }}"
if [ "$TAG" != "$HAVE" ]; then
echo "tag v$TAG does not name the version in pyproject.toml ($HAVE)" >&2
exit 1
fi
- name: the engine floor is SATISFIABLE on the index
# The ordering rule as a gate: a floor on a dependency that the index
# cannot serve installs broken for everyone, while a checkout, being an
# editable install, never sees it. The MCP server used to be that
# dependency; since 0.10.0 it ships in this package and the floor that
# matters is the engine's.
shell: bash
run: |
set -euo pipefail
FLOOR=$(python - <<'PY'
import re, tomllib
with open("pyproject.toml", "rb") as fh:
deps = tomllib.load(fh)["project"]["dependencies"]
want = [d for d in deps if re.match(r"invisible[-_]playwright\s*>=", d)]
if len(want) != 1:
raise SystemExit("expected exactly one engine dependency with a floor, found %r" % want)
print(re.search(r">=\s*([0-9]+(?:\.[0-9]+)*)", want[0]).group(1))
PY
)
echo "floor: $FLOOR"
BEST=$(curl -sf "https://pypi.org/pypi/invisible-playwright/json" \
| python -c 'import json,sys; print(json.load(sys.stdin)["info"]["version"])')
echo "index serves: $BEST"
python - "$FLOOR" "$BEST" <<'PY'
import re, sys
def key(v):
return tuple(int(p) for p in re.findall(r"[0-9]+", v))
floor, best = sys.argv[1], sys.argv[2]
if key(best) < key(floor):
sys.exit("the index serves invisible-playwright %s and this release needs >=%s" % (best, floor))
print("resolvable: %s satisfies >=%s" % (best, floor))
PY
- name: install and run the suite
# A tag push does not trigger the ordinary test workflow, which runs on
# branches and pull requests, so without this a tag could publish code
# that no job has ever run.
run: |
python -m pip install --upgrade pip
pip install -e ".[test]"
python -m pytest -q
# The server's browser tests are deselected by default (marker e2e);
# a release must drive the tools against a real engine, as the
# server's own gate did before it moved in here. The engine is
# fetched into the runner's cache and named to the tests that spawn
# the server as a subprocess, which otherwise skip.
export STEALTHFOX_BINARY="$(invisible-playwright fetch | tail -n 1)"
python -m pytest -q -m e2e tests/mcp_server
- name: the wheel installs into an empty environment and answers
# The published artifact is a wheel, and the suite above imports from
# src/ either way. Only a clean install can tell you that the thing
# about to reach users actually starts.
run: |
pip install build
python -m build --wheel
python -m venv /tmp/clean
/tmp/clean/bin/pip install dist/*.whl
/tmp/clean/bin/aihawk --help
/tmp/clean/bin/python -m aihawk --help > /dev/null
/tmp/clean/bin/python -c "import aihawk.mcp.server"
unzip -l dist/*.whl | grep -q "aihawk/mcp/server.py"
upload:
needs: [already-published, gate]
if: needs.already-published.outputs.present != 'yes'
runs-on: ubuntu-latest
environment: pypi # the OIDC trust relationship lives HERE, behind the gate
permissions:
id-token: write # trusted publishing; no token in a secret anywhere
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: python -m pip install --upgrade pip build
- run: python -m build
- name: twine check
run: |
pip install twine
twine check dist/*
- uses: pypa/gh-action-pypi-publish@release/v1