name: Nx release one package
description: >
  Version (conventional commits), generate changelog, commit, push, build,
  publish to npm with provenance, tag (only after successful publish), and
  verify registry propagation — for a single Nx-managed package.
  Caller owns checkout, pnpm/Node setup, and git config.

inputs:
  project:
    description: Nx project name (e.g. sdk, cli, adapters-connector).
    required: true
  package_json_path:
    description: Workspace-relative path to the package's package.json (e.g. libs/sdk/package.json).
    required: true
  dist_path:
    description: Workspace-relative path to the publishable dist directory (e.g. dist/libs/sdk).
    required: true
  dist_tag:
    description: npm dist-tag for the publish (e.g. latest, alpha, beta).
    required: true
    default: latest
  npm_access:
    description: npm publish access level (public or restricted).
    required: true
    default: public
  npm_token:
    description: npm automation token (NODE_AUTH_TOKEN). Pass via secrets.NPM_TOKEN.
    required: true
  first_release:
    description: Set to 'true' only for the very first Nx-managed release of this package.
    required: false
    default: 'false'
  specifier:
    description: Nx version specifier override. Leave empty or 'auto' for conventional commits.
    required: false
    default: auto
  preid:
    description: Prerelease identifier appended to bumped version (e.g. beta, rc). Empty for stable.
    required: false
    default: ''
  git_ref:
    description: Branch ref to push the release commit to (e.g. main).
    required: true
    default: main

outputs:
  bumped:
    description: "'true' if a publish is needed (newly versioned or prior publish failed); 'false' if version is current on npm."
    value: ${{ steps.version.outputs.bumped }}
  version:
    description: The resolved package version after versioning (unchanged if bumped=false).
    value: ${{ steps.version.outputs.version }}
  tag:
    description: The git tag created after successful publish (e.g. sdk@1.2.3).
    value: ${{ steps.version.outputs.tag }}

runs:
  using: composite
  steps:
    - name: Version package
      id: version
      shell: bash
      run: |
        set -euo pipefail
        before="$(node -p "require('./${{ inputs.package_json_path }}').version")"
        # Capture the most recent tag for this project before bumping so we can
        # pass --from to the changelog step. Nx changelog needs an explicit lower
        # bound when the new tag doesn't exist yet (it's created after publish).
        prev_tag="$(git tag --list '${{ inputs.project }}@*' --sort=-version:refname | head -1)"

        mapfile -t args < <(bash tools/ci-release/nx-release-version-args.sh \
          --project '${{ inputs.project }}' \
          --specifier '${{ inputs.specifier }}' \
          --preid '${{ inputs.preid }}' \
          --first-release '${{ inputs.first_release }}')
        pnpm nx "${args[@]}"

        after="$(node -p "require('./${{ inputs.package_json_path }}').version")"
        tag="${{ inputs.project }}@${after}"
        pkg_name="$(node -p "require('./${{ inputs.package_json_path }}').name")"

        {
          echo "version=$after"
          echo "tag=$tag"
          echo "prev_tag=${prev_tag}"
        } >> "$GITHUB_OUTPUT"

        if [[ "$before" != "$after" ]]; then
          echo "bumped=true"         >> "$GITHUB_OUTPUT"
          echo "needs_commit=true"   >> "$GITHUB_OUTPUT"
          echo "::notice::${{ inputs.project }} bumped $before → $after"
        elif npm view "${pkg_name}@${after}" version --json >/dev/null 2>&1; then
          echo "bumped=false"        >> "$GITHUB_OUTPUT"
          echo "needs_commit=false"  >> "$GITHUB_OUTPUT"
          echo "::notice::${{ inputs.project }} — version unchanged at $after and already on npm. Skipping."
        else
          # Version committed to git but npm publish never completed; re-publish without re-committing.
          echo "bumped=true"         >> "$GITHUB_OUTPUT"
          echo "needs_commit=false"  >> "$GITHUB_OUTPUT"
          echo "::notice::${{ inputs.project }} — $after missing from npm (prior publish failed). Re-publishing."
        fi

    - name: Generate changelog
      if: steps.version.outputs.needs_commit == 'true'
      shell: bash
      run: |
        set -euo pipefail
        args=(release changelog '${{ steps.version.outputs.version }}' --projects=${{ inputs.project }})
        if [[ '${{ inputs.first_release }}' == 'true' ]]; then
          args+=(--first-release)
        elif [[ -n '${{ steps.version.outputs.prev_tag }}' ]]; then
          # The new tag doesn't exist yet (created only after successful publish),
          # so Nx can't auto-resolve the lower bound. Pass the previous tag explicitly.
          args+=(--from '${{ steps.version.outputs.prev_tag }}')
        fi
        pnpm nx "${args[@]}"

    - name: Commit and push branch
      if: steps.version.outputs.needs_commit == 'true'
      shell: bash
      run: |
        set -euo pipefail
        pkg_json="${{ inputs.package_json_path }}"
        changelog="${pkg_json%/package.json}/CHANGELOG.md"
        tag='${{ steps.version.outputs.tag }}'

        git add "$pkg_json"
        [ -f "$changelog" ] && git add "$changelog" || true
        if git diff --cached --quiet; then
          echo "::error::Nothing staged after version+changelog for ${{ inputs.project }} — unexpected state." >&2
          exit 1
        fi

        git commit -m "chore(release): $tag [skip ci]"

        # Rebase onto the latest remote state before pushing.
        # Another workflow (e.g. contributors sync) may have pushed commits to
        # main between this job's checkout and now, causing a non-fast-forward
        # rejection. A rebase is safe here: the only conflicts possible are in
        # package.json/CHANGELOG.md of OTHER packages, which is very unlikely,
        # and if it does happen git rebase will abort and the error is explicit.
        for attempt in 1 2 3; do
          git fetch origin "${{ inputs.git_ref }}"
          if git rebase "origin/${{ inputs.git_ref }}"; then
            git push origin "HEAD:${{ inputs.git_ref }}" && break
          else
            git rebase --abort
            echo "::error::Rebase failed — conflicting changes on ${{ inputs.git_ref }} for ${{ inputs.project }}." >&2
            exit 1
          fi
          if [[ $attempt -eq 3 ]]; then
            echo "::error::Failed to push $tag after 3 attempts." >&2
            exit 1
          fi
          echo "Push attempt $attempt failed, retrying in $((attempt * 5))s..."
          sleep $((attempt * 5))
        done

    - name: Build
      if: steps.version.outputs.bumped == 'true'
      shell: bash
      run: pnpm nx build ${{ inputs.project }}

    - name: Publish to npm
      if: steps.version.outputs.bumped == 'true'
      shell: bash
      env:
        NODE_AUTH_TOKEN: ${{ inputs.npm_token }}
      run: |
        npm publish ${{ inputs.dist_path }} \
          --access ${{ inputs.npm_access }} \
          --tag ${{ inputs.dist_tag }}

    - name: Tag release
      if: steps.version.outputs.bumped == 'true'
      shell: bash
      run: |
        set -euo pipefail
        tag='${{ steps.version.outputs.tag }}'
        if git rev-parse "$tag" >/dev/null 2>&1; then
          echo "::warning::Tag $tag already exists — skipping tag creation."
        else
          git tag -a "$tag" -m "$tag"
          git push origin "$tag"
          echo "::notice::Tagged and pushed: $tag"
        fi

    - name: Verify registry propagation
      if: steps.version.outputs.bumped == 'true'
      shell: bash
      run: |
        set -euo pipefail
        pkg_name="$(node -p "require('./${{ inputs.package_json_path }}').name")"
        version='${{ steps.version.outputs.version }}'
        for attempt in 1 2 3 4 5; do
          npm view "${pkg_name}@${version}" version --json > /dev/null 2>&1 && break
          echo "Attempt $attempt: ${pkg_name}@${version} not yet propagated, waiting..."
          sleep $((attempt * 10))
        done
        echo "Verified: ${pkg_name}@${version}"
