Skip to content

[ci] Exec the cask we just installed, and fail on a hang - #8

Merged
drycode merged 4 commits into
mainfrom
dy/cask-exec-test
Aug 6, 2026
Merged

[ci] Exec the cask we just installed, and fail on a hang#8
drycode merged 4 commits into
mainfrom
dy/cask-exec-test

Conversation

@drycode

@drycode drycode commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Closes the gap that let tacet#25 reach users.

The gap

The cask job installs and uninstalls what changed. tacet 0.1.1 installed perfectly, passed --only-tap-syntax, style, audit and that install step — and then hung in _dyld_start on first exec. Casks quarantine by default, nothing launches these bundles through LaunchServices, and a directly-exec'd quarantined bundle blocks on a consent decision with no UI to answer it. Green CI, broken for every user.

The check

Exec the installed bundle with --help and fail on a non-zero exit or a hang. That is the cheapest proof a bundle reaches its own main() — a quarantine wedge never gets there.

Bounded by background-and-poll, not timeout(1): a stock macOS runner has neither timeout nor gtimeout, so depending on coreutils would leave the check silently unbounded on exactly the platform it protects. Same reasoning as run_bounded() in tacet's install-server.sh.

Verification

Bounded-exec logic tested locally against four cases — exit 0, exit 1, a 30s hang (rc=124), and the real tacet binary (rc=0).

The cask comment is touched on purpose: the step only fires when Casks/ changes, so a workflow-only PR would not run its own new code. With the cask in the diff, this PR's CI run exercises the check end to end against the published 0.1.2 cask — which also gives the first real verification that the quarantine postflight works through brew install.

drycode added 4 commits August 6, 2026 10:17
Installing is not evidence the thing runs. tacet#25 shipped a cask that
installed perfectly, passed tap-syntax, style, audit and the existing
install/uninstall step, and then hung in _dyld_start on first exec —
casks quarantine by default and nothing launches these bundles through
LaunchServices, so the consent gate had nobody to answer it. Every check
was green and it was broken for every user.

--help is the cheapest proof a bundle reaches its own main(): a
quarantine wedge blocks in the dynamic loader and never gets there.

Bounded by background-and-poll rather than timeout(1), because a stock
macOS runner has neither timeout nor gtimeout — depending on coreutils
would leave the check silently unbounded on the one platform it
protects. Same reasoning as run_bounded() in tacet's install-server.sh.
The failure mode here is a hang, so an unbounded check would burn the
job timeout instead of naming the problem.

The cask comment is touched deliberately: the step only runs when Casks/
changes, so a workflow-only PR would not exercise its own new code.

Solves: green CI on a cask that cannot start
Tests: bounded-exec logic verified locally against exit 0, exit 1, a
       30s hang (rc=124), and the real tacet binary; this PR's own CI
       run exercises it against the published 0.1.2 cask
The first version walked all of /Applications, so on a runner it execed
Firefox and "Google Chrome for Testing" before ever reaching the cask's
own bundle. Chrome for Testing never exits on --help, so the check
failed every cask PR and attributed the hang to the cask under test —
a false positive that would have taught everyone to ignore it.

Snapshot /Applications before install and diff after, so only what this
cask added gets exec'd. The error now names the bundle, not just the
cask, because a cask can ship more than one app.

Solves: false positive from runner-preinstalled apps
Tests: simulated locally against three cases — good app only (pass),
       good app plus a bundle that sleeps forever (fails 1, names the
       hanging bundle), and a cask that installs no .app (pass)
brew style runs actionlint over the workflows, and actionlint fails on
shellcheck's SC2012 even at info level. Linting Casks/tacet.rb alone
missed it; brew style drycodeworks/tap is the check that matches CI.

Solves: --only-tap-syntax failing on the cask exec check
Tests: brew style drycodeworks/tap — 3 files, no offenses
The published bundle is Mach-O thin (arm64), but the cask constrained
only macOS version — so brew installed it on an Intel Mac and the binary
could not execute. Rosetta does not cover this direction. The exec check
added in this PR caught it on the macos-15-intel runner on its first
real run, which is the first evidence any of this tooling has produced.

The workflow now reads depends_on arch and skips a cask the runner
cannot run, because a legitimate single-arch cask is not a defect on the
other half of the matrix. Homebrew normalises the token to arm/intel
rather than arm64/x86_64, so the comparison uses those.

The arch query is a python one-liner because a multi-line heredoc inside
a YAML block scalar must be indented to stay in the block, and python
then rejects the uniform leading whitespace.

Solves: arm64-only bundle installable on Intel
Tests: brew style drycodeworks/tap clean; arch gate simulated against
       the real cask for both runner types — arm installs and execs,
       intel skips
@drycode
drycode merged commit b2ca239 into main Aug 6, 2026
3 checks passed
@drycode
drycode deleted the dy/cask-exec-test branch August 6, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant