Commit Graph
2 Commits
Author SHA1 Message Date
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