<feed xmlns='http://www.w3.org/2005/Atom'>
<title>devtools.git/README.md, branch main</title>
<subtitle>devtools — build and test wrappers whose results cannot be misread
</subtitle>
<id>https://git.terst.org/devtools.git/atom?h=main</id>
<link rel='self' href='https://git.terst.org/devtools.git/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.terst.org/devtools.git/'/>
<updated>2026-09-25T07:36:19+00:00</updated>
<entry>
<title>publish-apk: allow publishing from another machine over ssh</title>
<updated>2026-09-25T07:36:19+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-25T07:36:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/devtools.git/commit/?id=0d04aefa0c5e7956cb2dbc80596910cbe53136bf'/>
<id>urn:sha1:0d04aefa0c5e7956cb2dbc80596910cbe53136bf</id>
<content type='text'>
The host gate stopped the Fedora laptop publishing at all, which left a build
made there with nowhere to go. It now takes PUBLISH_SSH and sends the file to
the real host instead.

The gate itself is unchanged in spirit: without PUBLISH_SSH a build from
elsewhere is still refused, because the failure it guards against is writing to
a local path that merely looks like the deploy host's. Two tests cover that --
the error names the way out, and a failed remote publish never falls back to
writing locally.

Transfers land on a temporary name and move into place only once complete, and
the remote copy is hash-compared against the source before the served bytes are
checked.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>publish-apk: cap the backups it creates</title>
<updated>2026-09-21T19:22:54+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-21T19:22:54+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/devtools.git/commit/?id=35fd5e07e4a2554f62ed350b9d193a255901ce02'/>
<id>urn:sha1:35fd5e07e4a2554f62ed350b9d193a255901ce02</id>
<content type='text'>
Four publishes in one evening left 448MB of .bak copies of a 112MB APK, in a
public web directory. A backup policy with no retention becomes the problem it
was guarding against.

Keeps the two most recent, configurable with --keep. Prunes before copying, so
a failed publish doesn't skip the pruning as well.

The first version of the test gave all four fixture backups the same mtime, so
'ls -t' ordered them arbitrarily and 'keeps the most recent' proved nothing --
it only failed because the arbitrary order happened to go the wrong way. Worth
recording: a test that passes for the wrong reason is the same class of
problem as the dead code these guards exist to catch.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>gradle-report: name UP-TO-DATE as the cause of a stale report</title>
<updated>2026-09-21T19:18:56+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-21T19:18:56+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/devtools.git/commit/?id=95f95acdb551168f2e5a52e000ff62167be266a8'/>
<id>urn:sha1:95f95acdb551168f2e5a52e000ff62167be266a8</id>
<content type='text'>
Found by running it: detekt exited 0 while the report predated the run,
because Gradle skipped the task. 'STALE' alone leaves the reader to work out
why nothing changed, so it now says which task was skipped and how to force
it, with --allow-stale to accept an older report deliberately. Same shape as
gradle-test, which learned this first.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Add gradle-report, android-ui and publish-apk</title>
<updated>2026-09-21T19:17:27+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-21T19:17:27+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/devtools.git/commit/?id=8f94e3caa0b1b477bacbbbd8954f2cea743a9337'/>
<id>urn:sha1:8f94e3caa0b1b477bacbbbd8954f2cea743a9337</id>
<content type='text'>
Three more wrappers, each from a failure that actually happened while building
Listen rather than an imagined one.

gradle-report: detekt rejects an invalid config property before analysing
anything, exits non-zero, and leaves the previous run's XML in place. That file
was read as current findings three times in one session; the tell was line
numbers that never moved after a refactor. It now names the rejected property
and refuses any report older than the run. Both real shapes of the message are
covered -- a misspelled property and structurally invalid YAML say different
things.

android-ui: a blind `input tap 400 300` landed in FeeDreader's alarm settings,
and the screenshot taken immediately after was reported as Listen. Every
command verifies the foreground package before acting, and taps resolve by
label rather than coordinate. A label matching several elements refuses rather
than taking the first -- in a list where every row has the same Play button,
the first match is rarely the one meant.

The node parsing is a separate file on purpose. The first version inlined it as
python3 -c inside a bash case block and the nested quoting mangled the regexes
silently: the dump reported one element and a tap landed somewhere else
entirely. Shell-quoted parsers are their own trap.

publish-apk: publishing is where a silent mistake costs most -- a stale
published APK once sat for eight weeks and installing it threw a Room
downgrade. It checks the host, proves the file is a real APK rather than an
HTML error page, byte-compares what landed against what was tested, and
confirms the server serves those same bytes.

Test suite is 25 cases, still weighted toward the negative ones: a wrapper that
only ever reports PASS is worse than no wrapper.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
</content>
</entry>
<entry>
<title>Add devtools: build/test wrappers that can't be misread</title>
<updated>2026-09-18T21:04:06+00:00</updated>
<author>
<name>Peter Stone</name>
<email>thepeterstone@gmail.com</email>
</author>
<published>2026-09-18T21:04:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/devtools.git/commit/?id=b51ba30f1c77bcadfbf96b2eb7b4917c20faa7b0'/>
<id>urn:sha1:b51ba30f1c77bcadfbf96b2eb7b4917c20faa7b0</id>
<content type='text'>
Three times in one session I reported a build or test result that was about
something other than what I thought I was testing:

- A failed `cd` meant Gradle ran in the previous directory; the "BUILD
  SUCCESSFUL" I reported was for the wrong tree.
- `adb shell monkey` + a single `pidof` sample reported a healthy app as
  crashed and a crashed app as healthy, on the same emulator minutes apart.
- `adb install` refused a filename and printed so to stdout, which was
  redirected to /dev/null; the old build stayed installed and got tested.

All three share a cause: concluding from console text rather than from exit
codes and structured artifacts. These wrappers make that mistake hard.

gradle-verify   pins the project with -p from an absolutely-resolved argument,
                treats a missing dir as fatal rather than falling back to cwd,
                and reports from the exit code.
gradle-test     parses JUnit XML for counts, and fails on zero tests executed,
                on no XML at all, and on results whose mtime predates the run
                (Gradle reporting UP-TO-DATE and skipping the task).
android-launch-check
                resolves the launcher activity from the package manager rather
                than guessing, starts logcat capture before launching, uses
                `am start -W`, samples the pid across a settle window so a
                process that starts and then dies is a failure, and prints the
                fatal stack. Optionally installs an APK first and verifies the
                install actually reported Success.

Two bugs of exactly this class were found and fixed in the scripts themselves
while testing them, both of which broke the crash-detection path specifically:
`... | grep -q` under pipefail SIGPIPEs its upstream and reports failure on a
successful match, and `pid=$(pidof ...)` under `set -e` aborts the script at the
moment the process is gone.

Verified against the real projects: nav/android-app 241 tests, nav/test-runner
122, feedreader 39, and both the pass and crash paths of android-launch-check on
a live emulator. gradle-test immediately caught nav reporting UP-TO-DATE with
33 stale result files, which naively read as "241 tests pass" without running
one.

12 self-tests, mostly negative cases.
</content>
</entry>
</feed>
