Files
zooqueen b60ea6958c publish: read the npm token from KMS
KMS is where the fleet's secrets live, so the publish step reads NPM_TOKEN at
run time with the same machine identity every other workflow here uses — the
same path @hanzo/ui publishes from. Nothing new to rotate, and no npm
credential sits on the forge.

Also drops the package.json path filter. The skip step already asks the
registry whether it serves something older than this tree, which is the one
question that matters; a path gate in front of it guesses the same answer from
the shape of a diff and gets it wrong in both directions.
2026-08-13 17:07:35 -07:00

95 lines
4.3 KiB
YAML

name: publish
# Publishes @hanzo/frame when the registry serves something older than this tree.
on:
push:
branches: [main]
# No `paths:` filter. There is ONE question here — "does npmjs serve
# something older than this tree?" — and the skip step below answers it
# against the registry itself. A path gate in front of it guesses the same
# answer from the shape of a diff, and guesses wrong in both directions: it
# misses a publish that a lockfile or build change should carry, and it
# fires on a version edit that changes nothing.
workflow_dispatch:
permissions:
contents: read
jobs:
publish:
runs-on: hanzo-build-linux-amd64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org/
- name: Skip if already published
id: check
run: |
NAME=$(jq -r .name package.json)
VER=$(jq -r .version package.json)
if curl -fsSL "https://registry.npmjs.org/$NAME/$VER" > /dev/null 2>&1; then
echo "$NAME@$VER already on registry; skipping"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
# `test` builds and then loads the built entry — dist is gitignored, so
# publishing without this ships an empty package. corepack takes the pnpm
# version from packageManager.
- name: Test
if: steps.check.outputs.skip != 'true'
run: |
corepack enable
pnpm install --frozen-lockfile
pnpm test
- name: Fetch the npm token from KMS
id: npmtok
if: steps.check.outputs.skip != 'true'
# KMS is where a secret lives. The token is read at run time from the org
# root with the same machine identity the rest of the fleet uses, so there
# is nothing new to rotate and no npm credential sits on the forge.
# @hanzo/ui's publish.yml reads it from the same place — one way, not two.
env:
KMS_CLIENT_ID: ${{ secrets.KMS_CLIENT_ID }}
KMS_CLIENT_SECRET: ${{ secrets.KMS_CLIENT_SECRET }}
KMS_ENDPOINT: ${{ vars.KMS_ENDPOINT || 'https://kms.hanzo.ai' }}
KMS_SECRET_ENV: ${{ vars.KMS_SECRET_ENV || 'prod' }}
# NPM_TOKEN sits at the org root, not under deploy/ (which holds
# CLOUDFLARE_*, GIT_TOKEN, KUBECONFIG, UNIVERSE_PIN_TOKEN). Empty means
# root; set KMS_NPM_PATH to move it without editing this file.
KMS_NPM_PATH: ${{ vars.KMS_NPM_PATH || '' }}
run: |
token=$(curl -fsS "${KMS_ENDPOINT}/v1/kms/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"clientId\":\"${KMS_CLIENT_ID}\",\"clientSecret\":\"${KMS_CLIENT_SECRET}\"}" \
| jq -r '.accessToken // empty')
if [ -z "${token}" ]; then
echo "::error::KMS login failed at ${KMS_ENDPOINT}/v1/kms/auth/login (check KMS_CLIENT_ID/KMS_CLIENT_SECRET)."
exit 1
fi
# The read is org-scoped by the TOKEN's owner claim, not by a path
# segment: the flat form is what cloud's embedded KMS serves, and it
# answers {name, env, value}. The older
# /v1/kms/orgs/<org>/secrets/... form with .secret.value 404s here.
secret_url="${KMS_ENDPOINT}/v1/kms/secrets/${KMS_NPM_PATH:+${KMS_NPM_PATH}/}NPM_TOKEN?env=${KMS_SECRET_ENV}"
# No -f: a 404 must report WHICH path was empty, not just exit 22.
npm_token=$(curl -sS "${secret_url}" -H "Authorization: Bearer ${token}" | jq -r '.value // empty')
if [ -z "${npm_token}" ]; then
echo "::error::No NPM_TOKEN at ${secret_url}."
echo "::error::Put it there, or set the KMS_NPM_PATH variable to the path that has it."
exit 1
fi
# Masked before it is ever an output, so it cannot surface in a log.
echo "::add-mask::${npm_token}"
echo "token=${npm_token}" >> "$GITHUB_OUTPUT"
- name: Publish
if: steps.check.outputs.skip != 'true'
# npm, not pnpm: pnpm publish adds git checks a CI checkout fails.
# --no-provenance: a token publish 422s without it.
run: npm publish --access public --no-provenance
env:
NODE_AUTH_TOKEN: ${{ steps.npmtok.outputs.token }}