Publish to npm #66
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 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 }} |