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>
57 lines
2.4 KiB
YAML
57 lines
2.4 KiB
YAML
name: lint
|
|
on:
|
|
pull_request:
|
|
branches:
|
|
- master
|
|
# A push trigger, unlike shell-tests in OpenIPC/firmware, which deliberately
|
|
# has none. The reasoning there -- every commit on master was already checked
|
|
# on its pull request -- holds only while master has not moved between the
|
|
# PR's last run and the merge. This job is the one thing standing between a
|
|
# workflow typo and a silently lost nightly, it costs seconds, and #121 cost
|
|
# a full night's release because nothing re-checked the merged result. Cheap
|
|
# enough to not be clever about.
|
|
push:
|
|
branches:
|
|
- master
|
|
paths:
|
|
- '.github/workflows/**'
|
|
- '.github/scripts/lint-workflow-shell.py'
|
|
workflow_dispatch:
|
|
|
|
# Reads the tree and reports; writes nothing. Declared rather than inherited so
|
|
# the job keeps the narrow token whatever the repo or org default becomes.
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# Same guard the jobs in master.yml carry. A clone pushed to a new repository
|
|
# inherits this file, and on a private mirror every master sync push would
|
|
# otherwise run on that owner's bill. Upstream the first disjunct is true, so
|
|
# the condition is a tautology here.
|
|
workflow-shell:
|
|
name: workflow run blocks parse
|
|
if: >-
|
|
github.repository == 'OpenIPC/builder' ||
|
|
github.event_name == 'workflow_dispatch' ||
|
|
(github.event_name == 'pull_request' && !github.event.repository.private)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
# ubuntu-latest ships PyYAML today, so the linter imports it fine without
|
|
# this -- which is the problem. An undeclared dependency on whatever the
|
|
# runner image happens to include is a check that stops being about this
|
|
# repo the day the image changes. apt rather than pip because 24.04 is
|
|
# PEP 668 and this is the same shape as the busybox install in
|
|
# OpenIPC/firmware's shell-tests.
|
|
- name: Install PyYAML
|
|
run: |
|
|
sudo apt-get update -qq
|
|
sudo apt-get install -y -qq python3-yaml
|
|
# Checks the checker before trusting it. The ${{ }} substitution it has to
|
|
# do is the kind of thing that breaks by making everything pass, which
|
|
# would look identical to a clean tree.
|
|
- name: Check the linter still catches what it should
|
|
run: python3 .github/scripts/lint-workflow-shell.py --self-test
|
|
- name: Parse every workflow run block
|
|
run: python3 .github/scripts/lint-workflow-shell.py
|