Skip to content

A verb for browser_watch (#1238) #10

A verb for browser_watch (#1238)

A verb for browser_watch (#1238) #10

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 MCP server floor is SATISFIABLE on the index
# The ordering rule, as a gate instead of a sentence in a runbook, and it
# is here because the rule was broken the day it was written: aihawk was
# committed using `browser_select_option`, a tool that existed only in an
# unpublished server. Locally everything was green - the checkout is an
# editable install and cannot see this class of failure at all - while a
# runner resolving from the index got a server without the tool.
#
# Measured that day, in a venv built to resolve the way CI does: the index
# served 14 tools, the checkout offered 15, and the aihawk suite failed on
# exactly the difference.
#
# A FLOOR, not an exact pin, so the question is not "is this version there"
# but "does the index serve anything that satisfies it". That is a
# different check from the wrapper's, and writing the wrapper's would pass
# on a floor no release can meet.
shell: bash
run: |
set -euo pipefail
# Read the floor from the dependency itself. A number typed here would be
# a second place to update, and the second place is the one that rots.
FLOOR=$(python - <<'PY'
import re, tomllib
with open("pyproject.toml", "rb") as fh:
deps = tomllib.load(fh)["project"]["dependencies"]
# PEP 503: `invisible-playwright-mcp` and `invisible_playwright_mcp` are
# the same project, so both spellings are accepted rather than assumed.
want = [d for d in deps if re.match(r"invisible[-_]playwright[-_]mcp", d)]
if len(want) != 1:
raise SystemExit("expected exactly one MCP dependency, found %r" % want)
m = re.search(r">=\s*([0-9]+(?:\.[0-9]+)*)", want[0])
if not m:
raise SystemExit("no >= floor on the MCP dependency: %r" % want[0])
print(m.group(1))
PY
)
echo "floor: $FLOOR"
BEST=$(curl -sf "https://pypi.org/pypi/invisible-playwright-mcp/json" \
| python -c 'import json,sys; print(json.load(sys.stdin)["info"]["version"])')
echo "index serves: $BEST"
python - "$FLOOR" "$BEST" <<'PY'
import sys
def key(v):
return tuple(int(p) for p in v.split("."))
floor, best = sys.argv[1], sys.argv[2]
if key(best) < key(floor):
sys.exit(
"the index serves invisible-playwright-mcp %s and this release "
"needs >=%s, so pip could not resolve it on any runner and the "
"package would install broken for everyone. Publish the server "
"first, then release this." % (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
- 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
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