quickseal 0.1.4

Create Kubernetes SealedSecrets with a single secure command
Documentation
name: Release

on:
  push:
    branches:
      - main
    tags:
      - "v*.*.*"
  workflow_dispatch:

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always

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

      - name: Determine version and create tag
        id: determine_version
        run: |
          # Check if this is a tag push
          if [[ "$GITHUB_REF" == refs/tags/* ]]; then
            VERSION="${GITHUB_REF#refs/tags/}"
            echo "version=$VERSION" >> $GITHUB_OUTPUT
            echo "should_release=true" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Main branch push - analyze commits for version bump
          LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")

          if [ "$LAST_TAG" = "v0.0.0" ]; then
            COMMITS=$(git log --pretty=format:"%s" HEAD)
          else
            COMMITS=$(git log --pretty=format:"%s" ${LAST_TAG}..HEAD)
          fi

          # Determine bump type
          BUMP_TYPE="none"
          if echo "$COMMITS" | grep -qE "^[a-zA-Z]+(\(.+\))?!:|BREAKING CHANGE:"; then
            BUMP_TYPE="major"
          elif echo "$COMMITS" | grep -qE "^feat(\(.+\))?:"; then
            BUMP_TYPE="minor"
          elif echo "$COMMITS" | grep -qE "^fix(\(.+\))?:"; then
            BUMP_TYPE="patch"
          fi

          if [ "$BUMP_TYPE" = "none" ]; then
            echo "should_release=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Calculate new version
          CURRENT_VERSION="${LAST_TAG#v}"
          IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION"

          case $BUMP_TYPE in
            major) MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 ;;
            minor) MINOR=$((MINOR + 1)); PATCH=0 ;;
            patch) PATCH=$((PATCH + 1)) ;;
          esac

          NEW_VERSION="$MAJOR.$MINOR.$PATCH"
          NEW_TAG="v$NEW_VERSION"

          # Update Cargo.toml
          sed -i.bak "s/^version = .*/version = \"$NEW_VERSION\"/" Cargo.toml
          rm Cargo.toml.bak

          # Commit and push
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Cargo.toml
          git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
          git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
          git push origin HEAD:main
          git push origin "$NEW_TAG"

          echo "version=$NEW_TAG" >> $GITHUB_OUTPUT
          echo "should_release=true" >> $GITHUB_OUTPUT

      - name: Create Release
        if: steps.determine_version.outputs.should_release == 'true'
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          VERSION="${{ steps.determine_version.outputs.version }}"

          # Check if release already exists
          if gh release view "$VERSION" >/dev/null 2>&1; then
            echo "Release $VERSION already exists, skipping creation"
          else
            gh release create "$VERSION" \
              --title "Release $VERSION" \
              --notes "Create Kubernetes SealedSecrets with a single secure command. Download the binary for your platform below and add it to your PATH."
          fi

  # Build and upload binaries for multiple platforms
  build-release:
    name: Build Release Binary
    needs: create-release
    if: needs.create-release.outputs.should_release == 'true'
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            artifact_name: seal
            asset_name: seal-linux-amd64

          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
            artifact_name: seal
            asset_name: seal-linux-arm64

          - os: macos-latest
            target: x86_64-apple-darwin
            artifact_name: seal
            asset_name: seal-macos-amd64

          - os: macos-latest
            target: aarch64-apple-darwin
            artifact_name: seal
            asset_name: seal-macos-arm64

          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact_name: seal.exe
            asset_name: seal-windows-amd64.exe

    runs-on: ${{ matrix.os }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

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

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

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

      - name: Strip binary (Linux and macOS)
        if: matrix.os != 'windows-latest'
        run: strip target/${{ matrix.target }}/release/${{ matrix.artifact_name }}

      - name: Upload Release Asset
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          cp "target/${{ matrix.target }}/release/${{ matrix.artifact_name }}" "${{ matrix.asset_name }}"
          gh release upload "${{ needs.create-release.outputs.version }}" "${{ matrix.asset_name }}"

  # Publish to crates.io
  publish-crate:
    name: Publish to crates.io
    needs: create-release
    if: needs.create-release.outputs.should_release == 'true' && startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

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

      # NEW: Security Audit (Constitution Principle II, FR-004)
      - name: Security Audit
        run: |
          cargo install cargo-audit
          cargo audit --deny warnings

      # NEW: Version Validation (FR-005)
      - name: Validate Version Consistency
        run: |
          # Extract version from Git tag (remove 'v' prefix)
          GIT_TAG="${GITHUB_REF#refs/tags/v}"

          # Extract version from Cargo.toml
          CARGO_VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')

          # Compare versions
          if [ "$GIT_TAG" != "$CARGO_VERSION" ]; then
            echo "❌ ERROR: Version mismatch!"
            echo "Git tag: v$GIT_TAG"
            echo "Cargo.toml: $CARGO_VERSION"
            echo ""
            echo "Please update Cargo.toml version to match the release tag."
            exit 1
          fi

          echo "✅ Version validation passed: $CARGO_VERSION"

      # NEW: Build Verification (FR-006)
      - name: Verify Build
        run: cargo build --release --verbose

      # NEW: Test Verification (FR-007)
      - name: Run Tests
        run: cargo test --all --verbose

      # MODIFIED: Better error handling for cargo publish
      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: |
          echo "đŸ“Ļ Publishing quickseal to crates.io..."

          # Attempt publication
          if cargo publish --token "$CARGO_REGISTRY_TOKEN"; then
            echo "✅ Successfully published to crates.io"
          else
            EXIT_CODE=$?

            # Check if already published (common scenario, not an error)
            if cargo search quickseal | grep -q "^quickseal ="; then
              echo "â„šī¸  Package version already published to crates.io"
              exit 0
            else
              echo "❌ Publication failed with exit code $EXIT_CODE"
              exit $EXIT_CODE
            fi
          fi

      # NEW: Update Release Notes
      - name: Update Release Notes
        if: success()
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release edit ${{ github.ref_name }} --notes-file - <<'NOTES'
          ## đŸ“Ļ crates.io Publication

          ✅ Successfully published to crates.io as **quickseal**

          **Installation:** `cargo install quickseal`

          **Verification:** `seal --version`

          **Package:** https://crates.io/crates/quickseal
          NOTES

          echo "✅ Release notes updated"

  # Create Docker image (optional - for future use)
  docker-image:
    name: Build Docker Image
    needs: create-release
    runs-on: ubuntu-latest
    if: false  # Disabled for now - enable when Dockerfile is added
    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=semver,pattern={{major}}
            type=sha

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          platforms: linux/amd64,linux/arm64