From 1189f341abc138a61b342b45787d781b132f822c Mon Sep 17 00:00:00 2001 From: Zachary Roth <100426704+zacharyr0th@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:31:39 -0700 Subject: [PATCH] ci: add trusted npm SDK publishing --- .github/workflows/publish-sdk.yml | 120 ++++++++++++++++++++++++++++++ docs/release.md | 15 +++- 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish-sdk.yml diff --git a/.github/workflows/publish-sdk.yml b/.github/workflows/publish-sdk.yml new file mode 100644 index 0000000..1799f1a --- /dev/null +++ b/.github/workflows/publish-sdk.yml @@ -0,0 +1,120 @@ +name: Publish SDK to npm + +# Publishes @raintree-technology/docpull-sdk with npm trusted publishing. +# Configure the package publisher for this repository and workflow filename; +# no npm token is stored in GitHub. + +on: + workflow_dispatch: + inputs: + version: + description: "Version to publish, matching sdk/js/package.json" + required: true + push: + tags: + - "v*.*.*" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + + - name: Set up Node 24 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: "24" + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 + with: + bun-version: "1.3.11" + + - name: Verify release version + id: meta + env: + REQUESTED_VERSION: ${{ github.event.inputs.version || '' }} + run: | + set -euo pipefail + SDK_VERSION=$(node -p "require('./sdk/js/package.json').version") + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + TAG_VERSION="${GITHUB_REF_NAME#v}" + if [ "$TAG_VERSION" != "$SDK_VERSION" ]; then + echo "::error::Tag $TAG_VERSION does not match SDK version $SDK_VERSION" + exit 1 + fi + elif [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + if [ "${GITHUB_REF_NAME}" != "main" ]; then + echo "::error::Manual publish must run from main, got ${GITHUB_REF_NAME}" + exit 1 + fi + if [ "$REQUESTED_VERSION" != "$SDK_VERSION" ]; then + echo "::error::Requested version $REQUESTED_VERSION does not match SDK version $SDK_VERSION" + exit 1 + fi + else + echo "::error::SDK publish only runs from vX.Y.Z tags or manual main dispatch" + exit 1 + fi + echo "version=$SDK_VERSION" >> "$GITHUB_OUTPUT" + + - name: Install locked dependencies + run: bun install --frozen-lockfile + + - name: Test, typecheck, and build SDK + run: | + bun run --cwd sdk/js test + bun run --cwd sdk/js typecheck + bun run --cwd sdk/js build + + - name: Verify package contents + working-directory: sdk/js + run: npm pack --dry-run + + - name: Publish with provenance + id: publish + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + if npm view "@raintree-technology/docpull-sdk@${VERSION}" version >/dev/null 2>&1; then + echo "@raintree-technology/docpull-sdk@${VERSION} is already published." + echo "published=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + cd sdk/js + npx --yes npm@11.7.0 publish --access public --provenance + echo "published=true" >> "$GITHUB_OUTPUT" + + - name: Smoke install from npm + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + set -euo pipefail + smoke_dir=$(mktemp -d) + cd "$smoke_dir" + npm init --yes >/dev/null + for attempt in 1 2 3 4 5; do + if npm install "@raintree-technology/docpull-sdk@${VERSION}"; then + break + fi + if [ "$attempt" = 5 ]; then + exit 1 + fi + sleep 10 + done + node --input-type=module -e "import('@raintree-technology/docpull-sdk').then((sdk) => { if (typeof sdk.readPack !== 'function') process.exit(1) })" diff --git a/docs/release.md b/docs/release.md index c1cb3d0..0bd1055 100644 --- a/docs/release.md +++ b/docs/release.md @@ -56,10 +56,15 @@ make release-publish VERSION=6.0.0 This verifies `origin/main` has the requested `pyproject.toml` version, puts `vX.Y.Z` on the merged `origin/main` commit, and pushes the tag to start the -PyPI workflow. After the PyPI trusted-publish step succeeds on a tag push, the -workflow creates or updates the GitHub Release for that tag and marks it as the -latest release. It uses `docs/release-post-vX.Y.md` when present and otherwise -falls back to generated notes. +PyPI and npm workflows. After the PyPI trusted-publish step succeeds on a tag +push, the workflow creates or updates the GitHub Release for that tag and marks +it as the latest release. The npm workflow publishes +`@raintree-technology/docpull-sdk` with provenance when `sdk/js/package.json` +has the same version. It uses no npm token: configure npm trusted publishing +for organization `raintree-technology`, repository `docpull`, and workflow +`publish-sdk.yml` before the next version is released. The GitHub Release uses +`docs/release-post-vX.Y.md` when present and otherwise falls back to generated +notes. Verify both public release surfaces: @@ -70,6 +75,7 @@ import json, urllib.request with urllib.request.urlopen("https://pypi.org/pypi/docpull/json", timeout=20) as r: print(json.load(r)["info"]["version"]) PY +npm view @raintree-technology/docpull-sdk version ``` The helper refuses to move an existing remote tag by default. @@ -88,6 +94,7 @@ the merged `main` commit: ```bash make release-dispatch VERSION=6.0.0 +gh workflow run publish-sdk.yml --ref main -f version=6.0.0 ``` The workflow refuses manual dispatch from any branch other than `main`, and the