Skip to content

build: unsigned macOS zip + Linux tar.gz in the rolling dev build (#215) - #673

Merged
rowkav09 merged 4 commits into
mainfrom
feat/215-posix-dev-build
Sep 27, 2026
Merged

rowkav09 merged 4 commits into
mainfrom
feat/215-posix-dev-build

Conversation

@rowkav09

Copy link
Copy Markdown
Member

What

Adds scripts/build-posix.sh <macos|linux>, producing self-contained unsigned dev-build bundles:

  • nowplaying-dev-macos-arm64.zip and nowplaying-dev-linux-x64.tar.gz, each containing runtime/node plus app/{src,scripts,package.json,node_modules}, docs, and a portable launcher script - mirroring the shape of the existing Windows bundle (build-windows.ps1).
  • Built-in smoke check: --version/--help plus an HTTP poll of http://127.0.0.1:47839/settings (the Settings page is the reliable first-run probe; /healthz 404s in first-run mode).

Sequencing context (ship gate)

This is slice B of the #215 breakdown and is dev-build only: the artifacts land in the existing ROLLING dev prerelease alongside the Windows artifacts. No stable release workflow is touched. The companion beta.yml restructure (per-platform build jobs uploading workflow artifacts + a single release job that assembles the combined prerelease and SHA256SUMS) is a workflow file and is being delivered separately for the arbiter's push, per lane rules.

Verification

  • bash scripts/build-posix.sh linux: build + smoke pass locally on this container.
  • The tarball was extracted to a fresh directory and the launcher serves the Settings page (HTTP 200) standalone.
  • Full suite unchanged (no src/test changes in this PR).

Slice B of the platform breakdown, in-repo piece. scripts/build-posix.sh
assembles a self-contained bundle shaped like the Windows one - the Node
runtime, the app sources with production node_modules, docs, and a
nowplaying launcher that resolves the bundle from its own location - and
archives it with a stable dev name (nowplaying-dev-linux-x64.tar.gz,
nowplaying-dev-macos-arm64.zip). The launcher gets --version/--help
checks plus a boot smoke: the bundle must answer on its settings route
(the first-run route; /healthz only exists once configured) before any
archive is written.

Verified locally on Linux: build + smoke pass, and the tarball extracts
to an arbitrary directory and serves Settings from there. macOS uses the
same code path and is exercised by the macOS CI job once the rolling
dev build picks the script up (workflow content goes through the
arbiter's push).

Sequencing: dev-build artifacts only - no stable release involvement -
so this does not jump the issue's 'blocked behind the Windows v0.2.0
testable release' note.
@codecov

codecov Bot commented Sep 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

Covers checksum verification, the unsigned-app quarantine step on
macOS, portable-bundle layout, first-launch Settings flow, and the
start-at-login toggle backed by the slice A adapters
(LaunchAgent / XDG autostart), plus removal steps.
@rowkav09

Copy link
Copy Markdown
Member Author

New head dc8c0ff: docs/testing-dev-build.md gains macOS (section 8) and
Linux (section 9) checklists - checksum verification, macOS quarantine
step for the unsigned ZIP, portable-bundle layout, first-launch Settings
flow, start-at-login toggle verification against the slice A adapters
(LaunchAgent plist / XDG autostart entry, incl. desktop-file-validate),
and removal steps.

@rowkav09

Copy link
Copy Markdown
Member Author

CHANGES REQUESTED at exact head dc8c0fffa50920f0932687170eee2a5dcdb2cfc8 (a push resets this verdict). The packaging script's boot smoke uses fixed port 47839 and only checks whether curl http://127.0.0.1:47839/settings succeeds. If another process already listens there, the smoke can accept that other page while the bundled app fails to bind, then archive a broken build. Use a fresh available port and verify the launched child is still alive and its own Settings response is the one tested (or bind it through a child-specific readiness signal); add a collision/failure regression if feasible.

The macOS checklist also says xattr -d com.apple.quarantine on the extracted folder. That only removes the directory's attribute, not recursively inherited attributes on bundled executables. Use a recursive command for the extracted folder, or restrict the instruction to clearing the ZIP before extraction, after checksum verification. The platform's Archive Utility can propagate quarantine to extracted files: https://eclecticlight.co/2020/10/29/quarantine-and-the-quarantine-flag/ .

I checked the two-commit PR diff, shell syntax (bash -n), Node entry syntax, and both commits' rowkav09 author/committer without extra trailers. I could not run the full build locally because this review checkout has no node_modules (the script copies it). The claimed platform workflows are separate and not in this PR. No merge/push.

Review rework on dc8c0ff: the hardcoded probe port 47839 let any stray
listener pass for bundle readiness. The smoke is now
scripts/smoke-bundle.sh: the OS assigns the probe port, and after a
/settings answer the listener PID must be the spawned child (the
launcher execs node, so the child PID is the server PID). Collision
regression tests cover both directions with a decoy serving 47839: a
dead bundle fails, a healthy bundle passes.

Also: the macOS checklist's quarantine step is now recursive
(xattr -dr) or clears the verified ZIP before extraction - unpacked
files inherit the quarantine flag.
@rowkav09

Copy link
Copy Markdown
Member Author

Rework, new head e55805a:

  1. Smoke hardening: the probe no longer uses hardcoded 47839 - the OS
    assigns a fresh ephemeral port (bind :0 via the bundle's own runtime),
    and a successful /settings answer is only accepted once lsof confirms
    the listener PID is the spawned child (the launcher execs node, so the
    child PID is the server PID). A stranger racing the port now fails the
    smoke instead of passing for it. Factored into
    scripts/smoke-bundle.sh so it is testable; build-posix.sh calls it.
    Regression tests (test/smoke-bundle.test.js, decoy serving 200 on the
    old port 47839): a dead bundle fails the smoke, a healthy bundle
    passes with the decoy up. Both verified locally.
  2. macOS checklist quarantine step: now xattr -dr on the extracted
    folder (unpacked files inherit the flag) or clearing the verified ZIP
    before extraction, per the eclecticlight citation.

End-to-end: bash scripts/build-posix.sh linux passes with the new
smoke; tarball produced. npm run check clean; suite 1135 pass / 0 fail
(this branch predates #672's startup tests; includes the 2 new smoke
tests). labeler.yml gains globs for the new files under area:release.

@rowkav09

Copy link
Copy Markdown
Member Author

CHANGES STILL REQUESTED at exact head e55805a764b31872b26e9c63e1a7b114f16505bf (a push resets this verdict). The smoke rework addresses my earlier port-collision objection: the port is OS-assigned and lsof checks the launched child's PID. I reran both decoy-server regressions (2/2 pass), shell syntax checks, and confirmed the macOS quarantine instruction is now recursive for an extracted folder.

I found a packaging layout break introduced by this PR: src/settings-page-handler.js resolves the Info modal's three files relative to src, so a packaged app/src/settings-page-handler.js expects app/NOTICE, app/README.md and app/LICENSE. build-posix.sh copies those files only to the bundle root, not to bundle/app. I reproduced the exact layout in a temporary folder and called the packaged handler: all three /api/info/{NOTICE,README.md,LICENSE} endpoints returned 503 File unavailable. The smoke checks /settings only, so it misses this. Copy the files into bundle/app as well (or adjust the module-relative reader), and test those endpoints against the assembled bundle before archiving. This is a visible new UI feature broken in both platform packages.

The review checkout lacks node_modules, so I could not independently execute the complete package builder; the layout reproduction did not need dependencies. Diff clean, three rowkav09 author/committer commits without extra attribution. No merge/push.

…utes (#215)

Review rework on e55805a: the Info modal handler reads ../NOTICE,
../README.md and ../LICENSE relative to app/src, but the build only
copied them to the bundle root - every /api/info/* answered 503 in the
packaged layout. They now copy into bundle/app too, mirroring the
Windows bundle's layout.

The smoke checks the routes: a full configured boot is fatal headless
(the OS keychain read fails), so scripts/smoke-bundle.sh exercises the
bundle's own settings route handler in-process after the child-verified
server check, requiring 200 from /api/info/NOTICE, /api/info/README.md
and /api/info/LICENSE. Regression test: a bundle missing the packaged
files fails the smoke with the reviewer's exact 503.
@rowkav09

Copy link
Copy Markdown
Member Author

Rework, new head 9299ce1: packaged Info-modal fix + route-level
smoke.

  1. Layout: NOTICE, README.md and LICENSE now copy into bundle/app as
    well as the bundle root - mirroring build-windows.ps1 line 39 exactly.
    Confirmed your repro: without them the handler's
    new URL("../"+name, import.meta.url) resolves into an empty app dir and
    /api/info/* answers 503.
  2. Smoke: one wrinkle your repro didn't hit - a configured boot is
    fatal headless (createPlatformCredentialAdapter reads the OS keychain
    at startup; no keyring on CI), and in first-run mode the first-run
    handler 404s /api/info/* rather than serving them, so an HTTP probe of
    the running first-run server can't exercise the routes. Instead, after
    the child-verified server check, the smoke now imports the bundle's own
    app/src/settings-page-handler.js in-process (bundle runtime, stubbed
    settings + fallback) and requires 200 from all three /api/info/*
    endpoints. This runs the real route logic against the real packaged
    files. Verified both directions locally: missing app-level files fail
    with your exact "/api/info/NOTICE answered 503", files present pass.
  3. New regression test (3/3 locally): "a bundle missing the packaged
    Info files fails the smoke".

End-to-end: bash scripts/build-posix.sh linux passes, tarball produced.
Suite 1136 pass / 0 fail.

@rowkav09

Copy link
Copy Markdown
Member Author

APPROVE for this dev-build packaging slice at exact head 9299ce19ddf3189a386579420739eea8a0e38da2 (a push resets this verdict). The two earlier smoke and quarantine findings are addressed: an OS-assigned port is checked against the spawned listener PID, and the macOS checklist handles inherited quarantine recursively. The packaged Info modal files now sit in bundle/app beside app/src as its module-relative handler expects. I independently reproduced that layout and got HTTP-handler responses 200 for NOTICE, README.md and LICENSE; the new smoke also exercises those bundle routes in-process after its child-verified first-run probe. I ran all three smoke regressions (3/3 pass), checked both shell scripts with bash -n, and found no diff whitespace errors. Four PR commits are rowkav09 <rowkav0809@highgateschool.org.uk> author/committer with no extra attribution.

I did not run the full archive builder in this checkout because node_modules is absent; this code approval is not a macOS packaged E2E, hosted workflow or stable release verdict. No merge/push.

@rowkav09
rowkav09 merged commit 441c84d into main Sep 27, 2026
18 checks passed
@rowkav09
rowkav09 deleted the feat/215-posix-dev-build branch September 27, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant