name: Publish to npm
# Manual recovery workflow. The primary release path is release.yml
# (triggered by a v* tag push). Use this only to re-run the npm publish
# in isolation when the main pipeline has partially failed.

on:
  workflow_dispatch:
    inputs:
      tag:
        description: "Release tag to publish (e.g. v0.9.5)"
        required: true
        type: string

concurrency:
  group: npm-publish-${{ inputs.tag }}
  cancel-in-progress: false

permissions:
  id-token: write
  contents: read

jobs:
  publish:
    name: Publish npm packages
    runs-on: ubuntu-latest
    timeout-minutes: 15

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "24"

      - name: Configure npm registry
        run: npm config set registry https://registry.npmjs.org

      - name: Install jq
        run: sudo apt-get install -y jq

      - name: Determine version
        id: version
        env:
          INPUT_TAG: ${{ inputs.tag }}
          RELEASE_TAG: ${{ github.event.release.tag_name }}
        run: |
          if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then
            TAG="$INPUT_TAG"
          else
            TAG="$RELEASE_TAG"
          fi

          # Validate tag format
          if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
            echo "::error::Invalid tag format: $TAG (expected vX.Y.Z)"
            exit 1
          fi

          VERSION="${TAG#v}"
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"
          echo "Publishing version: $VERSION (tag: $TAG)"

      - name: Publish to npm
        env:
          PUBLISH_VERSION: ${{ steps.version.outputs.version }}
          PUBLISH_TAG: ${{ steps.version.outputs.tag }}
        run: |
          npm/scripts/publish.sh \
            --version "$PUBLISH_VERSION" \
            --release-url "https://github.com/nubo-db/dynoxide/releases/download/$PUBLISH_TAG"

  # The browser engine package (@dynoxide/wasm-engine) is built from source
  # here, since wasm is architecture-independent. It rides its own preview track:
  # the wasm path is not conformance-tested. The `-preview` version string, not a
  # dist-tag, marks it a preview; it publishes to `latest` (npm's mandatory tag)
  # like a normal release. Published via OIDC trusted publishing (no token), so its
  # trusted publisher must be configured on npmjs.com first; the very first publish
  # is bootstrapped manually, and the idempotent guard below then skips that
  # already-published version. This job runs independently of the native `publish`
  # job (no `needs:`): the engine and CLI packages are separate artefacts, each
  # guarded by its own idempotent skip, so one failing neither blocks nor rolls
  # back the other - a re-dispatch re-publishes only what is still missing.
  publish-engine:
    name: Publish @dynoxide/wasm-engine
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      # Build from the tagged commit, not main HEAD: the dispatch runs on main,
      # but the published wasm must match the release tag (the native publish job
      # gets this for free by downloading the tagged release assets).
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag }}

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: wasm32-unknown-unknown
      - uses: Swatinem/rust-cache@v2

      # wasm-pack drives the wasm-bindgen build inside scripts/build-wasm.sh.
      - name: Install wasm-pack
        uses: taiki-e/install-action@v2
        with:
          tool: wasm-pack

      - uses: actions/setup-node@v4
        with:
          node-version: "24"

      # setup-node's registry-url writes an .npmrc auth line that breaks OIDC, so
      # set the registry directly instead.
      - name: Configure npm registry
        run: npm config set registry https://registry.npmjs.org

      - name: Determine version
        id: version
        env:
          INPUT_TAG: ${{ inputs.tag }}
        run: |
          TAG="$INPUT_TAG"
          if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
            echo "::error::Invalid tag format: $TAG (expected vX.Y.Z)"
            exit 1
          fi
          VERSION="${TAG#v}"
          # The engine rides its own preview track, versioned independently of the
          # crate: it always ships as <crate-base>-preview, never a plain stable
          # version. BASE strips any prerelease suffix off the tag, so a stable
          # v0.11.0 tag yields an engine version of 0.11.0-preview.
          BASE="${VERSION%%-*}"
          # Defence in depth: the tag regex above already guarantees a non-empty
          # version, but assert it before these values reach the skip check and the
          # publish. An empty engine version would make the `npm view ...@` probe
          # match the whole package and silently skip the publish.
          if [ -z "$VERSION" ] || [ -z "$BASE" ]; then
            echo "::error::could not derive a version from tag '$TAG'"
            exit 1
          fi
          {
            echo "version=$VERSION"
            echo "engine_version=${BASE}-preview"
          } >> "$GITHUB_OUTPUT"

      # Idempotent: skip if this version is already on npm (the manual bootstrap
      # publish, or a re-run) rather than fail on a duplicate.
      - name: Skip if this version is already published
        id: published
        env:
          ENGINE_VERSION: ${{ steps.version.outputs.engine_version }}
        run: |
          if npm view "@dynoxide/wasm-engine@$ENGINE_VERSION" version >/dev/null 2>&1; then
            echo "::notice::@dynoxide/wasm-engine@$ENGINE_VERSION already published; skipping."
            echo "skip=true" >> "$GITHUB_OUTPUT"
          else
            echo "skip=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Install npm dependencies
        if: steps.published.outputs.skip == 'false'
        run: npm ci

      - name: Build the engine package
        if: steps.published.outputs.skip == 'false'
        run: npm run build:wasm

      - name: Stamp the package version from the tag
        if: steps.published.outputs.skip == 'false'
        working-directory: npm/wasm-engine
        env:
          ENGINE_VERSION: ${{ steps.version.outputs.engine_version }}
        run: npm version "$ENGINE_VERSION" --no-git-tag-version --allow-same-version

      - name: Verify the engine version matches the release
        if: steps.published.outputs.skip == 'false'
        env:
          VERSION: ${{ steps.version.outputs.version }}
        run: |
          # build-wasm.sh stamps manifest.engineVersion from Cargo.toml verbatim,
          # which release.yml validate has already pinned to the full tag version,
          # so the manifest must equal VERSION. A mismatch means Cargo.toml was not
          # bumped to the release version. The published package layers the -preview
          # suffix on top of this (see the stamp step); the manifest tracks the
          # crate version, which is what this guards.
          MANIFEST_VERSION=$(node -p "require('./npm/wasm-engine/manifest.json').engineVersion")
          if [ "$MANIFEST_VERSION" != "$VERSION" ]; then
            echo "::error::engine manifest version ($MANIFEST_VERSION) != release version ($VERSION); is Cargo.toml bumped to $VERSION?"
            exit 1
          fi

      # Publish to `latest`. The `-preview` version string is the only preview
      # marker; OIDC publishing can only set the tag it publishes to, so we do not
      # maintain a separate `preview` dist-tag that would drift from `latest`, and
      # a normal `npm install @dynoxide/wasm-engine` gets the current preview. The
      # tag is passed explicitly because npm refuses a bare publish of a prerelease
      # version ("You must specify a tag using --tag"); it does not default one.
      # When the wasm path graduates (conformance-tested), drop the `-preview`
      # suffix in "Determine version" and this still lands on `latest`.
      - name: Publish (dry-run)
        if: steps.published.outputs.skip == 'false'
        working-directory: npm/wasm-engine
        run: npm publish --dry-run --access public --provenance --tag latest

      - name: Publish to npm
        if: steps.published.outputs.skip == 'false'
        working-directory: npm/wasm-engine
        run: npm publish --access public --provenance --tag latest
