Skip to content

feat: package Left as a universal macOS download - #1

Open
ItsFlow wants to merge 5 commits into
mainfrom
fm/left-dist
Open

feat: package Left as a universal macOS download#1
ItsFlow wants to merge 5 commits into
mainfrom
fm/left-dist

Conversation

@ItsFlow

@ItsFlow ItsFlow commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Intent

Package Left as a Mac-installable download the captain can offer from a website. Public repo https://github.com/ItsFlow/left, MIT, required base origin/main at 0f21484 ("Publish Left.").

Ship a .dmg containing Left.app plus a GitHub Release so the website can use a stable URL such as https://github.com/ItsFlow/left/releases/latest/download/Left.dmg (or a versioned name if cleaner; report the exact URL). Follow the Night Walker download pattern: ad-hoc signed, first launch is right-click → Open, not App Store. macOS 13+. Prefer a universal (arm64+x86_64) binary if this toolchain can produce one without a new dependency; otherwise ship native and state the arch in the README.

Also: a scripted, repeatable packager (scripts/package-dmg.sh) that builds, ad-hoc signs, verifies, and writes the DMG under .build/ (gitignored). Do not commit the .dmg or .app unless a tiny committed copy is required for Pages — prefer the GitHub Release asset. README: a Download section at the top of Install with the release URL and the one-line Gatekeeper instruction. Keep source ./scripts/install.sh for people building from checkout. App version already lives at scripts/install.sh (0.2.0); use that (or bump if packaging rules require). Tag/release v0.2.0 unless the existing version is wrong. Probe security find-identity -v -p codesigning for a Developer ID. If none, stay ad-hoc (do not invent notarization). If a Developer ID is present, use it and notarize only if the existing machine already has the notary credentials; otherwise stay ad-hoc and note it. Do not prompt the captain for an Apple password. Do not build a marketing site. Do not enable GitHub Pages unless a one-file download landing is the only way to get a stable URL (Release is preferred). Do not install or replace ~/Applications/Left.app. Do not invoke chrome-devtools-axi. Do not run live quota-axi --full.

Acceptance: repeatable DMG packager; Left.app inside; ad-hoc (or Developer ID) signature verifies. GitHub Release with the DMG attached; full https://github.com/ItsFlow/left/releases/... URL. README download + Gatekeeper line. ./scripts/verify.sh still passes. PR through no-mistakes, checks green. Firstmate owns merge.

Implementation decisions already made and accepted: no Developer ID identities on this machine, so stay ad-hoc and do not notarize. Dual swift build --arch arm64 --arch x86_64 needs Xcode xcbuild which Command Line Tools lack; the packager builds each slice and lipos them so the DMG is still universal. Notarization only if NOTARY_KEYCHAIN_PROFILE is already set (never prompt). The packager must not write the live install path. The GitHub Release v0.2.0 with Left.dmg is required; create it once the branch is on GitHub, targeting the validated commit.

What Changed

  • Add repeatable app and DMG packaging that builds a universal arm64/x86_64 binary, signs and verifies Left.app, and writes .build/Left.dmg with an Applications shortcut.
  • Share bundle identity and version metadata across packaging and installation while preserving the source-checkout install flow.
  • Document the stable GitHub Release download and Gatekeeper instructions, and extend verification to inspect the packaged disk image.

Risk Assessment

✅ Low: The packaging changes are well-bounded, prior universal-build and live-install-path issues are resolved, and no remaining source-verifiable defects or intent contradictions were found.

Testing

Built and mounted the real DMG end to end: it contains a valid ad-hoc-signed universal Left 0.2.0 app for macOS 13+ and an Applications shortcut. No UI screenshot was necessary because the reviewed product is the generated disk image itself. The public acceptance path still fails because v0.2.0 is absent and the README download URL returns 404; the worktree remained clean.

  • Evidence: Generated installable Left.dmg (local file: /var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/Left.dmg)
Evidence: Mounted DMG inspection
DMG contents:
/var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/mounted-left/Applications
/var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/mounted-left/Left.app
Application link target:
/Applications
Bundle version and minimum macOS:
0.2.0
13.0
Binary architectures:
x86_64 arm64
Signature verification:
/var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/mounted-left/Left.app: valid on disk
/var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/mounted-left/Left.app: satisfies its Designated Requirement
Identifier=local.firstmate.usagebar
Format=app bundle with Mach-O universal (x86_64 arm64)
Signature=adhoc
Evidence: Public release URL response headers
HTTP/2 404 
date: Fri, 28 Aug 2026 15:39:33 GMT
content-type: text/plain; charset=utf-8
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, X-Requested-With, X-GitHub-Client-Version, Sec-Fetch-Site,Accept-Encoding, Accept, X-Requested-With
cache-control: no-cache
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: no-referrer-when-downgrade
content-security-policy: default-src 'none'; base-uri 'self'; connect-src 'self'; form-action 'self'; img-src 'self' data:; script-src 'self'; style-src 'unsafe-inline'
server: github.com
set-cookie: _gh_sess=jEazveYhzxoY%2F5mU4O%2FbF7TCqrZcK5sHxFTi0STVGZ5cJqYSTyvV1WODJhJrIhNi7Q3nBAUPoF5DCVoals9cKgH2kRoYJFfcuKWJG9lZX6ujrli13kknemF1Yy5XNpDx0E1Uro22Szs7FgUUvoYD%2BEYPXxIYQngA49NZSNjSz1PadXkjRVI%2BWkYh9ere7O%2BXenAWp5sSFlwNH75lPLVLtBRsibxvZUVxs38YZZ7t7TuRNETNWN9dd%2F6x3mc3ycyyOvsWEnnsxbJJDlvOgO%2Bhjg%3D%3D--idX4duzFJO79jYR3--MMmDP0cyUAjiOoNcEAUnsw%3D%3D; path=/; HttpOnly; secure; SameSite=Lax
set-cookie: _octo=GH1.1.1589547809.1787931573; expires=Sat, 28 Aug 2027 15:39:33 GMT; domain=.github.com; path=/; secure; SameSite=Lax
set-cookie: logged_in=no; expires=Sat, 28 Aug 2027 15:39:33 GMT; domain=.github.com; path=/; HttpOnly; secure; SameSite=Lax
content-length: 9
x-github-request-id: D020:2973B7:1136310D:E51E89A:6A91ABB5
x-github-edge-region: fra

- Outcome: ⚠️ 1 error across 4 runs (14m10s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (3) ✅
  • 🚨 scripts/package-app.sh:60 - The live-install guard compares only the path string, but the later rm -rf follows symlinks in parent components. A destination such as /tmp/Applications/Left.app, where /tmp/Applications links to ~/Applications, bypasses the guard and deletes/replaces the live app. Canonicalize and validate the destination parent before any removal or write.
  • 🚨 scripts/package-app.sh:133 - The accepted intent says the per-architecture builds must be lipod so the DMG “is still universal,” and the README advertises Intel and Apple Silicon. However, failure of either slice silently produces a native-only DMG. Make the default universal path fail hard; native packaging should occur only through the explicit --native option.

🔧 Fix: Harden package destination and universal builds
1 error still open:

  • 🚨 scripts/package-app.sh:131 - The universal build still does not reliably fail hard. build_slice runs inside command substitution, where Bash normally clears errexit; if swift build fails, the function can continue through the successful --show-bin-path and printf. A stale executable from an earlier build then passes the -x check and gets included in the DMG. Explicitly return on either Swift command failure and validate each slice before lipo.

🔧 Fix: Fail universal packaging on stale architecture slices
1 error still open:

  • 🚨 scripts/package-dmg.sh:105 - The accepted requirement says “Native-only output is allowed only with explicit --native,” but --app copies any supplied bundle without checking its architectures. Therefore package-dmg.sh --app <native-only.app> silently creates a native-only DMG while UNIVERSAL remains true. Validate arm64+x86_64 for --app unless --native was explicitly passed.

🔧 Fix: Enforce universal architecture for supplied apps
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 error
  • 🚨 README.md:34 - The required GitHub Release v0.2.0 is not published: the repository reports zero releases and the documented stable download URL returns HTTP 404. Publish the validated DMG as Left.dmg on release v0.2.0.
  • security find-identity -v -p codesigning
  • ./scripts/package-dmg.sh --output /var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/Left.dmg
  • Mounted the generated DMG read-only with hdiutil attach; verified Left.app, the /Applications symlink, signature, architectures, version, minimum macOS version, bundle identifier, and SHA-256
  • gh-axi release view v0.2.0 --full and gh-axi release list --limit 10
  • curl -sSIL https://github.com/ItsFlow/left/releases/latest/download/Left.dmg
  • swift package clean and git status --short to remove generated build products and confirm no worktree changes

🔧 Fix: Release blocked until target commit is pushed
1 error still open:

  • 🚨 README.md:34 - The required GitHub Release v0.2.0 remains unpublished: GitHub reports the release as not found and the README’s stable releases/latest/download/Left.dmg URL returns HTTP 404. Publish the validated DMG as Left.dmg on release v0.2.0.
  • Inspected the base-to-target changed-file set with git diff --stat and git diff --name-status.
  • security find-identity -v -p codesigning (zero Developer ID identities).
  • ./scripts/package-dmg.sh --output /var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/Left.dmg.
  • Mounted the DMG read-only and verified its payload, Applications symlink, bundle version 0.2.0, macOS 13.0 minimum, bundle identifier, strict ad-hoc signature, and x86_64+arm64 architectures using hdiutil, plutil, codesign, and lipo.
  • Executed the mounted Left.app/Contents/MacOS/Left --render-preview Fixtures/demo-quota.json …/packaged-app-preview.png and visually inspected the rendered 340×430 app surface.
  • ./scripts/package-app.sh --destination /Users/flo/Applications/Left.app (expected refusal before building, exit 2).
  • gh-axi release view v0.2.0 --repo ItsFlow/left.
  • curl -fsSIL https://github.com/ItsFlow/left/releases/latest/download/Left.dmg.
  • Removed transient .build/ outputs with git clean -fdX .build and confirmed the worktree remained clean.

🔧 Fix: Release blocked by unpublished target commit
1 error still open:

  • 🚨 README.md:34 - The required GitHub Release v0.2.0 is still unpublished: GitHub reports release not found, and the README’s stable releases/latest/download/Left.dmg URL returns HTTP 404. Publish the validated DMG as Left.dmg on release v0.2.0, targeting the validated commit.
  • security find-identity -v -p codesigning
  • ./scripts/package-dmg.sh --output /var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/Left.dmg
  • Mounted the produced DMG read-only and verified Left.app, the /Applications symlink, version 0.2.0, macOS 13.0 minimum, x86_64 and arm64 slices, and the ad-hoc signature
  • gh-axi release view v0.2.0 --repo ItsFlow/left
  • curl -sSIL https://github.com/ItsFlow/left/releases/latest/download/Left.dmg
  • git status --short and checked for residual DMG/icon staging directories

🔧 Fix: Release blocked until target commit is pushed
1 error still open:

  • 🚨 README.md:34 - The required GitHub Release v0.2.0 remains unpublished: GitHub reports release not found, and the documented stable download URL returns HTTP 404. Publish the validated universal DMG as Left.dmg on release v0.2.0, targeting commit e509da93112e73de8a622aaae0762652c3caca35.
  • security find-identity -v -p codesigning confirmed no Developer ID identity is available.
  • ./scripts/package-dmg.sh --output /var/folders/16/p22jkpcn0z10z3lrwkpc4yq00000gn/T/no-mistakes-evidence/01M140VJSVEFQCMZ1EEVF1Q6QC/Left.dmg built the end-user disk image.
  • Mounted the generated DMG read-only and verified Left.app, the /Applications shortcut, version 0.2.0, macOS 13 minimum, arm64+x86_64 architectures, and the valid ad-hoc signature.
  • gh-axi release view v0.2.0 --repo ItsFlow/left returned release not found.
  • curl -sSIL https://github.com/ItsFlow/left/releases/latest/download/Left.dmg returned HTTP 404.
  • git ls-remote origin refs/heads/fm/left-dist found no published remote branch for the target change.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Flo and others added 5 commits August 28, 2026 12:05
scripts/package-dmg.sh builds a universal, ad-hoc-signed image under .build
so GitHub Releases can serve a stable download URL. install.sh still
installs from source; the packager never writes ~/Applications/Left.app.
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