Skip to content

Publish to npm

Publish to npm #19

Workflow file for this run

name: Publish to npm
on:
workflow_dispatch:
inputs:
npm_tag:
description: npm dist-tag to publish
required: true
default: next
type: choice
options:
- next
- latest
permissions:
contents: write
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Verify release branch
run: |
if [ "${GITHUB_REF_NAME}" != "next" ]; then
echo "This workflow publishes the Chart Kit v2 package set and must be run from the next branch."
exit 1
fi
- name: Checkout
uses: actions/checkout@v5
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: 24
registry-url: https://registry.npmjs.org
cache: npm
- name: Verify npm publish access
run: |
if [ -z "${NODE_AUTH_TOKEN:-}" ]; then
echo "NPM_TOKEN is not configured for this repository or environment."
echo "Add a GitHub Actions secret named NPM_TOKEN with npm publish access before rerunning."
exit 1
fi
if ! NPM_USER=$(npm whoami 2>&1); then
echo "NPM_TOKEN could not authenticate with npm."
echo "${NPM_USER}"
exit 1
fi
echo "Authenticated to npm as ${NPM_USER}."
if npm access list packages @chart-kit --json >/dev/null; then
echo "NPM_TOKEN can list packages in the @chart-kit npm scope."
else
echo "::warning::NPM_TOKEN authenticated, but npm refused the @chart-kit package-list preflight."
echo "::warning::Granular npm tokens can be allowed to publish while still being denied org package listing."
echo "::warning::Continuing; npm publish remains the authoritative write-permission check."
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Install dependencies
run: npm ci --ignore-scripts
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Build
run: npm run build
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm run test
- name: E2E
run: npm run test:e2e
- name: Surface
run: npm run surface:check
- name: Package pack check
run: npm run pack:check
- name: Security audit
run: npm run security:audit
- name: Docs
run: npm run docs:build
- name: React Native CLI example
run: npm run example:rn-cli:typecheck
- name: Release gate
run: |
if [ "${NPM_TAG}" = "next" ]; then
npm run release:preview:gate
elif [ "${NPM_TAG}" = "latest" ]; then
npm run release:gate
else
echo "Unsupported npm dist-tag ${NPM_TAG}."
exit 1
fi
env:
NPM_TAG: ${{ inputs.npm_tag }}
- name: Verify package publish state
id: package
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
NPM_TAG="${{ inputs.npm_tag }}"
TAG_NAME="v${PACKAGE_VERSION}"
echo "name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
echo "version=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
echo "npm_tag=${NPM_TAG}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG_NAME}" >> "$GITHUB_OUTPUT"
if [ "${NPM_TAG}" = "latest" ] && [[ "${PACKAGE_VERSION}" == *-* ]]; then
echo "Refusing to publish prerelease version ${PACKAGE_VERSION} with npm dist-tag latest"
exit 1
fi
if [ "${NPM_TAG}" = "next" ]; then
node scripts/check-owner-gate-status.mjs h5 approved
elif [ "${NPM_TAG}" = "latest" ]; then
node scripts/check-owner-gate-status.mjs h6 approved
fi
HAS_UNPUBLISHED_PACKAGE=0
for PACKAGE_DIR in $(node scripts/list-release-packages.mjs --publishable); do
PACKAGE_JSON="${PACKAGE_DIR}/package.json"
if [ "${PACKAGE_DIR}" = "." ]; then
PACKAGE_JSON="package.json"
fi
WORKSPACE_NAME=$(node -p "require('./${PACKAGE_JSON}').name")
WORKSPACE_VERSION=$(node -p "require('./${PACKAGE_JSON}').version")
if node scripts/check-npm-package-exists.mjs "${WORKSPACE_NAME}" "${WORKSPACE_VERSION}"; then
echo "${WORKSPACE_NAME}@${WORKSPACE_VERSION} is already published and will be skipped"
else
PACKAGE_EXISTS_STATUS=$?
if [ "${PACKAGE_EXISTS_STATUS}" = "1" ]; then
HAS_UNPUBLISHED_PACKAGE=1
echo "${WORKSPACE_NAME}@${WORKSPACE_VERSION} is not published yet"
else
exit "${PACKAGE_EXISTS_STATUS}"
fi
fi
done
if git ls-remote --exit-code --tags origin "refs/tags/${TAG_NAME}" >/dev/null 2>&1; then
if [ "${HAS_UNPUBLISHED_PACKAGE}" = "1" ]; then
echo "${TAG_NAME} already exists, but at least one publishable package is missing."
exit 1
fi
echo "${TAG_NAME} already exists and all publishable packages are already published; continuing idempotent rerun."
fi
- name: Publish
run: |
for PACKAGE_DIR in $(node scripts/list-release-packages.mjs --publishable); do
PACKAGE_JSON="${PACKAGE_DIR}/package.json"
PUBLISH_TARGET="./${PACKAGE_DIR}"
if [ "${PACKAGE_DIR}" = "." ]; then
PACKAGE_JSON="package.json"
PUBLISH_TARGET="."
fi
WORKSPACE_NAME=$(node -p "require('./${PACKAGE_JSON}').name")
WORKSPACE_VERSION=$(node -p "require('./${PACKAGE_JSON}').version")
if node scripts/check-npm-package-exists.mjs "${WORKSPACE_NAME}" "${WORKSPACE_VERSION}"; then
echo "Skipping ${WORKSPACE_NAME}@${WORKSPACE_VERSION}; already published"
continue
else
PACKAGE_EXISTS_STATUS=$?
if [ "${PACKAGE_EXISTS_STATUS}" != "1" ]; then
exit "${PACKAGE_EXISTS_STATUS}"
fi
fi
npm publish "${PUBLISH_TARGET}" --ignore-scripts --access public --provenance --tag "${NPM_TAG}"
done
env:
NPM_TAG: ${{ steps.package.outputs.npm_tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Remove prerelease latest dist-tags
run: |
if [ "${NPM_TAG}" = "latest" ] || [[ "${PACKAGE_VERSION}" != *-* ]]; then
echo "No prerelease latest tag cleanup needed."
exit 0
fi
for PACKAGE_DIR in $(node scripts/list-release-packages.mjs --publishable); do
PACKAGE_JSON="${PACKAGE_DIR}/package.json"
if [ "${PACKAGE_DIR}" = "." ]; then
PACKAGE_JSON="package.json"
fi
WORKSPACE_NAME=$(node -p "require('./${PACKAGE_JSON}').name")
if ! DIST_TAG_OUTPUT=$(timeout 30s npm dist-tag ls "${WORKSPACE_NAME}"); then
echo "Could not read npm dist-tags for ${WORKSPACE_NAME}."
exit 1
fi
CURRENT_LATEST=$(printf "%s\n" "${DIST_TAG_OUTPUT}" | awk '/^latest:/ {print $2}')
if [ "${CURRENT_LATEST}" = "${PACKAGE_VERSION}" ]; then
echo "Removing unintended latest tag from ${WORKSPACE_NAME}@${PACKAGE_VERSION}"
if ! npm dist-tag rm "${WORKSPACE_NAME}" latest; then
echo "::warning::npm did not allow removing latest from ${WORKSPACE_NAME}. This is expected for a new package with no stable version yet; release verification will fail if a stable latest was overwritten."
fi
else
echo "Leaving ${WORKSPACE_NAME} latest tag unchanged (${CURRENT_LATEST:-missing})."
fi
done
env:
NPM_TAG: ${{ steps.package.outputs.npm_tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
- name: Verify npm registry publish state
run: npm run release:publish:status -- --strict --dist-tag "${NPM_TAG}"
env:
NPM_TAG: ${{ steps.package.outputs.npm_tag }}
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
PACKAGE_NAME: ${{ steps.package.outputs.name }}
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
TAG_NAME: ${{ steps.package.outputs.tag }}
run: |
if gh release view "${TAG_NAME}" >/dev/null 2>&1; then
echo "${TAG_NAME} release already exists; skipping release creation."
exit 0
fi
awk "/^## v${PACKAGE_VERSION}$/{flag=1; next} /^## /{flag=0} flag" CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "CHANGELOG.md does not contain a release-notes section for v${PACKAGE_VERSION}."
exit 1
fi
if [[ "${PACKAGE_VERSION}" == *-* ]]; then
gh release create "${TAG_NAME}" \
--target "${GITHUB_SHA}" \
--title "${TAG_NAME}" \
--notes-file release-notes.md \
--prerelease
else
gh release create "${TAG_NAME}" \
--target "${GITHUB_SHA}" \
--title "${TAG_NAME}" \
--notes-file release-notes.md
fi
- name: Record npm publish evidence
if: ${{ steps.package.outputs.npm_tag == 'next' }}
env:
TAG_NAME: ${{ steps.package.outputs.tag }}
run: |
npm run release:publish:evidence -- \
--run-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
--release-url "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"
- name: Upload npm publish evidence
if: ${{ steps.package.outputs.npm_tag == 'next' }}
uses: actions/upload-artifact@v5
with:
name: npm-publish-evidence-${{ steps.package.outputs.version }}
path: docs/release/evidence/npm-publish-evidence.json