Skip to content

CI Size Report

CI Size Report #191

name: CI Size Report
# Uses workflow_run (rather than pull_request/push directly) so that
# secrets are available even for PRs from forks — same reasoning as
# pr-test-builds.yml.
#
# Listens to two different upstream workflows, for two different jobs:
# - publish-baseline listens for "Build pre-release" (nightly-build.yml),
# NOT "Build firmware" directly. ci.yml's own `on: push:` trigger is
# broken (a branches: list with only a negative pattern matches
# nothing — confirmed empirically, zero push-triggered "Build firmware"
# runs exist). "Build pre-release" is push-triggered correctly and
# already invokes ci.yml's jobs via `uses:` (workflow_call) as part of
# building nightly releases — piggybacking here costs nothing extra,
# no second build. Persists the size report as release assets in the
# companion iNavFlight/pr-test-builds repo, so PR runs never need to
# rebuild the base branch to get a comparison point. Two assets are
# written per push: size-baseline-<BRANCH> (latest-tip pointer, kept
# for backward compatibility) and size-baseline-<COMMIT_SHA> (primary —
# PR comparisons key off the exact base commit). Per-commit baselines
# are pruned to the newest 50 per branch (plus a global cap) so the
# companion repo doesn't grow unbounded; baselines for since-deleted
# branches aren't otherwise cleaned up (same known limitation
# pr-test-builds has for old PR releases).
# - pr-comment listens for "Build firmware" (ci.yml) directly — PR builds
# genuinely do trigger it via `pull_request`, that part isn't broken.
# Resolves the PR's TRUE base commit (merge-base of head and base ref),
# fetches the per-commit baseline for it (falling back to the nearest
# ancestor commit that has one), diffs the 4 representative targets,
# and posts/updates a PR comment showing which baseline was used.
#
# Requires the same repository secret PR_BUILDS_TOKEN as pr-test-builds.yml
# (Contents: write access to iNavFlight/pr-test-builds).
on:
workflow_run:
workflows: ["Build firmware", "Build pre-release"]
types: [completed]
jobs:
publish-baseline:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.name == 'Build pre-release' &&
github.event.workflow_run.event == 'push'
concurrency:
group: size-baseline-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
permissions:
actions: read # to download artifacts and query job conclusions from the triggering workflow run
steps:
# Don't gate on github.event.workflow_run.conclusion: "Build
# pre-release" also runs a separate Release job (nightly upload to
# iNavFlight/inav-nightly) that can fail for reasons unrelated to the
# build itself (e.g. an expired NIGHTLY_TOKEN) and drags the whole
# run's conclusion to failure even when the build succeeded and
# produced the artifacts we actually need. Check the specific job
# that produces them instead.
- name: Check build job succeeded
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
CONCLUSION=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}/jobs" --paginate \
--jq '.jobs[] | select(.name == "build / upload-artifacts") | .conclusion')
if [ -z "$CONCLUSION" ]; then
# Job not found at all usually means the caller/callee job names
# changed (e.g. nightly-build.yml's "build" job or ci.yml's
# "upload-artifacts" job got renamed) — that's a workflow
# structure mismatch, not an expected build failure, and is
# exactly the kind of thing that silently broke baseline
# publishing before. Warn louder than a plain failed build.
echo "::warning::build / upload-artifacts job not found in run ${RUN_ID} — job name may have changed, skipping baseline publish"
echo "proceed=false" >> "$GITHUB_OUTPUT"
elif [ "$CONCLUSION" != "success" ]; then
echo "::notice::build / upload-artifacts did not succeed (conclusion: ${CONCLUSION}), skipping baseline publish"
echo "proceed=false" >> "$GITHUB_OUTPUT"
else
echo "proceed=true" >> "$GITHUB_OUTPUT"
fi
- name: Download size report
if: steps.check.outputs.proceed == 'true'
uses: actions/download-artifact@v4
with:
name: size-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download branch name
if: steps.check.outputs.proceed == 'true'
uses: actions/download-artifact@v4
with:
name: branch-name
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish baseline
if: steps.check.outputs.proceed == 'true'
env:
GH_TOKEN: ${{ secrets.PR_BUILDS_TOKEN }}
COMMIT_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
BRANCH=$(cat branch.txt)
# Publishes size-baseline-<BRANCH> (latest-tip pointer) and
# size-baseline-<COMMIT_SHA> (primary), then prunes old per-commit
# baselines. Never delete+recreate the release: that leaves a
# window where the release doesn't exist at all, which a concurrent
# PR's "Fetch base branch baseline" step could hit and misreport as
# "no baseline available yet". Assets are replaced in place
# (--clobber) on later pushes — the release/tag stays continuously
# resolvable.
bash .github/scripts/publish-size-baseline.sh \
iNavFlight/pr-test-builds \
"${BRANCH}" \
"${COMMIT_SHA}" \
size-report.json
pr-comment:
runs-on: ubuntu-latest
if: >
github.event.workflow_run.name == 'Build firmware' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
concurrency:
group: pr-size-report-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
permissions:
actions: read
issues: write
pull-requests: write
contents: read
steps:
# Checks out this workflow's own ref (the default branch — workflow_run
# always runs the workflow file from the default branch), NOT the PR's
# head. We only need our own trusted .github/scripts/ here; the PR's
# untrusted code is never checked out in this privileged context.
- uses: actions/checkout@v4
- name: Download PR number
uses: actions/download-artifact@v4
with:
name: pr-number
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download base ref
uses: actions/download-artifact@v4
with:
name: base-ref
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download PR size report
uses: actions/download-artifact@v4
with:
name: size-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Read PR number and base ref
id: pr
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
PR_NUM=$(tr -dc '0-9' < pr_number.txt)
if [ -z "$PR_NUM" ]; then
echo "::error::Invalid PR number in artifact"
exit 1
fi
# base_ref.txt round-trips through an artifact produced by the
# (less-trusted) PR build, so validate it against safe git-ref
# characters before it's ever used to build a shell command or
# release tag downstream — never trust artifact content blindly.
BASE_REF=$(head -n1 base_ref.txt | tr -d '\r\n')
if ! [[ "$BASE_REF" =~ ^[A-Za-z0-9._/-]{1,100}$ ]]; then
echo "::error::Invalid base ref in artifact: $BASE_REF"
exit 1
fi
echo "number=${PR_NUM}" >> "$GITHUB_OUTPUT"
echo "base_ref=${BASE_REF}" >> "$GITHUB_OUTPUT"
echo "short_sha=${HEAD_SHA:0:7}" >> "$GITHUB_OUTPUT"
- name: Compute PR base commit (merge-base)
id: mergebase
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BASE_REF: ${{ steps.pr.outputs.base_ref }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
# The PR's TRUE base commit is the merge-base of the PR head and
# base ref — NOT the base branch's latest tip (comparing against
# the tip includes unrelated changes merged after the PR forked).
# The compare API resolves fork-PR head SHAs too (verified).
# base_ref was already regex-validated in the step above; the
# returned SHA is validated here before any use.
MERGE_BASE=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${BASE_REF}...${HEAD_SHA}" \
--jq '.merge_base_commit.sha // empty' 2>/dev/null || true)
if [[ "$MERGE_BASE" =~ ^[0-9a-f]{40}$ ]]; then
echo "merge_base=${MERGE_BASE}" >> "$GITHUB_OUTPUT"
else
echo "::warning::could not determine merge-base for ${BASE_REF}...${HEAD_SHA}"
echo "merge_base=" >> "$GITHUB_OUTPUT"
fi
- name: Fetch size baseline for PR base commit
id: baseline
env:
GH_TOKEN: ${{ secrets.PR_BUILDS_TOKEN }}
BASE_REF: ${{ steps.pr.outputs.base_ref }}
MERGE_BASE: ${{ steps.mergebase.outputs.merge_base }}
run: |
mkdir -p baseline
if [ -n "${MERGE_BASE}" ]; then
# Exact per-commit baseline for the merge-base first, then the
# nearest ancestor commit that has one; the branch-tip baseline
# is deliberately NOT a fallback (stale-delta bug).
bash .github/scripts/fetch-size-baseline.sh \
"${GITHUB_REPOSITORY}" \
iNavFlight/pr-test-builds \
"${BASE_REF}" \
"${MERGE_BASE}" \
baseline >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
- name: Check for doc on PR head commit
id: doc
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
run: |
if gh api "repos/${HEAD_REPO}/contents/docs/development/ram-and-flash-optimization.md?ref=${HEAD_SHA}" >/dev/null 2>&1; then
echo "link=https://github.com/${HEAD_REPO}/blob/${HEAD_SHA}/docs/development/ram-and-flash-optimization.md" >> "$GITHUB_OUTPUT"
else
echo "link=" >> "$GITHUB_OUTPUT"
fi
- name: Post or update PR comment
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
SHORT_SHA: ${{ steps.pr.outputs.short_sha }}
BASELINE_FOUND: ${{ steps.baseline.outputs.found }}
BASELINE_COMMIT: ${{ steps.baseline.outputs.baseline_commit_short }}
BASELINE_EXACT: ${{ steps.baseline.outputs.baseline_exact }}
DOC_LINK: ${{ steps.doc.outputs.link }}
with:
script: |
const fs = require('fs');
const path = require('path');
const { renderComment } = require(
path.join(process.env.GITHUB_WORKSPACE, '.github', 'scripts', 'size-diff-comment.js')
);
const prNumber = parseInt(process.env.PR_NUMBER, 10);
if (isNaN(prNumber)) throw new Error(`Invalid PR number: ${process.env.PR_NUMBER}`);
const prReport = JSON.parse(fs.readFileSync('size-report.json', 'utf8'));
const baselineReport = process.env.BASELINE_FOUND === 'true'
? JSON.parse(fs.readFileSync('baseline/size-report.json', 'utf8'))
: null;
const marker = '<!-- pr-size-diff -->';
const body = renderComment({
prReport,
baselineReport,
shortSha: process.env.SHORT_SHA,
baselineCommit: process.env.BASELINE_COMMIT || null,
baselineIsNearest: !!process.env.BASELINE_COMMIT && process.env.BASELINE_EXACT !== 'true',
docLink: process.env.DOC_LINK || null,
marker,
});
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber }
);
const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
}