Skip to content

ci: check the setting paths shipped scripts hand to cli - #146

Merged
widgetii merged 2 commits into
masterfrom
cli-path-lint
Sep 5, 2026
Merged

ci: check the setting paths shipped scripts hand to cli#146
widgetii merged 2 commits into
masterfrom
cli-path-lint

Conversation

@widgetii

@widgetii widgetii commented Sep 5, 2026

Copy link
Copy Markdown
Member

Problem

cli -s cannot fail. yaml-cli stores whatever dotted path it is given, creating the
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 — not at
build time, not at first boot, not ever.

devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh has been
shipping six of them:

cli -s .video0.bitrate: 4000
cli -s .video0.rcMode: avbr
cli -s .video0.profile: high
cli -s .video0.gopSize: 5
cli -s .video0.gopMode: smart
cli -s .osd.size: 0.8

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. It is the only device in
the tree with the bug, and nothing in CI or review was ever in a position to notice.

What this adds

.github/scripts/lint-cli-paths.py, wired into the existing lint job beside the two
checkers already there. No runners, no matrix, seconds.

It checks shape and nothing else, deliberately. 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, and curl localhost/api/v1/config.json is the thing that knows.
Shape is decidable from the tree alone, costs nothing, and is the whole of the class that
shipped here.

A path built at runtime is skipped rather than guessed at:
devices/gk7205v200_otg_generic/.../uvc-gadget-setup reads cli -g ".$1" out of a helper,
which is correct and unknowable statically. Markdown is not scanned either — CLAUDE.md
spells paths as .<path> on purpose.

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. Scanning
zero cli invocations is therefore itself a failure.

Also in this PR

The six paths are fixed, since the linter fails on them otherwise. That includes the
commented-out #cli -s .onvif.enabled:, which was a trap waiting for whoever uncommented it.

Evidence

Before:

$ python3 .github/scripts/lint-cli-paths.py
devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh:15: malformed setting path '.video0.bitrate:'
    cli -s .video0.bitrate: 4000
devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh:16: malformed setting path '.video0.rcMode:'
    cli -s .video0.rcMode: avbr
devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh:17: malformed setting path '.video0.profile:'
    cli -s .video0.profile: high
devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh:18: malformed setting path '.video0.gopSize:'
    cli -s .video0.gopSize: 5
devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh:19: malformed setting path '.video0.gopMode:'
    cli -s .video0.gopMode: smart
devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh:25: malformed setting path '.osd.size:'
    cli -s .osd.size: 0.8

FAIL: 6 malformed cli setting path(s) in 80 script(s)
$ echo $?
1

After:

$ python3 .github/scripts/lint-cli-paths.py
ok   cli setting paths well formed in 80 script(s)
$ echo $?
0

$ python3 .github/scripts/lint-cli-paths.py --self-test
ok   self-test: plain set
ok   self-test: the movols trailing colon
ok   self-test: trailing colon, float value
ok   self-test: get
ok   self-test: delete
ok   self-test: path-like value
ok   self-test: runtime path is not decidable
ok   self-test: digits in a component
ok   self-test: sequence index
ok   self-test: empty component
ok   self-test: trailing separator
ok   self-test: = is not a separator
ok   self-test: explicit -i first
ok   self-test: commented out
ok   self-test: sensor_cli applet

$ python3 .github/scripts/lint-workflow-shell.py
checked 34 run block(s)
all run blocks parse clean

$ busybox ash -n devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh
(parses clean)

Context

This came out of OpenIPC/firmware#2366, which makes cli -s ask majestic to apply the
change and refuses a malformed path at runtime. That helps the next camera; it does nothing
for a path already committed here, which is what this catches.

Not tested on a camera, and it does not need to be: the linter is CI-only and never reaches
an image. The one file it changes is a device customizer whose six edited lines have never
had any effect — the change is from "silently does nothing" to "does what it says", and the
script still parses under busybox ash.

`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.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Lint shipped CLI paths and fix malformed Movols settings

🐞 Bug fix ✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds static validation for malformed CLI setting paths in shipped shell scripts.
• Runs linter self-tests and repository scans in the existing lint job.
• Fixes six active Movols settings and one commented ONVIF path.
Diagram

graph TD
  A["Lint job"] --> B["CLI path linter"] --> C{"Self-test passes?"}
  C -- "No" --> G["Fail CI"]
  C -- "Yes" --> D["Device scripts"] --> E["Path validation"] --> F{"Malformed path?"}
  F -- "Yes" --> G
  F -- "No" --> H["Pass CI"]
Loading
High-Level Assessment

The shape-only static checker is the appropriate approach because it catches the demonstrated silent-failure class without requiring vendor-specific firmware binaries or a device matrix. Runtime schema validation was dismissed because available Majestic keys vary by build, while a full shell parser would add disproportionate complexity and dependencies for the narrowly defined invocation grammar.

Files changed (3) +186 / -7

Bug fix (1) +7 / -7
customizer.shCorrect malformed Movols CLI setting paths +7/-7

Correct malformed Movols CLI setting paths

• Removes trailing colons from six active Majestic setting paths so video and OSD configuration applies correctly. It also corrects the dormant commented ONVIF path to prevent the same failure if enabled later.

devices/t40_lite_movols-mo-805p/general/overlay/usr/share/openipc/customizer.sh

Other (2) +179 / -0
lint-cli-paths.pyAdd fail-safe validation for CLI setting paths +170/-0

Add fail-safe validation for CLI setting paths

• Introduces a dependency-free scanner for static cli, sensor_cli, and yaml-cli paths in shipped shell scripts. It skips runtime-generated paths, reports malformed dotted paths with source locations, fails when no invocations are discovered, and includes self-tests for supported syntax and regressions.

.github/scripts/lint-cli-paths.py

lint.ymlRun CLI path validation in the lint workflow +9/-0

Run CLI path validation in the lint workflow

• Adds workflow steps that execute the linter self-test before scanning the repository. This makes malformed static setting paths a pull-request and configured lint-event failure.

.github/workflows/lint.yml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Linter changes skip master CI ✓ Resolved 🐞 Bug ☼ Reliability
Description
The new checker is executed by the lint workflow, but the workflow's restrictive push.paths list
does not include .github/scripts/lint-cli-paths.py. A script-only master push or merge therefore
skips post-merge validation, despite the workflow's stated purpose of rechecking the merged result.
Code

.github/workflows/lint.yml[R80-83]

+      - name: Check the cli path linter still catches what it should
+        run: python3 .github/scripts/lint-cli-paths.py --self-test
+      - name: Check every shipped cli setting path
+        run: python3 .github/scripts/lint-cli-paths.py
Evidence
The master push trigger only accepts workflow changes or changes to lint-workflow-shell.py, while
the newly added steps explicitly execute lint-cli-paths.py; therefore a change limited to the new
checker does not start this workflow.

.github/workflows/lint.yml[13-19]
.github/workflows/lint.yml[80-83]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The lint workflow runs `lint-cli-paths.py`, but changes to that script do not satisfy the workflow's restrictive master push path filter. Add the checker to the trigger so script-only master changes receive post-merge validation.
## Issue Context
Pull requests are covered by the unrestricted pull-request trigger, but master pushes are filtered to workflow files and `lint-workflow-shell.py` only.
## Fix Focus Areas
- .github/workflows/lint.yml[13-19]
- .github/workflows/lint.yml[80-83]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread .github/workflows/lint.yml
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.
widgetii added a commit to OpenIPC/firmware that referenced this pull request Sep 5, 2026
Two mistakes around `cli` are silent on the camera, and neither the compliance
gates nor best_practices.md had anything to say about either.

The path. `cli -s` cannot fail: yaml-cli stores whatever dotted path it is
handed, creating the intermediate mappings as it goes, and exits 0; majestic
then ignores a key it does not recognise. So a typo applies nothing and reports
nothing for the life of the device. OpenIPC/builder shipped six of them on
t40_lite_movols-mo-805p behind a trailing colon -- bitrate, rate-control mode,
profile, GOP size, GOP mode and OSD size, none of which ever applied.

The signal. majestic reloads on SIGHUP and on nothing else. infinity6e's
zoom.sh sends `killall -10` in nine places; signal 10 is SIGUSR1, which the
bundled thread pool catches to park a thread and never resumes (#2365), so
those crops were never applied either. And a signal is only safe once majestic
can catch it: S95majestic starts it with start-stop-daemon -b, so it is visible
to pidof with SigCgt still 0000000000000000, and SIGHUP's default action is to
terminate.

The compliance rule takes the binary half -- a literal path carrying a stray
character or an empty component, and a signal that is not SIGHUP -- and
explicitly exempts a path built at runtime, which is legitimate and undecidable
from a diff. §7.2 of best_practices.md takes the judgement half and the
reasoning.

OpenIPC/builder#146 lints the path half of this mechanically, over the tree
where the 709 shipped `cli -s` lines actually live.
@widgetii
widgetii merged commit c2f5024 into master Sep 5, 2026
117 checks passed
@widgetii
widgetii deleted the cli-path-lint branch September 5, 2026 13:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant