skeletor 0.3.13

A blazing-fast Rust scaffolding tool with YAML-driven snapshots.
Documentation
name: Release

on:
  push:
    tags:
      - "v*.*.*"
  workflow_dispatch:
    inputs:
      tag:
        description: "Tag to release (e.g., v0.3.9)"
        required: true
        type: string
      publish_crates:
        description: "Publish to crates.io"
        required: false
        default: true
        type: boolean
      publish_npm:
        description: "Publish to npm"
        required: false
        default: true
        type: boolean
      update_tap:
        description: "Update Homebrew tap"
        required: false
        default: true
        type: boolean

permissions:
  contents: write
  id-token: write

jobs:
  preflight:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.vars.outputs.tag }}
      publish_crates: ${{ steps.vars.outputs.publish_crates }}
      publish_npm: ${{ steps.vars.outputs.publish_npm }}
      update_tap: ${{ steps.vars.outputs.update_tap }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag || github.ref_name }}
          fetch-depth: 0

      - name: Resolve inputs
        id: vars
        shell: bash
        run: |
          set -Eeuo pipefail
          TAG="${{ inputs.tag || github.ref_name }}"
          [[ -n "$TAG" ]] || { echo "Missing tag"; exit 1; }

          PUBLISH="${{ inputs.publish_crates }}"
          PUBLISH_NPM="${{ inputs.publish_npm }}"
          UPDATE_TAP="${{ inputs.update_tap }}"

          if [[ -z "$PUBLISH" ]]; then
            PUBLISH=true
          fi
          if [[ -z "$PUBLISH_NPM" ]]; then
            PUBLISH_NPM=true
          fi
          if [[ -z "$UPDATE_TAP" ]]; then
            UPDATE_TAP=true
          fi

          {
            echo "tag=$TAG"
            echo "publish_crates=$PUBLISH"
            echo "publish_npm=$PUBLISH_NPM"
            echo "update_tap=$UPDATE_TAP"
          } >> "$GITHUB_OUTPUT"

      - name: Check version consistency
        shell: bash
        run: scripts/check-version-consistency.sh

      - name: Check NPM version consistency
        shell: bash
        run: scripts/check-npm-versions.sh "${{ steps.vars.outputs.tag }}"

      - name: Validate tag matches Cargo.toml
        shell: bash
        run: |
          set -Eeuo pipefail
          TAG="${{ steps.vars.outputs.tag }}"
          VERSION="${TAG#v}"
          CARGO_VERSION=$(awk -F\" '/^version =/ {print $2; exit}' Cargo.toml)
          if [[ "$VERSION" != "$CARGO_VERSION" ]]; then
            echo "Tag $TAG does not match Cargo.toml version $CARGO_VERSION"
            exit 1
          fi

      - name: Check required secrets
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
          GH_PAT: ${{ secrets.GH_PAT }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        shell: bash
        run: |
          set -Eeuo pipefail
          if [[ "${{ steps.vars.outputs.publish_crates }}" == "true" ]] && [[ -z "${CARGO_REGISTRY_TOKEN:-}" ]]; then
            echo "CARGO_REGISTRY_TOKEN is missing but publish_crates=true"
            exit 1
          fi
          if [[ "${{ steps.vars.outputs.update_tap }}" == "true" ]] && [[ -z "${GH_PAT:-}" ]]; then
            echo "GH_PAT is missing but update_tap=true"
            exit 1
          fi
          if [[ "${{ steps.vars.outputs.publish_npm }}" == "true" ]] && [[ -z "${NPM_TOKEN:-}" ]]; then
            echo "NPM_TOKEN is missing but publish_npm=true"
            exit 1
          fi

  build-and-release:
    needs: [preflight, create-release]
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: macos-14
            target: aarch64-apple-darwin
            out: skeletor-macos-aarch64-apple-darwin
            npm_platform: darwin-arm64
          - os: macos-14
            target: x86_64-apple-darwin
            out: skeletor-macos-x86_64-apple-darwin
            npm_platform: darwin-x64
          - os: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
            out: skeletor-ubuntu-x86_64-unknown-linux-gnu
            npm_platform: linux-x64
          - os: ubuntu-22.04
            target: aarch64-unknown-linux-gnu
            out: skeletor-ubuntu-aarch64-unknown-linux-gnu
            npm_platform: linux-arm64
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            out: skeletor-windows-x86_64-pc-windows-msvc
            npm_platform: win32-x64
          - os: windows-latest
            target: aarch64-pc-windows-msvc
            out: skeletor-windows-aarch64-pc-windows-msvc
            npm_platform: win32-arm64
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.preflight.outputs.tag }}

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

      - name: Rust Cache
        uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}

      - name: Install cross-compilation tools
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu libc6-dev-arm64-cross

      - name: Build (release)
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
        run: cargo build --locked --release --target ${{ matrix.target }}

      - name: Package tarball
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p "dist/${{ matrix.out }}"
          
          # Handle potential .exe extension for Windows
          BIN="target/${{ matrix.target }}/release/skeletor"
          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            BIN="${BIN}.exe"
          fi
          
          cp "$BIN" "dist/${{ matrix.out }}/skeletor"
          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
             mv "dist/${{ matrix.out }}/skeletor" "dist/${{ matrix.out }}/skeletor.exe"
          fi

          tar -C dist -czf "${{ matrix.out }}.tar.gz" "${{ matrix.out }}"
          shasum -a 256 "${{ matrix.out }}.tar.gz" | awk '{print $1}' > "${{ matrix.out }}.sha256"

      - name: Upload assets
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -Eeuo pipefail
          TAG="${{ needs.preflight.outputs.tag }}"
          gh release upload "$TAG" \
            "${{ matrix.out }}.tar.gz" \
            "${{ matrix.out }}.sha256" \
            --clobber

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Publish platform-specific package
        if: ${{ matrix.npm_platform != '' }}
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        shell: bash
        run: |
          set -euo pipefail
          # Move binary to the correct location
          PKG_DIR="npm/platforms/${{ matrix.npm_platform }}"
          mkdir -p "$PKG_DIR/bin"
          
          BIN="target/${{ matrix.target }}/release/skeletor"
           if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
            BIN="${BIN}.exe"
          fi
          cp "$BIN" "$PKG_DIR/bin/skeletor"

          if [[ "${{ matrix.os }}" == "windows-latest" ]]; then
             mv "$PKG_DIR/bin/skeletor" "$PKG_DIR/bin/skeletor.exe"
          fi
          
          # Publish
          cd "$PKG_DIR"
          npm publish --access public

  create-release:
    needs: preflight
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.preflight.outputs.tag }}

      - name: Ensure GitHub Release exists
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        shell: bash
        run: |
          set -Eeuo pipefail
          TAG="${{ needs.preflight.outputs.tag }}"
          if gh release view "$TAG" >/dev/null 2>&1; then
            echo "Release $TAG already exists."
            exit 0
          fi
          gh release create "$TAG" --title "$TAG" --generate-notes

  publish-crates:
    needs: [preflight, build-and-release]
    if: ${{ needs.preflight.outputs.publish_crates == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.preflight.outputs.tag }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --locked

  publish-npm:
    needs: [preflight, build-and-release]
    if: ${{ needs.preflight.outputs.publish_npm == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ needs.preflight.outputs.tag }}

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org'
      
      - name: Publish main package
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: |
          cd npm/skeletor
          npm publish --access public

  update-tap:
    needs: [preflight, build-and-release]
    if: ${{ needs.preflight.outputs.update_tap == 'true' }}
    uses: ./.github/workflows/tap-update.yml
    with:
      tag: ${{ needs.preflight.outputs.tag }}
    secrets:
      GH_PAT: ${{ secrets.GH_PAT }}

  restore-unreleased:
    needs: [preflight, build-and-release]
    runs-on: ubuntu-latest
    if: ${{ always() }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main
      - name: Restore Unreleased header if missing
        shell: bash
        run: |
          set -Eeuo pipefail
          if rg -n "^## \\[Unreleased\\]" CHANGELOG.md >/dev/null 2>&1; then
            echo "Unreleased header already present."
            exit 0
          fi
          awk '
            { print }
            /<!-- next-header -->/ {
              print ""
              print "## [Unreleased] - ReleaseDate"
              print ""
            }
          ' CHANGELOG.md > CHANGELOG.md.tmp
          mv CHANGELOG.md.tmp CHANGELOG.md
      - name: Commit restored Unreleased header
        shell: bash
        run: |
          set -Eeuo pipefail
          if git diff --quiet -- CHANGELOG.md; then
            echo "No CHANGELOG updates to commit."
            exit 0
          fi
          git config user.name "skeletor-bot"
          git config user.email "skeletor-bot@users.noreply.github.com"
          git add CHANGELOG.md
          git commit -m "chore: restore Unreleased section"
          git push origin main