mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
ci: actually track the native workflows (.hanzo/ was gitignored)
The previous commit removed 13 GitHub workflows and added none, because `.hanzo/` is in .gitignore here — so `git add .hanzo` silently did nothing and the repo was left with no CI at all. `git status` reported the deletions cleanly; only the missing additions gave it away. `.hanzo/` is ignored on purpose (it also holds local agent scratch), so this negates just the workflows path — the same shape hanzoai/universe already uses to ignore `.hanzo/` while tracking its six workflow files. Now landed: .hanzo/workflows/publish-pypi.yml (native, runs on hanzo-build-linux-amd64) and .hanzo/workflows/sync-from-github.yml.
This commit is contained in:
@@ -37,6 +37,13 @@ QWEN.md
|
||||
|
||||
# Local databases and state
|
||||
.hanzo/
|
||||
# …except the native CI/CD workflows, which are source of truth on
|
||||
# git.hanzo.ai. `.hanzo/` is ignored because it also holds local agent
|
||||
# scratch; the workflows are not scratch. Without this negation a repo can
|
||||
# delete its GitHub workflows and land NO replacement — which is exactly
|
||||
# what happened here on the first attempt.
|
||||
!.hanzo/workflows/
|
||||
!.hanzo/workflows/**
|
||||
.grok/
|
||||
|
||||
# Documentation build
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
name: Publish PyPI
|
||||
# NATIVE CI. git.hanzo.ai is canonical; this runs on the Hanzo git-runner fleet
|
||||
# (`hanzo-build-linux-amd64`). GitHub is a downstream mirror only.
|
||||
#
|
||||
# This replaces .github/workflows/publish-pypi.yml, which could not be run at
|
||||
# all: dispatching it returns
|
||||
# HTTP 422: Actions has been disabled for this user
|
||||
# for the `hanzo-dev` account, and a tag push produced no run for the same
|
||||
# reason. So `hanzo` 0.4.4 sat tagged and unpublished while PyPI kept serving
|
||||
# 0.4.3, in which `hanzo auth login` cannot complete a login — Cloudflare
|
||||
# rejects urllib's default User-Agent with `error code: 1010`, so the token
|
||||
# exchange 403s AFTER the user has already signed in through the browser.
|
||||
# Depending on GitHub to ship a fix for our own CLI was the actual defect.
|
||||
#
|
||||
# Tag -> package mapping is unchanged from the retired workflow, including the
|
||||
# `hanzo-*` catch-all that maps `hanzo-v0.4.4` to the `hanzo` package.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
packages:
|
||||
description: 'Packages to publish (space-separated, or "all")'
|
||||
required: false
|
||||
default: ''
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
- 'hanzo-*'
|
||||
- 'hanzoai-*'
|
||||
|
||||
concurrency:
|
||||
group: publish-pypi-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Build and publish
|
||||
runs-on: [hanzo-build-linux-amd64]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install build tooling
|
||||
run: python -m pip install --quiet --upgrade build twine
|
||||
|
||||
- name: Determine packages
|
||||
id: pick
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
MANUAL="${{ github.event.inputs.packages }}"
|
||||
|
||||
# Auto-discovered so the list can never go stale — the retired
|
||||
# workflow's hardcoded lists silently dropped newly added packages.
|
||||
ALL="$( { echo hanzoai; ls -d pkg/*/ 2>/dev/null | xargs -n1 basename; } | sort -u | xargs)"
|
||||
|
||||
if [ -n "$MANUAL" ]; then
|
||||
[ "$MANUAL" = "all" ] && PACKAGES="$ALL" || PACKAGES="$MANUAL"
|
||||
elif [ "${GITHUB_REF}" != "${GITHUB_REF#refs/tags/}" ]; then
|
||||
case "$TAG" in
|
||||
hanzo-tools-*) PACKAGES="$(ls -d pkg/hanzo-tools-*/ 2>/dev/null | xargs -n1 basename | xargs)" ;;
|
||||
hanzoai-*) PACKAGES="hanzoai" ;;
|
||||
# Longest-match first, then the catch-all: `hanzo-v0.4.4` -> hanzo.
|
||||
hanzo-*)
|
||||
NAME="$(printf '%s' "$TAG" | sed -E 's/^(hanzo-[a-z0-9]+)-v?[0-9].*$/\1/')"
|
||||
if [ "$NAME" != "$TAG" ] && [ -d "pkg/$NAME" ]; then PACKAGES="$NAME"; else PACKAGES="hanzo"; fi ;;
|
||||
v*) PACKAGES="$ALL" ;;
|
||||
*) PACKAGES="" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo "PACKAGES=$PACKAGES" >> "$GITHUB_OUTPUT"
|
||||
echo "Publishing: ${PACKAGES:-<none>}"
|
||||
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
# Two tokens cover per-project scope: PYPI_TOKEN owns most hanzo-*
|
||||
# packages, HANZO_AI_PYPI_TOKEN the rest. Each upload tries the primary
|
||||
# then falls back, so a 403 scope-miss on one is covered by the other.
|
||||
PYPI_TOKEN_PRIMARY: ${{ secrets.PYPI_TOKEN }}
|
||||
PYPI_TOKEN_FALLBACK: ${{ secrets.HANZO_AI_PYPI_TOKEN }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
PACKAGES="${{ steps.pick.outputs.PACKAGES }}"
|
||||
[ -z "$PACKAGES" ] && { echo "Nothing to publish."; exit 0; }
|
||||
|
||||
if [ -z "${PYPI_TOKEN_PRIMARY:-}" ] && [ -z "${PYPI_TOKEN_FALLBACK:-}" ]; then
|
||||
echo "::error::No PyPI token on this forge. Add PYPI_TOKEN as a repo/org secret on git.hanzo.ai." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rc=0
|
||||
for package in $PACKAGES; do
|
||||
if [ "$package" = "hanzoai" ]; then PKG_DIR="."
|
||||
elif [ -d "pkg/$package" ]; then PKG_DIR="pkg/$package"
|
||||
else echo "skip: no pkg/$package"; continue; fi
|
||||
|
||||
echo "── $package ($PKG_DIR)"
|
||||
rm -rf "$PKG_DIR/dist" "$PKG_DIR/build"
|
||||
python -m build "$PKG_DIR" --outdir "$PKG_DIR/dist" || { rc=1; continue; }
|
||||
|
||||
# --skip-existing keeps retries idempotent; try primary then fallback.
|
||||
if ! TWINE_PASSWORD="$PYPI_TOKEN_PRIMARY" python -m twine upload "$PKG_DIR/dist/"* --skip-existing; then
|
||||
if [ -n "${PYPI_TOKEN_FALLBACK:-}" ]; then
|
||||
TWINE_PASSWORD="$PYPI_TOKEN_FALLBACK" python -m twine upload "$PKG_DIR/dist/"* --skip-existing || rc=1
|
||||
else
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
exit $rc
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Sync from GitHub
|
||||
# git.hanzo.ai is CANONICAL and builds natively; development also lands on
|
||||
# github.com/hanzoai/python-sdk. Together with the push-mirror going the other way
|
||||
# (native -> GitHub, sync_on_commit) this is the full bidirectional loop.
|
||||
#
|
||||
# The two compose rather than fight: a native commit reaches GitHub via the
|
||||
# push-mirror, so this job then sees LOCAL == REMOTE and exits "in sync". A
|
||||
# GitHub commit fast-forwards native here, and the resulting push-mirror is a
|
||||
# no-op because GitHub already has it. No echo, no loop.
|
||||
#
|
||||
# ONE deterministic direction per job: an in-cluster PULL. The runner reaches
|
||||
# both ends (GitHub outbound, this forge via the instance URL actions/checkout
|
||||
# already uses), so the sync has no ingress dependency.
|
||||
#
|
||||
# Fast-forward ONLY. A divergence fails LOUDLY here rather than force-pushing
|
||||
# either side and destroying whichever history lost the race.
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
workflow_dispatch: {}
|
||||
concurrency:
|
||||
group: sync-from-github
|
||||
cancel-in-progress: false
|
||||
jobs:
|
||||
ff-main:
|
||||
runs-on: [hanzo-build-linux-amd64]
|
||||
steps:
|
||||
- name: Checkout main (full history for the ancestry check)
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
- name: Fast-forward main from github.com/hanzoai/python-sdk
|
||||
env:
|
||||
GH_PAT: ${{ secrets.GH_PAT }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch --quiet "https://x-access-token:${GH_PAT}@github.com/hanzoai/python-sdk.git" main
|
||||
LOCAL="$(git rev-parse HEAD)"
|
||||
REMOTE="$(git rev-parse FETCH_HEAD)"
|
||||
if [ "$LOCAL" = "$REMOTE" ]; then
|
||||
echo "in sync at $LOCAL"
|
||||
exit 0
|
||||
fi
|
||||
if git merge-base --is-ancestor "$LOCAL" "$REMOTE"; then
|
||||
echo "fast-forwarding $LOCAL -> $REMOTE"
|
||||
git push origin "$REMOTE:refs/heads/main"
|
||||
# A push made with the workflow token does NOT trigger other workflows
|
||||
# (loop prevention), so synced commits would never build. Dispatch it
|
||||
# explicitly — a real fast-forward means real commits arrived.
|
||||
curl -fsS --max-time 20 -X POST \
|
||||
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/actions/workflows/deploy.yml/dispatches" \
|
||||
-d '{"ref":"main"}' || echo "build dispatch failed (non-fatal)"
|
||||
else
|
||||
echo "DIVERGED: native $LOCAL is not an ancestor of GitHub $REMOTE." >&2
|
||||
echo "Resolve by hand; this job will not force-push either side." >&2
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user