Skip to content

Bootstrap Publish

Bootstrap Publish #6

name: Bootstrap Publish
# Bare-bones manual publish workflow for the initial @shopify/shop-cli
# release(s). This is intentionally minimal: no version bumping, no
# changelog generation, no git tagging. Bump `version` in package.json on
# `main`, then dispatch this workflow.
#
# This will be replaced by Changesets once we are past the first few
# publishes and want automated version/tag/changelog management.
on:
workflow_dispatch:
inputs:
dry-run:
description: "Run the full pipeline + pack but skip the actual publish. Defaults to true; flip to false to actually publish."
required: true
type: boolean
default: true
tag:
description: "Dist-tag for this publish. 'next' for prereleases (e.g. 0.3.0-alpha.1), 'latest' only for stable versions."
required: true
type: choice
default: next
options:
- next
- latest
permissions:
contents: read
id-token: write
concurrency:
group: shop-cli-publish
cancel-in-progress: false
jobs:
publish:
name: Bootstrap publish @shopify/shop-cli to npm
# Refuse to run from anywhere but `main`.
if: github.ref == 'refs/heads/main'
environment:
name: npm-shop-cli
url: https://www.npmjs.com/package/@shopify/shop-cli
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- uses: shop/setup-javascript-action@main
with:
node-version-file: package.json
pnpm-version: 10.28.0
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm typecheck
- name: Test
run: pnpm test
- name: Build
run: pnpm build
- name: Pack and inspect contents
run: |
pnpm pack --pack-destination /tmp/shop-cli-bootstrap
echo "Tarball contents:"
tar -tzf /tmp/shop-cli-bootstrap/*.tgz | sort
- name: Print publish plan
env:
DIST_TAG: ${{ inputs.tag }}
DRY_RUN: ${{ inputs.dry-run }}
run: |
VERSION=$(node -p "require('./package.json').version")
echo "::notice::Plan: publish @shopify/shop-cli@${VERSION} with --tag ${DIST_TAG} (dry-run=${DRY_RUN})"
# Fail fast with a clear error if the NPM_TOKEN secret is missing or
# empty. Nothing about the token value is logged — only its presence
# as a boolean.
- name: Verify NPM_TOKEN is present
if: ${{ !inputs.dry-run }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "${NPM_TOKEN}" ]; then
echo "::error::NPM_TOKEN secret is missing or empty. Add it in repo Settings → Secrets and variables → Actions before re-running."
exit 1
fi
echo "::notice::NPM_TOKEN is present."
- name: Configure npm auth
if: ${{ !inputs.dry-run }}
run: |
echo "registry=https://registry.npmjs.org/" > ~/.npmrc
echo "//registry.npmjs.org/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc
echo "@shopify:registry=https://registry.npmjs.org/" >> ~/.npmrc
- name: Resolve unwrapped npm (bypass package-manager wrappers)
run: |
# Shopify's npm wrapper enforces install-time policy (minimum_dependency_age) that doesn't apply to `npm version` or `npm publish`; point at the npm bundled with node directly. OIDC auth is driven by ~/.npmrc and `id-token: write`, not by which binary runs.
echo "UNWRAPPED_NPM=$(dirname "$(command -v node)")/npm" >> "$GITHUB_ENV"
- name: Set up .npmrc for publishing
run: |
# Override project-level .npmrc that points to internal registry
if [ -f .npmrc ]; then
cp .npmrc .npmrc.backup
sed -i.bak 's|@shopify:registry=.*|@shopify:registry=https://registry.npmjs.org/|' .npmrc
echo "✅ Updated project .npmrc to use public npm for @shopify packages"
# Remove backup files created by sed and cp
rm -f .npmrc.bak .npmrc.backup
echo "✅ Cleaned up backup files"
fi
# Configure git for package tag creation.
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git config --global user.name "github-actions[bot]"
# Hide the local .npmrc registry override from git so npm version/publish
# see a clean source checkout without creating a synthetic commit.
if git diff --quiet .npmrc; then
echo "ℹ️ No .npmrc changes to hide"
else
git update-index --assume-unchanged .npmrc
echo "✅ Hid local .npmrc publishing override from git status"
fi
- name: Publish to npm
if: ${{ !inputs.dry-run }}
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
if [ "${{ inputs.dry_run }}" = "true" ]; then
echo "🧪 DRY RUN: Would publish $PACKAGE_NAME@$VERSION with tag $DIST_TAG"
exit 0
fi
if "$UNWRAPPED_NPM" view "$PACKAGE_NAME@$VERSION" version --registry=https://registry.npmjs.org/ >/dev/null 2>&1; then
echo "ℹ️ $PACKAGE_NAME@$VERSION already exists on npm; skipping publish"
exit 0
fi
"$UNWRAPPED_NPM" publish --tag "$DIST_TAG" --access public --verbose
echo "✅ Published $PACKAGE_NAME@$VERSION with tag $DIST_TAG"
env:
DIST_TAG: ${{ inputs.tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Dry-run summary
if: ${{ inputs.dry-run }}
env:
DIST_TAG: ${{ inputs.tag }}
run: |
echo "::notice::Dry-run requested — skipped npm publish."
echo "Would have published with: --tag $DIST_TAG --access public"