fiscal 0.7.0

Brazilian fiscal document library (NF-e/NFC-e) — Rust port of sped-nfe
Documentation
name: Publish @fiscal-rs/node to npm

# Publica via Trusted Publishing (OIDC) — sem token de longa duração.
#
# MANUTENÇÃO (o que pode quebrar a publicação):
#   1. Renomear este arquivo: o npm confia no caminho "napi-publish.yml".
#      Se renomear, atualize o trusted publisher de cada pacote em npmjs.com.
#   2. Mover o repo: a confiança aponta para JoaoHenriqueBarbosa/fiscal-rs.
#      Repo novo = reconfigurar os publishers.
#   3. Adicionar um pacote de plataforma novo: a confiança é por pacote.
#      Cada @fiscal-rs/node-* precisa do publisher configurado na UI do npm,
#      senão só ele falha com erro de permissão.
#
# Dispara em todo push na master. O job check-version compara a maior versão
# entre os crates com a do npm e só publica se mudou.
#
# Por que push:master e não a tag do release: o release-plz cria as tags
# (fiscal-core-v*) autenticado com GITHUB_TOKEN, e o GitHub NÃO dispara
# workflows por push de bot com esse token (proteção anti-loop). Então um
# gatilho on:push:tags nunca rodaria após um release. O merge do PR de
# release, porém, é um push na master que dispara normalmente.
on:
  push:
    branches:
      - master

permissions:
  contents: read

env:
  MACOSX_DEPLOYMENT_TARGET: '10.13'

jobs:
  check-version:
    name: Check if npm publish needed
    runs-on: ubuntu-latest
    outputs:
      should_publish: ${{ steps.check.outputs.should_publish }}
      version: ${{ steps.check.outputs.version }}
    steps:
      - uses: actions/checkout@v7

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      - name: Determine version from dependency crates
        id: check
        run: |
          CORE=$(grep '^version' crates/fiscal-core/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          SEFAZ=$(grep '^version' crates/fiscal-sefaz/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          CRYPTO=$(grep '^version' crates/fiscal-crypto/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')
          FACADE=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')

          echo "fiscal-core: $CORE | fiscal-sefaz: $SEFAZ | fiscal-crypto: $CRYPTO | fiscal: $FACADE"

          VERSION=$(printf '%s\n' "$CORE" "$SEFAZ" "$CRYPTO" "$FACADE" | sort -V | tail -1)
          echo "Resolved npm version: $VERSION"

          NPM_VERSION=$(curl -s https://registry.npmjs.org/@fiscal-rs/node/latest | jq -r '.version // "0.0.0"')
          echo "Current npm version: $NPM_VERSION"

          if [ "$VERSION" != "$NPM_VERSION" ]; then
            echo "should_publish=true" >> $GITHUB_OUTPUT
          else
            echo "should_publish=false" >> $GITHUB_OUTPUT
          fi
          echo "version=$VERSION" >> $GITHUB_OUTPUT

  build:
    name: Build ${{ matrix.target }}
    needs: check-version
    if: needs.check-version.outputs.should_publish == 'true'
    runs-on: ${{ matrix.os }}
    defaults:
      run:
        working-directory: crates/fiscal-napi
    strategy:
      fail-fast: false
      matrix:
        include:
          # Native builds
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          - target: aarch64-apple-darwin
            os: macos-latest
          # macOS x86_64 cross-compiled from ARM64 runner
          - target: x86_64-apple-darwin
            os: macos-latest
            cross: true
          # Native Windows build
          - target: x86_64-pc-windows-msvc
            os: windows-latest
          # Cross-compiled Linux builds (use --use-napi-cross)
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            cross: true
          # Cross-compiled Windows ARM64
          - target: aarch64-pc-windows-msvc
            os: windows-latest
            cross: true

    steps:
      - uses: actions/checkout@v7

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install dependencies
        run: bun install

      # Build: use --use-napi-cross for cross targets, plain build for native
      # OpenSSL is vendored (compiled from source) so no system dependency needed
      - name: Build native addon
        if: ${{ !matrix.cross }}
        run: bunx napi build --release --platform --target ${{ matrix.target }}

      - name: Build native addon (cross)
        if: ${{ matrix.cross }}
        run: bunx napi build --release --platform --target ${{ matrix.target }} --use-napi-cross

      # Sobe o binário nativo E o wrapper JS gerado (index.js/index.d.ts).
      # O wrapper é idêntico entre targets, mas o pacote main precisa dele:
      # sem ele o npm publica só o package.json e require() falha.
      - name: Upload artifact
        uses: actions/upload-artifact@v7
        with:
          name: bindings-${{ matrix.target }}
          path: |
            crates/fiscal-napi/fiscal.*.node
            crates/fiscal-napi/index.js
            crates/fiscal-napi/index.d.ts
          if-no-files-found: error

  publish:
    name: Publish to npm
    runs-on: ubuntu-latest
    needs: [check-version, build]
    permissions:
      id-token: write # OIDC: mina o token efêmero do trusted publishing
      contents: read
    steps:
      - uses: actions/checkout@v7

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      # setup-node configura o registry-url que o OIDC usa e garante Node 22+.
      - name: Setup Node
        uses: actions/setup-node@v6
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'

      # Trusted publishing exige npm >= 11.5.1; o que vem no Node 22 é mais antigo.
      - name: Upgrade npm for OIDC support
        run: npm install -g npm@latest

      - name: Install dependencies
        working-directory: crates/fiscal-napi
        run: bun install

      - name: Sync version to package.json
        working-directory: crates/fiscal-napi
        run: |
          VERSION=${{ needs.check-version.outputs.version }}
          jq --arg v "$VERSION" '.version = $v | .optionalDependencies |= with_entries(.value = $v)' package.json > package.json.tmp && mv package.json.tmp package.json
          for pkg in npm/*/package.json; do
            jq --arg v "$VERSION" '.version = $v' "$pkg" > "$pkg.tmp" && mv "$pkg.tmp" "$pkg"
          done

      - name: Download all artifacts
        uses: actions/download-artifact@v8
        with:
          path: crates/fiscal-napi/artifacts

      # Recupera o wrapper JS de um dos artefatos para a raiz do pacote main,
      # ANTES de `napi artifacts` (que consome os subdirs). O npm publica o
      # pacote main a partir daqui e `files` exige index.js/index.d.ts; sem
      # esta cópia o pacote sai sem entrypoint e require() falha.
      - name: Restore JS binding wrapper for main package
        working-directory: crates/fiscal-napi
        run: |
          src=$(ls -d artifacts/bindings-*/ | head -1)
          cp "${src}index.js" "${src}index.d.ts" .
          test -f index.js && test -f index.d.ts
          echo "wrapper restaurado de $src"

      - name: Move artifacts to npm dirs
        working-directory: crates/fiscal-napi
        run: bunx napi artifacts --output-dir artifacts

      - name: List packages
        working-directory: crates/fiscal-napi
        run: |
          echo "Version: ${{ needs.check-version.outputs.version }}"
          echo "main package files: $(ls index.js index.d.ts 2>/dev/null | tr '\n' ' ')"
          for d in npm/*/; do
            echo "$d: $(ls "$d"fiscal.*.node 2>/dev/null | wc -l) binary"
          done

      - name: Prepare platform packages
        working-directory: crates/fiscal-napi
        run: bunx napi prepublish --no-gh-release --skip-optional-publish -t npm

      # Publica cada platform package. Tolera apenas "já publicado"
      # (idempotência em rerun); qualquer outro erro — auth, permissão — falha.
      - name: Publish platform packages
        working-directory: crates/fiscal-napi
        run: |
          for pkg in npm/*/; do
            ( cd "$pkg"
              out=$(npm publish --access public 2>&1) || {
                echo "$out"
                echo "$out" | grep -qiE "cannot publish over|previously published|EPUBLISHCONFLICT" || exit 1
                echo "já publicado, ignorando"
              }
            )
          done

      - name: Publish main package
        working-directory: crates/fiscal-napi
        run: npm publish --access public