Files
gui/.hanzo/workflows/publish-gui.yml
T
zeekayandHanzo Dev 58255138df ci: a runner label names the platform, not the org
hanzo-build-linux-amd64, lux-build-linux-amd64, zoo-build-linux-amd64 and
every other org-prefixed spelling all meant the same machine. Which org a
job belongs to does not change which host can build it, so the org bought
one pool name per org per arch and each could go stale on its own -- the
evo host alone answered to fourteen names for one label.

One vocabulary now: linux-amd64, linux-arm64, macos-arm64, win-amd64. The
generic labels every plain workflow uses (ubuntu-latest, self-hosted) are
untouched, so naive CI keeps working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:25:29 -07:00

213 lines
9.1 KiB
YAML

name: Publish gui
# Triggered only on `release/gui-v*` tags. Bumping a workspace
# version alone does NOT publish — a tag push gates the run so an
# accidental version bump on `main` cannot ship to npm. Tag scheme:
# release/gui-v7.1.0 → [email protected]
#
# Note: @hanzogui/admin moved to hanzoai/admin and publishes from there.
#
# NPM_TOKEN resolution order:
# 1. KMS via Universal Auth (KMS_CLIENT_ID/SECRET → short-lived token)
# 2. KMS via long-lived HANZO_API_KEY (legacy bootstrap)
# 3. Direct repo secret NPM_TOKEN (fallback until KMS is provisioned)
# Same shape as `~/work/hanzo/login/.github/workflows/deploy.yml`.
#
# Smoke test: after pack, install the .tgz tarball into a throwaway
# /tmp consumer and require() it. If the static extractor or vite
# build chokes on an import shape, this catches it before publish.
on:
push:
tags:
- 'release/gui-v*'
permissions:
contents: read
id-token: write
jobs:
publish:
name: Build, smoke-test, publish
# Retargeted arm64 self-hosted (spark, frequently offline) → the always-up
# in-cluster amd64 ARC pool. Pure-JS publish is arch-independent.
runs-on: [linux-amd64]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install
uses: ./.github/actions/install
- name: Resolve NPM_TOKEN
id: token
uses: ./.github/actions/npm-token
with:
kms_client_id: ${{ secrets.KMS_CLIENT_ID }}
kms_client_secret: ${{ secrets.KMS_CLIENT_SECRET }}
fallback: ${{ secrets.NPM_TOKEN }}
- name: Configure npm registry
env:
NPM_TOKEN: ${{ steps.token.outputs.token }}
run: |
echo 'registry=https://registry.npmjs.org/' > ~/.npmrc
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> ~/.npmrc
- name: Build (postinstall already built js with SKIP_TYPES=1)
# The install step ran `bun install` which fires postinstall
# → `bun run build:js` (SKIP_TYPES=1 turbo run build --force).
# That covers the publish target and every workspace consumer,
# so a separate filtered build here would be a no-op rebuild.
# Verifying the dist tree exists is enough — pack will fail
# loudly if it doesn't.
run: |
test -d pkgs/ui/hanzogui/dist || (echo "::error::gui/dist missing"; exit 1)
- name: Build workspace version map
# Map every workspace package name to its on-disk version so we can
# resolve `workspace:*` deps to the actual version each subpackage is
# at. The umbrella is on the 7.x track but the @hanzogui/* primitives
# are still on the 102.x canary stream — they are NOT republished
# here, only referenced.
id: ws-versions
run: |
set -eu
node -e "
const fs=require('fs'); const path=require('path');
const map={};
const dirs=['pkgs/ui','pkgs/core','pkgs/compiler','pkgs'];
const collect=(d)=>{
if (!fs.existsSync(d)) return;
const stat=fs.statSync(d);
if (stat.isFile() && d.endsWith('package.json')) {
try { const j=JSON.parse(fs.readFileSync(d)); if (j.name && j.version) map[j.name]=j.version; } catch{}
return;
}
if (!stat.isDirectory()) return;
const pkg=path.join(d,'package.json');
if (fs.existsSync(pkg)) collect(pkg);
for (const e of fs.readdirSync(d)) {
if (e==='node_modules') continue;
collect(path.join(d,e));
}
};
for (const d of dirs) collect(d);
fs.writeFileSync('/tmp/ws-versions.json', JSON.stringify(map));
console.log('mapped', Object.keys(map).length, 'workspace packages');
"
- name: Pack gui
# `npm pack` does NOT resolve `workspace:*` specifiers; the resulting
# tarball would ship literal `workspace:*` and break consumers. We
# mirror what `scripts/release.ts` does — copy to a temp dir, rewrite
# every `workspace:*` to the matching package's on-disk version (from
# the workspace map built above), then pack.
id: pack-gui
run: |
set -eu
tmpdir=$(mktemp -d)
cp -R pkgs/ui/hanzogui/. "$tmpdir/"
rm -rf "$tmpdir/node_modules"
node -e "
const fs=require('fs');
const map=JSON.parse(fs.readFileSync('/tmp/ws-versions.json'));
const p='$tmpdir/package.json';
const j=JSON.parse(fs.readFileSync(p));
const ownVer=j.version;
for (const f of ['dependencies','devDependencies','peerDependencies','optionalDependencies']) {
if (!j[f]) continue;
for (const k of Object.keys(j[f])) {
if (j[f][k].startsWith('workspace:')) {
const v=map[k]; if (!v) { console.error('no version for',k); process.exit(1); }
j[f][k]=v;
}
}
}
fs.writeFileSync(p, JSON.stringify(j, null, 2)+'\n');
"
cd "$tmpdir"
tgz=$(npm pack --silent | tail -1)
echo "tgz=$tmpdir/${tgz}" >> "$GITHUB_OUTPUT"
echo "tmpdir=$tmpdir" >> "$GITHUB_OUTPUT"
- name: Smoke-test packed tarball (metadata-only)
env:
GUI_TGZ: ${{ steps.pack-gui.outputs.tgz }}
run: |
set -eu
# Metadata-only smoke: extract package.json + key dist files from
# the tarball without npm-installing it. Full consumer install
# would chicken-egg on first release (hanzogui's @hanzogui/*
# workspace deps aren't on npm yet at the new version), so this
# checks the tarball SHAPE — name, exports map resolvability,
# required dist files present — without resolving the dep tree.
inspect=$(mktemp -d)
tar -xzf "${GUI_TGZ}" -C "$inspect"
cd "$inspect/package"
# Name must be exactly 'hanzogui' (the static extractor scans
# for this literal token; never call this package 'Gui').
test "$(node -p "require('./package.json').name")" = "hanzogui"
# All workspace:* specs must be rewritten by the pack step
# (literal 'workspace:' in the published tarball would break
# consumer installs with EUNSUPPORTEDPROTOCOL).
! grep -q '"workspace:' package.json
# Every dist file referenced from exports must actually exist
# in the tarball — catches broken exports maps + missing files.
node -e "
const fs = require('fs');
const pkg = require('./package.json');
const walk = (v) => {
if (typeof v === 'string') {
if (v.startsWith('./')) {
if (!fs.existsSync(v)) { console.error('missing:', v); process.exitCode = 1; }
}
} else if (v && typeof v === 'object') {
for (const k of Object.keys(v)) walk(v[k]);
}
};
walk(pkg.exports || {});
walk(pkg.main); walk(pkg.module); walk(pkg.types);
"
echo "smoke OK: $(jq -r '.name + \"@\" + .version' package.json)"
- name: Publish gui
env:
NPM_TOKEN: ${{ steps.token.outputs.token }}
NODE_AUTH_TOKEN: ${{ steps.token.outputs.token }}
TGZ: ${{ steps.pack-gui.outputs.tgz }}
working-directory: pkgs/ui/hanzogui
run: |
version=$(node -p "require('./package.json').version")
if npm view "hanzogui@${version}" version >/dev/null 2>&1; then
echo "hanzogui@${version} already on npm — skipping."
exit 0
fi
npm publish "${TGZ}" --access public --tag stable
- name: Pack + Publish @hanzo/gui (alias of gui)
# Republish the umbrella tarball under the @hanzo/gui scoped name so
# downstream consumers can import either alias. Same dist tree as
# gui — only `name` differs. The static extractor scans for the
# literal 'gui' import token, so this alias is purely a consumer
# ergonomics shim and never participates in extractor input. We reuse
# the prepared tmpdir from the gui pack step (workspace specs
# already rewritten) and only mutate `name`.
env:
NPM_TOKEN: ${{ steps.token.outputs.token }}
NODE_AUTH_TOKEN: ${{ steps.token.outputs.token }}
TMPDIR_GUI: ${{ steps.pack-gui.outputs.tmpdir }}
run: |
set -eu
aliasdir=$(mktemp -d)
cp -R "$TMPDIR_GUI"/. "$aliasdir/"
cd "$aliasdir"
jq '.name = "@hanzo/gui"' package.json > package.json.new
mv package.json.new package.json
version=$(node -p "require('./package.json').version")
if npm view "@hanzo/gui@${version}" version >/dev/null 2>&1; then
echo "@hanzo/gui@${version} already on npm — skipping."
exit 0
fi
tgz=$(npm pack --silent | tail -1)
npm publish "${tgz}" --access public --tag stable