4 Commits
Author SHA1 Message Date
Dmitry IlyinandGitHub c2f5024972 ci: check the setting paths shipped scripts hand to cli (#146)
* ci: check the setting paths shipped scripts hand to cli

`cli -s` cannot fail. yaml-cli stores whatever dotted path it is given,
creating intermediate mappings as it goes, and exits 0; majestic then silently
ignores a key it does not recognise. So a typo in a setting path applies
nothing and reports nothing, from the moment it is written to the end of the
device's life.

t40_lite_movols-mo-805p has been shipping six of them:

    cli -s .video0.bitrate: 4000
    cli -s .video0.rcMode: avbr

The path is `.video0.bitrate:`, not `.video0.bitrate`. Bitrate, rate-control
mode, profile, GOP size, GOP mode and OSD size have never applied on that
camera. The trailing colons are dropped here, including the one on the
commented-out `.onvif.enabled:`, which was a trap for whoever uncommented it.

The linter checks shape and nothing else. Whether a well-formed key is one a
given majestic build actually declares can only be answered by the running
binary -- it varies by vendor and by flavour -- while shape is decidable from
the tree, costs nothing, and is the whole of the class that shipped. A path
built at runtime is skipped rather than guessed at: uvc-gadget-setup reads
`cli -g ".$1"`, which is fine and unknowable here.

It carries a floor check for the same reason lint-workflow-shell.py does. A
matcher that quietly stops matching finds no bad paths, which looks exactly
like a clean tree -- so finding no cli invocation at all is itself a failure.

80 scripts scanned, 610 invocations, 6 bad paths, all in the one device.

* ci: recheck the merged result when the cli linter itself changes

lint.yml's push trigger names the checkers it guards one by one, and the new
one was not on the list -- so a master push touching only
.github/scripts/lint-cli-paths.py would have skipped the very job that exists
because #121 shipped a broken merged result nothing re-checked.

Same reasoning as the lint-workflow-shell.py entry beside it, and the same
cost: seconds, no runners.
2026-09-05 16:31:10 +03:00
OpenIPC AI teamandGitHub 6fef62c0f3 ci: watch for drift from firmware instead of finding it by accident (#129)
* ci: watch for drift from firmware instead of finding it by accident

builder.sh copies devices/<item>/* over a fresh firmware clone, so a device
directory that ships its own copy of a firmware file replaces it outright. A
fleet-wide change in firmware reaches every device that only references a path
and none that ships its own copy, silently, because nothing in either repository
is looking. One comment on OpenIPC/firmware#2308 turned up three instances of
that in an afternoon: 13 board configs that missed the CONFIG_VT sweep (#126),
97 defconfigs that kept BR2_PACKAGE_JSONFILTER after firmware retired it and put
three devices over their rootfs cap (#128), and excludes lists naming files that
no longer exist (firmware#2313 reports those at build time).

The trigger is the design. Every one of those was caused by a commit in
firmware while this repository sat untouched, so a pull_request check would
never have fired for any of them -- nobody opens a builder PR when firmware
changes. firmware-drift.yml runs on a schedule and files an issue rather than
failing: drift someone else introduced is not a reason to redden a nightly that
is otherwise fine. Only the self-test gates a merge here, out of lint.yml.

Two checks, because the two kinds of drift are not the same shape.

Shadowed files: comparing content is useless, a builder copy is *supposed* to
differ. What matters is whether firmware's version moved since a human last
reconciled them, so each of the 13 entries pins the blob it was reconciled
against. Re-pinning is the act of looking again. The mapping is hand-authored --
nothing can infer that gk7205v200.generic-fpv derives from gk7205v200.generic.

Defconfig symbols: nothing here copies a firmware defconfig, so pinning does not
apply. Every BR2_PACKAGE_*=y is resolved against buildroot, firmware and this
tree and sorted into "resolves nowhere" (a dead line kconfig ignores) and
"resolves, but no firmware defconfig selects it" (needs an allowlist entry). An
allowlist entry can also fence a symbol to the devices that need it; without
that, writing JSONFILTER down as builder-only for devices/apfpv would equally
bless it creeping back onto 95 unrelated defconfigs, which is #128 verbatim.

Replayed against the three regressions, each reproduced by hand on a clean tree:

    firmware moves a shadowed config
      -> br-ext-chip-goke/board/gk7205v200/gk7205v200.generic.config moved in
         firmware since this copy was reconciled (2026-08-25).
         builder copy: devices/common/.../gk7205v200.generic-fpv.config
         pinned 7450c6803f6d, firmware now f97dce84c0f8
         firmware commits since:
           85233f0e kernel: drop CONFIG_VT and the busybox applets ... (#2308)

    jsonfilter creeps back onto a non-apfpv device
      -> BR2_PACKAGE_JSONFILTER is allowlisted only for apfpv/*/configs/*_defconfig,
         but 1 other defconfig(s) select it.

    firmware retires a symbol builder still selects
      -> BR2_PACKAGE_LIBUBOX is selected here but by no firmware defconfig.

Resolving against buildroot is not optional and getting it wrong is how a
checker lies. Buildroot is vendored by neither repository, so a first pass over
OpenIPC packages alone called HOSTAPD, IW, PHP, UHTTPD, LIBZIP and BWM_NG dead;
all six are upstream. Matching only "Config.in" then called PHP_EXT_ZIP dead,
because buildroot declares php's extensions in package/php/Config.ext. Without
--buildroot that half is skipped and says so rather than inventing findings.

Two symbols really do resolve nowhere today -- BR2_PACKAGE_APFPV_GREG on the
four greg-generic devices and BR2_PACKAGE_WIFIBROADCAST_EXT in devices/common.
Both are recorded as known-dead and reported as a notice on every run rather
than removed: whether those devices still want the packages is a maintainer's
call, not a side effect of adding a check.

The three new paths are classified in ci-matrix.py so this PR and every future
re-pin cost no device builds. Without that a blob update is a 107-device run.

* ci: discover shadowed files instead of listing them by hand

Qodo caught the shadow list omitting the device-local t21 and t31 kernel configs.
It was right, and the hole was much bigger than the five it named: a hand-written
list of 13 board configs against 93 same-path shadows covering 35 firmware paths.
The missing ones are not incidental -- load_goke, load_hisilicon, load_sigmastar
and four vendor .mk files are all shadowed by some device, and those are exactly
the shared files a fleet-wide fix lands in. firmware regression-tests
load_hisilicon's os_mem_size derivation; two devices here ship their own copy of
that script and would not have seen the fix.

Listing them by hand was the wrong shape. builder.sh copies devices/<item>/*
over the firmware clone, so any file at a path firmware also has replaces it --
which makes same-path shadows discoverable rather than something to remember.
discover_shadows() walks devices/ and reports anything sitting on a firmware
path with no entry in firmware-drift.json, so the list cannot silently rot and a
device added tomorrow cannot quietly introduce an unreconciled shadow.

The manifest goes from 13 entries to 101: the 88 discovered ones, plus the 13
hand-authored entries that discovery cannot find because the name differs from
the file they replace -- gk7205v200.generic-fpv shadows gk7205v200.generic, and
nothing can infer that.

Two self-test cases for the new path: a same-path shadow with no entry is
reported, and an entry silences it. Verified against the tree by dropping the
load_hisilicon entries, which surfaces both devices immediately.
2026-08-26 01:27:10 +03:00
6872639ed8 ci: stop the lint job's apt step hanging for six minutes (#123)
The unconditional `apt-get update` added to satisfy review on #122 was
fine on the PR runs (~5s) and then sat for six minutes on the first
master push, still in progress when it was cancelled. Nothing was wrong
with the tree; apt was just slow.

That is a bad trade for this job in particular. Its argument for
existing -- and for carrying a push trigger at all -- is that it answers
in seconds, and it was made to depend on a network fetch it does not
normally need. ubuntu-latest ships PyYAML.

So import first and install only if that fails. The dependency is still
handled rather than assumed, which was the point of the review finding,
but a working runner pays nothing for it. `if` rather than
`python3 -c 'import yaml' && exit 0`, because under `bash -e` the latter
fails the step on the branch where the import fails -- exactly when the
install needs to run. Both paths checked under -e.

Also timeout-minutes: 10. The default is six hours, which is how a step
that hangs rather than fails occupies a runner and tells nobody.

The whole job now completes in three seconds, and selects 0 devices
rather than 107 because #122 classified lint.yml as no-build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 18:55:19 +03:00
0455db0027 ci: close the if that cost last night's release, and parse-check the rest (#122)
The Collect guard added to the publish job in #121 shipped without its
closing `fi`. Run 32096242353 -- the first nightly to reach the new
single publish job -- built all 107 devices, staged 219 assets, and then
died in `bash -e` with

    line 25: syntax error: unexpected end of file from `if' command on line 14

before writing anything. No nightly-20260818-* release exists, `nightly`
and `latest` still carry the 17th's images, and ci-gate went red. The
gate did its job; there was simply nothing left to publish by then.

Nothing caught it earlier because the publish job carries
`if: github.event_name != 'pull_request'` -- there are no artifacts to
publish on a PR, so the entire path is unreachable from PR CI by
construction. Both of #121's runs were green without ever executing the
step. #121 said as much ("none of this path can run on a pull request")
and checked the four states offline, but offline-checking the logic does
not check that the script parses, and that is what broke.

So the one-line fix, plus the check that would have caught it in seconds
without a runner: lint-workflow-shell.py parses every `run:` block in
.github/workflows/ with `bash -n`, and lint.yml runs it on PRs and on
master pushes that touch either.

Notes on the checker:

- ${{ }} is not shell, so each expression is replaced with a plain word
  first. That is a real limitation -- an expression interpolating shell
  syntax is checked as the word, not as what it expands to -- and the
  substitution preserves line counts so reported lines still point at
  the right line. --self-test asserts an unterminated `if` containing an
  expression is still rejected while the closed form passes, because the
  way this breaks is by making everything pass.

- Steps are found by walking for any mapping with a `run:` key rather
  than by the jobs.*.steps[*] path, so nothing depends on guessing where
  they live, and a discovery floor fails the run if the walk stops
  finding them. A checker that silently checks nothing looks exactly
  like a clean tree.

- Syntax only. It says nothing about quoting or `-e` semantics; a
  shellcheck pass wants per-block disables for the substitution above
  and is a separate change. actionlint would cover more and is worth
  considering later; keeping this narrow is what makes it noise-free now.

ci-matrix.py classifies both new files as no-build. They were names it
had never heard of, and unknown widens by design, so the first push
queued all 107 devices to prove a one-line YAML fix -- #121, which
touched only master.yml, ran 19. This PR still took the full matrix
because it edits ci-matrix.py itself, which always widens; the selector
is not trusted to pick a smaller matrix for its own changes.

PyYAML is installed explicitly rather than inherited from the runner
image, and the workflow declares `contents: read` rather than taking the
repo default. Both from review; neither would have failed the job today.

Verified: the checker flags the merged master.yml with the same message
the runner gave, passes the fixed tree, and all five guard states behave
as #121 intended -- normal night publishes, a broken download and a
green-but-empty dist both fail, and a dead matrix with nothing built
still exits clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-18 18:44:03 +03:00