PR Build Test #5
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 | |
| - riscv64-Linux | |
| - ALL | |
| default: x86_64-Linux | |
| 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@v4 | |
| 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 }} | |
| run: | | |
| # Get changed files between base and PR head | |
| CHANGED_FILES=$(git diff --name-only "${BASE_SHA}" HEAD -- 'binaries/**/*.yaml' 'packages/**/*.yaml' 2>/dev/null || true) | |
| echo "Changed recipe files:" | |
| echo "$CHANGED_FILES" | |
| CHANGED_RECIPES="[]" | |
| for file in $CHANGED_FILES; do | |
| if [ -f "$file" ]; then | |
| CHANGED_RECIPES=$(echo "$CHANGED_RECIPES" | jq --arg path "$file" '. + [{"path": $path}]') | |
| fi | |
| done | |
| RECIPE_COUNT=$(echo "$CHANGED_RECIPES" | jq 'length') | |
| echo "::notice::Found ${RECIPE_COUNT} changed recipes" | |
| 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: ${{ contains(matrix.recipe.path, 'packages/') && format('ghcr.io/{0}/pkgcache', github.repository_owner) || format('ghcr.io/{0}/bincache', 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@v4 | |
| with: | |
| ref: ${{ needs.detect-changes.outputs.pr_head_sha }} | |
| - name: Download sbuild-linter (for hash) | |
| run: | | |
| curl -fsSL "https://github.com/pkgforge/sbuilder/releases/download/latest/sbuild-linter-x86_64-linux" \ | |
| -o /usr/local/bin/sbuild-linter || exit 0 | |
| chmod +x /usr/local/bin/sbuild-linter | |
| - name: Download sbuild-cache | |
| run: | | |
| curl -fsSL "https://github.com/pkgforge/sbuilder/releases/download/latest/sbuild-cache-x86_64-linux" \ | |
| -o /usr/local/bin/sbuild-cache || exit 0 | |
| chmod +x /usr/local/bin/sbuild-cache | |
| - name: Download existing cache | |
| continue-on-error: true | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh release download build-cache -p build_cache.sdb -D /tmp/ --repo "${{ github.repository }}" || \ | |
| sbuild-cache --cache /tmp/build_cache.sdb init | |
| - name: Update cache with recipe hashes | |
| run: | | |
| RECIPES='${{ needs.detect-changes.outputs.changed_recipes }}' | |
| BUILD_RESULT="${{ needs.build.result }}" | |
| echo "$RECIPES" | jq -c '.[]' | while read -r recipe; do | |
| path=$(echo "$recipe" | jq -r '.path') | |
| pkg_name=$(basename "$(dirname "$path")") | |
| # Extract version from recipe's pkgver field | |
| pkg_version="unknown" | |
| if [ -f "$path" ]; then | |
| pkg_version=$(grep -E "^pkgver:" "$path" | head -1 | sed 's/pkgver:[[:space:]]*//; s/^["'"'"']//; s/["'"'"']$//' || echo "unknown") | |
| [ -z "$pkg_version" ] && pkg_version="unknown" | |
| fi | |
| # Compute recipe hash (excluding version for consistency) | |
| if [ -f "$path" ]; then | |
| recipe_hash=$(sbuild-linter hash --exclude-version "$path" 2>/dev/null || sha256sum "$path" | cut -d' ' -f1) | |
| else | |
| recipe_hash="unknown" | |
| fi | |
| status="success" | |
| if [ "$BUILD_RESULT" != "success" ]; then | |
| status="failure" | |
| fi | |
| echo "Caching: $pkg_name v${pkg_version} (hash: ${recipe_hash:0:16}..., status: $status)" | |
| sbuild-cache --cache /tmp/build_cache.sdb update \ | |
| --package "$pkg_name" \ | |
| --version "$pkg_version" \ | |
| --hash "$recipe_hash" \ | |
| --status "$status" || true | |
| done | |
| - name: Upload updated cache | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| 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@v4 | |
| 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 path | |
| if echo "$recipe_path" | grep -q "^packages/"; then | |
| CACHE_TYPE="pkgcache" | |
| else | |
| CACHE_TYPE="bincache" | |
| fi | |
| case "$status" in | |
| success) | |
| STATUS_ICON="✅" | |
| SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) | |
| if [ -n "$ghcr_url" ] && [ "$ghcr_url" != "null" ] && [ "$ghcr_url" != "" ]; then | |
| # Convert ghcr.io URL to GitHub packages URL | |
| # ghcr.io/owner/repo/path:tag -> github.com/owner/repo/pkgs/container/path | |
| PKG_PATH=$(echo "$ghcr_url" | sed 's|ghcr.io/||' | cut -d':' -f1) | |
| # URL encode forward slashes after the repo name | |
| OWNER=$(echo "$PKG_PATH" | cut -d'/' -f1) | |
| REPO=$(echo "$PKG_PATH" | cut -d'/' -f2) | |
| CONTAINER_PATH=$(echo "$PKG_PATH" | cut -d'/' -f3- | sed 's|/|%2F|g') | |
| GITHUB_PKG_URL="https://github.com/${OWNER}/${REPO}/pkgs/container/${CONTAINER_PATH}" | |
| PKG_LINK="[📦 View](${GITHUB_PKG_URL})" | |
| else | |
| # Fallback: construct URL from recipe info | |
| GITHUB_PKG_URL="https://github.com/${REPO_OWNER}/${CACHE_TYPE}/pkgs/container/${pkg_family}%2F${recipe_name}" | |
| PKG_LINK="[📦 View](${GITHUB_PKG_URL})" | |
| 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 }}" |