name: CI on: push: branches: ['master', 'develop'] tags: - 'v*.*.*' pull_request: branches: ['master', 'develop', 'feature/*'] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test: name: Test runs-on: ubuntu-latest permissions: contents: write strategy: matrix: go: - 1.21.x - 1.22.x - 1.23.x - 1.24.x steps: - uses: actions/checkout@v4 - name: Set up Go ${{ matrix.go }} uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} check-latest: true # Disable built-in cache to use our custom cache cache: false - name: Cache Go modules uses: actions/cache@v4 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum', '**/go.mod') }}-v1 restore-keys: | ${{ runner.os }}-go-${{ matrix.go }}- continue-on-error: true - name: Download dependencies with retry run: | set -e echo "Downloading Go dependencies..." # Function to download with retry download_with_retry() { local attempt=1 local max_attempts=3 while [ $attempt -le $max_attempts ]; do echo "Attempt $attempt of $max_attempts" if go mod download; then echo "Download successful on attempt $attempt" return 0 else echo "Download failed on attempt $attempt" if [ $attempt -lt $max_attempts ]; then echo "Cleaning cache and retrying..." go clean -modcache go clean -cache sleep 2 fi attempt=$((attempt + 1)) fi done echo "All download attempts failed" return 1 } # Try download with retry logic download_with_retry echo "Verifying module dependencies..." go mod verify echo "Dependencies verified successfully" - name: Build run: go build -v ./... - name: Run tests with enhanced reporting id: test run: | echo "## ๐Ÿ”ง Test Environment" >> $GITHUB_STEP_SUMMARY echo "- **Go Version:** ${{ matrix.go }}" >> $GITHUB_STEP_SUMMARY echo "- **OS:** ubuntu-latest" >> $GITHUB_STEP_SUMMARY echo "- **Timestamp:** $(date -u)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Running tests with coverage..." go test -v -race -coverprofile=coverage.out ./... 2>&1 | tee test-output.log # Extract test results for summary TEST_STATUS=$? TOTAL_TESTS=$(grep -c "=== RUN" test-output.log || echo "0") PASSED_TESTS=$(grep -c "--- PASS:" test-output.log || echo "0") FAILED_TESTS=$(grep -c "--- FAIL:" test-output.log || echo "0") SKIPPED_TESTS=$(grep -c "--- SKIP:" test-output.log || echo "0") # Generate test summary echo "## ๐Ÿงช Test Results (Go ${{ matrix.go }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Total Tests | $TOTAL_TESTS |" >> $GITHUB_STEP_SUMMARY echo "| Passed | โœ… $PASSED_TESTS |" >> $GITHUB_STEP_SUMMARY echo "| Failed | โŒ $FAILED_TESTS |" >> $GITHUB_STEP_SUMMARY echo "| Skipped | โญ๏ธ $SKIPPED_TESTS |" >> $GITHUB_STEP_SUMMARY echo "| Status | $([ $TEST_STATUS -eq 0 ] && echo "โœ… PASSED" || echo "โŒ FAILED") |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Add package breakdown echo "### ๐Ÿ“ฆ Package Test Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Package | Status |" >> $GITHUB_STEP_SUMMARY echo "|---------|--------|" >> $GITHUB_STEP_SUMMARY # Extract package results grep "^ok\|^FAIL" test-output.log | while read line; do if [[ $line == ok* ]]; then pkg=$(echo $line | awk '{print $2}') echo "| $pkg | โœ… PASS |" >> $GITHUB_STEP_SUMMARY elif [[ $line == FAIL* ]]; then pkg=$(echo $line | awk '{print $2}') echo "| $pkg | โŒ FAIL |" >> $GITHUB_STEP_SUMMARY fi done echo "" >> $GITHUB_STEP_SUMMARY # Add detailed results if tests failed if [ $TEST_STATUS -ne 0 ]; then echo "### โŒ Failed Tests Details" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY grep -A 10 "--- FAIL:" test-output.log | head -100 >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi # Set outputs for other steps echo "test-status=$TEST_STATUS" >> $GITHUB_OUTPUT echo "total-tests=$TOTAL_TESTS" >> $GITHUB_OUTPUT echo "passed-tests=$PASSED_TESTS" >> $GITHUB_OUTPUT echo "failed-tests=$FAILED_TESTS" >> $GITHUB_OUTPUT # Exit with the original test status exit $TEST_STATUS - name: Generate coverage report if: always() run: | if [ -f coverage.out ]; then go tool cover -html=coverage.out -o coverage.html COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}') echo "## ๐Ÿ“Š Code Coverage (Go ${{ matrix.go }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Total Coverage: $COVERAGE**" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Add coverage by package echo "
" >> $GITHUB_STEP_SUMMARY echo "Click to expand ๐Ÿ“‹ Coverage by Package details" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Package | Coverage |" >> $GITHUB_STEP_SUMMARY echo "|---------|----------|" >> $GITHUB_STEP_SUMMARY # Create temporary file for package coverage aggregation temp_coverage=$(mktemp) # Extract package-level coverage data go tool cover -func=coverage.out | grep -v total | while read line; do if [[ $line == *".go:"* ]]; then # Extract package path from file path (everything before the filename) filepath=$(echo "$line" | awk '{print $1}') pkg_path=$(dirname "$filepath" | sed 's|github.com/kjanat/articulate-parser/||' | sed 's|^\./||') coverage=$(echo "$line" | awk '{print $3}' | sed 's/%//') # Use root package if no subdirectory if [[ "$pkg_path" == "." || "$pkg_path" == "" ]]; then pkg_path="root" fi echo "$pkg_path $coverage" >> "$temp_coverage" fi done # Aggregate coverage by package (average) awk '{ packages[$1] += $2; counts[$1]++ } END { for (pkg in packages) { avg = packages[pkg] / counts[pkg] printf "| %s | %.1f%% |\n", pkg, avg } }' $temp_coverage | sort >> $GITHUB_STEP_SUMMARY rm -f $temp_coverage echo "
" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY else echo "## โš ๏ธ Coverage Report" >> $GITHUB_STEP_SUMMARY echo "No coverage file generated" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY fi - name: Upload test artifacts if: failure() uses: actions/upload-artifact@v4 with: name: test-results-go-${{ matrix.go }} path: | test-output.log coverage.out coverage.html retention-days: 7 - name: Run go vet run: | echo "## ๐Ÿ” Static Analysis (Go ${{ matrix.go }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY VET_OUTPUT=$(go vet ./... 2>&1 || echo "") VET_STATUS=$? if [ $VET_STATUS -eq 0 ]; then echo "โœ… **go vet:** No issues found" >> $GITHUB_STEP_SUMMARY else echo "โŒ **go vet:** Issues found" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "$VET_OUTPUT" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY fi echo "" >> $GITHUB_STEP_SUMMARY exit $VET_STATUS - name: Run go fmt run: | FMT_OUTPUT=$(gofmt -s -l . 2>&1 || echo "") if [ -z "$FMT_OUTPUT" ]; then echo "โœ… **go fmt:** All files properly formatted" >> $GITHUB_STEP_SUMMARY else echo "โŒ **go fmt:** Files need formatting" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "$FMT_OUTPUT" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY exit 1 fi - name: Job Summary if: always() run: | echo "## ๐Ÿ“‹ Job Summary (Go ${{ matrix.go }})" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY echo "|------|--------|" >> $GITHUB_STEP_SUMMARY echo "| Dependencies | โœ… Success |" >> $GITHUB_STEP_SUMMARY echo "| Build | โœ… Success |" >> $GITHUB_STEP_SUMMARY echo "| Tests | ${{ steps.test.outcome == 'success' && 'โœ… Success' || 'โŒ Failed' }} |" >> $GITHUB_STEP_SUMMARY echo "| Coverage | ${{ job.status == 'success' && 'โœ… Generated' || 'โš ๏ธ Partial' }} |" >> $GITHUB_STEP_SUMMARY echo "| Static Analysis | ${{ job.status == 'success' && 'โœ… Clean' || 'โŒ Issues' }} |" >> $GITHUB_STEP_SUMMARY echo "| Code Formatting | ${{ job.status == 'success' && 'โœ… Clean' || 'โŒ Issues' }} |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5 with: flags: Go ${{ matrix.go }} slug: kjanat/articulate-parser token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} uses: codecov/test-results-action@v1 with: flags: Go ${{ matrix.go }} token: ${{ secrets.CODECOV_TOKEN }} docker-test: name: Docker Build Test runs-on: ubuntu-latest if: github.event_name == 'pull_request' permissions: contents: read steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Capture build date run: echo "BUILD_TIME=$(git log -1 --format=%cd --date=iso-strict)" >> $GITHUB_ENV - name: Build Docker image (test) uses: docker/build-push-action@v6 with: context: . push: false load: true tags: test:latest build-args: | VERSION=test BUILD_TIME=${{ env.BUILD_TIME }} GIT_COMMIT=${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max - name: Test Docker image run: | echo "## ๐Ÿงช Docker Image Tests" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Test that the image runs and shows help echo "**Testing help command:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY docker run --rm test:latest --help >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Test image size IMAGE_SIZE=$(docker image inspect test:latest --format='{{.Size}}' | numfmt --to=iec-i --suffix=B) echo "**Image size:** $IMAGE_SIZE" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY dependency-review: name: Dependency Review runs-on: ubuntu-latest permissions: contents: read if: github.event_name == 'pull_request' steps: - name: 'Checkout Repository' uses: actions/checkout@v4 - name: 'Dependency Review' uses: actions/dependency-review-action@v4 with: fail-on-severity: moderate comment-summary-in-pr: always release: name: Release runs-on: ubuntu-latest if: github.ref_type == 'tag' permissions: contents: write needs: ['test'] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: 'go.mod' check-latest: true - name: Run tests run: | echo "## ๐Ÿš€ Release Tests" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY go test -v ./... 2>&1 | tee release-test-output.log TEST_STATUS=$? TOTAL_TESTS=$(grep -c "=== RUN" release-test-output.log || echo "0") PASSED_TESTS=$(grep -c "--- PASS:" release-test-output.log || echo "0") FAILED_TESTS=$(grep -c "--- FAIL:" release-test-output.log || echo "0") echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY echo "| Total Tests | $TOTAL_TESTS |" >> $GITHUB_STEP_SUMMARY echo "| Passed | โœ… $PASSED_TESTS |" >> $GITHUB_STEP_SUMMARY echo "| Failed | โŒ $FAILED_TESTS |" >> $GITHUB_STEP_SUMMARY echo "| Status | $([ $TEST_STATUS -eq 0 ] && echo "โœ… PASSED" || echo "โŒ FAILED") |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY exit $TEST_STATUS - name: Install UPX run: | sudo apt-get update sudo apt-get install -y upx - name: Build binaries run: | echo "## ๐Ÿ”จ Build Process" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Set the build time environment variable using git commit timestamp BUILD_TIME=$(git log -1 --format=%cd --date=iso-strict) # Add run permissions to the build script chmod +x ./scripts/build.sh # Display help information for the build script ./scripts/build.sh --help echo "**Build Configuration:**" >> $GITHUB_STEP_SUMMARY echo "- Version: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY echo "- Build Time: $BUILD_TIME" >> $GITHUB_STEP_SUMMARY echo "- Git Commit: ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Build for all platforms ./scripts/build.sh \ --verbose \ -ldflags "-s -w -X github.com/kjanat/articulate-parser/internal/version.Version=${{ github.ref_name }} -X github.com/kjanat/articulate-parser/internal/version.BuildTime=$BUILD_TIME -X github.com/kjanat/articulate-parser/internal/version.GitCommit=${{ github.sha }}" - name: Compress binaries with UPX run: | echo "## ๐Ÿ“ฆ Binary Compression" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Compressing binaries with UPX..." cd build/ # Get original sizes echo "**Original sizes:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY ls -lah >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Compress all binaries except Darwin (macOS) binaries as UPX doesn't work well with recent macOS versions for binary in articulate-parser-*; do echo "Compressing $binary..." upx --best "$binary" || { echo "Warning: UPX compression failed for $binary, keeping original" } # if [[ "$binary" == *"darwin"* ]]; then # echo "Skipping UPX compression for $binary (macOS compatibility)" # else # echo "Compressing $binary..." # upx --best "$binary" || { # removed `--lzma` # echo "Warning: UPX compression failed for $binary, keeping original" # } # fi done echo "**Final sizes:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY ls -lah >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - name: Upload a Build Artifact uses: actions/upload-artifact@v4.6.2 with: name: build-artifacts path: build/ if-no-files-found: ignore retention-days: 1 compression-level: 9 overwrite: true include-hidden-files: true - name: Create Release uses: softprops/action-gh-release@v2 with: files: build/* generate_release_notes: true draft: false # Mark v0.x.x releases as prerelease (pre-1.0 versions are considered unstable) prerelease: ${{ startsWith(github.ref, 'refs/tags/v0.') }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} docker: name: Docker Build & Push runs-on: ubuntu-latest permissions: contents: read packages: write needs: ['test'] if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/feature/docker')) steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}} type=raw,value=latest,enable={{is_default_branch}} labels: | org.opencontainers.image.title=Articulate Parser org.opencontainers.image.description=A powerful CLI tool to parse Articulate Rise courses and export them to multiple formats including Markdown HTML and DOCX. Supports media extraction content cleaning and batch processing for educational content conversion. org.opencontainers.image.vendor=kjanat org.opencontainers.image.licenses=MIT org.opencontainers.image.url=https://github.com/${{ github.repository }} org.opencontainers.image.source=https://github.com/${{ github.repository }} org.opencontainers.image.documentation=https://github.com/${{ github.repository }}/blob/master/DOCKER.md - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . # Multi-architecture build - Docker automatically provides TARGETOS, TARGETARCH, etc. # Based on Go's supported platforms from 'go tool dist list' platforms: | linux/amd64 linux/arm64 linux/arm/v7 linux/386 linux/ppc64le linux/s390x push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} annotations: ${{ steps.meta.outputs.labels }} build-args: | VERSION=${{ github.ref_type == 'tag' && github.ref_name || github.sha }} BUILD_TIME=${{ github.event.head_commit.timestamp }} GIT_COMMIT=${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=A powerful CLI tool to parse Articulate Rise courses and export them to multiple formats including Markdown HTML and DOCX. Supports media extraction content cleaning and batch processing for educational content conversion. sbom: true provenance: true - name: Generate Docker summary run: | echo "## ๐Ÿณ Docker Build Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Image:** \`ghcr.io/${{ github.repository }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Tags built:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Features:**" >> $GITHUB_STEP_SUMMARY echo "- **Platforms:** linux/amd64, linux/arm64, linux/arm/v7, linux/386, linux/ppc64le, linux/s390x" >> $GITHUB_STEP_SUMMARY echo "- **Architecture optimization:** โœ… Native compilation for each platform" >> $GITHUB_STEP_SUMMARY echo "- **Multi-arch image description:** โœ… Enabled" >> $GITHUB_STEP_SUMMARY echo "- **SBOM (Software Bill of Materials):** โœ… Generated" >> $GITHUB_STEP_SUMMARY echo "- **Provenance attestation:** โœ… Generated" >> $GITHUB_STEP_SUMMARY echo "- **Security scanning:** Ready for vulnerability analysis" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Usage:**" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY echo "# Pull the image" >> $GITHUB_STEP_SUMMARY echo "docker pull ghcr.io/${{ github.repository }}:latest" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "# Run with help" >> $GITHUB_STEP_SUMMARY echo "docker run --rm ghcr.io/${{ github.repository }}:latest --help" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "# Process a local file (mount current directory)" >> $GITHUB_STEP_SUMMARY echo "docker run --rm -v \$(pwd):/workspace ghcr.io/${{ github.repository }}:latest /workspace/input.json markdown /workspace/output.md" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY # Security and quality analysis workflows codeql-analysis: name: CodeQL Analysis uses: ./.github/workflows/codeql.yml permissions: security-events: write packages: read actions: read contents: read