PR Build Test #93
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR Build Test | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: 'PR number to build' | |
| type: number | |
| required: true | |
| host: | |
| description: 'Target host' | |
| type: choice | |
| options: | |
| - x86_64-linux | |
| - aarch64-linux | |
| - ALL | |
| default: x86_64-linux | |
| package: | |
| description: 'Specific package to build (e.g. "bat" or "packages/bat/static.stable.yaml"). Leave empty to auto-detect from PR diff.' | |
| type: string | |
| required: false | |
| permissions: | |
| attestations: write | |
| contents: write | |
| id-token: write | |
| packages: write | |
| pull-requests: write | |
| concurrency: | |
| group: pr-build-${{ inputs.pr_number }} | |
| cancel-in-progress: true | |
| jobs: | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| changed_recipes: ${{ steps.detect.outputs.changed_recipes }} | |
| has_changes: ${{ steps.detect.outputs.has_changes }} | |
| pr_head_sha: ${{ steps.pr-info.outputs.head_sha }} | |
| steps: | |
| - name: Get PR info | |
| id: pr-info | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| PR_DATA=$(gh api repos/${{ github.repository }}/pulls/${{ inputs.pr_number }}) | |
| HEAD_SHA=$(echo "$PR_DATA" | jq -r '.head.sha') | |
| BASE_SHA=$(echo "$PR_DATA" | jq -r '.base.sha') | |
| HEAD_REF=$(echo "$PR_DATA" | jq -r '.head.ref') | |
| echo "head_sha=${HEAD_SHA}" >> $GITHUB_OUTPUT | |
| echo "base_sha=${BASE_SHA}" >> $GITHUB_OUTPUT | |
| echo "head_ref=${HEAD_REF}" >> $GITHUB_OUTPUT | |
| echo "::notice::PR #${{ inputs.pr_number }}: ${HEAD_REF} (${HEAD_SHA})" | |
| - name: Checkout PR | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ steps.pr-info.outputs.head_sha }} | |
| fetch-depth: 0 | |
| - name: Detect changed recipes | |
| id: detect | |
| env: | |
| BASE_SHA: ${{ steps.pr-info.outputs.base_sha }} | |
| PACKAGE_INPUT: ${{ inputs.package }} | |
| run: | | |
| CHANGED_RECIPES="[]" | |
| if [ -n "$PACKAGE_INPUT" ]; then | |
| echo "::notice::Package specified: ${PACKAGE_INPUT}" | |
| if [ -f "$PACKAGE_INPUT" ]; then | |
| # Full path provided (e.g. packages/bat/static.stable.yaml) | |
| CHANGED_RECIPES=$(echo "$CHANGED_RECIPES" | jq --arg path "$PACKAGE_INPUT" '. + [{"path": $path}]') | |
| else | |
| # Package name provided (e.g. "bat") - find all recipes under it | |
| for dir in packages binaries; do | |
| for file in "${dir}/${PACKAGE_INPUT}"/*.yaml; do | |
| if [ -f "$file" ]; then | |
| CHANGED_RECIPES=$(echo "$CHANGED_RECIPES" | jq --arg path "$file" '. + [{"path": $path}]') | |
| fi | |
| done | |
| done | |
| fi | |
| else | |
| # Auto-detect from PR diff | |
| CHANGED_FILES=$(git diff --name-only "${BASE_SHA}" HEAD -- 'binaries/**/*.yaml' 'packages/**/*.yaml' 2>/dev/null || true) | |
| echo "Changed recipe files:" | |
| echo "$CHANGED_FILES" | |
| for file in $CHANGED_FILES; do | |
| if [ -f "$file" ]; then | |
| CHANGED_RECIPES=$(echo "$CHANGED_RECIPES" | jq --arg path "$file" '. + [{"path": $path}]') | |
| fi | |
| done | |
| fi | |
| RECIPE_COUNT=$(echo "$CHANGED_RECIPES" | jq 'length') | |
| echo "::notice::Found ${RECIPE_COUNT} recipes to build" | |
| echo "changed_recipes=$(echo "$CHANGED_RECIPES" | jq -c .)" >> $GITHUB_OUTPUT | |
| if [ "$RECIPE_COUNT" -gt 0 ]; then | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| fi | |
| build: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.has_changes == 'true' | |
| strategy: | |
| fail-fast: false | |
| max-parallel: 2 | |
| matrix: | |
| recipe: ${{ fromJson(needs.detect-changes.outputs.changed_recipes) }} | |
| uses: ./.github/workflows/matrix_builds.yaml | |
| with: | |
| host: ${{ inputs.host }} | |
| sbuild-url: "https://raw.githubusercontent.com/${{ github.repository }}/${{ needs.detect-changes.outputs.pr_head_sha }}/${{ matrix.recipe.path }}" | |
| ghcr-url: ${{ format('ghcr.io/{0}', github.repository_owner) }} | |
| pkg-family: ${{ github.event.repository.name }} | |
| rebuild: true | |
| logs: true | |
| metadata-release: false | |
| secrets: inherit | |
| update-cache: | |
| needs: [detect-changes, build] | |
| if: always() && needs.detect-changes.outputs.has_changes == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout PR | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ needs.detect-changes.outputs.pr_head_sha }} | |
| - name: Download sbuild | |
| run: | | |
| curl -fsSL "https://github.com/pkgforge/sbuilder/releases/download/nightly/sbuild-x86_64-linux" \ | |
| -o /usr/local/bin/sbuild && chmod +x /usr/local/bin/sbuild | |
| sbuild --version | |
| - name: Download build status artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: build-status-* | |
| path: /tmp/build-statuses | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Update cache with build results | |
| env: | |
| SBUILD_CACHE_URI: ${{ secrets.SBUILD_CACHE_URI }} | |
| run: | | |
| # sbuild updates cache directly on success via SBUILD_CACHE_URI. | |
| # This step only records failures/skips that sbuild didn't handle. | |
| RECIPES='${{ needs.detect-changes.outputs.changed_recipes }}' | |
| echo "$RECIPES" | jq -c '.[]' | while read -r recipe; do | |
| path=$(echo "$recipe" | jq -r '.path') | |
| # Extract pkg_id and pkg name from recipe YAML for cache operations | |
| pkg_id="" | |
| pkg_name="" | |
| if [ -f "$path" ]; then | |
| pkg_id=$(grep -E '^pkg_id:' "$path" | head -1 | sed 's/pkg_id:[[:space:]]*//; s/^["'"'"']//; s/["'"'"']$//') | |
| pkg_name=$(grep -E '^pkg:' "$path" | head -1 | sed 's/pkg:[[:space:]]*//; s/^["'"'"']//; s/["'"'"']$//') | |
| fi | |
| # Fallback to pkg name from recipe | |
| if [ -z "$pkg_id" ]; then | |
| pkg_id="${pkg_name:-$(basename "$(dirname "$path")")}" | |
| fi | |
| # Extract version from recipe's pkgver field | |
| pkg_version="" | |
| if [ -f "$path" ]; then | |
| pkg_version=$(grep -E "^pkgver:" "$path" | head -1 | sed 's/pkgver:[[:space:]]*//; s/^["'"'"']//; s/["'"'"']$//' || echo "") | |
| fi | |
| # Skip if we have no version | |
| if [ -z "$pkg_version" ]; then | |
| echo "Skipping $pkg_id: no version available" | |
| continue | |
| fi | |
| # Compute recipe hash | |
| if [ -f "$path" ] && command -v sbuild &>/dev/null; then | |
| recipe_hash=$(sbuild meta hash --exclude-version "$path" 2>/dev/null || sha256sum "$path" | cut -d' ' -f1) | |
| else | |
| recipe_hash=$(sha256sum "$path" 2>/dev/null | cut -d' ' -f1 || echo "") | |
| fi | |
| # Process build status for each host architecture | |
| for host in x86_64-linux aarch64-linux; do | |
| status="" | |
| if [ -d "/tmp/build-statuses" ]; then | |
| for status_file in /tmp/build-statuses/build-status.json /tmp/build-statuses/*/build-status.json; do | |
| [ -f "$status_file" ] || continue | |
| recipe_url=$(jq -r '.recipe_url // ""' "$status_file" 2>/dev/null || echo "") | |
| file_host=$(jq -r '.host // ""' "$status_file" 2>/dev/null || echo "") | |
| if echo "$recipe_url" | grep -q "$path" && [ "$file_host" = "$host" ]; then | |
| file_status=$(jq -r '.status // ""' "$status_file" 2>/dev/null || echo "") | |
| case "$file_status" in | |
| failure) status="failed" ;; | |
| skipped) status="skipped" ;; | |
| success) status="" ;; # Already handled by sbuild | |
| *) status="" ;; | |
| esac | |
| break | |
| fi | |
| done | |
| fi | |
| # Only update cache for failed/skipped (success handled by sbuild) | |
| if [ -z "$status" ]; then | |
| continue | |
| fi | |
| echo "Package: $pkg_id ($host), Version: $pkg_version, Status: $status" | |
| sbuild cache update \ | |
| --package "$pkg_id" \ | |
| --host "$host" \ | |
| --version "$pkg_version" \ | |
| --hash "$recipe_hash" \ | |
| --status "$status" || echo "Warning: Failed to update cache for $pkg_id ($host)" | |
| done | |
| done | |
| - name: Generate build summary | |
| env: | |
| SBUILD_CACHE_URI: ${{ secrets.SBUILD_CACHE_URI }} | |
| run: | | |
| sbuild cache gh-summary \ | |
| --title "PR Build Results" \ | |
| --host x86_64-linux || true | |
| - name: Export cache to SQLite and upload | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| SBUILD_CACHE_URI: ${{ secrets.SBUILD_CACHE_URI }} | |
| run: | | |
| sbuild cache export --output /tmp/build_cache.sdb || true | |
| if [ -f "/tmp/build_cache.sdb" ]; then | |
| gh release upload build-cache /tmp/build_cache.sdb --clobber --repo "${{ github.repository }}" || { | |
| gh release create build-cache \ | |
| --title "Build Cache" \ | |
| --notes "Build cache for CI" \ | |
| --prerelease \ | |
| --repo "${{ github.repository }}" \ | |
| /tmp/build_cache.sdb | |
| } | |
| fi | |
| comment-result: | |
| needs: [detect-changes, build] | |
| if: always() && needs.detect-changes.outputs.has_changes == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| actions: read | |
| steps: | |
| - name: Download build status artifacts | |
| uses: actions/download-artifact@v7 | |
| with: | |
| pattern: build-status-* | |
| path: /tmp/build-status | |
| merge-multiple: false | |
| continue-on-error: true | |
| - name: Generate detailed comment | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -euo pipefail | |
| BUILD_STATUS="${{ needs.build.result }}" | |
| RECIPES='${{ needs.detect-changes.outputs.changed_recipes }}' | |
| HOST="${{ inputs.host }}" | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| REPO_OWNER="${{ github.repository_owner }}" | |
| # Build results table header | |
| echo "| Recipe | Host | Status | Package |" > /tmp/results_table.md | |
| echo "|--------|------|--------|---------|" >> /tmp/results_table.md | |
| # Process build status artifacts | |
| SUCCESS_COUNT=0 | |
| FAILURE_COUNT=0 | |
| SKIPPED_COUNT=0 | |
| if [ -d "/tmp/build-status" ]; then | |
| for status_dir in /tmp/build-status/build-status-*/; do | |
| [ -d "$status_dir" ] || continue | |
| status_file="${status_dir}build-status.json" | |
| [ -f "$status_file" ] || continue | |
| host=$(jq -r '.host // "unknown"' "$status_file") | |
| status=$(jq -r '.status // "unknown"' "$status_file") | |
| recipe_url=$(jq -r '.recipe_url // ""' "$status_file") | |
| ghcr_url=$(jq -r '.ghcr_url // ""' "$status_file") | |
| # Extract recipe path from URL | |
| recipe_path=$(echo "$recipe_url" | grep -oE '(binaries|packages)/[^"]+\.yaml' || echo "unknown") | |
| pkg_family=$(echo "$recipe_path" | cut -d'/' -f2) | |
| recipe_name=$(basename "$recipe_path" .yaml 2>/dev/null || echo "unknown") | |
| # Determine cache type from pkg_type (first part of recipe name) | |
| pkg_type=$(echo "$recipe_name" | cut -d'.' -f1) | |
| if [ "$pkg_type" = "static" ] || [ "$pkg_type" = "dynamic" ]; then | |
| CACHE_TYPE="bincache" | |
| else | |
| CACHE_TYPE="pkgcache" | |
| fi | |
| case "$status" in | |
| success) | |
| STATUS_ICON="✅" | |
| SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) | |
| if [ -n "$ghcr_url" ] && [ "$ghcr_url" != "null" ] && [ "$ghcr_url" != "" ]; then | |
| # Show GHCR URLs directly (more useful for pulling) | |
| PKG_COUNT=$(echo "$ghcr_url" | tr ',' '\n' | wc -l) | |
| if [ "$PKG_COUNT" -gt 1 ]; then | |
| # Multiple packages - show as code block with all URLs | |
| PKG_LINK=$(echo "$ghcr_url" | tr ',' '\n' | sed 's/^/`/; s/$/`/' | tr '\n' ' ') | |
| else | |
| PKG_LINK="\`${ghcr_url}\`" | |
| fi | |
| else | |
| PKG_LINK="-" | |
| fi | |
| ;; | |
| failure) | |
| STATUS_ICON="❌" | |
| FAILURE_COUNT=$((FAILURE_COUNT + 1)) | |
| PKG_LINK="-" | |
| ;; | |
| skipped) | |
| STATUS_ICON="⏭️" | |
| SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) | |
| PKG_LINK="-" | |
| ;; | |
| *) | |
| STATUS_ICON="⚠️" | |
| PKG_LINK="-" | |
| ;; | |
| esac | |
| echo "| \`${recipe_path}\` | \`${host}\` | ${STATUS_ICON} ${status} | ${PKG_LINK} |" >> /tmp/results_table.md | |
| done | |
| fi | |
| RESULTS_TABLE=$(cat /tmp/results_table.md) | |
| TOTAL=$((SUCCESS_COUNT + FAILURE_COUNT + SKIPPED_COUNT)) | |
| # Overall status header | |
| if [ "$BUILD_STATUS" == "success" ]; then | |
| HEADER_EMOJI="✅" | |
| HEADER_TEXT="Build Test Passed" | |
| elif [ "$BUILD_STATUS" == "failure" ]; then | |
| HEADER_EMOJI="❌" | |
| HEADER_TEXT="Build Test Failed" | |
| elif [ "$BUILD_STATUS" == "skipped" ]; then | |
| HEADER_EMOJI="⏭️" | |
| HEADER_TEXT="Build Test Skipped" | |
| else | |
| HEADER_EMOJI="⚠️" | |
| HEADER_TEXT="Build Test: ${BUILD_STATUS}" | |
| fi | |
| # Summary line | |
| SUMMARY="**${SUCCESS_COUNT}** passed" | |
| [ "$FAILURE_COUNT" -gt 0 ] && SUMMARY="${SUMMARY}, **${FAILURE_COUNT}** failed" | |
| [ "$SKIPPED_COUNT" -gt 0 ] && SUMMARY="${SUMMARY}, **${SKIPPED_COUNT}** skipped" | |
| # Build the comment using heredoc for proper formatting | |
| cat > /tmp/comment.md << EOF | |
| ## ${HEADER_EMOJI} ${HEADER_TEXT} | |
| | | | | |
| |---|---| | |
| | **Target** | \`${HOST}\` | | |
| | **Commit** | \`${{ needs.detect-changes.outputs.pr_head_sha }}\` | | |
| | **Summary** | ${SUMMARY} | | |
| ### Build Results | |
| ${RESULTS_TABLE} | |
| <details> | |
| <summary>📋 View workflow logs</summary> | |
| **Workflow Run:** [${RUN_URL}](${RUN_URL}) | |
| </details> | |
| --- | |
| <sub>🤖 Triggered via workflow_dispatch by @${{ github.actor }}</sub> | |
| EOF | |
| gh pr comment "${{ inputs.pr_number }}" \ | |
| --repo "${{ github.repository }}" \ | |
| --body-file /tmp/comment.md | |
| no-changes: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.has_changes != 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: No recipes to build | |
| run: | | |
| echo "::warning::No recipe changes detected in PR #${{ inputs.pr_number }}" |