qbak 1.5.1

A single-command backup helper for Linux and POSIX systems
Documentation
name: Release

on:
  push:
    tags:
      - 'v*'

env:
  CARGO_TERM_COLOR: always

permissions:
  contents: write
  packages: write

jobs:
  # Create release
  create-release:
    name: Create Release
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get_version.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get version from tag
        id: get_version
        run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Generate changelog
        id: changelog
        run: |
          # Extract changelog for this version
          if [ -f CHANGELOG.md ]; then
            # Get content between version headers
            awk '/^## \['${{ steps.get_version.outputs.version }}'\]/{flag=1; next} /^## \[/{flag=0} flag' CHANGELOG.md > release_notes.md
            echo "has_changelog=true" >> $GITHUB_OUTPUT
          else
            echo "Release ${{ steps.get_version.outputs.version }}" > release_notes.md
            echo "has_changelog=false" >> $GITHUB_OUTPUT
          fi

      - name: Create Release
        id: create_release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          name: qbak v${{ steps.get_version.outputs.version }}
          body_path: release_notes.md
          draft: false
          prerelease: ${{ contains(steps.get_version.outputs.version, '-') }}

  # Build release binaries
  build-release:
    name: Build Release
    needs: create-release
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            name: qbak-linux-x86_64
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            name: qbak-linux-x86_64-musl
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            name: qbak-linux-arm64
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
            name: qbak-linux-arm64-musl
          - target: armv7-unknown-linux-gnueabihf
            os: ubuntu-latest
            name: qbak-linux-armv7l
          - target: x86_64-apple-darwin
            os: macos-latest
            name: qbak-macos-x86_64
          - target: aarch64-apple-darwin
            os: macos-latest
            name: qbak-macos-arm64
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            name: qbak-windows-x86_64
            ext: .exe
    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Install musl tools (Linux musl only)
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: sudo apt-get install -y musl-tools

      - name: Install ARM64 musl tools (ARM64 musl only)
        if: matrix.target == 'aarch64-unknown-linux-musl'
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools gcc-aarch64-linux-gnu

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

      - name: Install ARMv7 cross-compilation tools
        if: matrix.target == 'armv7-unknown-linux-gnueabihf'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-arm-linux-gnueabihf

      - name: Configure cross-compilation (ARM64 Linux)
        if: contains(matrix.target, 'aarch64-unknown-linux')
        run: |
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV

      - name: Configure cross-compilation (ARMv7 Linux)
        if: matrix.target == 'armv7-unknown-linux-gnueabihf'
        run: |
          echo "CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV

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

      - name: Build release binary
        run: cargo build --release --target ${{ matrix.target }}

      - name: Upload to VirusTotal (Windows only)
        if: matrix.os == 'windows-latest'
        continue-on-error: true
        run: |
          if [ -n "${{ secrets.VIRUSTOTAL_API_KEY }}" ]; then
            echo "Uploading ${{ matrix.name }}.exe to VirusTotal..."
            response=$(curl --silent --request POST \
              --url https://www.virustotal.com/api/v3/files \
              --header "x-apikey: ${{ secrets.VIRUSTOTAL_API_KEY }}" \
              --form "file=@target/${{ matrix.target }}/release/qbak.exe")
            
            echo "VirusTotal Response:"
            echo "$response" | jq .
            
            # Extract analysis ID and provide link
            if echo "$response" | jq -e '.data.id' >/dev/null 2>&1; then
              analysis_id=$(echo "$response" | jq -r '.data.id')
              echo "✅ VirusTotal scan initiated successfully"
              echo "📊 Analysis ID: $analysis_id"
              echo "🔗 Check results at: https://www.virustotal.com/gui/file-analysis/$analysis_id"
            elif echo "$response" | jq -e '.error' >/dev/null 2>&1; then
              error_code=$(echo "$response" | jq -r '.error.code // "unknown"')
              error_message=$(echo "$response" | jq -r '.error.message // "unknown"')
              echo "❌ VirusTotal API Error: $error_code - $error_message"
            else
              echo "❓ Unexpected response format from VirusTotal"
            fi
          else
            echo "VirusTotal API key not configured, skipping scan"
          fi
        shell: bash

      - name: Strip binary (Unix)
        if: matrix.os != 'windows-latest'
        run: |
          if [[ "${{ matrix.target }}" == "aarch64-unknown-linux-gnu" ]] || [[ "${{ matrix.target }}" == "aarch64-unknown-linux-musl" ]]; then
            aarch64-linux-gnu-strip target/${{ matrix.target }}/release/qbak
          elif [[ "${{ matrix.target }}" == "armv7-unknown-linux-gnueabihf" ]]; then
            arm-linux-gnueabihf-strip target/${{ matrix.target }}/release/qbak
          else
            strip target/${{ matrix.target }}/release/qbak
          fi

      - name: Create archive (Unix)
        if: matrix.os != 'windows-latest'
        run: |
          cd target/${{ matrix.target }}/release
          tar czf ../../../${{ matrix.name }}.tar.gz qbak
          cd ../../..

      - name: Create archive (Windows)
        if: matrix.os == 'windows-latest'
        run: |
          cd target/${{ matrix.target }}/release
          7z a ../../../${{ matrix.name }}.zip qbak.exe
          cd ../../..

      - name: Upload release asset (Unix)
        if: matrix.os != 'windows-latest'
        uses: softprops/action-gh-release@v2
        with:
          files: ${{ matrix.name }}.tar.gz

      - name: Upload release asset (Windows)
        if: matrix.os == 'windows-latest'
        uses: softprops/action-gh-release@v2
        with:
          files: ${{ matrix.name }}.zip

  # Publish to crates.io
  publish-crate:
    name: Publish to crates.io
    needs: [create-release, build-release]
    runs-on: ubuntu-latest
    if: ${{ !contains(needs.create-release.outputs.version, '-') }}  # Only for stable releases when token is manually configured
    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Cache dependencies
        uses: Swatinem/rust-cache@v2

      - name: Verify version matches tag
        run: |
          CARGO_VERSION=$(grep '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
          TAG_VERSION="${{ needs.create-release.outputs.version }}"
          if [ "$CARGO_VERSION" != "$TAG_VERSION" ]; then
            echo "❌ Version mismatch: Cargo.toml has $CARGO_VERSION, tag is $TAG_VERSION"
            exit 1
          fi
          echo "✅ Version check passed: $CARGO_VERSION"

      - name: Run tests before publish
        run: cargo test --all-features -- --test-threads=1

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

  # Create checksums
  create-checksums:
    name: Create Checksums
    needs: [create-release, build-release]
    runs-on: ubuntu-latest
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4

      - name: Create checksums
        run: |
          echo "# Checksums for qbak v${{ needs.create-release.outputs.version }}" > checksums.txt
          echo "" >> checksums.txt
          for file in qbak-*.tar.gz qbak-*.zip; do
            if [ -f "$file" ]; then
              echo "## $file" >> checksums.txt
              sha256sum "$file" >> checksums.txt
              echo "" >> checksums.txt
            fi
          done

      - name: Upload checksums
        uses: softprops/action-gh-release@v2
        with:
          files: checksums.txt