| name: PR Comment | |
| on: | |
| workflow_run: | |
| workflows: ["Build"] | |
| types: | |
| - completed | |
| jobs: | |
| # Resolve the PR number from trusted workflow_run metadata, NOT from build artifacts: a forked PR | |
| # controls its own Build run and could plant any number, which the privileged jobs below would | |
| # then post to. Same-repo PRs populate workflow_run.pull_requests; for forks it is empty, so look | |
| # the PR up by the trusted head SHA. | |
| pr_number: | |
| if: github.event.workflow_run.event == 'pull_request' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| number: ${{ steps.resolve.outputs.number }} | |
| steps: | |
| - name: Resolve PR number | |
| id: resolve | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} | |
| PRS_JSON: ${{ toJSON(github.event.workflow_run.pull_requests) }} | |
| run: | | |
| # Every lookup is best-effort: on any miss the number stays empty and the comment jobs | |
| # below simply skip (never a failed check). | |
| # Same-repo PRs: workflow_run.pull_requests is populated. | |
| num=$(printf '%s' "$PRS_JSON" | jq -r '.[0].number // empty') | |
| # Fork PRs: pull_requests is empty. Find the open PR by its trusted head ref and confirm | |
| # its head SHA matches the built commit. | |
| if [ -z "$num" ] && [ -n "$HEAD_BRANCH" ] && [ -n "$HEAD_REPO" ]; then | |
| num=$(gh api --method GET "repos/$REPO/pulls" \ | |
| -f state=open -f head="${HEAD_REPO%%/*}:$HEAD_BRANCH" \ | |
| --jq '[.[] | select(.head.sha == env.HEAD_SHA)][0].number // empty' 2>/dev/null || true) | |
| fi | |
| echo "number=$num" >> "$GITHUB_OUTPUT" | |
| # --------------------------------------- | |
| # Combine the rigs' HIL reports into one sticky PR comment (one table per rig). | |
| # Runs here (workflow_run / base-repo context) rather than in build.yml so it also works on | |
| # forked PRs, whose build-side GITHUB_TOKEN is read-only and cannot post comments. Posts even | |
| # on build/HIL failure (when the report matters most); skips only on cancellation. | |
| # --------------------------------------- | |
| hil-comment: | |
| needs: pr_number | |
| if: > | |
| github.event.workflow_run.conclusion != 'cancelled' && | |
| needs.pr_number.outputs.number != '' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| steps: | |
| # the SystemView section's scripts: the default branch's, never the PR's; its records are data | |
| - name: Checkout default branch scripts | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Download HIL reports | |
| uses: actions/download-artifact@v5 | |
| with: | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| pattern: hil-report-* | |
| path: hil-reports | |
| continue-on-error: true | |
| - name: Download SystemView captures | |
| id: sv-download | |
| uses: actions/download-artifact@v5 | |
| with: | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| pattern: sysview-* | |
| path: ${{ runner.temp }}/sysview/pr | |
| continue-on-error: true | |
| timeout-minutes: 5 | |
| # a run a newer push or attempt supersedes would only discard the SystemView work below; "Skip a superseded | |
| # run" checks again before posting | |
| - name: Check the run is still the PR's latest | |
| id: early-current | |
| env: | |
| PR: ${{ needs.pr_number.outputs.number }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| ATTEMPT: ${{ github.event.workflow_run.run_attempt }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| # outcome, not conclusion: continue-on-error turns a failed download's conclusion into success | |
| PR_DOWNLOAD: ${{ steps.sv-download.outcome }} | |
| run: | | |
| SV="$RUNNER_TEMP/sysview" | |
| mkdir -p "$SV" | |
| # find, not a glob: a download of one artifact extracts into the path itself, of several each into its own | |
| # subdirectory | |
| if [ -n "$(find "$SV/pr" -name 'sysview-*.json' -print -quit 2>/dev/null)" ]; then | |
| # until a later step renders the report, or names its own failure | |
| echo '_SystemView report unavailable: a step did not finish (see the job log)._' > "$SV/notice.md" | |
| elif [ "$PR_DOWNLOAD" != success ]; then | |
| echo '_SystemView report unavailable: the captures did not download (see the job log)._' > "$SV/notice.md" | |
| # no record at all: silent unless a HIL report shows a board tested whose capture should have recorded | |
| elif ! python3 test/hil/sysview_ci.py unrecorded hil-reports "$SV/pr" > "$SV/notice.md"; then | |
| echo '_SystemView report unavailable: checking the HIL reports for captures failed (see the job log)._' \ | |
| > "$SV/notice.md" | |
| fi | |
| if python3 .github/scripts/code_size_ci.py is-current --repo "$REPO" --pr "$PR" \ | |
| --run-id "$RUN_ID" --attempt "$ATTEMPT" --head-sha "$HEAD_SHA"; then | |
| echo "current=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Every SystemView step is best effort and bounded: a failure or a step its timeout cut leaves a notice in the | |
| # section, never the HIL comment unposted. | |
| - name: Find the SystemView baseline | |
| id: sv-baseline | |
| if: steps.early-current.outputs.current == 'true' | |
| continue-on-error: true | |
| timeout-minutes: 5 | |
| env: | |
| PR: ${{ needs.pr_number.outputs.number }} | |
| run: | | |
| SV="$RUNNER_TEMP/sysview" | |
| mkdir -p "$SV/pr" | |
| if [ -z "$(find "$SV/pr" -name 'sysview-*.json' -print -quit)" ]; then exit 0; fi | |
| python3 test/hil/sysview_ci.py baseline --repo "$REPO" --pr "$PR" --current "$SV/pr" \ | |
| --info "$SV/baseline.json" | |
| echo "run_id=$(jq -r '.run_id // empty' "$SV/baseline.json")" >> "$GITHUB_OUTPUT" | |
| echo "ready=true" >> "$GITHUB_OUTPUT" | |
| - name: Download SystemView baseline captures | |
| id: sv-base-download | |
| if: steps.sv-baseline.outputs.run_id != '' | |
| continue-on-error: true | |
| timeout-minutes: 5 | |
| uses: actions/download-artifact@v5 | |
| with: | |
| run-id: ${{ steps.sv-baseline.outputs.run_id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| pattern: sysview-* | |
| path: ${{ runner.temp }}/sysview/base | |
| - name: Render the SystemView report | |
| if: steps.sv-baseline.outputs.ready == 'true' | |
| continue-on-error: true | |
| timeout-minutes: 15 | |
| env: | |
| BASELINE_RUN_ID: ${{ steps.sv-baseline.outputs.run_id }} | |
| # outcome, not conclusion: continue-on-error turns a failed download's conclusion into success | |
| PR_DOWNLOAD: ${{ steps.sv-download.outcome }} | |
| BASE_DOWNLOAD: ${{ steps.sv-base-download.outcome }} | |
| run: | | |
| SV="$RUNNER_TEMP/sysview" | |
| mkdir -p "$SV/base" | |
| # a failed or partial baseline download would read its missing boards as "new": report the PR alone | |
| if [ -n "$BASELINE_RUN_ID" ] && { [ "$BASE_DOWNLOAD" != success ] || | |
| [ -z "$(find "$SV/base" -name 'sysview-*.json' -print -quit)" ]; }; then | |
| jq -n --arg run "$BASELINE_RUN_ID" '{sha: null, url: null, exact: false, run_id: null, | |
| note: "the captures of baseline run \($run) did not download (see the job log)"}' > "$SV/baseline.json" | |
| rm -rf "$SV/base" && mkdir "$SV/base" | |
| fi | |
| # the default branch's decoder (lib/agentrc at its pin); a failed fetch leaves every record undecoded | |
| python3 tools/get_deps.py lib/agentrc || true | |
| python3 test/hil/sysview_ci.py decode "$SV/pr" --budget-s 300 || true | |
| python3 test/hil/sysview_ci.py decode "$SV/base" --budget-s 300 || true | |
| if python3 test/hil/sysview_ci.py report "$SV/base" "$SV/pr" --baseline-info "$SV/baseline.json" \ | |
| --hil-reports hil-reports -o "$SV/report.md"; then | |
| rm -f "$SV/notice.md" | |
| # a failed download can leave some of the run's sysview-* artifacts extracted and not others | |
| if [ "$PR_DOWNLOAD" != success ] && [ -s "$SV/report.md" ]; then | |
| printf '\n%s\n' '_Some SystemView captures did not download (see the job log): boards may be missing._' \ | |
| >> "$SV/report.md" | |
| fi | |
| else | |
| echo '_SystemView report unavailable: rendering it failed (see the job log)._' > "$SV/notice.md" | |
| fi | |
| - name: Combine rig reports (one table per rig) | |
| id: combine | |
| run: | | |
| shopt -s nullglob | |
| SV="$RUNNER_TEMP/sysview" | |
| dirs=(hil-reports/hil-report-*) | |
| [ ${#dirs[@]} -eq 0 ] && echo "No HIL reports found" | |
| { | |
| echo "## Hardware-in-the-loop (HIL) Test Report" | |
| echo | |
| if [ ${#dirs[@]} -eq 0 ]; then | |
| # No rig produced a report: PR selection matched no board anywhere, or the | |
| # HIL jobs did not run at all. Post it rather than exiting, so a table from | |
| # an earlier push is replaced instead of being left to look current. | |
| echo "_No HIL run for this push (no affected boards, or hardware testing did not run)._" | |
| fi | |
| for d in "${dirs[@]}"; do | |
| [ -d "$d" ] || continue | |
| echo "### ${d#hil-reports/hil-report-}" | |
| echo | |
| if [ -e "$d/hil_report.unprepared" ]; then | |
| # the leg could not prepare its report: never show Markdown it did not vouch for | |
| echo "_HIL report unavailable: the latest attempt could not prepare it (see the job log)._" | |
| else | |
| cat "$d/hil_report.md" 2>/dev/null || echo "_no report produced_" | |
| fi | |
| echo | |
| done | |
| # the SystemView section, or why it is missing | |
| for f in "$SV/report.md" "$SV/notice.md"; do | |
| if [ -s "$f" ]; then echo; cat "$f"; break; fi | |
| done | |
| } > hil_combined.md | |
| # Fork PRs can influence report content and this job posts in base-repo context, so | |
| # neutralize @-mentions (insert a zero-width space) to prevent notification abuse. | |
| zwsp=$(printf '\342\200\213') | |
| sed -i -E "s/@([A-Za-z0-9_-])/@${zwsp}\1/g" hil_combined.md | |
| cat hil_combined.md | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| - name: Skip a superseded run | |
| id: current | |
| if: steps.combine.outputs.found == 'true' | |
| env: | |
| PR: ${{ needs.pr_number.outputs.number }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| ATTEMPT: ${{ github.event.workflow_run.run_attempt }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| run: | | |
| if python3 .github/scripts/code_size_ci.py is-current --repo "$REPO" --pr "$PR" \ | |
| --run-id "$RUN_ID" --attempt "$ATTEMPT" --head-sha "$HEAD_SHA"; then | |
| echo "post=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Post HIL report as sticky PR comment | |
| if: steps.current.outputs.post == 'true' | |
| uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 | |
| with: | |
| header: hil-report | |
| path: hil_combined.md | |
| number: ${{ needs.pr_number.outputs.number }} | |
| # --------------------------------------- | |
| # Code size of the PR's CI-pinned boards against the master push run of the commit it was | |
| # built against (code_size_ci.py baseline): no base build, only the snapshots both Build | |
| # runs uploaded. Artifacts are PR-built data, parsed only by the default branch's scripts | |
| # and rendered escaped. Report only: this job never fails the PR. | |
| # --------------------------------------- | |
| code-size-comment: | |
| needs: pr_number | |
| if: > | |
| github.event.workflow_run.conclusion != 'cancelled' && | |
| needs.pr_number.outputs.number != '' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| steps: | |
| - name: Checkout default branch scripts | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| # the code-size engine is agentrc's, at the default branch's pin; without it the | |
| # comparison below fails and the comment says so | |
| - name: Get agentrc | |
| continue-on-error: true | |
| run: python3 tools/get_deps.py lib/agentrc | |
| # every step up to the freshness check must succeed, even with nothing to compare: | |
| # a failed one would skip the post and leave an earlier push's table looking current | |
| - name: Download code size snapshots | |
| continue-on-error: true | |
| uses: actions/download-artifact@v5 | |
| with: | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| pattern: code-size-* | |
| path: ${{ runner.temp }}/code-size/current | |
| - name: Find the baseline | |
| id: baseline | |
| run: | | |
| CS="$RUNNER_TEMP/code-size" | |
| mkdir -p "$CS/current" "$CS/out" | |
| # it reports its own failures in the info; a crash still must not skip the post | |
| python3 .github/scripts/code_size_ci.py baseline --repo "$REPO" --current "$CS/current" \ | |
| --info "$CS/baseline.json" || echo "::warning::code_size_ci.py baseline crashed" | |
| echo "run_id=$(jq -r '.run_id // empty' "$CS/baseline.json")" >> "$GITHUB_OUTPUT" | |
| - name: Download baseline snapshots | |
| if: steps.baseline.outputs.run_id != '' | |
| continue-on-error: true | |
| uses: actions/download-artifact@v5 | |
| with: | |
| run-id: ${{ steps.baseline.outputs.run_id }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| pattern: code-size-* | |
| path: ${{ runner.temp }}/code-size/base | |
| - name: Compare against the baseline | |
| env: | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: | | |
| CS="$RUNNER_TEMP/code-size" | |
| if ! python3 tools/code_size.py compare "$CS/current" "$CS/base" -o "$CS/out" --symbols \ | |
| --baseline-info "$CS/baseline.json"; then | |
| printf '## Code size\n\n_The comparison failed._\n' > "$CS/out/comment.md" | |
| fi | |
| echo "_[Full report]($RUN_URL)_" >> "$CS/out/comment.md" | |
| # the job summary caps at 1 MiB | |
| if [ -f "$CS/out/code-size.md" ]; then head -c 1000000 "$CS/out/code-size.md" >> "$GITHUB_STEP_SUMMARY"; fi | |
| - name: Upload full report | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: code-size-report | |
| path: ${{ runner.temp }}/code-size/out/code-size.* | |
| - name: Skip a superseded run | |
| id: current | |
| env: | |
| PR: ${{ needs.pr_number.outputs.number }} | |
| RUN_ID: ${{ github.event.workflow_run.id }} | |
| ATTEMPT: ${{ github.event.workflow_run.run_attempt }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| run: | | |
| if python3 .github/scripts/code_size_ci.py is-current --repo "$REPO" --pr "$PR" \ | |
| --run-id "$RUN_ID" --attempt "$ATTEMPT" --head-sha "$HEAD_SHA"; then | |
| echo "post=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Post code size as sticky PR comment | |
| if: steps.current.outputs.post == 'true' | |
| uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4 | |
| with: | |
| header: code-size | |
| path: ${{ runner.temp }}/code-size/out/comment.md | |
| number: ${{ needs.pr_number.outputs.number }} |