Files
zeekayandHanzo Dev 136f3e0bf8 runs-on names a machine, not a purpose
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]>
2026-09-15 11:36:51 -07:00

117 lines
5.5 KiB
YAML

name: Publish
# The one thing here allowed to reach a registry.
#
# What this replaces had never published anything: `@hanzo/kms-sdk` is absent
# from both npmjs and pkg.hanzo.ai while the tree carries 4.0.7 and a 4.0.6 tag
# from the September before last. The inherited workflow explains why — it lived
# in `.github/workflows`, which Hanzo Git does not read, and asked for
# `ubuntu-22.04`, a hosted runner this fleet does not have. It was waiting on a
# tag it could not have acted on anyway.
#
# No `paths:` filter, deliberately. The question is "does the registry 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
# lockfile-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.
on:
push:
branches: [main]
workflow_dispatch:
# Serialized, newest wins. Two builds in flight both read the registry before
# either writes to it, so both believe the same version is unpublished and the
# slower one ships a stale tree under a number the faster one defined.
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-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: npm
- run: npm ci
# `test` builds first, so this is the build gate too, and it runs on every
# push rather than only a releasing one — a regression that only surfaces
# on release day surfaces on the day it is most expensive.
- run: npm test
- name: Is the tree ahead of the registry
id: ahead
run: |
NAME=$(jq -r .name package.json)
NEW=$(jq -r .version package.json)
OLD=$(curl -sf "https://registry.npmjs.org/${NAME/\//%2f}" \
| jq -r '."dist-tags".latest // "0.0.0"' 2>/dev/null || echo "0.0.0")
# FORWARD ONLY. `!=` fires in both directions and a revert would walk
# the registry backwards; only a strictly greater local version is a
# release. Asking the registry 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 "registry serves $OLD; tree is $NEW — nothing to publish."
echo "yes=false" >> "$GITHUB_OUTPUT"
else
echo "registry serves $OLD; tree is $NEW — publishing."
echo "yes=true" >> "$GITHUB_OUTPUT"
echo "old=$OLD" >> "$GITHUB_OUTPUT"
echo "new=$NEW" >> "$GITHUB_OUTPUT"
fi
- name: Load the publish credential from KMS
if: steps.ahead.outputs.yes == 'true'
env:
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
run: |
# Only the KMS machine identity lives as an Actions secret; a raw
# NPM_TOKEN beside it would be a second place to hold one thing —
# and the one that rots silently, since npm answers an unauthorised
# publish with 404 on PUT, not 401.
if [ -z "$KMS_CLIENT_ID" ] || [ -z "$KMS_CLIENT_SECRET" ]; then
echo "::error::KMS machine identity absent — cannot pull the npm 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
TOKEN=$(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 "$TOKEN" ] && [ "$TOKEN" != 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: /v1/kms/secrets/<name>?env=<env>.
URL="$KMS/v1/kms/secrets/NPM_TOKEN?env=$ENVN"
# No -f: under `bash -e -o pipefail` a `curl -sf` on a 404 exits 22
# and kills the step before any message of ours prints, which reads
# like a broken network. Capture the status and say it.
BODY=$(curl -s -w '\n%{http_code}' "$URL" -H "Authorization: Bearer $TOKEN") || true
CODE=$(printf '%s' "$BODY" | tail -1)
VAL=$(printf '%s' "$BODY" | sed '$d' | jq -r '.value // empty' 2>/dev/null || true)
if [ -z "$VAL" ]; then
echo "::error::KMS returned HTTP $CODE for $URL with no value — seed NPM_TOKEN there, not as an Actions secret." >&2
exit 1
fi
echo "::add-mask::$VAL"
echo "NODE_AUTH_TOKEN=$VAL" >> "$GITHUB_ENV"
- name: Publish
if: steps.ahead.outputs.yes == 'true'
run: |
echo "Publishing ${{ steps.ahead.outputs.old }} -> ${{ steps.ahead.outputs.new }}"
npm publish --access public