11name : Publish to npm
22
33on :
4+ workflow_run :
5+ workflows : ["CI"]
6+ types : [completed]
7+ branches : ['release/**']
48 schedule :
59 - cron : ' 0 2 * * 1-5' # Weekdays at 2 AM UTC (Mon-Fri)
610 workflow_dispatch :
711 inputs :
812 release_type :
9- description : ' Release type'
13+ description : ' Release type (ignored for release branch workflow_run — always stable) '
1014 required : true
1115 default : ' nightly'
1216 type : choice
@@ -21,44 +25,96 @@ jobs:
2125 publish :
2226 name : Publish
2327 runs-on : ubuntu-latest
28+ if : >-
29+ github.event_name != 'workflow_run' ||
30+ github.event.workflow_run.conclusion == 'success'
2431 permissions :
2532 contents : write # For creating GitHub releases and tags
2633 id-token : write # Required for npm OIDC trusted publishers
34+ pull-requests : write # For creating merge-back PRs
2735 steps :
2836 - name : Checkout
2937 uses : actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
3038 with :
39+ ref : ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || '' }}
3140 fetch-depth : 0 # Needed for docs tag detection
3241
3342 - name : Determine release type
3443 id : release-type
3544 run : |
36- if [[ "${{ github.event.inputs.release_type }}" == "stable" ]]; then
45+ if [[ "${{ github.event.inputs.release_type }}" == "stable" ]] || [[ "${{ github.event_name }}" == "workflow_run" ]] ; then
3746 echo "type=stable" >> $GITHUB_OUTPUT
3847 echo "tag=latest" >> $GITHUB_OUTPUT
3948 else
4049 echo "type=nightly" >> $GITHUB_OUTPUT
4150 echo "tag=nightly" >> $GITHUB_OUTPUT
4251 fi
4352
53+ - name : Check for pending changesets
54+ if : steps.release-type.outputs.type == 'stable'
55+ id : changesets
56+ run : |
57+ PENDING=$(find .changeset -name '*.md' ! -name 'README.md' 2>/dev/null | wc -l | tr -d ' ')
58+ if [[ "$PENDING" -gt 0 ]]; then
59+ echo "skip=true" >> $GITHUB_OUTPUT
60+ echo "::notice::Found $PENDING pending changeset(s) — skipping publish"
61+ else
62+ echo "skip=false" >> $GITHUB_OUTPUT
63+ fi
64+
65+ - name : Quick version check
66+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true'
67+ id : quick-check
68+ run : |
69+ HAS_CHANGES=false
70+ for spec in "@salesforce/b2c-tooling-sdk:packages/b2c-tooling-sdk" \
71+ "@salesforce/b2c-cli:packages/b2c-cli" \
72+ "@salesforce/b2c-dx-mcp:packages/b2c-dx-mcp"; do
73+ PKG_NAME="${spec%%:*}"
74+ PKG_PATH="${spec##*:}"
75+ LOCAL=$(node -p "require('./${PKG_PATH}/package.json').version")
76+ NPM=$(npm view "$PKG_NAME" version 2>/dev/null || echo "0.0.0")
77+ if [ "$LOCAL" != "$NPM" ]; then
78+ HAS_CHANGES=true
79+ break
80+ fi
81+ done
82+ # Also check docs tag
83+ if [ "$HAS_CHANGES" = "false" ]; then
84+ DOCS_VERSION=$(node -p "require('./docs/package.json').version")
85+ if ! git rev-parse "docs@${DOCS_VERSION}" >/dev/null 2>&1; then
86+ HAS_CHANGES=true
87+ fi
88+ fi
89+ if [ "$HAS_CHANGES" = "false" ]; then
90+ echo "skip=true" >> $GITHUB_OUTPUT
91+ echo "::notice::All package versions match npm — nothing to publish"
92+ else
93+ echo "skip=false" >> $GITHUB_OUTPUT
94+ fi
95+
4496 - name : Setup pnpm
97+ if : steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
4598 uses : pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
4699
47100 - name : Setup Node.js
101+ if : steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
48102 uses : actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
49103 with :
50104 node-version : 22
51105 cache : ' pnpm'
52106 registry-url : ' https://registry.npmjs.org'
53107
54108 - name : Upgrade npm for trusted publishing
109+ if : steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
55110 run : npm install -g npm@latest
56111
57112 - name : Install dependencies
113+ if : steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
58114 run : pnpm install --frozen-lockfile
59115
60116 - name : Determine packages to publish
61- if : steps.release-type.outputs.type == 'stable'
117+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
62118 id : packages
63119 run : |
64120 check_package() {
@@ -74,6 +130,19 @@ jobs:
74130 if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
75131 echo "publish_${output_key}=true" >> $GITHUB_OUTPUT
76132 echo "version_${output_key}=${LOCAL_VERSION}" >> $GITHUB_OUTPUT
133+
134+ # Determine appropriate npm dist-tag via semver comparison
135+ IS_NEWER=$(node -e "
136+ const [a,b,c] = '${LOCAL_VERSION}'.split('.').map(Number);
137+ const [x,y,z] = '${NPM_VERSION}'.split('.').map(Number);
138+ console.log(a>x||(a===x&&(b>y||(b===y&&c>z))));
139+ ")
140+ if [ "$IS_NEWER" = "true" ]; then
141+ echo "tag_${output_key}=latest" >> $GITHUB_OUTPUT
142+ else
143+ MINOR=$(echo "$LOCAL_VERSION" | sed 's/\.[0-9]*$//')
144+ echo "tag_${output_key}=release-${MINOR}" >> $GITHUB_OUTPUT
145+ fi
77146 else
78147 echo "publish_${output_key}=false" >> $GITHUB_OUTPUT
79148 fi
@@ -109,25 +178,33 @@ jobs:
109178 echo "Set snapshot version: $SNAPSHOT"
110179
111180 - name : Build packages
181+ if : steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
112182 run : pnpm run build
113183
114184 - name : Run tests
185+ if : steps.release-type.outputs.type == 'nightly' || (steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true')
115186 run : pnpm --filter '!b2c-vs-extension' run test
116187
117188 - name : Publish SDK to npm
118189 if : steps.release-type.outputs.type == 'nightly' || steps.packages.outputs.publish_sdk == 'true'
119- run : pnpm --filter @salesforce/b2c-tooling-sdk publish --provenance --no-git-checks --tag ${{ steps.release-type.outputs.tag }}
190+ run : >-
191+ pnpm --filter @salesforce/b2c-tooling-sdk publish --provenance --no-git-checks
192+ --tag ${{ steps.release-type.outputs.type == 'nightly' && 'nightly' || steps.packages.outputs.tag_sdk }}
120193
121194 - name : Publish CLI to npm
122195 if : steps.release-type.outputs.type == 'nightly' || steps.packages.outputs.publish_cli == 'true'
123- run : pnpm --filter @salesforce/b2c-cli publish --provenance --no-git-checks --tag ${{ steps.release-type.outputs.tag }}
196+ run : >-
197+ pnpm --filter @salesforce/b2c-cli publish --provenance --no-git-checks
198+ --tag ${{ steps.release-type.outputs.type == 'nightly' && 'nightly' || steps.packages.outputs.tag_cli }}
124199
125200 - name : Publish MCP to npm
126201 if : steps.release-type.outputs.type == 'nightly' || steps.packages.outputs.publish_mcp == 'true'
127- run : pnpm --filter @salesforce/b2c-dx-mcp publish --provenance --no-git-checks --tag ${{ steps.release-type.outputs.tag }}
202+ run : >-
203+ pnpm --filter @salesforce/b2c-dx-mcp publish --provenance --no-git-checks
204+ --tag ${{ steps.release-type.outputs.type == 'nightly' && 'nightly' || steps.packages.outputs.tag_mcp }}
128205
129206 - name : Create git tags
130- if : steps.release-type.outputs.type == 'stable'
207+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
131208 run : |
132209 git config user.name "github-actions[bot]"
133210 git config user.email "github-actions[bot]@users.noreply.github.com"
@@ -160,15 +237,15 @@ jobs:
160237 fi
161238
162239 - name : Create docs tag if version changed
163- if : steps.release-type.outputs.type == 'stable' && steps.packages.outputs.publish_docs == 'true'
240+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true' && steps. packages.outputs.publish_docs == 'true'
164241 run : |
165242 DOCS_TAG="docs@${{ steps.packages.outputs.version_docs }}"
166243 git tag "$DOCS_TAG"
167244 git push origin "$DOCS_TAG"
168245 echo "Created docs tag: $DOCS_TAG"
169246
170247 - name : Extract changelogs for release
171- if : steps.release-type.outputs.type == 'stable'
248+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
172249 run : |
173250 # Function to extract the latest version section from a changelog
174251 extract_latest() {
@@ -210,7 +287,7 @@ jobs:
210287 } > /tmp/release-notes.md
211288
212289 - name : Create GitHub Release
213- if : steps.release-type.outputs.type == 'stable'
290+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
214291 run : |
215292 # Determine the release tag — prefer CLI as the user-facing product
216293 if [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]]; then
@@ -231,7 +308,7 @@ jobs:
231308 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
232309
233310 - name : Package skills artifacts
234- if : steps.release-type.outputs.type == 'stable'
311+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
235312 run : |
236313 # Create b2c-skills.zip containing skills/b2c/skills/
237314 cd skills/b2c && zip -r ../../b2c-skills.zip skills/
@@ -243,7 +320,7 @@ jobs:
243320 ls -la *.zip
244321
245322 - name : Upload skills to release
246- if : steps.release-type.outputs.type == 'stable'
323+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
247324 run : |
248325 # Determine the release tag (same logic as Create GitHub Release)
249326 if [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]]; then
@@ -262,7 +339,32 @@ jobs:
262339 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
263340
264341 - name : Trigger documentation deployment
265- if : steps.release-type.outputs.type == 'stable'
342+ if : steps.release-type.outputs.type == 'stable' && steps.changesets.outputs.skip != 'true' && steps.quick-check.outputs.skip != 'true'
266343 run : gh workflow run deploy-docs.yml
267344 env :
268345 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
346+
347+ - name : Create PR to merge version bumps back to main
348+ if : github.event_name == 'workflow_run'
349+ run : |
350+ if [[ "${{ steps.packages.outputs.publish_sdk }}" == "true" ]] || \
351+ [[ "${{ steps.packages.outputs.publish_cli }}" == "true" ]] || \
352+ [[ "${{ steps.packages.outputs.publish_mcp }}" == "true" ]] || \
353+ [[ "${{ steps.packages.outputs.publish_docs }}" == "true" ]]; then
354+ BRANCH="${{ github.event.workflow_run.head_branch }}"
355+ gh pr create --base main --head "$BRANCH" \
356+ --title "Merge version bumps from ${BRANCH}" \
357+ --body "$(cat <<'EOF'
358+ Auto-created after hotfix publish from `'"${BRANCH}"'`.
359+
360+ Merges version bump commits back to main to prevent version collisions on the next regular release.
361+
362+ **Review checklist:**
363+ - [ ] Version bumps in package.json files look correct
364+ - [ ] CHANGELOG entries are accurate
365+ - [ ] No merge conflicts with pending changesets on main
366+ EOF
367+ )" || echo "::warning::PR already exists or could not be created"
368+ fi
369+ env :
370+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
0 commit comments