diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 6c3c774..314d182 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -1,7 +1,7 @@ name: Package -# Builds monitor.app, zips it, and — when it is given a release tag — attaches -# the zip to that release. +# Builds monitor.app and the headless tools, packages them as a disk image and +# a zip, and — when it is given a release tag — attaches both to that release. # # It is a reusable workflow rather than a step in the release workflow because # a release can begin two ways: a merge to main that bumps the version, or a @@ -11,7 +11,7 @@ on: workflow_call: inputs: tag: - description: Release tag to attach the zip to. Omit to only build it. + description: Release tag to attach the files to. Omit to only build them. required: false type: string workflow_dispatch: @@ -21,7 +21,7 @@ permissions: jobs: package: - name: Build and zip monitor.app + name: Build and package monitor.app # A self-hosted runner — a Mac on a desk. Only a merge to main, a published # release or a manual run reaches this workflow, and all three need write # access to the repository, so there is no fork path to guard against. @@ -38,7 +38,7 @@ jobs: - name: Swift version run: swift --version - # A published zip must not contain code that fails its own gates, and a + # A published build must not contain code that fails its own gates, and a # tag can point anywhere — including at a commit that never ran CI. - name: Test run: swift test @@ -53,50 +53,80 @@ jobs: # # Unset, a release is ad-hoc signed and unnotarized, exactly as before. # Set, a failure to sign or notarize fails the release rather than - # quietly publishing a zip nobody can open. See docs/signing.md. + # quietly publishing files nobody can open. See docs/signing.md. - name: Build monitor.app env: MONITOR_SIGN_IDENTITY: ${{ vars.SIGN_IDENTITY }} run: Scripts/make-app.sh - # ditto, not zip: a bundle holds symlinks and extended attributes, and - # zip flattens them. This is the command Apple's own notarization - # instructions use. + # Before packaging, because the app has to be stapled before it goes into + # the disk image: stapler writes into the bundle, so an image made first + # would hold an unstapled app. The tools go in the same submission. + - name: Notarize + if: vars.NOTARY_PROFILE != '' + env: + MONITOR_NOTARY_PROFILE: ${{ vars.NOTARY_PROFILE }} + run: Scripts/notarize.sh .build/package + + # Two files, not one. The app goes in a disk image beside a link to + # /Applications, so installing it is one drag. The headless tools go in a + # zip of their own: binaries in the drag window would make it unclear what + # to drag. - name: Package id: package + env: + MONITOR_SIGN_IDENTITY: ${{ vars.SIGN_IDENTITY }} + MONITOR_NOTARY_PROFILE: ${{ vars.NOTARY_PROFILE }} run: | + set -euo pipefail version="$(sed -n 's/.*static let string = "\(.*\)".*/\1/p' \ Sources/MonitorCore/Version.swift)" - name="monitor-${version}.zip" - # The package dir holds monitor.app/, monitord, monitor-exporter and - # install-exporter.sh side by side; ditto without --keepParent puts - # them all at the top level of the zip. - ditto -c -k .build/package "$name" - echo "name=$name" >> "$GITHUB_OUTPUT" + dmg="monitor-${version}.dmg" + zip="monitor-tools-${version}.zip" - # Between zipping and the checksum, because notarize.sh rebuilds the zip - # around the stapled bundle: a checksum taken before this describes a file - # that no longer exists. - - name: Notarize - if: vars.NOTARY_PROFILE != '' - env: - MONITOR_NOTARY_PROFILE: ${{ vars.NOTARY_PROFILE }} - run: Scripts/notarize.sh .build/package "${{ steps.package.outputs.name }}" + Scripts/make-dmg.sh .build/package/monitor.app "$dmg" + + # Everything make-app.sh staged except the app. ditto, not zip, and + # without --keepParent so the tools sit at the top level of the zip, + # where install-exporter.sh expects to find the binary beside it. + # + # No extended attributes: the runner tags every file with + # com.apple.provenance, and ditto stores each attribute as a `._` file + # beside it, which `unzip` in Terminal leaves behind as clutter. The + # tools lose nothing, since a binary's signature is inside the file. + rm -rf .build/tools + mkdir .build/tools + rsync -a --exclude monitor.app .build/package/ .build/tools/ + ditto -c -k --norsrc --noextattr --noacl .build/tools "$zip" + + echo "dmg=$dmg" >> "$GITHUB_OUTPUT" + echo "zip=$zip" >> "$GITHUB_OUTPUT" + ls -lh "$dmg" "$zip" + # After every step that writes the files, so each checksum describes + # what is uploaded. The checksums ride along as files rather than lines + # in the notes: re-running a failed publish then replaces them instead of + # appending a second copy. - name: Checksum - id: checksum env: - ZIP: ${{ steps.package.outputs.name }} + DMG: ${{ steps.package.outputs.dmg }} + ZIP: ${{ steps.package.outputs.zip }} run: | - echo "sha=$(shasum -a 256 "$ZIP" | cut -d' ' -f1)" >> "$GITHUB_OUTPUT" - ls -lh "$ZIP" + set -euo pipefail + shasum -a 256 "$DMG" > "$DMG.sha256" + shasum -a 256 "$ZIP" > "$ZIP.sha256" + cat "$DMG.sha256" "$ZIP.sha256" # Uploaded on every run, released or not, so a manual run is a way to get # a build without publishing anything. - uses: actions/upload-artifact@v4 with: name: monitor-app - path: ${{ steps.package.outputs.name }} + path: | + ${{ steps.package.outputs.dmg }} + ${{ steps.package.outputs.dmg }}.sha256 + ${{ steps.package.outputs.zip }} + ${{ steps.package.outputs.zip }}.sha256 if-no-files-found: error - name: Attach to release @@ -104,14 +134,10 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ inputs.tag }} - ZIP: ${{ steps.package.outputs.name }} - SHA: ${{ steps.checksum.outputs.sha }} + DMG: ${{ steps.package.outputs.dmg }} + ZIP: ${{ steps.package.outputs.zip }} run: | if ! command -v gh >/dev/null 2>&1; then brew install gh; fi - # The checksum rides along as a file rather than a line in the notes: - # re-running a failed publish then replaces it instead of appending a - # second copy. - echo "$SHA $ZIP" > "$ZIP.sha256" # --clobber so a re-run replaces the assets rather than failing on a # name that already exists. - gh release upload "$TAG" "$ZIP" "$ZIP.sha256" --clobber + gh release upload "$TAG" "$DMG" "$DMG.sha256" "$ZIP" "$ZIP.sha256" --clobber diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f68092c..0c0ecef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -203,12 +203,17 @@ jobs: command -v gh >/dev/null 2>&1 \ || { echo "gh is not installed on this runner" >&2; exit 1; } cat > notes.md <<'NOTES' - Download the zip below, unzip it, and drag `monitor.app` to - Applications. macOS 14 or later, Apple silicon or Intel. + Download `monitor-*.dmg` below, open it, and drag Monitor to + Applications. macOS 14 or later, Apple silicon. - The zip also contains `monitord`, a headless daemon that logs every - metric to rotating CSV files. Run `./monitord` to log at 1s with 1d - retention, or pass `--retention` and `--interval` to change it. + `monitor-tools-*.zip` holds the headless tools, for use without the + app: + + - `monitord` logs every metric to rotating CSV files. Run + `./monitord` to log at 1s with 1d retention, or pass + `--retention` and `--interval` to change it. + - `monitor-exporter` serves the SMC and GPU sensors as Prometheus + metrics. `./install-exporter.sh` installs it as a launchd agent. NOTES if [ "$NOTARIZED" != "true" ]; then @@ -233,7 +238,8 @@ jobs: --generate-notes # A merge that bumped the version, or a release published by hand. Either - # way the zip is built the same way and attached to the tag. + # way the disk image and the tools zip are built the same way and attached + # to the tag. package: name: Package needs: tag diff --git a/AGENTS.md b/AGENTS.md index 2afdbad..5c22152 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,8 @@ swift run monitord --version # version and the commit it was built from swiftformat Sources Tests Plugins --lint --cache ignore # CI lint gate Scripts/make-app.sh [dest] # wrap the release binary in monitor.app Scripts/make-icon.swift out.icns # draw the app icon (make-app.sh calls this) -Scripts/notarize.sh app zip # notarize a Developer ID build and staple it +Scripts/notarize.sh .build/package # notarize the app and tools, staple the app +Scripts/make-dmg.sh app out.dmg # the app in a disk image beside /Applications Scripts/test-release-pr-number.sh # the release label lookup's cases (CI runs it) ``` @@ -111,7 +112,8 @@ Plugins/ before every build, so the title bar cannot go stale Scripts/ make-app.sh, which builds monitor.app, make-icon.swift, which draws its icon, notarize.sh, which notarizes and - staples a Developer ID build, and release-pr-number.sh, + staples a Developer ID build, make-dmg.sh, which puts + the app in a disk image, and release-pr-number.sh, which release.yml uses to find a merge's pull request (tested by test-release-pr-number.sh) Tests/ MonitorCoreTests, MonitorSourcesTests, MonitorStoreTests, @@ -123,7 +125,8 @@ docs/ README.md is the index ci.yml build, test, release build, CLI smoke tests (including --help/--version and that monitord --help writes no CSV), lint release.yml bumps the version on a merge to main, tags it, releases it - package.yml reusable: builds monitor.app, zips it, attaches it to a tag + package.yml reusable: builds monitor.app into a .dmg and the tools into a + zip, attaches both to a tag ``` One SwiftPM package: one build and one test command cover all of it, so there @@ -351,7 +354,10 @@ are no component-level AGENTS.md files. - **Releasing is a merge, nothing else.** `release.yml` bumps `MonitorVersion.string` itself on every merge to main, commits it back as `Version x.y.z [skip ci]`, tags it, publishes the release and attaches - `monitor-.zip` built by `package.yml`. **Never remove the + `monitor-.dmg` (the app) and `monitor-tools-.zip` + (`monitord`, `monitor-exporter`, `install-exporter.sh`) built by + `package.yml`. Two files because a drag-to-Applications window offers one + gesture, and binaries beside the app would make it unclear what to drag. **Never remove the `[skip ci]`** — it is what stops the workflow triggering itself into an endless bump. - **The default is a patch bump.** `release:minor` or `release:major` on the @@ -376,19 +382,23 @@ are no component-level AGENTS.md files. docs *and* code still ships. - **Two escape hatches:** a pull request that sets `MonitorVersion.string` itself ships exactly that version, and a release published by hand from the - GitHub UI gets its zip the same way. + GitHub UI gets its files the same way. - **Signing is off until two repository variables are set.** `SIGN_IDENTITY` and `NOTARY_PROFILE` turn on Developer ID signing and notarization in `package.yml`; unset, a release is ad-hoc signed exactly as before. Set, - failing to sign or notarize fails the release rather than publishing a zip + failing to sign or notarize fails the release rather than publishing files nobody can open. The credentials live in the Mac runner's keychain, not in GitHub secrets — which is why the runner's LaunchAgent needs `SessionCreate = false`: with the default `true`, every job gets its own security session, cannot reach the login keychain, and `codesign` fails with `errSecInternalComponent`. A signing runner cannot be headless. - **`notarize.sh` rebuilds the zip after stapling** — `stapler` - writes into the bundle, not the archive, so the uploaded copy is unstapled - until it is made again. `docs/signing.md` is the setup. + **Staple the app before packaging it** — `stapler` writes into the bundle, + not into any archive around it, so `notarize.sh` staples the app and only + then does `make-dmg.sh` put it in the image, which is notarized and stapled + in turn. `docs/signing.md` is the setup. A macOS update can reset the signing + key's partition list, and then `codesign` fails with the same + `errSecInternalComponent` (#61); `set-key-partition-list` at the Mac's own + console fixes it. - **The title bar says which build this is.** The app's name with `BuildStamp.label` under it — the commit and when it was made — as one block, **white on black in the system font**, with the window's own title removed diff --git a/README.md b/README.md index b7a436c..33bae1c 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ and a headless CLI for reading the same metrics from a terminal. Three executables, five libraries and a build plugin, in one SwiftPM package. -| Program | What it is | Shipped in the release zip | -|---------|------------|----------------------------| -| `monitor` | The SwiftUI app. Realtime panel of gauges and charts, ten minutes of in-memory history. | yes, as `monitor.app` | -| `monitord` | Headless daemon. Samples every metric on one clock and writes rotating CSV. | yes, as a bare binary | +| Program | What it is | Shipped in a release | +|---------|------------|----------------------| +| `monitor` | The SwiftUI app. Realtime panel of gauges and charts, ten minutes of in-memory history. | `monitor-*.dmg`, as `monitor.app` | +| `monitord` | Headless daemon. Samples every metric on one clock and writes rotating CSV. | `monitor-tools-*.zip`, as a bare binary | | `monitorctl` | Headless CLI. Lists, reads and watches the same metrics in a terminal. | no — a development tool | -| `monitor-exporter` | Headless daemon. Serves the SMC and GPU sensors as Prometheus metrics on `GET /metrics`. | yes, as a bare binary | +| `monitor-exporter` | Headless daemon. Serves the SMC and GPU sensors as Prometheus metrics on `GET /metrics`. | `monitor-tools-*.zip`, as a bare binary | | Library | What it holds | |---------|---------------| @@ -46,12 +46,14 @@ Swift constant, so the app's title bar cannot claim a stale build. ## Download -Grab the latest `monitor-*.zip` from -[Releases](https://github.com/evanwtf/monitor/releases/latest), unzip it, and -drag `monitor.app` to Applications. The zip also contains `monitord`, -`monitor-exporter` and `install-exporter.sh`, so a downloader runs either binary -with no toolchain — or `./install-exporter.sh` to set the exporter up as a -launchd service. +Each [release](https://github.com/evanwtf/monitor/releases/latest) has two +files. Apple silicon, macOS 14 or later. + +- **`monitor-*.dmg`** is the app. Open it and drag Monitor onto the + Applications folder beside it. +- **`monitor-tools-*.zip`** holds `monitord`, `monitor-exporter` and + `install-exporter.sh`, so a downloader runs either binary with no toolchain — + or `./install-exporter.sh` to set the exporter up as a launchd service. Releases are ad-hoc signed and not notarized unless the repository's `SIGN_IDENTITY` and `NOTARY_PROFILE` variables are set, in which case the @@ -200,7 +202,8 @@ than values. CI runs build, test, release build, CLI smoke tests and the format check on every pull request. Every merge to main ships a release: `release.yml` bumps -the version, tags it, and attaches the zip built by `package.yml`. A +the version, tags it, and attaches the disk image and the tools zip built by +`package.yml`. A `release:minor`, `release:major` or `release:skip` label on the pull request changes that; a merge touching only docs and workflows publishes nothing. diff --git a/Scripts/make-dmg.sh b/Scripts/make-dmg.sh new file mode 100755 index 0000000..aa0c14e --- /dev/null +++ b/Scripts/make-dmg.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# +# Put monitor.app in a disk image beside a link to /Applications, so that +# installing it is one drag. +# +# Usage: +# Scripts/make-dmg.sh .build/package/monitor.app monitor-1.9.0.dmg +# +# Only the app goes in the image. The headless tools ship in their own zip: +# a drag-to-Applications window offers one gesture, and binaries beside the +# app would make it unclear what to drag. +# +# Signing and notarizing follow make-app.sh and notarize.sh: +# +# MONITOR_SIGN_IDENTITY codesign identity for the image. Set, signing must +# succeed. Unset, a "Developer ID Application" +# identity in the keychain is used if there is one, +# and the image is left unsigned if not. +# MONITOR_NOTARY_PROFILE notarytool profile. Set, the image is notarized +# and stapled too, and the image must be signed. +# +# Notarize and staple the app *before* this runs (Scripts/notarize.sh). The +# ticket stapled to the image covers the image. The ticket stapled to the app +# travels with the app when it is dragged out, so it opens offline. +# +# No background picture and no icon positions. Both live in a .DS_Store that +# only Finder writes: scripting Finder on the runner is fragile, and a +# committed .DS_Store is a binary blob. The plain window still shows the app +# and the Applications link side by side. +# +# See docs/signing.md. + +set -euo pipefail + +app="${1:-}" +dmg="${2:-}" +[ -d "$app" ] && [ -n "$dmg" ] || { echo "usage: $0 " >&2; exit 1; } +profile="${MONITOR_NOTARY_PROFILE:-}" + +identity="${MONITOR_SIGN_IDENTITY:-}" +required=1 +if [ -z "$identity" ]; then + required=0 + identity="$(security find-identity -v -p codesigning 2>/dev/null \ + | sed -n 's/.*"\(Developer ID Application: .*\)"/\1/p' | head -1)" +fi +if [ -n "$profile" ] && [ -z "$identity" ]; then + echo "error: MONITOR_NOTARY_PROFILE is set, but there is no identity to sign with" >&2 + echo " the notary service refuses an unsigned image" >&2 + exit 1 +fi + +stage="$(mktemp -d)" +trap 'rm -rf "$stage"' EXIT + +# ditto, not cp: it keeps the bundle's symlinks, extended attributes and the +# stapled ticket exactly as they are. +ditto "$app" "$stage/monitor.app" +ln -s /Applications "$stage/Applications" + +echo "Creating ${dmg}…" +rm -f "$dmg" +# UDZO: compressed and read-only, the format every downloaded image uses. +# +# macOS 27 prints a deprecation warning for hdiutil and points at `diskutil +# image`. Kept anyway: `diskutil image create from` takes a disk or an image, +# not a folder, so replacing -srcfolder means creating a blank image, mounting +# it, copying the app in and converting it — four new ways to fail for a +# command that still works. If hdiutil goes, this line fails the release +# loudly, and that sequence is the replacement. +hdiutil create -volname "Monitor" -srcfolder "$stage" -format UDZO -ov "$dmg" >/dev/null +hdiutil verify "$dmg" >/dev/null + +if [ -n "$identity" ]; then + echo "Signing ${dmg} as ${identity}…" + if ! codesign --force --timestamp --sign "$identity" "$dmg"; then + echo "error: could not sign $dmg as $identity" >&2 + exit 1 + fi + codesign --verify --strict --verbose=2 "$dmg" 2>&1 | sed 's/^/ /' +elif [ "$required" -eq 1 ]; then + echo "error: MONITOR_SIGN_IDENTITY is set but empty" >&2 + exit 1 +else + echo "Unsigned — fine locally, blocked by Gatekeeper on another Mac." +fi + +if [ -n "$profile" ]; then + echo "Submitting ${dmg} to the notary service…" + xcrun notarytool submit "$dmg" --keychain-profile "$profile" --wait + echo "Stapling the ticket to ${dmg}…" + xcrun stapler staple "$dmg" + xcrun stapler validate "$dmg" + # Gatekeeper's verdict on opening the image, which is what a downloader + # does first. It reads the staple, so it passes with the network off. + spctl --assess --type open --context context:primary-signature --verbose=2 "$dmg" +fi + +echo "Built $dmg" diff --git a/Scripts/notarize.sh b/Scripts/notarize.sh index cbce932..6996fa1 100755 --- a/Scripts/notarize.sh +++ b/Scripts/notarize.sh @@ -1,12 +1,15 @@ #!/bin/bash # -# Notarize a signed monitor.app and staple the ticket to it. +# Notarize a signed monitor.app and the tools beside it, and staple the ticket +# to the app. # # Usage: -# Scripts/notarize.sh .build/package monitor-1.1.0.zip +# Scripts/notarize.sh .build/package # -# The first argument is the package directory holding monitor.app/ and monitord -# side by side. The zip is rebuilt from it, so the daemon survives notarization. +# The argument is the package directory that make-app.sh stages: monitor.app/, +# monitord, monitor-exporter and install-exporter.sh side by side. All of it +# goes to the notary service in one submission, so the tools are notarized as +# well as the app. # # Needs a notarytool credential profile in the keychain, named by # MONITOR_NOTARY_PROFILE. Create it once per machine: @@ -14,25 +17,26 @@ # xcrun notarytool store-credentials monitor-notary \ # --apple-id you@example.com --team-id 94S5PZTVPY --password # -# The zip is an argument rather than something this builds, because the caller -# has already made one and notarization takes an archive: the notary service -# does not accept a bare .app. +# The submission zip is made here and thrown away. The notary service takes an +# archive, not a bare .app, but what is published is built afterwards from the +# stapled bundle: the disk image by Scripts/make-dmg.sh, and the tools zip by +# package.yml. # -# **The zip is rebuilt at the end, and that is the point.** `stapler` writes the -# ticket into the bundle, not into the archive, so the copy that was uploaded is -# still unstapled. Publishing that one gives every downloader a Gatekeeper round -# trip to Apple on first launch, and a straight failure if they are offline. +# **Staple before packaging, and that is the point.** `stapler` writes the +# ticket into the bundle, not into any archive around it. An archive made +# before stapling holds an unstapled app, which gives every downloader a +# Gatekeeper round trip to Apple on first launch, and a straight failure if +# they are offline. The bare tools cannot hold a ticket at all; Gatekeeper +# looks theirs up online. # # See docs/signing.md. set -euo pipefail package="${1:-}" -zip="${2:-}" profile="${MONITOR_NOTARY_PROFILE:-}" -[ -d "$package" ] || { echo "usage: $0 " >&2; exit 1; } -[ -f "$zip" ] || { echo "usage: $0 " >&2; exit 1; } +[ -d "$package" ] || { echo "usage: $0 " >&2; exit 1; } [ -n "$profile" ] || { echo "MONITOR_NOTARY_PROFILE is not set" >&2; exit 1; } app="$package/monitor.app" @@ -53,20 +57,22 @@ if ! codesign --verify --strict --deep "$app" 2>/dev/null || [ -z "$authority" ] exit 1 fi -echo "Submitting ${zip} to the notary service…" -# --wait, because the whole point here is that the zip we publish is stapled, -# and stapling needs the ticket to exist. Apple usually answers in a couple of -# minutes. -xcrun notarytool submit "$zip" --keychain-profile "$profile" --wait +scratch="$(mktemp -d)" +trap 'rm -rf "$scratch"' EXIT +submission="$scratch/submission.zip" +# ditto, not zip: a bundle holds symlinks and extended attributes, and zip +# flattens them. +ditto -c -k "$package" "$submission" + +echo "Submitting ${package} to the notary service…" +# --wait, because stapling needs the ticket to exist. Apple usually answers in +# a couple of minutes. +xcrun notarytool submit "$submission" --keychain-profile "$profile" --wait echo "Stapling the ticket to ${app}…" xcrun stapler staple "$app" xcrun stapler validate "$app" -echo "Rebuilding ${zip} around the stapled bundle…" -rm -f "$zip" -ditto -c -k "$package" "$zip" - # Gatekeeper's own verdict, which is the question a downloader is really asking. # It reads the staple rather than calling Apple, so this passes with the network # off — which is the difference stapling buys. diff --git a/docs/exporter.md b/docs/exporter.md index d38dd24..bc612e3 100644 --- a/docs/exporter.md +++ b/docs/exporter.md @@ -35,11 +35,11 @@ label; the scrape target's `instance` identifies the Mac. ### The one-command way -From a repo checkout or an unpacked release zip: +From a repo checkout or an unpacked `monitor-tools-*.zip`: ```sh Scripts/install-exporter.sh # from a checkout — builds a release binary first -./install-exporter.sh # from a release zip — installs the binary beside it +./install-exporter.sh # from the tools zip — installs the binary beside it ``` It installs the compiled binary to `/usr/local/bin/monitor-exporter` (the one diff --git a/docs/signing.md b/docs/signing.md index e48e59e..d26e6cf 100644 --- a/docs/signing.md +++ b/docs/signing.md @@ -146,20 +146,34 @@ It signs with `--options runtime` and a secure timestamp. Neither is optional: the notary service rejects a bundle without them, and neither can be added afterwards without signing again. -`Scripts/notarize.sh` submits the zip, waits, staples the ticket to the bundle -and **rebuilds the zip**. That last step is the one that is easy to miss. -`stapler` writes into the bundle, not into the archive, so the zip that was -uploaded is still unstapled: publishing it gives every downloader a round trip -to Apple on first launch, and a plain failure if they are offline. The zip has -to be made again, after stapling, around the stapled bundle. +A release is two files, and the order they are made in matters: + +1. `Scripts/notarize.sh .build/package` submits the app and the tools in one + throwaway zip, waits, and **staples the ticket to the app**. +2. `Scripts/make-dmg.sh` puts the stapled app in `monitor-.dmg` beside + a link to `/Applications`, signs the image, and notarizes and staples the + image too. +3. `package.yml` zips the tools into `monitor-tools-.zip`. + +Stapling comes before packaging because `stapler` writes into the bundle, not +into any archive around it. An image made first would hold an unstapled app: +every downloader would get a round trip to Apple on first launch, and a plain +failure when offline. The image carries its own ticket for opening it; the app +carries its own for when it is dragged out. The bare tools cannot hold a +ticket, so Gatekeeper looks theirs up online. + +The image has no background picture and no icon positions. Both live in a +`.DS_Store` that only Finder writes, and scripting Finder on the runner is +fragile. The plain window still shows the app and the Applications link side +by side. ## Doing it by hand ```sh Scripts/make-app.sh -ditto -c -k --keepParent .build/monitor.app monitor-1.1.0.zip -MONITOR_NOTARY_PROFILE=monitor-notary \ - Scripts/notarize.sh .build/monitor.app monitor-1.1.0.zip +export MONITOR_NOTARY_PROFILE=monitor-notary +Scripts/notarize.sh .build/package +Scripts/make-dmg.sh .build/package/monitor.app monitor-1.9.0.dmg ``` ## Checking it worked