node-app-build 6.1.1

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
name: Build and Release node-app .deb (native cdylib)

# Standalone-repo CI workflow for a native-cdylib node-app-* package.
# The app is a `.so` installed at /usr/lib/node/apps/{{name}}/ and loaded
# in-process by node-server's native_loader — NOT a standalone systemd
# service.
#
# Three jobs, in order:
#   1. prepare-release — resolves the version, creates the tag + GitHub
#                        Release if missing. Runs once.
#   2. build-and-publish — matrix-builds the .deb per arch, GPG-signs,
#                          uploads to the release.
#   3. notify-aggregator — pings econ-v1/node-releases so `apt update`
#                          finds the new version. Runs once, after the
#                          matrix.
#
# Required org-level secrets (visibility=private):
#   GPG_PRIVATE_KEY, GPG_PASSPHRASE, APT_REPO_DISPATCH_TOKEN

on:
  push:
    tags:
      - 'v*'
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      version:
        description: "Version tag (defaults to manifest.version)"
        required: false
        type: string

permissions:
  contents: write

jobs:
  # ── Step 1: resolve version + ensure release exists ────────────────────────
  prepare-release:
    name: Resolve version + ensure GitHub Release exists
    runs-on: ubuntu-22.04
    outputs:
      version: ${{ steps.resolve.outputs.version }}
      name: ${{ steps.resolve.outputs.name }}
      tag: ${{ steps.resolve.outputs.tag }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Resolve version
        id: resolve
        run: |
          NAME=$(jq -r .name manifest.json)
          VERSION="${{ github.event.inputs.version }}"
          if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
            VERSION="${{ github.event.release.tag_name }}"
          fi
          if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
            if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
              VERSION="${{ github.ref_name }}"
            fi
          fi
          if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
            VERSION=$(jq -r .version manifest.json)
          fi
          VERSION="${VERSION#v}"
          TAG="v${VERSION}"
          echo "name=${NAME}" >> "$GITHUB_OUTPUT"
          echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
          echo "Resolved tag=${TAG} (name=${NAME})"

      - name: Create tag + release if missing
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          TAG="${{ steps.resolve.outputs.tag }}"
          if gh release view "$TAG" >/dev/null 2>&1; then
            echo "::notice::release $TAG already exists; will upload to it"
          else
            gh release create "$TAG" \
              --title "$TAG" \
              --notes "Release $TAG" \
              --target "${{ github.sha }}"
            echo "::notice::created release $TAG @ ${{ github.sha }}"
          fi

  # ── Step 2: matrix .deb build + GPG sign + upload to the release ───────────
  build-and-publish:
    name: Build node-app-${{ needs.prepare-release.outputs.name }} (${{ matrix.arch }})
    needs: prepare-release
    runs-on: ubuntu-22.04
    timeout-minutes: 60
    strategy:
      fail-fast: false
      matrix:
        arch: [amd64, arm64]
        include:
          - arch: amd64
            rust_target: x86_64-unknown-linux-gnu
          - arch: arm64
            rust_target: aarch64-unknown-linux-gnu

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Sync Cargo.toml + manifest.json to resolved version
        # cargo-deb reads Cargo.toml; keep both files in lockstep with the
        # resolved tag so the deb filename matches the aggregator's expectation.
        run: |
          V="${{ needs.prepare-release.outputs.version }}"
          sed -i "s/^version = .*/version = \"${V}\"/" Cargo.toml
          jq --arg v "$V" '.version=$v' manifest.json > manifest.json.tmp \
            && mv manifest.json.tmp manifest.json

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

      - name: Install cross-compile linker (arm64)
        if: matrix.arch == 'arm64'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu
          mkdir -p .cargo
          cat > .cargo/config.toml <<'EOF'
          [target.aarch64-unknown-linux-gnu]
          linker = "aarch64-linux-gnu-gcc"
          EOF
          # If your app pulls in any cc-rs-built native deps (e.g. rusqlite's
          # bundled SQLite), without these overrides cc would invoke host
          # x86_64 gcc and emit x86_64 objects.
          echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> "$GITHUB_ENV"
          echo "AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar" >> "$GITHUB_ENV"

      - name: Install cargo-deb
        run: cargo install cargo-deb --locked

      - name: Write LICENSE
        # cargo-deb's license-file points at LICENSE; ensure it exists.
        run: |
          if [ ! -f LICENSE ]; then
            cat > LICENSE <<'EOF'
          Licensed under either of

           * Apache License, Version 2.0
             (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
           * MIT license
             (LICENSE-MIT or http://opensource.org/licenses/MIT)

          at your option.
          EOF
          fi

      - name: Build .deb
        # cargo-deb runs `cargo build --release` for the target, then stages
        # files per [package.metadata.deb] assets, then invokes dpkg-deb.
        run: cargo deb --target ${{ matrix.rust_target }}

      - name: Rename .deb to aggregator-expected format
        # cargo-deb names files <crate>_<version>-1_<arch>.deb (debian
        # revision suffix). The aggregator expects no revision suffix:
        # node-app-<name>_<version>_<arch>.deb
        run: |
          SRC=$(find target/${{ matrix.rust_target }}/debian -maxdepth 1 -name '*.deb' | head -1)
          if [ -z "$SRC" ]; then
            echo "ERROR: no .deb produced by cargo deb" >&2
            ls -la target/${{ matrix.rust_target }}/debian/ || true
            exit 1
          fi
          DEST="node-app-${{ needs.prepare-release.outputs.name }}_${{ needs.prepare-release.outputs.version }}_${{ matrix.arch }}.deb"
          cp "$SRC" "$DEST"
          echo "DEB_NAME=${DEST}" >> "$GITHUB_ENV"
          echo "=== Built ==="
          ls -lh "$DEST"
          dpkg-deb -I "$DEST"
          echo "=== Contents ==="
          dpkg-deb -c "$DEST"

      - name: Sign .deb (GPG detached)
        env:
          GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
        run: |
          echo "$GPG_PRIVATE_KEY" | gpg --batch --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --import
          gpg --batch --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --detach-sign --armor "${DEB_NAME}"

      - name: Publish manifest sidecar
        env:
          GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
        run: |
          mkdir -p Manifests
          SIDECAR="Manifests/node-app-${{ needs.prepare-release.outputs.name }}_${{ needs.prepare-release.outputs.version }}.json"
          cp manifest.json "$SIDECAR"
          gpg --batch --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --detach-sign --armor "$SIDECAR"
          echo "SIDECAR=$SIDECAR" >> "$GITHUB_ENV"

      - name: Upload artifacts to GitHub Release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release upload "${{ needs.prepare-release.outputs.tag }}" \
            "${DEB_NAME}" "${DEB_NAME}.asc" \
            "${SIDECAR}" "${SIDECAR}.asc" \
            --clobber

  # ── Step 3: dispatch the apt-repo aggregator ───────────────────────────────
  notify-aggregator:
    name: Notify apt-repo indexer
    needs: [prepare-release, build-and-publish]
    runs-on: ubuntu-22.04
    steps:
      - name: Dispatch update-apt-repo.yml on econ-v1/node-releases
        env:
          GH_TOKEN: ${{ secrets.APT_REPO_DISPATCH_TOKEN }}
        run: |
          if [ -z "$GH_TOKEN" ]; then
            echo "::notice::APT_REPO_DISPATCH_TOKEN not set — skipping aggregator dispatch"
            exit 0
          fi
          TAG="${{ needs.prepare-release.outputs.tag }}"
          set +e
          for attempt in 1 2 3 4 5; do
            gh workflow run update-apt-repo.yml \
              --repo econ-v1/node-releases \
              --ref main \
              -F version="${TAG}" \
              -F source_repo="${{ github.repository }}"
            if [ $? -eq 0 ]; then
              echo "::notice::aggregator dispatched on attempt $attempt"
              exit 0
            fi
            echo "::warning::dispatch attempt $attempt failed; retrying in 5s"
            sleep 5
          done
          echo "::warning::aggregator dispatch failed after 5 attempts; release artifacts are still uploaded"