Publish #70
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |