Check the shell, and stop one-off issues falling off the board #69

Merged
JMR-dev merged 3 commits from tools/file-issue-script into main 2026-08-24 21:52:16 +00:00
JMR-dev commented 2026-08-24 21:03:32 +00:00 (Migrated from github.com)

Two gaps, both found because something went wrong quietly.

tools/github/file-issue.sh

gh issue create does not touch the project board — the issue exists, carries its labels, and is invisible in the Kanban, which reads exactly like a ticket nobody filed. Measured 2026-08-24: eight issues filed as a scripted batch (#57–#64) all reached the board; #66, filed as a one-off minutes later, did not, and surfaced only because someone went looking.

Three things it does that a two-command snippet would not:

  • Resolves the project, Status field and option ids by name, every run. Caching them is the obvious optimisation and the wrong one — a renamed or reordered column would then have this writing a stale id with no error anywhere.
  • Reads the item back. A mutation returning 200 says the request was accepted, not that the board shows what was asked for. This has to be GraphQL: the fields array REST returns on a project item carries Title and nothing else, so a REST-only check reports every Status as unset.
  • Exits 3, loudly, with the issue number on its own line when the issue was created but the board step failed. That combination is the failure being prevented; it must never be the quiet path.

Verified end-to-end by filing #68 with it, then confirming board placement independently. Validation paths exercised: missing title, both body forms, unreadable body file, non-numeric project, unknown status (lists the real options), and --dry-run canonicalising "ready for development" → Ready for Development.

shellcheck in CI

Shell was the other language here with nothing checking it — four scripts, one of them the CI entry point. It now runs in the Static analysis job (already a required check, so no ruleset change) over git ls-files '*.sh', so a script added later is covered without editing the workflow.

Full severity, info included. The two findings it raises today are both the tool being wrong, so both get a targeted disable with its reason rather than a lowered severity:

  • run-e2e.sh's on_signal reported as never invoked — it is installed as the INT and TERM trap eleven lines below.
  • The $names in file-issue.sh's queries are GraphQL variables that must not expand; expanding them would send the shell's idea of $owner to the API instead of declaring a parameter.

A blanket --severity=warning would have hidden both, and the next real finding with them.

The gradle step gains if: !cancelled() so a shellcheck failure cannot cost the ktlint/detekt/lint lists — the same reason it already passes --continue.

Not covered, deliberately

shellcheck here reads .sh files, not the inline run: blocks in the workflows, where a good deal of this repo's bash lives. actionlint does read them and finds one pre-existing info issue in build.yml. Wiring it in means pinning a container digest — every action here is pinned by SHA and actionlint's usual installer is a curl | bash off a moving branch. Filed separately rather than bolted on.

Closes nothing; CLAUDE.md carries both as standing instructions.

Two gaps, both found because something went wrong quietly. ## `tools/github/file-issue.sh` `gh issue create` does not touch the project board — the issue exists, carries its labels, and is invisible in the Kanban, which reads exactly like a ticket nobody filed. **Measured 2026-08-24:** eight issues filed as a scripted batch (#57–#64) all reached the board; #66, filed as a one-off minutes later, did not, and surfaced only because someone went looking. Three things it does that a two-command snippet would not: - **Resolves the project, Status field and option ids by name, every run.** Caching them is the obvious optimisation and the wrong one — a renamed or reordered column would then have this writing a stale id with no error anywhere. - **Reads the item back.** A mutation returning 200 says the request was accepted, not that the board shows what was asked for. This has to be GraphQL: the `fields` array REST returns on a project item carries Title and nothing else, so a REST-only check reports every Status as unset. - **Exits 3, loudly, with the issue number on its own line** when the issue was created but the board step failed. That combination is the failure being prevented; it must never be the quiet path. Verified end-to-end by filing #68 with it, then confirming board placement independently. Validation paths exercised: missing title, both body forms, unreadable body file, non-numeric project, unknown status (lists the real options), and `--dry-run` canonicalising `"ready for development"` → `Ready for Development`. ## shellcheck in CI Shell was the other language here with nothing checking it — four scripts, one of them the CI entry point. It now runs in the **Static analysis** job (already a required check, so no ruleset change) over `git ls-files '*.sh'`, so a script added later is covered without editing the workflow. Full severity, `info` included. The two findings it raises today are **both the tool being wrong**, so both get a targeted `disable` with its reason rather than a lowered severity: - `run-e2e.sh`'s `on_signal` reported as never invoked — it is installed as the INT and TERM trap eleven lines below. - The `$names` in `file-issue.sh`'s queries are GraphQL variables that must not expand; expanding them would send the shell's idea of `$owner` to the API instead of declaring a parameter. A blanket `--severity=warning` would have hidden both, and the next real finding with them. The gradle step gains `if: !cancelled()` so a shellcheck failure cannot cost the ktlint/detekt/lint lists — the same reason it already passes `--continue`. ## Not covered, deliberately shellcheck here reads `.sh` files, **not the inline `run:` blocks** in the workflows, where a good deal of this repo's bash lives. `actionlint` does read them and finds one pre-existing `info` issue in `build.yml`. Wiring it in means pinning a container digest — every action here is pinned by SHA and actionlint's usual installer is a `curl | bash` off a moving branch. Filed separately rather than bolted on. Closes nothing; `CLAUDE.md` carries both as standing instructions.
Sign in to join this conversation.