Skip to content

Build Results

Build Results #7420

Workflow file for this run

name: Build Results
on:
workflow_run:
workflows: [Linux, Windows, MacOS, Android, pre-commit]
types: [completed]
permissions:
contents: read
actions: read
checks: write
pull-requests: write
issues: write
env:
PLATFORM_WORKFLOWS: Linux,Windows,MacOS,Android
QGC_GH_API_MODE: http
jobs:
post-pr-comment:
name: Post Build Results
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
steps:
- name: Harden Runner
uses: step-security/harden-runner@v2
with:
egress-policy: audit
- name: Get PR number
id: pr
uses: actions/github-script@v8
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 : null;
- name: Check if all platform workflows are complete
id: gate
if: steps.pr.outputs.result != 'null'
uses: actions/github-script@v8
env:
PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with:
script: |
const wanted = new Set(process.env.PLATFORM_WORKFLOWS.split(',').map(s => s.trim()));
const runs = await github.paginate(
github.rest.actions.listWorkflowRunsForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: process.env.HEAD_SHA,
event: 'pull_request',
per_page: 100
}
);
const latest = new Map();
for (const run of runs) {
if (!wanted.has(run.name)) continue;
const prev = latest.get(run.name);
if (!prev || new Date(run.created_at) > new Date(prev.created_at)) {
latest.set(run.name, run);
}
}
const missing = [...wanted].filter(n => !latest.has(n));
const incomplete = [...latest.values()].filter(r => r.status !== 'completed');
const ready = missing.length === 0 && incomplete.length === 0;
if (!ready) {
const reasons = [];
if (missing.length) reasons.push(`missing: ${missing.join(', ')}`);
if (incomplete.length) reasons.push(`still running: ${incomplete.map(r => r.name).join(', ')}`);
core.info(`Skipping early — ${reasons.join('; ')}`);
}
core.setOutput('ready', ready ? 'true' : 'false');
- name: Checkout
if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true'
uses: actions/checkout@v6
with:
sparse-checkout: |
.github/actions/download-all-artifacts
.github/actions/collect-artifact-sizes
.github/actions/setup-python
.github/scripts/collect_build_status.py
.github/scripts/collect_artifact_sizes.py
.github/scripts/ci_bootstrap.py
.github/scripts/generate_build_results_comment.py
.github/scripts/templates/build_results.md.j2
.github/scripts/xml_utils.py
tools/common/build_config.py
tools/common/file_traversal.py
tools/common/gh_actions.py
tools/common/github_runs.py
tools/pyproject.toml
tools/uv.lock
tools/setup/setup_bootstrap.py
tools/setup/install_python.py
.github/scripts/download_artifacts.py
sparse-checkout-cone-mode: false
- name: Setup Python dependencies
if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true'
uses: ./.github/actions/setup-python
with:
groups: scripts
- name: Collect build status
if: steps.pr.outputs.result != 'null' && steps.gate.outputs.ready == 'true'
id: builds
env:
PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }}
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
python3 "${GITHUB_WORKSPACE}/.github/scripts/collect_build_status.py" \
--repo "$REPO" \
--head-sha "$HEAD_SHA" \
--platform-workflows "${PLATFORM_WORKFLOWS}" \
--event pull_request \
--runs-cache workflow-runs-cache.json
- name: Download artifacts from all platform workflows
if: steps.pr.outputs.result != 'null' && 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: ${{ env.PLATFORM_WORKFLOWS }}
event: pull_request
runs-file: workflow-runs-cache.json
artifact-prefixes: coverage-report,test-results-
artifact-metadata-file: workflow-artifacts.json
- name: Collect artifact sizes
if: steps.pr.outputs.result != 'null' && steps.builds.outputs.all_complete == 'true'
uses: ./.github/actions/collect-artifact-sizes
with:
head-sha: ${{ github.event.workflow_run.head_sha }}
workflows: ${{ env.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 != 'null' && 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 != 'null' && steps.builds.outputs.all_complete == 'true'
uses: actions/cache/restore@v5
continue-on-error: true
with:
path: baseline-sizes.json
key: artifact-sizes-baseline-latest
- name: Restore baseline coverage
if: steps.pr.outputs.result != 'null' && steps.builds.outputs.all_complete == 'true'
uses: actions/cache/restore@v5
continue-on-error: true
with:
path: baseline-coverage.xml
key: coverage-baseline-latest
- name: Generate combined report
if: steps.pr.outputs.result != 'null' && 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 }}
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 != 'null' && steps.gate.outputs.ready == 'true'
uses: actions/github-script@v8
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 != 'null' && 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
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: Check if all platform workflows succeeded
id: gate
uses: actions/github-script@v8
env:
PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with:
script: |
const wanted = new Set(process.env.PLATFORM_WORKFLOWS.split(',').map(s => s.trim()));
const runs = await github.paginate(
github.rest.actions.listWorkflowRunsForRepo,
{
owner: context.repo.owner,
repo: context.repo.repo,
head_sha: process.env.HEAD_SHA,
event: 'push',
per_page: 100
}
);
const latest = new Map();
for (const run of runs) {
if (!wanted.has(run.name)) continue;
const prev = latest.get(run.name);
if (!prev || new Date(run.created_at) > new Date(prev.created_at)) {
latest.set(run.name, run);
}
}
const missing = [...wanted].filter(n => !latest.has(n));
const failed = [...latest.values()].filter(r => r.status !== 'completed' || r.conclusion !== 'success');
const ready = missing.length === 0 && failed.length === 0;
if (!ready) {
const reasons = [];
if (missing.length) reasons.push(`missing: ${missing.join(', ')}`);
if (failed.length) reasons.push(`not successful: ${failed.map(r => `${r.name} (${r.status}/${r.conclusion})`).join(', ')}`);
core.info(`Skipping baseline save — ${reasons.join('; ')}`);
}
core.setOutput('ready', ready ? 'true' : 'false');
- name: Checkout
if: steps.gate.outputs.ready == 'true'
uses: actions/checkout@v6
with:
sparse-checkout: |
.github/actions/download-all-artifacts
.github/actions/collect-artifact-sizes
.github/actions/setup-python
.github/scripts/check_baseline_ready.py
.github/scripts/collect_artifact_sizes.py
.github/scripts/ci_bootstrap.py
tools/common/build_config.py
tools/common/file_traversal.py
tools/common/gh_actions.py
tools/common/github_runs.py
tools/pyproject.toml
tools/uv.lock
tools/setup/setup_bootstrap.py
tools/setup/install_python.py
.github/scripts/download_artifacts.py
sparse-checkout-cone-mode: false
- name: Setup Python dependencies
if: steps.gate.outputs.ready == 'true'
uses: ./.github/actions/setup-python
with:
groups: scripts
- name: Verify all platform workflows succeeded for baseline SHA
if: steps.gate.outputs.ready == 'true'
id: ready
env:
PLATFORM_WORKFLOWS: ${{ env.PLATFORM_WORKFLOWS }}
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
python3 "${GITHUB_WORKSPACE}/.github/scripts/check_baseline_ready.py" \
--repo "$REPO" \
--head-sha "$HEAD_SHA" \
--platform-workflows "${PLATFORM_WORKFLOWS}" \
--event push \
--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: ${{ env.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: ${{ env.PLATFORM_WORKFLOWS }}
event: push
output-file: baseline-sizes.json
runs-file: workflow-runs-cache.json
artifacts-file: workflow-artifacts.json
- name: Delete stale artifact sizes baseline cache
if: steps.ready.outputs.ready == 'true' && hashFiles('baseline-sizes.json') != ''
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh actions-cache delete artifact-sizes-baseline-latest -R "${GH_REPO}" --confirm || true
- name: Cache artifact sizes as latest
if: steps.ready.outputs.ready == 'true' && hashFiles('baseline-sizes.json') != ''
uses: actions/cache/save@v5
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: Delete stale coverage baseline cache
if: steps.ready.outputs.ready == 'true' && hashFiles('baseline-coverage.xml') != ''
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh actions-cache delete coverage-baseline-latest -R "${GH_REPO}" --confirm || true
- name: Save coverage baseline
if: steps.ready.outputs.ready == 'true' && hashFiles('baseline-coverage.xml') != ''
uses: actions/cache/save@v5
with:
path: baseline-coverage.xml
key: coverage-baseline-latest