A column of sessions, and each one drives its own browsers (0.17.0) (… #28
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |