chore(release): publish #1
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: Release Workflow | |
| on: | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| # Matches the Nx releaseTag pattern in nx.json: "{version}-{projectName}" | |
| - '*-*' | |
| workflow_dispatch: | |
| inputs: | |
| dist-tag: | |
| description: "npm dist-tag to use (e.g. latest | next | canary)" | |
| required: false | |
| type: string | |
| default: next | |
| dry-run: | |
| description: "Run release steps without making changes (no git push, no publish)" | |
| required: false | |
| type: boolean | |
| default: false | |
| release-group: | |
| description: "Optional Nx project pattern to scope the release (empty = default behavior)" | |
| required: false | |
| type: string | |
| default: "" | |
| concurrency: | |
| # Avoid overlapping publishes on the same ref/branch | |
| group: nx-release-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write # needed to push version commits and tags | |
| id-token: write # required for npm provenance / trusted publishing (OIDC) | |
| jobs: | |
| release: | |
| name: Version and Publish (gated by environment) | |
| if: ${{ github.actor != 'github-actions[bot]' }} | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: ${{ (github.event_name == 'workflow_dispatch' && inputs.dry-run) && 'npm-publish-dry-run' || 'npm-publish' }} | |
| env: | |
| # Optional: provide Nx Cloud token if used in this repo | |
| NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} | |
| steps: | |
| - name: Harden the runner (Audit all outbound calls) | |
| uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout repository (full history for tagging) | |
| uses: actions/checkout@v6.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '24' | |
| registry-url: 'https://registry.npmjs.org' | |
| cache: 'npm' | |
| - name: Update npm (required for OIDC trusted publishing) | |
| run: | | |
| npm install -g npm@^11.5.1 | |
| npm --version | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Repo setup | |
| run: npm run setup | |
| - name: Resolve release context | |
| id: ctx | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| dist_tag="${{ inputs['dist-tag'] }}" | |
| scope="${{ inputs['release-group'] }}" | |
| dry_run="${{ inputs['dry-run'] }}" | |
| mode="dispatch" | |
| elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| dist_tag="" | |
| scope="" | |
| dry_run="false" | |
| mode="tag" | |
| else | |
| dist_tag="next" | |
| scope="" | |
| dry_run="false" | |
| mode="main" | |
| fi | |
| echo "mode=${mode}" >> "$GITHUB_OUTPUT" | |
| echo "dist_tag=${dist_tag}" >> "$GITHUB_OUTPUT" | |
| echo "scope=${scope}" >> "$GITHUB_OUTPUT" | |
| echo "dry_run=${dry_run}" >> "$GITHUB_OUTPUT" | |
| - name: Determine affected release projects (main) | |
| id: affected | |
| if: ${{ steps.ctx.outputs.mode == 'main' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| base='${{ github.event.before }}' | |
| head='${{ github.sha }}' | |
| # Handle edge cases where base commit doesn't exist (first push, force-push, etc.) | |
| # Use HEAD~1 as fallback, or just compare against HEAD if no parent exists | |
| if [[ "$base" == "0000000000000000000000000000000000000000" ]] || ! git cat-file -e "$base" 2>/dev/null; then | |
| echo "Base commit not available, falling back to HEAD~1" | |
| base="HEAD~1" | |
| # If HEAD~1 doesn't exist (first commit), use empty tree | |
| if ! git cat-file -e "$base" 2>/dev/null; then | |
| base="$(git hash-object -t tree /dev/null)" | |
| fi | |
| fi | |
| # Only consider libs under packages/* and exclude items configured as non-releaseable. | |
| 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) | |
| 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(","));});') | |
| 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));});') | |
| echo "projects=${affected_list}" >> "$GITHUB_OUTPUT" | |
| echo "count=${affected_count}" >> "$GITHUB_OUTPUT" | |
| - name: Determine tag release project and dist-tag (tags) | |
| id: taginfo | |
| if: ${{ steps.ctx.outputs.mode == 'tag' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| tag_name="${GITHUB_REF_NAME}" | |
| # Find the project by matching the tag suffix against known releaseable packages. | |
| projects=$(npx nx show projects --projects "packages/*" --type lib --exclude "ui-mobile-base,types-minimal,winter-tc,types,types-ios,types-android" --sep ' ') | |
| best_match="" | |
| best_len=0 | |
| for p in $projects; do | |
| suffix="-${p}" | |
| if [[ "$tag_name" == *"$suffix" ]]; then | |
| if (( ${#p} > best_len )); then | |
| best_match="$p" | |
| best_len=${#p} | |
| fi | |
| fi | |
| done | |
| if [[ -z "$best_match" ]]; then | |
| echo "Could not determine project from tag '$tag_name'. Expected '{version}-{projectName}'." >&2 | |
| exit 1 | |
| fi | |
| version_part="${tag_name%-$best_match}" | |
| if [[ "$version_part" == *-* ]]; then | |
| dist_tag="next" | |
| else | |
| dist_tag="latest" | |
| fi | |
| echo "project=${best_match}" >> "$GITHUB_OUTPUT" | |
| echo "version=${version_part}" >> "$GITHUB_OUTPUT" | |
| echo "dist_tag=${dist_tag}" >> "$GITHUB_OUTPUT" | |
| - name: Configure git user for automated commits | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # VERSION: updates versions and creates git tags following nx.json releaseTag.pattern. | |
| - name: nx release version (main) | |
| if: ${{ steps.ctx.outputs.mode == 'main' && steps.affected.outputs.count != '0' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| npx nx release version prerelease \ | |
| --preid next \ | |
| --projects "${{ steps.affected.outputs.projects }}" \ | |
| --git-commit \ | |
| --git-push \ | |
| --verbose | |
| - name: nx release version (main, no-op) | |
| if: ${{ steps.ctx.outputs.mode == 'main' && steps.affected.outputs.count == '0' }} | |
| run: echo "No affected release projects on main; skipping version + publish." | |
| - name: nx release version (dispatch) | |
| if: ${{ steps.ctx.outputs.mode == 'dispatch' && !inputs.dry-run }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| scope="${{ steps.ctx.outputs.scope }}" | |
| if [[ -n "$scope" ]]; then | |
| projects_arg=(--projects "$scope") | |
| else | |
| projects_arg=() | |
| fi | |
| npx nx release version prerelease \ | |
| --preid "${{ steps.ctx.outputs.dist_tag }}" \ | |
| "${projects_arg[@]}" \ | |
| --git-commit \ | |
| --git-push \ | |
| --verbose | |
| - name: nx release version (dispatch, dry-run) | |
| if: ${{ steps.ctx.outputs.mode == 'dispatch' && inputs.dry-run }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| scope="${{ steps.ctx.outputs.scope }}" | |
| if [[ -n "$scope" ]]; then | |
| projects_arg=(--projects "$scope") | |
| else | |
| projects_arg=() | |
| fi | |
| npx nx release version prerelease \ | |
| --preid "${{ steps.ctx.outputs.dist_tag }}" \ | |
| "${projects_arg[@]}" \ | |
| --verbose \ | |
| --dry-run | |
| # BUILD: Ensure projects are built before publishing | |
| - name: Build affected projects (main) | |
| if: ${{ steps.ctx.outputs.mode == 'main' && steps.affected.outputs.count != '0' }} | |
| run: npx nx run-many -t build --projects "${{ steps.affected.outputs.projects }}" --verbose | |
| - name: Build projects (dispatch) | |
| if: ${{ steps.ctx.outputs.mode == 'dispatch' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| scope="${{ steps.ctx.outputs.scope }}" | |
| if [[ -n "$scope" ]]; then | |
| npx nx run-many -t build --projects "$scope" --verbose | |
| else | |
| npx nx run-many -t build --all --verbose | |
| fi | |
| # PUBLISH: OIDC trusted publishing (default). Avoid any lingering token auth. | |
| - name: nx release publish (OIDC, main) | |
| if: ${{ steps.ctx.outputs.mode == 'main' && steps.affected.outputs.count != '0' && vars.USE_NPM_TOKEN != 'true' }} | |
| shell: bash | |
| env: | |
| NPM_CONFIG_PROVENANCE: true | |
| NODE_AUTH_TOKEN: "" | |
| run: | | |
| set -euo pipefail | |
| unset NODE_AUTH_TOKEN | |
| rm -f ~/.npmrc || true | |
| if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then | |
| rm -f "$NPM_CONFIG_USERCONFIG" || true | |
| fi | |
| npx nx release publish \ | |
| --projects "${{ steps.affected.outputs.projects }}" \ | |
| --tag "${{ steps.ctx.outputs.dist_tag }}" \ | |
| --access public \ | |
| --verbose | |
| - name: nx release publish (OIDC, dispatch) | |
| if: ${{ steps.ctx.outputs.mode == 'dispatch' && steps.ctx.outputs.dry_run != 'true' && vars.USE_NPM_TOKEN != 'true' }} | |
| shell: bash | |
| env: | |
| NPM_CONFIG_PROVENANCE: true | |
| NODE_AUTH_TOKEN: "" | |
| run: | | |
| set -euo pipefail | |
| unset NODE_AUTH_TOKEN | |
| rm -f ~/.npmrc || true | |
| if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then | |
| rm -f "$NPM_CONFIG_USERCONFIG" || true | |
| fi | |
| scope="${{ steps.ctx.outputs.scope }}" | |
| if [[ -n "$scope" ]]; then | |
| projects_arg="--projects $scope" | |
| else | |
| projects_arg="" | |
| fi | |
| npx nx release publish \ | |
| $projects_arg \ | |
| --tag "${{ steps.ctx.outputs.dist_tag }}" \ | |
| --access public \ | |
| --verbose | |
| - name: nx release publish (OIDC, dispatch dry-run) | |
| if: ${{ steps.ctx.outputs.mode == 'dispatch' && inputs.dry-run && vars.USE_NPM_TOKEN != 'true' }} | |
| shell: bash | |
| env: | |
| NPM_CONFIG_PROVENANCE: true | |
| NODE_AUTH_TOKEN: "" | |
| run: | | |
| set -euo pipefail | |
| unset NODE_AUTH_TOKEN | |
| rm -f ~/.npmrc || true | |
| if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then | |
| rm -f "$NPM_CONFIG_USERCONFIG" || true | |
| fi | |
| scope="${{ steps.ctx.outputs.scope }}" | |
| if [[ -n "$scope" ]]; then | |
| projects_arg="--projects $scope" | |
| else | |
| projects_arg="" | |
| fi | |
| npx nx release publish \ | |
| $projects_arg \ | |
| --tag "${{ steps.ctx.outputs.dist_tag }}" \ | |
| --access public \ | |
| --verbose \ | |
| --dry-run | |
| # PUBLISH: token fallback (only when explicitly enabled via repo/environment variable USE_NPM_TOKEN=true). | |
| - name: nx release publish (token, main) | |
| if: ${{ steps.ctx.outputs.mode == 'main' && steps.affected.outputs.count != '0' && vars.USE_NPM_TOKEN == 'true' }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: true | |
| run: | | |
| npx nx release publish --projects "${{ steps.affected.outputs.projects }}" --tag "${{ steps.ctx.outputs.dist_tag }}" --access public --verbose | |
| - name: nx release publish (token, dispatch) | |
| if: ${{ steps.ctx.outputs.mode == 'dispatch' && steps.ctx.outputs.dry_run != 'true' && vars.USE_NPM_TOKEN == 'true' }} | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: true | |
| run: | | |
| set -euo pipefail | |
| scope="${{ steps.ctx.outputs.scope }}" | |
| if [[ -n "$scope" ]]; then | |
| projects_arg="--projects $scope" | |
| else | |
| projects_arg="" | |
| fi | |
| npx nx release publish $projects_arg --tag "${{ steps.ctx.outputs.dist_tag }}" --access public --verbose | |
| - name: nx release publish (token, dispatch dry-run) | |
| if: ${{ steps.ctx.outputs.mode == 'dispatch' && inputs.dry-run && vars.USE_NPM_TOKEN == 'true' }} | |
| shell: bash | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: true | |
| run: | | |
| set -euo pipefail | |
| scope="${{ steps.ctx.outputs.scope }}" | |
| if [[ -n "$scope" ]]; then | |
| projects_arg="--projects $scope" | |
| else | |
| projects_arg="" | |
| fi | |
| npx nx release publish $projects_arg --tag "${{ steps.ctx.outputs.dist_tag }}" --access public --verbose --dry-run | |
| # Tag-triggered publishing: publish the single package referenced by the tag. | |
| - name: Build project before publish (tag) | |
| if: ${{ steps.ctx.outputs.mode == 'tag' }} | |
| run: npx nx build "${{ steps.taginfo.outputs.project }}" --verbose | |
| - name: nx release publish (tag) | |
| if: ${{ steps.ctx.outputs.mode == 'tag' && vars.USE_NPM_TOKEN != 'true' }} | |
| shell: bash | |
| env: | |
| NPM_CONFIG_PROVENANCE: true | |
| NODE_AUTH_TOKEN: "" | |
| run: | | |
| set -euo pipefail | |
| unset NODE_AUTH_TOKEN | |
| rm -f ~/.npmrc || true | |
| if [[ -n "${NPM_CONFIG_USERCONFIG:-}" ]]; then | |
| rm -f "$NPM_CONFIG_USERCONFIG" || true | |
| fi | |
| npx nx release publish \ | |
| --projects "${{ steps.taginfo.outputs.project }}" \ | |
| --tag "${{ steps.taginfo.outputs.dist_tag }}" \ | |
| --access public \ | |
| --verbose | |
| - name: nx release publish (tag, token) | |
| if: ${{ steps.ctx.outputs.mode == 'tag' && vars.USE_NPM_TOKEN == 'true' }} | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} | |
| NPM_CONFIG_PROVENANCE: true | |
| run: | | |
| npx nx release publish --projects "${{ steps.taginfo.outputs.project }}" --tag "${{ steps.taginfo.outputs.dist_tag }}" --access public --verbose | |
| - name: Summary | |
| if: always() | |
| run: | | |
| echo "Nx Release completed." | |
| echo "- mode: ${{ steps.ctx.outputs.mode }}" | |
| echo "- dist-tag: ${{ steps.ctx.outputs.mode == 'tag' && steps.taginfo.outputs.dist_tag || steps.ctx.outputs.dist_tag }}" | |
| echo "- scope: '${{ steps.ctx.outputs.scope }}'" | |
| echo "- dry-run: ${{ steps.ctx.outputs.dry_run }}" | |
| echo "- use-token: ${{ vars.USE_NPM_TOKEN == 'true' }}" |