mirror of
https://github.com/hanzokms/python-sdk.git
synced 2026-10-30 20:30:11 +00:00
The purpose-prefixed labels (hanzo-build-*, hanzo-deploy-*, hanzo-docs-build-*) are retired in favour of the bare <os>-<arch> every runner already advertises. They were not a capability, only a habit, and the habit cost builds: papers asked for hanzo-build-linux-amd64, no online runner offered it, and two idle linux-amd64 runners sat beside the queue while the build simply never ran. The lux tree lost three days to lux-linux-amd64 the same way. What a job is FOR is the workflow's name. runs-on is the machine it needs. Co-authored-by: Hanzo Dev <[email protected]>
115 lines
5.7 KiB
YAML
115 lines
5.7 KiB
YAML
name: Publish
|
|
|
|
# The one thing here allowed to reach PyPI.
|
|
#
|
|
# What this replaces could not run: it lived in `.github/workflows`, which the
|
|
# Hanzo Git does not read, and asked for `ubuntu-latest`, which nothing here
|
|
# advertises. It was also waiting on a tag, and a tag is a step people skip.
|
|
#
|
|
# No `paths:` filter, deliberately. The question is "does PyPI serve something
|
|
# older than this tree?", and the comparison below is the only thing that reads
|
|
# the truth. A path gate guesses it from the shape of a diff, so a packaging-only
|
|
# fix to a failed release matches no path and cannot re-run the job it repaired.
|
|
# With no gate this is idempotent by construction, which is also what makes the
|
|
# manual dispatch safe.
|
|
#
|
|
# NOTE: `setup.py` and `pyproject.toml` currently disagree (1.0.1 vs 1.0.0) and
|
|
# PyPI is ahead of both at 1.1.1, so this publishes nothing until one of them is
|
|
# made the single source and moved past the registry. That is the guard working
|
|
# rather than a fault: forward-only means a tree behind the index cannot walk it
|
|
# backwards. `setup.py` is read here because it is what the build consumes.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
|
|
# Serialized, newest wins. Two builds in flight both read PyPI before either
|
|
# uploads, so both would see the same version as absent and the slower one would
|
|
# ship a stale tree under a version that can never be replaced.
|
|
concurrency:
|
|
group: publish
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
publish:
|
|
# linux-amd64, not hanzokms-build-linux-amd64, and the difference is
|
|
# only whether the label exists yet. The runner declares its labels on daemon
|
|
# boot and nothing restarts it when its ConfigMap changes, so the hanzokms
|
|
# alias sits in universe undeclared — a job naming it waits for a runner that
|
|
# never answers. It is one alias onto these same slots; switch to it once the
|
|
# runner has rolled.
|
|
runs-on: [linux-amd64]
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.12'
|
|
|
|
- run: python -m pip install --upgrade build twine flake8
|
|
|
|
# Every push, not just a releasing one. Building only inside the publish
|
|
# branch means a push that does not bump the version never builds at all,
|
|
# so a packaging regression stays invisible until release day — the one
|
|
# day it is most expensive. There are no tests to run here (the inherited
|
|
# workflow says so and its pytest step is commented out), so the build and
|
|
# a syntax pass are the whole gate, honestly stated rather than dressed up.
|
|
- name: Build and check
|
|
run: |
|
|
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
python -m build
|
|
|
|
- name: Publish if the tree is ahead of PyPI
|
|
env:
|
|
TWINE_USERNAME: __token__
|
|
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
|
|
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
|
|
run: |
|
|
NAME=$(sed -n 's/^NAME *= *"\(.*\)"/\1/p' setup.py)
|
|
NEW=$(sed -n 's/^VERSION *= *"\(.*\)"/\1/p' setup.py)
|
|
[ -n "$NAME" ] && [ -n "$NEW" ] || { echo "::error::could not read NAME/VERSION from setup.py"; exit 1; }
|
|
|
|
OLD=$(curl -sf "https://pypi.org/pypi/${NAME}/json" \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin)['info']['version'])" 2>/dev/null || echo "0.0.0")
|
|
|
|
# FORWARD ONLY. `!=` fires in both directions and a revert would walk
|
|
# PyPI backwards; only a strictly greater local version is a release.
|
|
# Asking PyPI rather than the previous commit also survives a merge
|
|
# whose first parent already carries the bump.
|
|
NEWEST=$(printf '%s\n%s\n' "$OLD" "$NEW" | sort -V | tail -1)
|
|
if [ "$OLD" = "$NEW" ] || [ "$NEWEST" != "$NEW" ]; then
|
|
echo "PyPI serves $OLD; tree is $NEW — nothing to publish."
|
|
exit 0
|
|
fi
|
|
|
|
# Only the KMS machine identity lives as an Actions secret. A raw
|
|
# PYPI_TOKEN beside it would be a second place to hold one thing,
|
|
# and the one that rots silently — nothing reads it until a release
|
|
# day, which is the worst day to discover it expired.
|
|
if [ -z "$KMS_CLIENT_ID" ] || [ -z "$KMS_CLIENT_SECRET" ]; then
|
|
echo "::error::KMS machine identity absent — cannot pull the PyPI credential. Seed KMS_CLIENT_ID/KMS_CLIENT_SECRET as Hanzo Git Actions secrets (org or repo scope)." >&2
|
|
exit 1
|
|
fi
|
|
KMS=https://kms.hanzo.ai; ENVN=prod
|
|
KT=$(curl -sf "$KMS/v1/kms/auth/login" -H 'Content-Type: application/json' \
|
|
-d "{\"clientId\":\"$KMS_CLIENT_ID\",\"clientSecret\":\"$KMS_CLIENT_SECRET\"}" | jq -r .accessToken)
|
|
[ -n "$KT" ] && [ "$KT" != null ] || { echo "::error::KMS login failed"; exit 1; }
|
|
# The org is the token's owner claim, not a path segment, so the read
|
|
# names only the secret.
|
|
URL="$KMS/v1/kms/secrets/PYPI_TOKEN?env=$ENVN"
|
|
# No -f: a `curl -sf` on a 404 exits 22 and kills the step before any
|
|
# message of ours prints, which reads like a broken network.
|
|
BODY=$(curl -s -w '\n%{http_code}' "$URL" -H "Authorization: Bearer $KT") || true
|
|
CODE=$(printf '%s' "$BODY" | tail -1)
|
|
TWINE_PASSWORD=$(printf '%s' "$BODY" | sed '$d' | jq -r '.value // empty' 2>/dev/null || true)
|
|
if [ -z "$TWINE_PASSWORD" ]; then
|
|
echo "::error::KMS returned HTTP $CODE for $URL with no value — seed PYPI_TOKEN there, not as an Actions secret." >&2
|
|
exit 1
|
|
fi
|
|
echo "::add-mask::$TWINE_PASSWORD"
|
|
export TWINE_PASSWORD
|
|
|
|
# dist/ already exists from the gate above.
|
|
echo "Publishing $NAME $OLD -> $NEW"
|
|
twine upload dist/*
|