ci: build pull requests, and only the devices they reach - #120
Conversation
This repo had no pull_request trigger at all. A change landed unbuilt, and the nightly found out the next morning -- or a user did. Turning PR CI on meant 107 device builds per push, which is why it stayed off. Almost every change here is one device: 639 of the file touches in the last 200 commits sit under a single devices/<dir>/, and the median commit reaches 2 devices. So narrowing first is what makes PR CI affordable at all. .github/scripts/ci-matrix.py maps the paths a PR touches to the devices that build them, and master.yml takes its matrix from it. The mapping is not written down, it is read off the tree the same way builder.sh reads it. builder.sh line 121 locates a device by its defconfig and copies that WHOLE devices/<dir>/ tree over the firmware clone, so the devices a file affects are exactly the devices whose defconfig shares its directory. That distinction is load-bearing: devices/common/ backs 18 targets and devices/apfpv/ backs 2, and treating a directory as one device would skip 17 real builds. A defconfig still names one target even inside a shared directory. THE NIGHTLY IS NOT NARROWED. builder.sh re-clones OpenIPC/firmware at HEAD on every run, so what a nightly builds is decided mostly outside this repo; master.yml already carries the scar of an earlier gate that skipped on this repo's HEAD and left upstream fixes invisible to users. Every event except pull_request gets the whole matrix, and --self-test asserts that for schedule, workflow_dispatch and push. Nothing on a pull_request may write a release. The three upload steps were gated only on an image existing, so with a pull_request trigger added they would have clobbered the published nightly and latest assets that users flash. They and the Telegram post are now guarded on the event. master.yml and check-adjacent plumbing get a 15-device smoke set rather than all 107: they decide how a build runs, not what it produces. --self-test enforces the cover -- every vendor, architecture, toolchain tuple and variant, plus both shared device directories -- rather than the list, so the set can be re-picked while the property holds. It already earned itself: the first run rejected my hand-picked set for missing arm-openipc-linux-gnueabi, which exactly one target in the tree has, and musleabihf. Also adds the CI Gate umbrella check, since a dynamic matrix cannot be named in branch protection and nothing reported a verdict before; and freezes the 7 defconfigs that exist but are in no matrix, so a device falling out of CI is a test failure rather than a silent skip. Replaying the last 200 commits: 20330 device-builds if every PR built everything, 5930 with the selector, 70.8% less, no coverage leaks. 43 of the 49 that still widen do so on paths that no longer exist (devices/ssc338q_apfpv/ before it became devices/apfpv/, the old top-level common/, a deleted builder-local.sh); restricted to commits whose paths survive, it is 78.2%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR Summary by Qodoci: enable PR builds with path-based device matrix narrowing
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1.
|
Adding a camera IS the work in this repo: 8 of the 10 PRs open right now add a device, and 14% of recent commits do. With the list written down in ci-matrix.py, every one of those PRs also had to edit that file -- and a change to that file cannot narrow, because it is what does the narrowing. So registering one camera cost a full 107-device build to prove one device. The most common change got the worst outcome. The matrix is now every defconfig under devices/, minus a NOT_BUILT opt-out that keeps the 7 already in the tree but out of CI. Adding a device is creating its directory, nothing else, and it builds exactly that device. This is the opposite call to OpenIPC/firmware's ci-matrix.py, on purpose. There the matrix is curated -- 29 defconfigs are deliberately never built and boards are not the product -- so an explicit list is the honest description and an opt-out list would be noise. Here devices are the product and the list is just "all of them". The derived matrix is identical to the 107 master.yml carried: nothing lost, nothing gained. --self-test now checks the opt-outs still name real defconfigs, so a rename cannot leave a device silently building under its new name while its old name sits in NOT_BUILT. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The docstring said the default mode writes $GITHUB_OUTPUT; it printed key=value lines to stdout and left the workflow to redirect them there. Raised in review on #120. Fixing the prose would have made the file honest, but the redirect itself is the weaker half. Under `>> ${GITHUB_OUTPUT}` every print() in this file is one keystroke away from corrupting the step outputs, and a crash between the first line and the last leaves a half-written file that Actions still reads. That the diagnostics go to stderr is what keeps it safe today -- a rule that has to hold forever, rather than a property of the code. So the script appends the three lines itself when $GITHUB_OUTPUT is set, and prints them when it is not, so a local run still shows its decision. --stdin never touches the file: it is the mode for reading with your eyes. master.yml drops the redirect, though leaving one in place would have been harmless -- with the file written directly, stdout is empty. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 6 of "Adding a new device" still says to add `- <device>` to the matrix in .github/workflows/master.yml, and calls that matrix "the *only* build registry". Neither has been true since #120 moved the matrix into .github/scripts/ci-matrix.py, where it is derived from the tree rather than listed. master.yml now has no device list at all. This is not a hypothetical staleness. Two of the pull requests open right now do exactly what step 6 says: #111 (Tapo TC70 v3) and #108 (KC110) each append a line to a block that no longer exists, so both are merge conflicts rather than the one-line changes they were written as. Their authors did the documented thing. So step 6 is now the opposite instruction -- there is nothing to add, and adding it is the mistake. It also names NOT_BUILT, since that is the one list a contributor could reasonably go looking for, and says to leave it alone. Two adjacent claims went stale with the same change and are fixed here: the master.yml bullet described a cron-and-dispatch workflow with no pull_request trigger, and nothing in the file mentioned ci-matrix.py at all -- including that --self-test runs in the Select devices job and will fail a PR that puts the tree and the selector out of step. Step 8 no longer says "locally" as though that were the only way a device gets built.
) Step 6 of "Adding a new device" still said to add `- <device>` to the matrix in .github/workflows/master.yml, and called that matrix "the *only* build registry". Neither has been true since #120 moved the matrix into .github/scripts/ci-matrix.py, where it is derived from the tree rather than listed. master.yml has no device list at all. Not a hypothetical staleness: #111 (Tapo TC70 v3) and #108 (KC110) each append a line to a block that no longer exists, so both are merge conflicts rather than the one-line changes they were written as. Their authors did the documented thing. So step 6 is now the opposite instruction -- there is nothing to add, and adding it is the mistake. It also names NOT_BUILT, since that is the one list a contributor could reasonably go looking for, and says to leave it alone. Two adjacent claims went stale with the same change: the master.yml bullet described a cron-and-dispatch workflow with no pull_request trigger, and nothing mentioned ci-matrix.py at all -- including that --self-test runs in the Select devices job and will fail a PR that puts the tree and the selector out of step. A second correction, caught by Qodo in review: "opening the PR is what gets the device built" holds only for a non-draft PR. classify() returns needs_build=False for a draft and self_test() asserts it; master.yml lists ready_for_review in its triggers precisely because a PR opened as a draft would otherwise never build once marked ready. Left as written, a contributor could open a draft, see no build, and read that as the device being fine. The ci:full label is documented for the same reason -- it is the other contributor-facing lever over the matrix.
Both devices have had a complete devices/<dir>/ in this tree for a long time and had never once been built. #120 froze that state rather than changing it -- it put the 7 defconfigs that were in no master.yml matrix into NOT_BUILT so that a device falling out of CI would be a test failure instead of a silent skip. Freezing was right for a change whose job was to not alter what gets built; it was never meant to be permanent for devices whose only problem is that nobody added the matrix line. Two contributors did try to add that line. #111 (Tapo TC70 v3, July) and #108 (KC110, June) both append to the device list in master.yml, which is where the registry lived when they were written. It does not live there any more -- the matrix is read off the tree -- so both PRs are merge conflicts now, and the devices they asked for are exactly the two entries removed here. This is their CI half, landed the way the selector wants it. #108 also carries a BR2_OPENIPC_MAJESTIC="ultimate" line and a vendor libsns_ov2735.so for the no-video bug; those are unaffected and stay with that PR. The two self-test cases that used t31_lite_tp-link-tapo-tc70-v3 as their example of an unbuilt device move to t31_lite_xiaomi-mjsxj05hl, which is still in NOT_BUILT. The overlay case also switches to a path that exists, since the device it now names has no etc/inittab. Neither device had ever been through a build, so this PR was where we found out. That was affordable exactly once: a change to ci-matrix.py cannot narrow -- it is what does the narrowing -- so this ran the full matrix and proved both in the same run. Both are green: Firmware (hi3518ev200_ultimate_tplink-kasa-kc110) SUCCESS Firmware (t31_lite_tp-link-tapo-tc70-v3) SUCCESS
This repo has no
pull_requesttrigger at all. A change lands unbuilt, and the nightly finds out the next morning — or a user does. Turning PR CI on meant 107 device builds per push, which is why it stayed off.Almost every change here is one device: 639 of the file touches in the last 200 commits sit under a single
devices/<dir>/, and the median commit reaches 2 devices. Narrowing first is what makes PR CI affordable at all.Ported from OpenIPC/firmware#2273 and #2275.
The mapping is read off the tree, the way builder.sh reads it
builder.sh:121locates a device by its defconfig and copies that wholedevices/<dir>/tree over the firmware clone:So the devices a file affects are exactly the devices whose defconfig shares its directory. That distinction is load-bearing —
devices/common/backs 18 targets anddevices/apfpv/backs 2, so treating a directory as one device would skip 17 real builds. A defconfig still names one target even inside a shared directory.devices/t31_lite_wyze-v3b/…/customizer.shdevices/apfpv/general/overlay/etc/udhcpd.confdevices/common/general/overlay/etc/inittabdevices/common/…/configs/gk7205v200_fpv_defconfigpackage/…,builder.sh.github/workflows/master.ymlREADME.md,repack.sh,package.sh, manifest workflowrepack.shandpackage.share developer/end-user tools — no workflow invokes them; onlybuilder.shis on the CI path.The device list is derived, not written down
Adding a camera is the work here: 8 of the 10 other PRs open right now add a device, and 14% of recent commits do. A written-down matrix would mean every one of those PRs also edits
ci-matrix.py— and a change to that file cannot narrow, because it is what does the narrowing. Registering one camera would have cost a full 107-device build to prove one device: the most common change getting the worst outcome.So the matrix is every defconfig under
devices/, minus aNOT_BUILTopt-out holding the 7 that are in the tree but out of CI. Adding a device is creating its directory, nothing else — and it builds exactly that device. It no longer needs registering inmaster.ymleither.This is the opposite call to firmware's
ci-matrix.py, on purpose: there the matrix is curated (29 defconfigs deliberately never built, boards are not the product), so an explicit list is the honest description. The derived list here is identical to the 107master.ymlcarried — nothing lost, nothing gained.The nightly is NOT narrowed
builder.shre-clones OpenIPC/firmware at HEAD every run, so what a nightly builds is decided mostly outside this repo.master.ymlalready carries the scar of an earlier gate that skipped on this repo's HEAD and left upstream fixes invisible to users. Every event exceptpull_requestgets the whole matrix, and--self-testasserts that forschedule,workflow_dispatchandpush.A release-clobbering bug this would have introduced
The three upload steps were gated only on
env.NORFW || env.NANDFW. Adding apull_requesttrigger without touching them would have had every PR build overwrite the publishednightlyandlatestassets that users flash. They and the Telegram post are now guarded on the event.Smoke tier
master.ymlgets a 15-device smoke set rather than all 107 — it decides how a build runs, not what it produces.--self-testenforces the cover (every vendor, architecture, toolchain tuple and variant, plus both shared device directories) rather than the list.It earned itself immediately: the first run rejected my hand-picked set for missing
arm-openipc-linux-gnueabi— which exactly one target in the tree has (hi3536dv100_fpv) — andmusleabihf.Also
Numbers
Replaying the last 200 commits:
Zero coverage leaks. 43 of the 49 that still widen do so on paths that no longer exist —
devices/ssc338q_apfpv/before it becamedevices/apfpv/, the old top-levelcommon/, a deletedbuilder-local.sh. Restricted to commits whose paths survive: 78.2%.Note on this PR's own run
It touches
.github/scripts/ci-matrix.py, which is deliberately excluded from the smoke tier — it decides the matrix, so it cannot be trusted to decide a smaller one for itself. So it runs all 107, which is also the regression test you want for turning PR CI on. Adding a device will not do this.🤖 Generated with Claude Code