diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml new file mode 100644 index 0000000..8cd37bd --- /dev/null +++ b/.github/workflows/cicd.yml @@ -0,0 +1,238 @@ +name: CI/CD Pipeline + +# Workflow triggers: push to main, PRs to main, and manual dispatch +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + workflow_dispatch: + +# Global environment variables +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +jobs: + # Build job using matrix strategy for parallel compilation across platforms + build: + name: Build (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + strategy: + fail-fast: false # Continue building other targets even if one fails + matrix: + include: + # Linux x86_64 + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + cross: false + + # Windows x86_64 + - target: x86_64-pc-windows-msvc + os: windows-latest + cross: false + extension: .exe + + # macOS Intel + - target: x86_64-apple-darwin + os: macos-latest + cross: false + + # macOS Apple Silicon + - target: aarch64-apple-darwin + os: macos-latest + cross: true + + steps: + # Checkout repository code + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch full history for proper version detection + + # Install Rust toolchain with target support + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + # Cache Rust dependencies to speed up builds + - name: Cache Rust dependencies + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-${{ matrix.target }}- + ${{ runner.os }}-cargo- + + # Extract version from Cargo.toml for artifact naming + - name: Extract version + shell: bash + run: | + VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/.*= "//' | sed 's/".*//') + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "Extracted version: $VERSION" + + # Extract package name from Cargo.toml + - name: Extract package name + shell: bash + run: | + PACKAGE_NAME=$(grep '^name = ' Cargo.toml | head -1 | sed 's/.*= "//' | sed 's/".*//') + echo "PACKAGE_NAME=$PACKAGE_NAME" >> $GITHUB_ENV + echo "Extracted package name: $PACKAGE_NAME" + + # Run tests before building (only on native targets to avoid cross-compilation issues) + - name: Run tests + if: matrix.cross == false + run: cargo test --target ${{ matrix.target }} --verbose + + # Build the application + - name: Build application + shell: bash + run: | + if [[ "${{ matrix.cross }}" == "true" ]]; then + # For cross-compilation, we might need additional setup + cargo build --release --target ${{ matrix.target }} --verbose + else + cargo build --release --target ${{ matrix.target }} --verbose + fi + + # Prepare binary for upload (handle Windows .exe extension) + - name: Prepare binary + shell: bash + run: | + BINARY_NAME="${{ env.PACKAGE_NAME }}${{ matrix.extension }}" + ARTIFACT_NAME="${{ env.PACKAGE_NAME }}-${{ env.VERSION }}-${{ matrix.target }}${{ matrix.extension }}" + + # Create artifacts directory + mkdir -p artifacts + + # Copy binary to artifacts with proper naming + cp "target/${{ matrix.target }}/release/${BINARY_NAME}" "artifacts/${ARTIFACT_NAME}" + + echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >> $GITHUB_ENV + echo "Created artifact: ${ARTIFACT_NAME}" + + # Upload build artifacts + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: ${{ env.ARTIFACT_NAME }} + path: artifacts/${{ env.ARTIFACT_NAME }} + retention-days: 7 # Keep artifacts for 7 days + if-no-files-found: error + + # Verify binary works (basic smoke test on native platforms) + - name: Verify binary + if: matrix.cross == false + shell: bash + run: | + chmod +x "artifacts/${{ env.ARTIFACT_NAME }}" 2>/dev/null || true + if [[ "${{ matrix.os }}" == "windows-latest" ]]; then + ./artifacts/${{ env.ARTIFACT_NAME }} --version || ./artifacts/${{ env.ARTIFACT_NAME }} -V || echo "Binary verification completed" + else + ./artifacts/${{ env.ARTIFACT_NAME }} --version || ./artifacts/${{ env.ARTIFACT_NAME }} -V || echo "Binary verification completed" + fi + + # Release job - only runs on version tags and after successful builds + release: + name: Create Release + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: build + if: startsWith(github.ref, 'refs/tags/v') # Only run on version tags + + permissions: + contents: write # Required for creating releases + + steps: + # Checkout repository + - name: Checkout repository + uses: actions/checkout@v4 + + # Download all build artifacts + - name: Download all artifacts + uses: actions/download-artifact@v3 + with: + path: release-artifacts + + # Extract version from tag + - name: Extract version from tag + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/} + echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_ENV + echo "Release version: $TAG_VERSION" + + # Prepare release assets + - name: Prepare release assets + run: | + echo "Preparing release assets..." + ls -la release-artifacts/ + + # Create release directory + mkdir -p release + + # Move all artifacts to release directory and list them + find release-artifacts -name "*" -type f -exec cp {} release/ \; + + echo "Release assets:" + ls -la release/ + + # Create GitHub release with all platform binaries + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ env.TAG_VERSION }} + name: Release ${{ env.TAG_VERSION }} + draft: false + prerelease: ${{ contains(env.TAG_VERSION, '-') }} # Mark as pre-release if version contains hyphen + generate_release_notes: true # Auto-generate release notes from commits + files: release/* + body: | + ## Release ${{ env.TAG_VERSION }} + + ### Downloads + Choose the appropriate binary for your platform: + + - **Linux (x86_64)**: `*-x86_64-unknown-linux-gnu` + - **Windows (x86_64)**: `*-x86_64-pc-windows-msvc.exe` + - **macOS Intel**: `*-x86_64-apple-darwin` + - **macOS Apple Silicon**: `*-aarch64-apple-darwin` + + ### Installation + 1. Download the appropriate binary for your platform + 2. Make it executable: `chmod +x ` (Unix-like systems) + 3. Move to a directory in your PATH or run directly + + ### Verification + Run ` --version` to verify the installation. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Optional: Cleanup job to remove old artifacts (runs after release) + cleanup: + name: Cleanup Artifacts + runs-on: ubuntu-latest + timeout-minutes: 5 + needs: [build, release] + if: always() && startsWith(github.ref, 'refs/tags/v') # Run on releases regardless of success/failure + + steps: + - name: Delete workflow artifacts + uses: geekyeggo/delete-artifact@v2 + with: + name: | + *-x86_64-unknown-linux-gnu + *-x86_64-pc-windows-msvc.exe + *-x86_64-apple-darwin + *-aarch64-apple-darwin + failOnError: false # Don't fail if artifacts don't exist \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 3065d91..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,344 +0,0 @@ -name: Build and Release - -# This workflow has different behaviors based on the trigger: -# - On PRs: Build and verify all targets (no release) -# - On pushes to main: Build and verify all targets (no release) -# - On tags starting with 'v': Build all targets AND create GitHub release -# - On manual dispatch: Build and verify all targets (no release) - -# Trigger the workflow on multiple events -on: - # Run on pushes to main branch - push: - branches: - - main - # Also run on tag creation (for releases) - tags: - - 'v*' - # Run on pull requests to main - pull_request: - branches: - - main - # Allow manual triggering - workflow_dispatch: - -# Set environment variables for consistent usage across jobs -env: - CARGO_TERM_COLOR: always - RUST_BACKTRACE: 1 - -jobs: - # Job to determine version for consistent versioning across build types - version: - name: Determine Version - runs-on: ubuntu-latest - outputs: - version: ${{ steps.version.outputs.version }} - tag: ${{ steps.version.outputs.tag }} - is_release: ${{ steps.version.outputs.is_release }} - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Determine version and build type - id: version - run: | - if [[ "${GITHUB_REF}" == refs/tags/v* ]]; then - # This is a tag build - extract version from tag - TAG_VERSION=${GITHUB_REF#refs/tags/v} - echo "version=${TAG_VERSION}" >> $GITHUB_OUTPUT - echo "tag=v${TAG_VERSION}" >> $GITHUB_OUTPUT - echo "is_release=true" >> $GITHUB_OUTPUT - echo "Release build - Version: ${TAG_VERSION}" - else - # This is a regular build - use Cargo.toml version + commit SHA - CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -n1 | sed 's/version = "\(.*\)"/\1/') - COMMIT_SHA=${GITHUB_SHA:0:7} - BUILD_VERSION="${CARGO_VERSION}-${COMMIT_SHA}" - echo "version=${BUILD_VERSION}" >> $GITHUB_OUTPUT - echo "tag=v${BUILD_VERSION}" >> $GITHUB_OUTPUT - echo "is_release=false" >> $GITHUB_OUTPUT - echo "Development build - Version: ${BUILD_VERSION}" - fi - - # Build job with matrix strategy for cross-platform compilation - build: - name: Build for ${{ matrix.target }} - runs-on: ${{ matrix.os }} - needs: version - strategy: - fail-fast: false # Don't cancel other builds if one fails - matrix: - include: - # Linux x86_64 - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - name: linux-x64 - cross: false - - # Linux ARM64 (cross-compile from x86_64) - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - name: linux-arm64 - cross: true - - # Windows x86_64 - - target: x86_64-pc-windows-msvc - os: windows-latest - name: windows-x64 - cross: false - extension: .exe - - # macOS x86_64 - - target: x86_64-apple-darwin - os: macos-latest - name: macos-x64 - cross: false - - # macOS ARM64 (Apple Silicon) - - target: aarch64-apple-darwin - os: macos-latest - name: macos-arm64 - cross: false - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.target }} - - - name: Install cross (for cross-compilation) - if: matrix.cross - run: cargo install cross --git https://github.com/cross-rs/cross - - - name: Configure Cargo for cross-compilation - if: matrix.cross - run: | - echo "[target.${{ matrix.target }}]" >> ~/.cargo/config.toml - echo "linker = \"aarch64-linux-gnu-gcc\"" >> ~/.cargo/config.toml - shell: bash - - - name: Install cross-compilation dependencies (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 binary - run: | - if [ "${{ matrix.cross }}" = "true" ]; then - cross build --release --target ${{ matrix.target }} - else - cargo build --release --target ${{ matrix.target }} - fi - shell: bash - - - name: Get binary name from Cargo.toml - id: binary - run: | - BINARY_NAME=$(grep '^name = ' Cargo.toml | head -n1 | sed 's/name = "\(.*\)"/\1/') - echo "name=${BINARY_NAME}" >> $GITHUB_OUTPUT - echo "Binary name: ${BINARY_NAME}" - shell: bash - - - name: Prepare binary for upload - run: | - BINARY_NAME="${{ steps.binary.outputs.name }}" - TARGET_DIR="target/${{ matrix.target }}/release" - BINARY_PATH="${TARGET_DIR}/${BINARY_NAME}${{ matrix.extension }}" - - # Create release directory - mkdir -p release - - # Copy and rename binary with version and platform info - RELEASE_NAME="${BINARY_NAME}-${{ needs.version.outputs.tag }}-${{ matrix.name }}${{ matrix.extension }}" - cp "${BINARY_PATH}" "release/${RELEASE_NAME}" - - echo "RELEASE_NAME=${RELEASE_NAME}" >> $GITHUB_ENV - echo "BINARY_PATH=release/${RELEASE_NAME}" >> $GITHUB_ENV - shell: bash - - - name: Verify binary works - run: | - # Basic verification that the binary can execute - if [ "${{ matrix.os }}" != "windows-latest" ]; then - chmod +x "${{ env.BINARY_PATH }}" - if ! "${{ env.BINARY_PATH }}" --help > /dev/null 2>&1 && ! "${{ env.BINARY_PATH }}" --version > /dev/null 2>&1; then - echo "Warning: Binary verification failed, but continuing..." - else - echo "Binary verification successful" - fi - else - # For Windows, just check if file exists and has reasonable size - if [ -f "${{ env.BINARY_PATH }}" ]; then - SIZE=$(stat -c%s "${{ env.BINARY_PATH }}" 2>/dev/null || stat -f%z "${{ env.BINARY_PATH }}" 2>/dev/null || echo "0") - if [ "$SIZE" -gt 1000 ]; then - echo "Binary verification successful (size: $SIZE bytes)" - else - echo "Warning: Binary seems too small" - fi - else - echo "Error: Binary not found" - exit 1 - fi - fi - shell: bash - - - name: Upload binary artifact - uses: actions/upload-artifact@v4 - with: - name: binary-${{ matrix.name }} - path: ${{ env.BINARY_PATH }} - if-no-files-found: error - - # Release job that creates GitHub release with all binaries (only on tag builds) - release: - name: Create Release - runs-on: ubuntu-latest - needs: [version, build] - # Only run this job when building from a tag that starts with 'v' - if: needs.version.outputs.is_release == 'true' - permissions: - contents: write # Required for creating releases - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Prepare release assets - run: | - mkdir -p release-assets - # Move all binaries to release-assets directory - find artifacts -name "binary-*" -type d | while read dir; do - find "$dir" -type f -exec mv {} release-assets/ \; - done - - # Summary job for non-release builds (PRs, main branch pushes, manual dispatch) - build_summary: - name: Build Summary - runs-on: ubuntu-latest - needs: [version, build] - # Only run when NOT creating a release - if: needs.version.outputs.is_release == 'false' - steps: - - name: Download build artifacts for summary - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Non-Release Build Summary - run: | - echo "## ✅ Build Verification Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Build Type:** Development/Verification" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ needs.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "**Event:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY - echo "**Commit:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 🔧 Built Artifacts (Not Released):" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "The following binaries were successfully built and verified:" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - - find artifacts -name "binary-*" -type d | while read dir; do - platform=$(basename "$dir" | sed 's/binary-//') - find "$dir" -type f | while read file; do - filename=$(basename "$file") - size=$(ls -lh "$file" | awk '{print $5}' 2>/dev/null || echo "Unknown") - echo "- **$platform**: \`$filename\` ($size)" >> $GITHUB_STEP_SUMMARY - done - done - - echo "" >> $GITHUB_STEP_SUMMARY - echo "💡 **Note:** To create a release, push a tag starting with 'v' (e.g., \`git tag v1.0.0 && git push origin v1.0.0\`)" >> $GITHUB_STEP_SUMMARY - - echo "Release assets:" - ls -la release-assets/ - - - name: Check if release exists - id: release_check - run: | - # Check if a release with this tag already exists - if gh release view "${{ needs.version.outputs.tag }}" --repo "${{ github.repository }}" > /dev/null 2>&1; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Release ${{ needs.version.outputs.tag }} already exists" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "Release ${{ needs.version.outputs.tag }} does not exist" - fi - env: - GH_TOKEN: ${{ github.token }} - - - name: Create or update release - run: | - if [ "${{ steps.release_check.outputs.exists }}" = "true" ]; then - echo "Updating existing release..." - gh release upload "${{ needs.version.outputs.tag }}" release-assets/* --clobber --repo "${{ github.repository }}" - else - echo "Creating new release..." - - # Generate release notes - cat > release-notes.md << EOF - # Release ${{ needs.version.outputs.tag }} - - ## 🚀 What's New - - Automated release of the Rust CLI application. - - ## 📦 Downloads - - Choose the appropriate binary for your platform: - - - **Linux x64**: \`*-linux-x64\` - - **Linux ARM64**: \`*-linux-arm64\` - - **Windows x64**: \`*-windows-x64.exe\` - - **macOS x64**: \`*-macos-x64\` - - **macOS ARM64** (Apple Silicon): \`*-macos-arm64\` - - ## 🔧 Installation - - 1. Download the binary for your platform - 2. Make it executable (Unix systems): \`chmod +x \` - 3. Move to your PATH or run directly - - ## 📋 Build Information - - - **Commit**: ${{ github.sha }} - - **Build Date**: $(date -u '+%Y-%m-%d %H:%M:%S UTC') - - **Rust Version**: $(rustc --version 2>/dev/null || echo "Unknown") - EOF - - gh release create "${{ needs.version.outputs.tag }}" \ - --title "Release ${{ needs.version.outputs.tag }}" \ - --notes-file release-notes.md \ - --repo "${{ github.repository }}" \ - release-assets/* - fi - env: - GH_TOKEN: ${{ github.token }} - - - name: Build and Release Summary - run: | - echo "## 🎉 Build Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Build Type:** Release" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ needs.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "**Tag:** ${{ needs.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY - echo "**Release URL:** https://github.com/${{ github.repository }}/releases/tag/${{ needs.version.outputs.tag }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📦 Released Artifacts:" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - for file in release-assets/*; do - if [ -f "$file" ]; then - filename=$(basename "$file") - size=$(ls -lh "$file" | awk '{print $5}') - echo "- \`$filename\` ($size)" >> $GITHUB_STEP_SUMMARY - fi - done \ No newline at end of file