Skip to content

Publish

Publish #70

Workflow file for this run

name: Publish
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
auth_mode:
description: 'npm auth: oidc (tokenless trusted publishing — requires the npmjs Trusted Publisher config) or token (NPM_TOKEN secret fallback)'
type: choice
options:
- oidc
- token
default: oidc
jobs:
# Verify both packages build and pass tests, in lockstep.
# If either fails, neither publishes.
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: https://registry.npmjs.org
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install JS deps
run: pnpm install --frozen-lockfile
- name: Typecheck JS
run: pnpm typecheck
- name: Test JS
run: pnpm test
- name: Build JS and emit OpenAPI spec
run: pnpm build
- name: Verify packed package exports
run: pnpm run verify:package
- name: Verify version lock between npm and PyPI packages
run: |
NPM_VERSION=$(node -p "require('./package.json').version")
PY_VERSION=$(grep -E '^version' clients/python/pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
PY_RUNTIME_VERSION=$(python -c "import pathlib,re; text=pathlib.Path('clients/python/src/agent_eval_rpc/__init__.py').read_text(); match=re.search(r'__version__ = \"([^\"]+)\"', text); print(match.group(1) if match else '')")
if [ "$NPM_VERSION" != "$PY_VERSION" ]; then
echo "::error::Version mismatch: npm=$NPM_VERSION pypi=$PY_VERSION. Bump them together."
exit 1
fi
if [ -n "$PY_RUNTIME_VERSION" ] && [ "$NPM_VERSION" != "$PY_RUNTIME_VERSION" ]; then
echo "::error::Version mismatch: npm=$NPM_VERSION python_runtime=$PY_RUNTIME_VERSION. Bump them together."
exit 1
fi
if [[ "${GITHUB_REF:-}" == refs/tags/v* ]]; then
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
if [ "$TAG_VERSION" != "$NPM_VERSION" ]; then
echo "::error::Tag/version mismatch: tag=$TAG_VERSION package=$NPM_VERSION."
exit 1
fi
fi
echo "Versions locked: $NPM_VERSION"
- name: Install Python client
working-directory: clients/python
run: pip install -e ".[dev]"
- name: Test Python client (incl. real subprocess integration)
working-directory: clients/python
run: pytest -v
- name: Upload OpenAPI artifact
uses: actions/upload-artifact@v4
with:
name: openapi
path: dist/openapi.json
- name: Upload Python build context
uses: actions/upload-artifact@v4
with:
name: python-source
path: clients/python
publish-npm:
needs: verify
# Publish on a v* tag (token path) OR a manual dispatch. A dispatch defaults
# to OIDC trusted publishing (tokenless) — see `auth_mode`.
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: read
# OIDC: with a Trusted Publisher configured on npmjs (package Settings →
# Trusted Publisher → GitHub Actions: org tangle-network, repo agent-eval,
# workflow publish.yml), pnpm exchanges this token for a short-lived publish
# credential — no NPM_TOKEN, nothing to rotate, plus provenance attestation.
id-token: write
# Resolve the auth mode once: tag push always uses the token; a dispatch uses
# its `auth_mode` input (default oidc). `IS_OIDC` gates the steps below.
env:
IS_OIDC: ${{ github.event_name == 'workflow_dispatch' && inputs.auth_mode == 'oidc' }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: https://registry.npmjs.org
- name: Verify npm publish credentials
# Token path only — OIDC has no NODE_AUTH_TOKEN to verify (the registry
# mints the credential at publish time from the id-token).
if: env.IS_OIDC != 'true'
run: |
set -euo pipefail
NAME=$(node -p "require('./package.json').name")
SCOPE="${NAME%%/*}"
if [ -z "${NODE_AUTH_TOKEN:-}" ]; then
echo "::error::NPM_TOKEN is not configured. Configure an npm automation token with publish access to $NAME before tagging a release."
exit 1
fi
if ! npm whoami --registry=https://registry.npmjs.org >/tmp/npm-whoami.txt; then
echo "::error::NPM_TOKEN is not accepted by registry.npmjs.org. The previous symptom was npm publish returning 404 for $NAME."
exit 1
fi
echo "npm publisher: $(cat /tmp/npm-whoami.txt)"
if ! npm access ls-packages "$SCOPE" --json --registry=https://registry.npmjs.org >/tmp/npm-access.json; then
echo "::error::NPM_TOKEN cannot read package access for $SCOPE. It will not be able to publish $NAME."
exit 1
fi
node - "$NAME" /tmp/npm-access.json <<'NODE'
const [name, accessFile] = process.argv.slice(2)
const fs = require('node:fs')
const access = JSON.parse(fs.readFileSync(accessFile, 'utf8'))
if (!Object.prototype.hasOwnProperty.call(access, name)) {
console.error(`::error::NPM_TOKEN can read the scope but ${name} is missing from npm access output.`)
process.exit(1)
}
if (access[name] !== 'read-write') {
console.error(`::error::NPM_TOKEN has ${access[name]} access for ${name}; release publish requires read-write access.`)
process.exit(1)
}
NODE
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: pnpm install --frozen-lockfile
- run: pnpm build
# Both publish steps are idempotent: re-running a version already on the
# registry (e.g. after a manual publish) skips rather than fails, so the
# downstream PyPI step still runs.
# OIDC path (dispatch, auth_mode=oidc): tokenless. pnpm 10.22 exchanges the
# id-token for a publish credential and emits provenance. No secret.
- name: Publish to npm (OIDC trusted publishing)
if: env.IS_OIDC == 'true'
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "$NAME@$VERSION" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then
echo "$NAME@$VERSION already on registry; skipping publish"
else
pnpm publish --no-git-checks --access public --provenance --registry=https://registry.npmjs.org
fi
# Token path (tag push, or dispatch auth_mode=token): the NPM_TOKEN secret.
- name: Publish to npm (token, skip if already published)
if: env.IS_OIDC != 'true'
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if npm view "$NAME@$VERSION" version --registry=https://registry.npmjs.org >/dev/null 2>&1; then
echo "$NAME@$VERSION already on registry; skipping publish"
else
pnpm publish --no-git-checks --access public --provenance --registry=https://registry.npmjs.org
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-pypi:
# PyPI must not publish a version npm failed to publish. The npm job is
# idempotent and succeeds when the version is already on the registry, so
# reruns can still repair a missing PyPI artifact without creating skew.
needs: [verify, publish-npm]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install build tools
run: pip install build twine
- name: Build wheel + sdist
working-directory: clients/python
run: python -m build
- name: Check whether this version is already on PyPI
id: pypi-check
run: |
VERSION=$(grep -E '^version' clients/python/pyproject.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')
if curl -sf "https://pypi.org/pypi/agent-eval-rpc/$VERSION/json" >/dev/null; then
echo "agent-eval-rpc==$VERSION already on PyPI; skipping publish"
echo "skip=true" >> "$GITHUB_OUTPUT"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish to PyPI (trusted publishing)
if: steps.pypi-check.outputs.skip != 'true'
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: clients/python/dist