Skip to content

Publish to npm

Publish to npm #67

Workflow file for this run

name: Publish to npm
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: ['release/**']
schedule:
- cron: '0 2 * * 1-5' # Weekdays at 2 AM UTC (Mon-Fri)
workflow_dispatch:
inputs:
release_type:
description: 'Release type (ignored for release branch workflow_run — always stable)'
required: true
default: 'nightly'
type: choice
options:
- nightly
- stable
env:
SFCC_DISABLE_TELEMETRY: ${{ vars.SFCC_DISABLE_TELEMETRY }}
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
if: >-
github.event_name != 'workflow_run' ||
github.event.workflow_run.conclusion == 'success'
permissions:
actions: write # For triggering deploy-docs.yml via workflow_dispatch
contents: write # For creating GitHub releases and tags
id-token: write # Required for npm OIDC trusted publishers
pull-requests: write # For creating merge-back PRs
steps:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || '' }}
fetch-depth: 0 # Needed for docs tag detection
- name: Determine release type
id: release-type
run: |
if [[ "${{ github.event.inputs.release_type }}" == "stable" ]] || [[ "${{ github.event_name }}" == "workflow_run" ]]; then
echo "type=stable" >> $GITHUB_OUTPUT
echo "tag=latest" >> $GITHUB_OUTPUT
else
echo "type=nightly" >> $GITHUB_OUTPUT
echo "tag=nightly" >> $GITHUB_OUTPUT
fi
- name: Check for pending changesets
if: steps.release-type.outputs.type == 'stable'
id: changesets
run: |
PENDING=$(find .changeset -name '*.md' ! -name 'README.md' 2>/dev/null | wc -l | tr -d ' ')
if [[ "$PENDING" -gt 0 ]]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::notice::Found $PENDING pending changeset(s) — skipping publish"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Quick version check
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true'
id: quick-check
run: |
HAS_CHANGES=false
for spec in "@salesforce/b2c-tooling-sdk:packages/b2c-tooling-sdk" \
"@salesforce/b2c-cli:packages/b2c-cli" \
"@salesforce/b2c-dx-mcp:packages/b2c-dx-mcp"; do
PKG_NAME="${spec%%:*}"
PKG_PATH="${spec##*:}"
LOCAL=$(node -p "require('./${PKG_PATH}/package.json').version")
NPM=$(npm view "$PKG_NAME" version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL" != "$NPM" ]; then
HAS_CHANGES=true
break
fi
done
# Also check docs tag
if [ "$HAS_CHANGES" = "false" ]; then
DOCS_VERSION=$(node -p "require('./docs/package.json').version")
if ! git rev-parse "docs@${DOCS_VERSION}" >/dev/null 2>&1; then
HAS_CHANGES=true
fi
fi
if [ "$HAS_CHANGES" = "false" ]; then
echo "skip=true" >> $GITHUB_OUTPUT
echo "::notice::All package versions match npm — nothing to publish"
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Setup pnpm
if: steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup Node.js
if: steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm for trusted publishing
if: steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
run: npm install -g npm@latest
- name: Install dependencies
if: steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
run: pnpm install --frozen-lockfile
- name: Determine packages to publish
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
id: packages
run: |
check_package() {
local pkg_name=$1
local pkg_path=$2
local output_key=$3
LOCAL_VERSION=$(node -p "require('./${pkg_path}/package.json').version")
NPM_VERSION=$(npm view "$pkg_name" version 2>/dev/null || echo "0.0.0")
echo "${pkg_name}: local=${LOCAL_VERSION} npm=${NPM_VERSION}"
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
echo "publish_${output_key}=true" >> $GITHUB_OUTPUT
echo "version_${output_key}=${LOCAL_VERSION}" >> $GITHUB_OUTPUT
# Determine appropriate npm dist-tag via semver comparison
IS_NEWER=$(node -e "
const [a,b,c] = '${LOCAL_VERSION}'.split('.').map(Number);
const [x,y,z] = '${NPM_VERSION}'.split('.').map(Number);
console.log(a>x||(a===x&&(b>y||(b===y&&c>z))));
")
if [ "$IS_NEWER" = "true" ]; then
echo "tag_${output_key}=latest" >> $GITHUB_OUTPUT
else
MINOR=$(echo "$LOCAL_VERSION" | sed 's/\.[0-9]*$//')
echo "tag_${output_key}=release-${MINOR}" >> $GITHUB_OUTPUT
fi
else
echo "publish_${output_key}=false" >> $GITHUB_OUTPUT
fi
}
check_package "@salesforce/b2c-tooling-sdk" "packages/b2c-tooling-sdk" "sdk"
check_package "@salesforce/b2c-cli" "packages/b2c-cli" "cli"
check_package "@salesforce/b2c-dx-mcp" "packages/b2c-dx-mcp" "mcp"
# VS Code extension — compare against git tags (not npm)
LOCAL_VSX_VERSION=$(node -p "require('./packages/b2c-vs-extension/package.json').version")
LAST_VSX_TAG=$(git tag -l "b2c-vs-extension@*" --sort=-v:refname | head -1 | sed 's/b2c-vs-extension@//')
echo "b2c-vs-extension: local=${LOCAL_VSX_VERSION} tag=${LAST_VSX_TAG:-none}"
if [ "$LOCAL_VSX_VERSION" != "$LAST_VSX_TAG" ]; then
echo "publish_vsx=true" >> $GITHUB_OUTPUT
echo "version_vsx=${LOCAL_VSX_VERSION}" >> $GITHUB_OUTPUT
else
echo "publish_vsx=false" >> $GITHUB_OUTPUT
fi
# Check if docs version changed (private package — not published to npm, uses git tag)
DOCS_VERSION=$(node -p "require('./docs/package.json').version")
if git rev-parse "docs@${DOCS_VERSION}" >/dev/null 2>&1; then
echo "publish_docs=false" >> $GITHUB_OUTPUT
else
echo "publish_docs=true" >> $GITHUB_OUTPUT
echo "version_docs=${DOCS_VERSION}" >> $GITHUB_OUTPUT
fi
echo "@salesforce/b2c-docs: version=${DOCS_VERSION}"
- name: Create snapshot versions
if: steps.release-type.outputs.type == 'nightly'
run: |
SNAPSHOT="0.0.0-nightly.$(date +%Y%m%d%H%M%S)"
for pkg in packages/b2c-tooling-sdk packages/b2c-cli packages/b2c-dx-mcp; do
node -e "
const fs = require('fs');
const path = '$pkg/package.json';
const pkg = JSON.parse(fs.readFileSync(path));
pkg.version = '$SNAPSHOT';
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
"
done
echo "Set snapshot version: $SNAPSHOT"
- name: Build packages
if: steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
run: pnpm run build
- name: Run tests
if: steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
run: pnpm --filter '!b2c-vs-extension' run test
- name: Publish SDK to npm
if: steps.release-type.outputs.type == 'nightly' || steps.packages.outputs.publish_sdk == 'true'
run: >-
pnpm --filter @salesforce/b2c-tooling-sdk publish --provenance --no-git-checks
--tag ${{ steps.release-type.outputs.type == 'nightly' && 'nightly' || steps.packages.outputs.tag_sdk }}
- name: Publish CLI to npm
if: steps.release-type.outputs.type == 'nightly' || steps.packages.outputs.publish_cli == 'true'
run: >-
pnpm --filter @salesforce/b2c-cli publish --provenance --no-git-checks
--tag ${{ steps.release-type.outputs.type == 'nightly' && 'nightly' || steps.packages.outputs.tag_cli }}
- name: Publish MCP to npm
if: steps.release-type.outputs.type == 'nightly' || steps.packages.outputs.publish_mcp == 'true'
run: >-
pnpm --filter @salesforce/b2c-dx-mcp publish --provenance --no-git-checks
--tag ${{ steps.release-type.outputs.type == 'nightly' && 'nightly' || steps.packages.outputs.tag_mcp }}
- name: Package VS Code extension
if: steps.release-type.outputs.type == 'stable' && steps.packages.outputs.publish_vsx == 'true'
working-directory: packages/b2c-vs-extension
run: pnpm run package
- name: Create git tags
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
TAGS_CREATED=""
if [[ "${{ steps.packages.outputs.publish_sdk }}" == "true" ]]; then
TAG="@salesforce/b2c-tooling-sdk@${{ steps.packages.outputs.version_sdk }}"
git tag "$TAG"
TAGS_CREATED="$TAGS_CREATED $TAG"
fi
if [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]]; then
TAG="@salesforce/b2c-cli@${{ steps.packages.outputs.version_cli }}"
git tag "$TAG"
TAGS_CREATED="$TAGS_CREATED $TAG"
fi
if [[ "${{ steps.packages.outputs.publish_mcp }}" == "true" ]]; then
TAG="@salesforce/b2c-dx-mcp@${{ steps.packages.outputs.version_mcp }}"
git tag "$TAG"
TAGS_CREATED="$TAGS_CREATED $TAG"
fi
if [[ "${{ steps.packages.outputs.publish_vsx }}" == "true" ]]; then
TAG="b2c-vs-extension@${{ steps.packages.outputs.version_vsx }}"
git tag "$TAG"
TAGS_CREATED="$TAGS_CREATED $TAG"
fi
if [ -n "$TAGS_CREATED" ]; then
git push origin $TAGS_CREATED
echo "Created tags:$TAGS_CREATED"
else
echo "No tags to create"
fi
- name: Create docs tag if version changed
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true' && steps.packages.outputs.publish_docs == 'true'
run: |
DOCS_TAG="docs@${{ steps.packages.outputs.version_docs }}"
git tag "$DOCS_TAG"
git push origin "$DOCS_TAG"
echo "Created docs tag: $DOCS_TAG"
- name: Extract changelogs for release
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
run: |
# Function to extract the latest version section from a changelog
extract_latest() {
awk '
/^## / { if (found) exit; found=1; next }
found { print }
' "$1"
}
# Build combined release notes for published packages
{
if [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]]; then
echo "## @salesforce/b2c-cli@${{ steps.packages.outputs.version_cli }}"
echo ""
extract_latest packages/b2c-cli/CHANGELOG.md
echo ""
fi
if [[ "${{ steps.packages.outputs.publish_mcp }}" == "true" ]]; then
echo "## @salesforce/b2c-dx-mcp@${{ steps.packages.outputs.version_mcp }}"
echo ""
extract_latest packages/b2c-dx-mcp/CHANGELOG.md
echo ""
fi
if [[ "${{ steps.packages.outputs.publish_sdk }}" == "true" ]]; then
echo "## @salesforce/b2c-tooling-sdk@${{ steps.packages.outputs.version_sdk }}"
echo ""
extract_latest packages/b2c-tooling-sdk/CHANGELOG.md
echo ""
fi
if [[ "${{ steps.packages.outputs.publish_vsx }}" == "true" ]]; then
echo "## b2c-vs-extension@${{ steps.packages.outputs.version_vsx }}"
echo ""
extract_latest packages/b2c-vs-extension/CHANGELOG.md
echo ""
fi
if [[ "${{ steps.packages.outputs.publish_docs }}" == "true" && -f docs/CHANGELOG.md ]]; then
echo "## Documentation"
echo ""
extract_latest docs/CHANGELOG.md
echo ""
fi
} > /tmp/release-notes.md
- name: Create GitHub Release
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
run: |
# Determine the release tag — prefer CLI as the user-facing product
if [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-cli@${{ steps.packages.outputs.version_cli }}"
elif [[ "${{ steps.packages.outputs.publish_sdk }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-tooling-sdk@${{ steps.packages.outputs.version_sdk }}"
elif [[ "${{ steps.packages.outputs.publish_mcp }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-dx-mcp@${{ steps.packages.outputs.version_mcp }}"
elif [[ "${{ steps.packages.outputs.publish_vsx }}" == "true" ]]; then
RELEASE_TAG="b2c-vs-extension@${{ steps.packages.outputs.version_vsx }}"
elif [[ "${{ steps.packages.outputs.publish_docs }}" == "true" ]]; then
RELEASE_TAG="docs@${{ steps.packages.outputs.version_docs }}"
else
echo "No packages published, skipping release"
exit 0
fi
gh release create "$RELEASE_TAG" --notes-file /tmp/release-notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Package skills artifacts
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
run: |
# Create b2c-skills.zip containing skills/b2c/skills/
cd skills/b2c && zip -r ../../b2c-skills.zip skills/
cd ../..
# Create b2c-cli-skills.zip containing skills/b2c-cli/skills/
cd skills/b2c-cli && zip -r ../../b2c-cli-skills.zip skills/
cd ../..
echo "Created skills artifacts:"
ls -la *.zip
- name: Upload skills to release
if: steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
run: |
# Determine the release tag (same logic as Create GitHub Release)
if [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-cli@${{ steps.packages.outputs.version_cli }}"
elif [[ "${{ steps.packages.outputs.publish_sdk }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-tooling-sdk@${{ steps.packages.outputs.version_sdk }}"
elif [[ "${{ steps.packages.outputs.publish_mcp }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-dx-mcp@${{ steps.packages.outputs.version_mcp }}"
elif [[ "${{ steps.packages.outputs.publish_vsx }}" == "true" ]]; then
RELEASE_TAG="b2c-vs-extension@${{ steps.packages.outputs.version_vsx }}"
else
echo "No package release to upload to"
exit 0
fi
gh release upload "$RELEASE_TAG" b2c-skills.zip b2c-cli-skills.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload VS Code extension to release
if: steps.release-type.outputs.type == 'stable' && steps.packages.outputs.publish_vsx == 'true'
run: |
# Determine the release tag (same logic as Create GitHub Release)
if [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-cli@${{ steps.packages.outputs.version_cli }}"
elif [[ "${{ steps.packages.outputs.publish_sdk }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-tooling-sdk@${{ steps.packages.outputs.version_sdk }}"
elif [[ "${{ steps.packages.outputs.publish_mcp }}" == "true" ]]; then
RELEASE_TAG="@salesforce/b2c-dx-mcp@${{ steps.packages.outputs.version_mcp }}"
elif [[ "${{ steps.packages.outputs.publish_vsx }}" == "true" ]]; then
RELEASE_TAG="b2c-vs-extension@${{ steps.packages.outputs.version_vsx }}"
else
echo "No release to upload to"
exit 0
fi
gh release upload "$RELEASE_TAG" packages/b2c-vs-extension/*.vsix
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger documentation deployment
if: >-
steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
&& (steps.packages.outputs.tag_cli == 'latest' || steps.packages.outputs.tag_sdk == 'latest' || steps.packages.outputs.tag_mcp == 'latest' || steps.packages.outputs.publish_docs == 'true')
run: gh workflow run deploy-docs.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create PR to merge version bumps back to main
if: github.event_name == 'workflow_run'
run: |
if [[ "${{ steps.packages.outputs.publish_sdk }}" == "true" ]] || \
[[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]] || \
[[ "${{ steps.packages.outputs.publish_mcp }}" == "true" ]] || \
[[ "${{ steps.packages.outputs.publish_docs }}" == "true" ]]; then
BRANCH="${{ github.event.workflow_run.head_branch }}"
gh pr create --base main --head "$BRANCH" \
--title "Merge version bumps from ${BRANCH}" \
--body "$(cat <<'EOF'
Auto-created after release branch publish from `'"${BRANCH}"'`.
Merges version bump commits back to main to prevent version collisions on the next regular release.
**Review checklist:**
- [ ] Version bumps in package.json files look correct
- [ ] CHANGELOG entries are accurate
- [ ] No merge conflicts with pending changesets on main
EOF
)" || echo "::warning::PR already exists or could not be created"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}