11name : Release Workflow
22
33on :
4+ push :
5+ branches :
6+ - main
7+ tags :
8+ # Matches the Nx releaseTag pattern in nx.json: "{version}-{projectName}"
9+ - ' *-*'
410 workflow_dispatch :
511 inputs :
612 dist-tag :
1420 type : boolean
1521 default : false
1622 release-group :
17- description : " Optional Nx release group or project to scope the release (empty = default behavior)"
23+ description : " Optional Nx project pattern to scope the release (empty = default behavior)"
1824 required : false
1925 type : string
2026 default : " "
@@ -26,19 +32,17 @@ concurrency:
2632
2733permissions :
2834 contents : write # needed to push version commits and tags
29- pull-requests : write # for changelog PRs/comments if Nx uses them
30- id-token : write # required for npm provenance (OIDC)
35+ id-token : write # required for npm provenance / trusted publishing (OIDC)
3136
3237jobs :
3338 release :
3439 name : Version and Publish (gated by environment)
40+ if : ${{ github.actor != 'github-actions[bot]' }}
3541 runs-on : ubuntu-latest
3642 environment :
37- name : ${{ inputs.dry-run && 'npm-publish-dry-run' || 'npm-publish' }}
43+ name : ${{ (github.event_name == 'workflow_dispatch' && inputs.dry-run) && 'npm-publish-dry-run' || 'npm-publish' }}
3844
3945 env :
40- # Default dist-tag if not provided via workflow_dispatch input
41- NPM_DIST_TAG : ${{ inputs['dist-tag'] || 'next' }}
4246 # Optional: provide Nx Cloud token if used in this repo
4347 NX_CLOUD_ACCESS_TOKEN : ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
4448
@@ -60,83 +64,258 @@ jobs:
6064 registry-url : ' https://registry.npmjs.org'
6165 cache : ' npm'
6266
67+ - name : Update npm (required for OIDC trusted publishing)
68+ run : |
69+ npm install -g npm@^11.5.1
70+ npm --version
71+
6372 - name : Install dependencies
6473 run : npm ci
6574
6675 - name : Repo setup
6776 run : npm run setup
6877
69- # Collect a one-time password (OTP) from a reviewer via the environment approval gate.
70- - id : wait_for_otp
71- name : Wait for npm OTP (2FA)
72- if : ${{ !inputs.dry-run }}
73- uses : step-security/wait-for-secrets@v1
74- with :
75- secrets : |
76- NPM_OTP
77- timeout-minutes : 30
78+ - name : Resolve release context
79+ id : ctx
80+ shell : bash
81+ run : |
82+ set -euo pipefail
7883
79- - name : Configure npm auth
80- if : ${{ !inputs.dry-run }}
81- env :
82- NPM_TOKEN : ${{ secrets.NPM_PUBLISH_TOKEN }}
84+ if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
85+ dist_tag="${{ inputs['dist-tag'] }}"
86+ scope="${{ inputs['release-group'] }}"
87+ dry_run="${{ inputs['dry-run'] }}"
88+ mode="dispatch"
89+ elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
90+ dist_tag=""
91+ scope=""
92+ dry_run="false"
93+ mode="tag"
94+ else
95+ dist_tag="next"
96+ scope=""
97+ dry_run="false"
98+ mode="main"
99+ fi
100+
101+ echo "mode=${mode}" >> "$GITHUB_OUTPUT"
102+ echo "dist_tag=${dist_tag}" >> "$GITHUB_OUTPUT"
103+ echo "scope=${scope}" >> "$GITHUB_OUTPUT"
104+ echo "dry_run=${dry_run}" >> "$GITHUB_OUTPUT"
105+
106+ - name : Determine affected release projects (main)
107+ id : affected
108+ if : ${{ steps.ctx.outputs.mode == 'main' }}
109+ shell : bash
83110 run : |
84- test -n "$NPM_TOKEN" || { echo "NPM_PUBLISH_TOKEN secret is required"; exit 1; }
85- echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
111+ set -euo pipefail
112+
113+ base='${{ github.event.before }}'
114+ head='${{ github.sha }}'
115+
116+ # Only consider libs under packages/* and exclude items configured as non-releaseable.
117+ affected_json=$(npx nx show projects --affected --base "$base" --head "$head" --type lib --projects "packages/*" --exclude "ui-mobile-base,types-minimal,winter-tc,types,types-ios,types-android" --json)
118+ affected_list=$(printf '%s' "$affected_json" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s||"[]");process.stdout.write(a.join(" "));});')
119+ affected_count=$(printf '%s' "$affected_json" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const a=JSON.parse(s||"[]");process.stdout.write(String(a.length));});')
120+
121+ echo "projects=${affected_list}" >> "$GITHUB_OUTPUT"
122+ echo "count=${affected_count}" >> "$GITHUB_OUTPUT"
123+
124+ - name : Determine tag release project and dist-tag (tags)
125+ id : taginfo
126+ if : ${{ steps.ctx.outputs.mode == 'tag' }}
127+ shell : bash
128+ run : |
129+ set -euo pipefail
130+
131+ tag_name="${GITHUB_REF_NAME}"
132+
133+ # Find the project by matching the tag suffix against known releaseable packages.
134+ projects=$(npx nx show projects --projects "packages/*" --type lib --exclude "ui-mobile-base,types-minimal,winter-tc,types,types-ios,types-android" --sep ' ')
135+
136+ best_match=""
137+ best_len=0
138+ for p in $projects; do
139+ suffix="-${p}"
140+ if [[ "$tag_name" == *"$suffix" ]]; then
141+ if (( ${#p} > best_len )); then
142+ best_match="$p"
143+ best_len=${#p}
144+ fi
145+ fi
146+ done
147+
148+ if [[ -z "$best_match" ]]; then
149+ echo "Could not determine project from tag '$tag_name'. Expected '{version}-{projectName}'." >&2
150+ exit 1
151+ fi
152+
153+ version_part="${tag_name%-$best_match}"
154+ if [[ "$version_part" == *-* ]]; then
155+ dist_tag="next"
156+ else
157+ dist_tag="latest"
158+ fi
159+
160+ echo "project=${best_match}" >> "$GITHUB_OUTPUT"
161+ echo "version=${version_part}" >> "$GITHUB_OUTPUT"
162+ echo "dist_tag=${dist_tag}" >> "$GITHUB_OUTPUT"
86163
87164 - name : Configure git user for automated commits
88165 run : |
89166 git config user.name "github-actions[bot]"
90167 git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
91168
92- # VERSION: updates versions, changelogs, creates git tags following nx.json releaseTag pattern.
93- - name : nx release version
94- if : ${{ !inputs.dry-run }}
169+ # VERSION: updates versions and creates git tags following nx.json releaseTag.pattern.
170+ - name : nx release version (main)
171+ if : ${{ steps.ctx.outputs.mode == 'main' && steps.affected.outputs.count != '0' }}
172+ shell : bash
173+ run : |
174+ set -euo pipefail
175+ npx nx release version prerelease \
176+ --preid next \
177+ --projects "${{ steps.affected.outputs.projects }}" \
178+ --git-commit \
179+ --git-tag \
180+ --git-push \
181+ --verbose
182+
183+ - name : nx release version (main, no-op)
184+ if : ${{ steps.ctx.outputs.mode == 'main' && steps.affected.outputs.count == '0' }}
185+ run : echo "No affected release projects on main; skipping version + publish."
186+
187+ - name : nx release version (dispatch)
188+ if : ${{ steps.ctx.outputs.mode == 'dispatch' && !inputs.dry-run }}
189+ shell : bash
190+ run : |
191+ set -euo pipefail
192+
193+ scope="${{ steps.ctx.outputs.scope }}"
194+ if [[ -n "$scope" ]]; then
195+ projects_arg=(--projects "$scope")
196+ else
197+ projects_arg=()
198+ fi
199+
200+ npx nx release version prerelease \
201+ --preid "${{ steps.ctx.outputs.dist_tag }}" \
202+ "${projects_arg[@]}" \
203+ --git-commit \
204+ --git-tag \
205+ --git-push \
206+ --verbose
207+
208+ - name : nx release version (dispatch, dry-run)
209+ if : ${{ steps.ctx.outputs.mode == 'dispatch' && inputs.dry-run }}
210+ shell : bash
211+ run : |
212+ set -euo pipefail
213+
214+ scope="${{ steps.ctx.outputs.scope }}"
215+ if [[ -n "$scope" ]]; then
216+ projects_arg=(--projects "$scope")
217+ else
218+ projects_arg=()
219+ fi
220+
221+ npx nx release version prerelease \
222+ --preid "${{ steps.ctx.outputs.dist_tag }}" \
223+ "${projects_arg[@]}" \
224+ --verbose \
225+ --dry-run
226+
227+ # PUBLISH: OIDC trusted publishing (default). Avoid any lingering token auth.
228+ - name : nx release publish (OIDC)
229+ if : ${{ steps.ctx.outputs.mode != 'tag' && steps.ctx.outputs.dry_run != 'true' && vars.USE_NPM_TOKEN != 'true' && (steps.ctx.outputs.mode != 'main' || steps.affected.outputs.count != '0') }}
230+ shell : bash
95231 env :
96- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
97- NX_GROUP_ARG : ${{ inputs['release-group'] != '' && format('--group {0}', inputs['release-group']) || '' }}
232+ NPM_CONFIG_PROVENANCE : true
233+ NODE_AUTH_TOKEN : " "
98234 run : |
99- npx nx release version ${NX_GROUP_ARG} --yes --verbose
235+ set -euo pipefail
236+ unset NODE_AUTH_TOKEN
237+ rm -f ~/.npmrc || true
238+ if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then
239+ rm -f "$NPM_CONFIG_USERCONFIG" || true
240+ fi
100241
101- - name : nx release version (dry-run)
102- if : ${{ inputs.dry-run }}
242+ npx nx release publish \
243+ --tag "${{ steps.ctx.outputs.dist_tag }}" \
244+ --access public \
245+ --verbose
246+
247+ - name : nx release publish (OIDC, dry-run)
248+ if : ${{ steps.ctx.outputs.mode == 'dispatch' && inputs.dry-run && vars.USE_NPM_TOKEN != 'true' }}
249+ shell : bash
103250 env :
104- GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
105- NX_GROUP_ARG : ${{ inputs['release-group'] != '' && format('--group {0}', inputs['release-group']) || '' }}
251+ NPM_CONFIG_PROVENANCE : true
252+ NODE_AUTH_TOKEN : " "
106253 run : |
107- npx nx release version ${NX_GROUP_ARG} --yes --verbose --dry-run
254+ set -euo pipefail
255+ unset NODE_AUTH_TOKEN
256+ rm -f ~/.npmrc || true
257+ if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then
258+ rm -f "$NPM_CONFIG_USERCONFIG" || true
259+ fi
260+
261+ npx nx release publish \
262+ --tag "${{ steps.ctx.outputs.dist_tag }}" \
263+ --access public \
264+ --verbose \
265+ --dry-run
108266
109- # Ensure version commits and tags are pushed if version step created them.
110- - name : Push version commits and tags
111- if : ${{ !inputs.dry-run }}
267+ # PUBLISH: token fallback (only when explicitly enabled via repo/environment variable USE_NPM_TOKEN=true).
268+ - name : nx release publish (token)
269+ if : ${{ steps.ctx.outputs.mode != 'tag' && steps.ctx.outputs.dry_run != 'true' && vars.USE_NPM_TOKEN == 'true' && (steps.ctx.outputs.mode != 'main' || steps.affected.outputs.count != '0') }}
270+ env :
271+ NODE_AUTH_TOKEN : ${{ secrets.NPM_PUBLISH_TOKEN }}
272+ NPM_CONFIG_PROVENANCE : true
112273 run : |
113- # Push commits (if any) and tags created by Nx Release
114- git push --follow-tags || true
274+ npx nx release publish --tag "${{ steps.ctx.outputs.dist_tag }}" --access public --verbose
115275
116- # PUBLISH: perform npm publish using Nx Release, with 2FA OTP and provenance.
117- - name : nx release publish
118- if : ${{ !inputs.dry-run }}
276+ - name : nx release publish (token, dry-run)
277+ if : ${{ steps.ctx.outputs.mode == 'dispatch' && inputs.dry-run && vars.USE_NPM_TOKEN == 'true' }}
119278 env :
120- NPM_CONFIG_OTP : ${{ steps.wait_for_otp.outputs.NPM_OTP }}
121- # For npm provenance via OIDC
122279 NODE_AUTH_TOKEN : ${{ secrets.NPM_PUBLISH_TOKEN }}
123- NX_GROUP_ARG : ${{ inputs['release-group'] != '' && format('--group {0}', inputs['release-group']) || '' }}
280+ NPM_CONFIG_PROVENANCE : true
124281 run : |
125- test -n "$NPM_CONFIG_OTP" || { echo "Missing NPM OTP from environment approval"; exit 1; }
126- # Use Nx Release to publish all changed packages; tag controls npm dist-tag; provenance enables supply chain attestations
127- npx nx release publish ${NX_GROUP_ARG} --tag "$NPM_DIST_TAG" --provenance --yes --verbose
282+ npx nx release publish --tag "${{ steps.ctx.outputs.dist_tag }}" --access public --verbose --dry-run
128283
129- - name : nx release publish (dry-run)
130- if : ${{ inputs.dry-run }}
284+ # Tag-triggered publishing: publish the single package referenced by the tag.
285+ - name : nx release publish (tag)
286+ if : ${{ steps.ctx.outputs.mode == 'tag' && vars.USE_NPM_TOKEN != 'true' }}
287+ shell : bash
131288 env :
132- NX_GROUP_ARG : ${{ inputs['release-group'] != '' && format('--group {0}', inputs['release-group']) || '' }}
289+ NPM_CONFIG_PROVENANCE : true
290+ NODE_AUTH_TOKEN : " "
291+ run : |
292+ set -euo pipefail
293+ unset NODE_AUTH_TOKEN
294+ rm -f ~/.npmrc || true
295+ if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then
296+ rm -f "$NPM_CONFIG_USERCONFIG" || true
297+ fi
298+
299+ npx nx release publish \
300+ --projects "${{ steps.taginfo.outputs.project }}" \
301+ --tag "${{ steps.taginfo.outputs.dist_tag }}" \
302+ --access public \
303+ --verbose
304+
305+ - name : nx release publish (tag, token)
306+ if : ${{ steps.ctx.outputs.mode == 'tag' && vars.USE_NPM_TOKEN == 'true' }}
307+ env :
308+ NODE_AUTH_TOKEN : ${{ secrets.NPM_PUBLISH_TOKEN }}
309+ NPM_CONFIG_PROVENANCE : true
133310 run : |
134- npx nx release publish ${NX_GROUP_ARG} --tag "$NPM_DIST_TAG " --provenance --yes --verbose --dry-run
311+ npx nx release publish --projects "${{ steps.taginfo.outputs.project }}" --tag "${{ steps.taginfo.outputs.dist_tag }} " --access public --verbose
135312
136313 - name : Summary
137314 if : always()
138315 run : |
139316 echo "Nx Release completed."
140- echo "- dist-tag: $NPM_DIST_TAG"
141- echo "- release-group: '${{ inputs['release-group'] }}'"
142- echo "- dry-run: ${{ inputs['dry-run'] }}"
317+ echo "- mode: ${{ steps.ctx.outputs.mode }}"
318+ echo "- dist-tag: ${{ steps.ctx.outputs.mode == 'tag' && steps.taginfo.outputs.dist_tag || steps.ctx.outputs.dist_tag }}"
319+ echo "- scope: '${{ steps.ctx.outputs.scope }}'"
320+ echo "- dry-run: ${{ steps.ctx.outputs.dry_run }}"
321+ echo "- use-token: ${{ vars.USE_NPM_TOKEN == 'true' }}"
0 commit comments