<feed xmlns='http://www.w3.org/2005/Atom'>
<title>devtools.git/bin/publish-apk, 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>publish-apk: fix a SIGPIPE this repo's own README warns about</title>
<updated>2026-09-21T19:21:32+00:00</updated>
<author>
<name>Claude</name>
<email>noreply@anthropic.com</email>
</author>
<published>2026-09-21T19:21:32+00:00</published>
<link rel='alternate' type='text/html' href='https://git.terst.org/devtools.git/commit/?id=97e9b5e6f18705666b9d622b9eab0b31836321ba'/>
<id>urn:sha1:97e9b5e6f18705666b9d622b9eab0b31836321ba</id>
<content type='text'>
The zip check used `unzip -l | grep -q`. Under pipefail, grep exits on its
first match and SIGPIPEs unzip, so the pipeline fails on a perfectly valid
file. It rejected a real 112MB APK as 'not an APK'.

That exact trap is in the README's table, written after it broke crash
detection in android-launch-check. Knowing about a trap is not the same as not
falling into it, which is the argument for these scripts existing at all.

Captures the listing first and tests it with `case`, and the new test builds a
300-entry zip so grep matches long before the listing ends -- which is when
SIGPIPE actually bites. A single-entry fixture would have passed either way.

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>
</feed>
