Build Results #74
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: Build Results | |
| on: | |
| workflow_run: # zizmor: ignore[dangerous-triggers] consumes completed-build artifacts + PR metadata; never checks out or runs PR head code | |
| # GitHub requires literal workflow names here (no expressions). Keep this | |
| # list in sync with build-config.json's `platform_workflows` plus any | |
| # non-platform triggers (pre-commit). Drift is enforced by | |
| # .github/scripts/tests/test_platform_workflow_names.py. | |
| workflows: [Linux, Windows, MacOS, Android, pre-commit] | |
| types: [completed] | |
| permissions: | |
| contents: read | |
| actions: read | |
| env: | |
| QGC_GH_API_MODE: http | |
| jobs: | |
| load-config: | |
| name: Load Build Config | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| outputs: | |
| platform_workflows: ${{ steps.read.outputs.platform_workflows }} | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - uses: actions/checkout@v7 | |
| with: | |
| sparse-checkout: | | |
| .github/build-config.json | |
| tools/_bootstrap.py | |
| tools/setup/read_config.py | |
| tools/common/__init__.py | |
| tools/common/build_config.py | |
| tools/common/file_traversal.py | |
| tools/common/gh_actions.py | |
| tools/common/io.py | |
| sparse-checkout-cone-mode: false | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Read platform_workflows | |
| id: read | |
| run: | | |
| value=$(python3 tools/setup/read_config.py --get build.platform_workflows) | |
| if [[ -z "$value" || "$value" == "null" ]]; then | |
| echo "::error::platform_workflows missing from build-config.json" | |
| exit 1 | |
| fi | |
| echo "platform_workflows=${value}" >> "$GITHUB_OUTPUT" | |
| post-pr-comment: | |
| name: Post Build Results | |
| needs: load-config | |
| runs-on: ubuntu-latest | |
| if: github.event.workflow_run.event == 'pull_request' | |
| concurrency: | |
| group: build-results-pr-${{ github.event.workflow_run.head_sha }} | |
| cancel-in-progress: false | |
| timeout-minutes: 10 | |
| # checks/pull-requests/issues write used only by this results-posting job. | |
| permissions: | |
| contents: read | |
| actions: read | |
| checks: write | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout local actions | |
| uses: actions/checkout@v7 | |
| with: | |
| sparse-checkout: | | |
| .github/actions/gate-platform-workflows | |
| .github/actions/build-results-bootstrap | |
| sparse-checkout-cone-mode: false | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Get PR number | |
| id: pr | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const run = context.payload.workflow_run; | |
| if (run.pull_requests && run.pull_requests.length > 0) { | |
| return run.pull_requests[0].number; | |
| } | |
| // For fork PRs, run.pull_requests is empty. Use head_repository owner. | |
| const headOwner = run.head_repository?.owner?.login || context.repo.owner; | |
| const prs = await github.paginate( | |
| github.rest.pulls.list, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| head: `${headOwner}:${run.head_branch}`, | |
| per_page: 100 | |
| } | |
| ); | |
| const exact = prs.find(pr => pr.head?.sha === run.head_sha); | |
| return exact ? exact.number : ''; | |
| - name: Check if all platform workflows are complete | |
| id: gate | |
| if: steps.pr.outputs.result != '' | |
| uses: ./.github/actions/gate-platform-workflows | |
| with: | |
| platform-workflows: ${{ needs.load-config.outputs.platform_workflows }} | |
| head-sha: ${{ github.event.workflow_run.head_sha }} | |
| event: pull_request | |
| runs-cache-path: ${{ runner.temp }}/workflow-runs-cache.json | |
| - name: Bootstrap (checkout + Python deps) | |
| if: steps.pr.outputs.result != '' && steps.gate.outputs.ready == 'true' | |
| uses: ./.github/actions/build-results-bootstrap | |
| - name: Collect build status | |
| if: steps.pr.outputs.result != '' && steps.gate.outputs.ready == 'true' | |
| id: builds | |
| env: | |
| PLATFORM_WORKFLOWS: ${{ needs.load-config.outputs.platform_workflows }} | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| RUNS_CACHE: ${{ runner.temp }}/workflow-runs-cache.json | |
| run: | | |
| python3 "${GITHUB_WORKSPACE}/.github/scripts/collect_build_status.py" \ | |
| --repo "$REPO" \ | |
| --head-sha "$HEAD_SHA" \ | |
| --platform-workflows "${PLATFORM_WORKFLOWS}" \ | |
| --event pull_request \ | |
| --runs-input "${RUNS_CACHE}" \ | |
| --runs-cache workflow-runs-cache.json | |
| - name: Download artifacts from all platform workflows | |
| id: download | |
| if: steps.pr.outputs.result != '' && steps.builds.outputs.all_complete == 'true' | |
| continue-on-error: true | |
| uses: ./.github/actions/download-all-artifacts | |
| with: | |
| head-sha: ${{ github.event.workflow_run.head_sha }} | |
| workflows: ${{ needs.load-config.outputs.platform_workflows }} | |
| event: pull_request | |
| runs-file: workflow-runs-cache.json | |
| artifact-prefixes: coverage-report,test-results- | |
| artifact-metadata-file: workflow-artifacts.json | |
| - name: Post fallback comment on artifact download failure | |
| if: steps.pr.outputs.result != '' && steps.download.outcome == 'failure' | |
| uses: thollander/actions-comment-pull-request@v3 | |
| with: | |
| pr-number: ${{ steps.pr.outputs.result }} | |
| comment-tag: build-results | |
| message: | | |
| <!-- thollander/actions-comment-pull-request "build-results" --> | |
| ⚠️ **Build results unavailable** — artifact download from one or more platform workflows failed (likely artifact retention expiry or transient API error). The combined report cannot be generated for this run. | |
| See the [Build Results workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. | |
| - name: Stop if artifact download failed | |
| if: steps.pr.outputs.result != '' && steps.download.outcome == 'failure' | |
| run: | | |
| echo "::error::Artifact download failed; skipping combined report generation." | |
| exit 1 | |
| - name: Collect artifact sizes | |
| if: steps.pr.outputs.result != '' && steps.builds.outputs.all_complete == 'true' | |
| uses: ./.github/actions/collect-artifact-sizes | |
| with: | |
| head-sha: ${{ github.event.workflow_run.head_sha }} | |
| workflows: ${{ needs.load-config.outputs.platform_workflows }} | |
| event: pull_request | |
| output-file: pr-sizes.json | |
| runs-file: workflow-runs-cache.json | |
| artifacts-file: workflow-artifacts.json | |
| - name: Download pre-commit results artifact | |
| if: steps.pr.outputs.result != '' && steps.builds.outputs.all_complete == 'true' && steps.builds.outputs.precommit_run_id != '' | |
| continue-on-error: true | |
| uses: actions/download-artifact@v8 | |
| with: | |
| run-id: ${{ steps.builds.outputs.precommit_run_id }} | |
| github-token: ${{ github.token }} | |
| name: pre-commit-results | |
| path: artifacts/pre-commit-results | |
| - name: Restore baseline sizes | |
| if: steps.pr.outputs.result != '' && steps.builds.outputs.all_complete == 'true' | |
| uses: actions/cache/restore@v6 | |
| continue-on-error: true | |
| with: | |
| path: baseline-sizes.json | |
| key: artifact-sizes-baseline-latest | |
| - name: Restore baseline coverage | |
| if: steps.pr.outputs.result != '' && steps.builds.outputs.all_complete == 'true' | |
| uses: actions/cache/restore@v6 | |
| continue-on-error: true | |
| with: | |
| path: baseline-coverage.xml | |
| key: coverage-baseline-latest | |
| - name: Generate combined report | |
| if: steps.pr.outputs.result != '' && steps.gate.outputs.ready == 'true' | |
| env: | |
| BUILD_TABLE: ${{ steps.builds.outputs.table }} | |
| BUILD_SUMMARY: ${{ steps.builds.outputs.summary }} | |
| PRECOMMIT_STATUS: ${{ steps.builds.outputs.precommit_status }} | |
| PRECOMMIT_URL: ${{ steps.builds.outputs.precommit_url }} | |
| PRECOMMIT_RESULTS_PATH: artifacts/pre-commit-results/pre-commit-results.json | |
| TEST_RESULTS_GLOB: artifacts/test-results-*/test-output*.txt | |
| COVERAGE_XML: artifacts/coverage-report/coverage.xml | |
| BASELINE_COVERAGE_XML: baseline-coverage.xml | |
| PR_SIZES_JSON: pr-sizes.json | |
| BASELINE_SIZES_JSON: baseline-sizes.json | |
| TRIGGERED_BY: ${{ github.event.workflow_run.name }} | |
| COMMIT_SHA: ${{ github.event.workflow_run.head_sha }} | |
| run: | | |
| python3 "${GITHUB_WORKSPACE}/.github/scripts/generate_build_results_comment.py" \ | |
| --base-dir "${GITHUB_WORKSPACE}" \ | |
| --output comment.md | |
| cat comment.md | |
| - name: Deduplicate prior build-results comments | |
| if: steps.pr.outputs.result != '' && steps.gate.outputs.ready == 'true' | |
| uses: actions/github-script@v9 | |
| env: | |
| PR_NUMBER: ${{ steps.pr.outputs.result }} | |
| with: | |
| script: | | |
| const issue_number = Number(process.env.PR_NUMBER); | |
| if (!Number.isInteger(issue_number) || issue_number <= 0) { | |
| return; | |
| } | |
| const marker = 'thollander/actions-comment-pull-request "build-results"'; | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number, | |
| per_page: 100 | |
| } | |
| ); | |
| const tagged = comments | |
| .filter(c => c.user?.type === 'Bot' && c.body?.includes(marker)) | |
| .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); | |
| for (const comment of tagged.slice(1)) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: comment.id | |
| }); | |
| } | |
| - name: Post combined comment | |
| if: steps.pr.outputs.result != '' && steps.gate.outputs.ready == 'true' | |
| uses: thollander/actions-comment-pull-request@v3 | |
| with: | |
| pr-number: ${{ steps.pr.outputs.result }} | |
| comment-tag: build-results | |
| file-path: comment.md | |
| save-baselines: | |
| name: Save Baselines | |
| needs: load-config | |
| runs-on: ubuntu-latest | |
| if: > | |
| github.event.workflow_run.head_branch == 'master' && | |
| github.event.workflow_run.conclusion == 'success' | |
| concurrency: | |
| group: save-baselines | |
| cancel-in-progress: false | |
| timeout-minutes: 10 | |
| permissions: | |
| actions: write | |
| contents: read | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout local actions | |
| uses: actions/checkout@v7 | |
| with: | |
| sparse-checkout: | | |
| .github/actions/gate-platform-workflows | |
| .github/actions/build-results-bootstrap | |
| sparse-checkout-cone-mode: false | |
| fetch-depth: 1 | |
| persist-credentials: false | |
| - name: Check if all platform workflows succeeded | |
| id: gate | |
| uses: ./.github/actions/gate-platform-workflows | |
| with: | |
| platform-workflows: ${{ needs.load-config.outputs.platform_workflows }} | |
| head-sha: ${{ github.event.workflow_run.head_sha }} | |
| event: push | |
| require-success: 'true' | |
| runs-cache-path: ${{ runner.temp }}/workflow-runs-cache.json | |
| - name: Bootstrap (checkout + Python deps) | |
| if: steps.gate.outputs.ready == 'true' | |
| uses: ./.github/actions/build-results-bootstrap | |
| - name: Verify all platform workflows succeeded for baseline SHA | |
| if: steps.gate.outputs.ready == 'true' | |
| id: ready | |
| env: | |
| PLATFORM_WORKFLOWS: ${{ needs.load-config.outputs.platform_workflows }} | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| RUNS_CACHE: ${{ runner.temp }}/workflow-runs-cache.json | |
| run: | | |
| python3 "${GITHUB_WORKSPACE}/.github/scripts/check_baseline_ready.py" \ | |
| --repo "$REPO" \ | |
| --head-sha "$HEAD_SHA" \ | |
| --platform-workflows "${PLATFORM_WORKFLOWS}" \ | |
| --event push \ | |
| --runs-input "${RUNS_CACHE}" \ | |
| --runs-cache workflow-runs-cache.json | |
| - name: Download artifacts from all platform workflows | |
| if: steps.ready.outputs.ready == 'true' | |
| continue-on-error: true | |
| uses: ./.github/actions/download-all-artifacts | |
| with: | |
| head-sha: ${{ github.event.workflow_run.head_sha }} | |
| workflows: ${{ needs.load-config.outputs.platform_workflows }} | |
| event: push | |
| runs-file: workflow-runs-cache.json | |
| artifact-prefixes: coverage-report,test-results- | |
| artifact-metadata-file: workflow-artifacts.json | |
| - name: Collect artifact sizes | |
| if: steps.ready.outputs.ready == 'true' | |
| uses: ./.github/actions/collect-artifact-sizes | |
| with: | |
| head-sha: ${{ github.event.workflow_run.head_sha }} | |
| workflows: ${{ needs.load-config.outputs.platform_workflows }} | |
| event: push | |
| output-file: baseline-sizes.json | |
| runs-file: workflow-runs-cache.json | |
| artifacts-file: workflow-artifacts.json | |
| - name: Publish artifact sizes baseline | |
| if: steps.ready.outputs.ready == 'true' && hashFiles('baseline-sizes.json') != '' | |
| uses: ./.github/actions/replace-cache-entry | |
| with: | |
| path: baseline-sizes.json | |
| key: artifact-sizes-baseline-latest | |
| - name: Copy coverage to baseline path | |
| if: steps.ready.outputs.ready == 'true' && hashFiles('artifacts/coverage-report/coverage.xml') != '' | |
| run: cp artifacts/coverage-report/coverage.xml baseline-coverage.xml | |
| - name: Publish coverage baseline | |
| if: steps.ready.outputs.ready == 'true' && hashFiles('baseline-coverage.xml') != '' | |
| uses: ./.github/actions/replace-cache-entry | |
| with: | |
| path: baseline-coverage.xml | |
| key: coverage-baseline-latest |