From aada0ac0af1fb958c9cb5940a6ca8ed3aafc82a2 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Sun, 8 Nov 2026 11:19:19 +0100 Subject: [PATCH 1/2] Notarize releases with a Developer ID Releases are now signed with a Developer ID Application certificate and notarized by Apple, so the install docs and the Homebrew cask no longer tell people to choose Open Anyway. The getting-started page says that copies up to 0.1.6, signed with the old self-signed certificate, can't install a notarized version themselves and need one download by hand. Apple took close to half an hour over the first submission, and notarytool's 30-minute wait gave up just before it answered, so release.sh now waits up to 90 minutes for each submission and the release job has room for both. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 3 ++- Casks/codec.rb | 5 ----- README.md | 2 +- docs/getting-started.md | 6 +++--- scripts/release.sh | 2 +- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0986335..94aa848 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,8 @@ jobs: release: name: Build, sign, notarize and publish runs-on: macos-26 - timeout-minutes: 75 + # Room for Apple: notarizing waits up to 90 minutes for the app and again for the disk image. + timeout-minutes: 210 env: HAS_CERTIFICATE: ${{ secrets.SIGNING_CERTIFICATE != '' }} steps: diff --git a/Casks/codec.rb b/Casks/codec.rb index 229707c..8aecfc4 100644 --- a/Casks/codec.rb +++ b/Casks/codec.rb @@ -25,9 +25,4 @@ "~/Library/Preferences/com.srozen.codec.plist", "~/Library/Saved Application State/com.srozen.codec.savedState", ] - - caveats <<~EOS - Codec isn't notarized by Apple yet, so macOS stops it the first time you open it, and again - after each upgrade. Open System Settings > Privacy & Security and choose Open Anyway beside Codec. - EOS end diff --git a/README.md b/README.md index 932feae..3fbac06 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Four themes (Codec, Arctic, Amber and Daylight) drawn through everything from th Codec needs **macOS 26** or later, on Apple silicon or Intel. -**Download** the disk image from the [latest release](https://github.com/srozen/codec/releases/latest), open it and drag Codec to Applications. Codec isn't notarized by Apple yet, so the first time you open it, macOS stops it: choose **Open Anyway** in System Settings › Privacy & Security. +**Download** the disk image from the [latest release](https://github.com/srozen/codec/releases/latest), open it and drag Codec to Applications. **Or with Homebrew:** diff --git a/docs/getting-started.md b/docs/getting-started.md index 88e9864..9b34f40 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -9,9 +9,9 @@ Download `Codec-.dmg` from the [latest release](https://github.com/srozen/codec/releases/latest), open it and drag Codec to Applications. -Codec isn't notarized by Apple yet, so the first time you open it, macOS says it can't check it for malicious software. Open System Settings › Privacy & Security, scroll to Security, choose **Open Anyway** beside Codec and confirm. Updates Codec installs itself don't ask again; a version you download by hand does. Permissions you've given Codec, like Full Disk Access, carry over. +Codec is signed with a Developer ID and notarized by Apple, so macOS opens it like any app from the web. Versions up to 0.1.6 weren't: if you have one of those, download the latest by hand once (or upgrade it with Homebrew), as it can't install a notarized version itself, and macOS asks once more for the permissions you'd given it. -Or install it with Homebrew, from the cask in this repository (macOS asks the same way the first time): +Or install it with Homebrew, from the cask in this repository: ```bash brew tap srozen/codec https://github.com/srozen/codec @@ -22,7 +22,7 @@ brew install --cask srozen/codec/codec Once a day, Codec asks GitHub for its latest release. When there's a newer one, it posts a notification, and Codec › Check for Updates… becomes Update to Codec 1.2.0…: choose it, or click the notification, to see what's new. **Skip This Version** stops it from announcing that one. -**Install** downloads the new version and gets it ready beside this one. Then **Install and Relaunch** puts it in place and reopens Codec with your windows, tabs and splits (programs running in them stop, and Codec asks first), or **Install on Quit** leaves it for the next time you quit. Codec only installs a version signed with the same certificate as the one you have, and it checks the download against the checksum GitHub lists for it. +**Install** downloads the new version and gets it ready beside this one. Then **Install and Relaunch** puts it in place and reopens Codec with your windows, tabs and splits (programs running in them stop, and Codec asks first), or **Install on Quit** leaves it for the next time you quit. Codec only installs a version signed by the same developer as the one you have, and it checks the download against the checksum GitHub lists for it. Where Codec can't replace itself, the alert offers **Download** instead, which opens the new disk image: drag Codec to Applications, then reopen it. That's when it runs from the disk image or from Downloads, or from a folder you can't write to. If Homebrew installed Codec, **Upgrade** puts `brew upgrade --cask srozen/codec/codec` at the prompt for you to run instead. diff --git a/scripts/release.sh b/scripts/release.sh index 00e554a..7cf6d2f 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -41,7 +41,7 @@ fi notarize() { local file="$1" staple="$2" result status id echo "Notarizing $(basename "$file")…" - result="$(xcrun notarytool submit "$file" "${notary[@]}" --wait --timeout 30m --output-format json)" + result="$(xcrun notarytool submit "$file" "${notary[@]}" --wait --timeout 90m --output-format json)" status="$(plutil -extract status raw -o - - <<<"$result")" if [[ "$status" != "Accepted" ]]; then id="$(plutil -extract id raw -o - - <<<"$result" || true)" From 66a871cbb1fcd1475bf6423e67b7c391f7ada813 Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Sun, 8 Nov 2026 11:45:50 +0100 Subject: [PATCH 2/2] Give zsh more time to start on CI, and wait for a command's output before saving it Since GitHub's macOS 26 runners moved to macOS 26.7, zsh sometimes takes more than 15 seconds to start while the whole suite runs, and different zsh tests failed from run to run: one timed out waiting for its first command although the shell ran it a moment later. The wait helpers now allow 60 seconds when CI is set. They return as soon as their condition holds, and no test waits for one to time out, so this only costs time when a test fails. Keeping panes also had a race of its own: a command's output is kept once the prompt after it is drawn, which can come just after the pane is back at the command line, so the test could save the pane before the output of seq 1 5 was in its timeline. It now waits for that output. Co-Authored-By: Claude Opus 5.5 --- Tests/CodecCoreTests/ShellIntegrationTests.swift | 6 ++++-- Tests/CodecUITests/PaneContentsTests.swift | 3 +++ Tests/CodecUITests/TestSupport.swift | 8 +++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Tests/CodecCoreTests/ShellIntegrationTests.swift b/Tests/CodecCoreTests/ShellIntegrationTests.swift index d1fc57a..565490f 100644 --- a/Tests/CodecCoreTests/ShellIntegrationTests.swift +++ b/Tests/CodecCoreTests/ShellIntegrationTests.swift @@ -187,8 +187,10 @@ private final class ZshHarness: @unchecked Sendable { log.events.withLock { $0.filter { $0.code == 133 && $0.payload == Array("B".utf8) }.map(\.cursor.col) } } - /// Polls until `condition` holds or the timeout passes. - func wait(timeout: Duration = .seconds(15), until condition: () -> Bool) async -> Bool { + /// Polls until `condition` holds or the timeout passes. Longer on CI, whose runners can take more + /// than 15 seconds to start zsh while the whole suite runs; it only costs time when a test fails. + func wait(timeout: Duration = ProcessInfo.processInfo.environment["CI"] == nil ? .seconds(15) : .seconds(60), + until condition: () -> Bool) async -> Bool { let deadline = ContinuousClock.now + timeout while ContinuousClock.now < deadline { if condition() { return true } diff --git a/Tests/CodecUITests/PaneContentsTests.swift b/Tests/CodecUITests/PaneContentsTests.swift index 1b418f6..49582cb 100644 --- a/Tests/CodecUITests/PaneContentsTests.swift +++ b/Tests/CodecUITests/PaneContentsTests.swift @@ -156,6 +156,9 @@ struct PaneContentsTests { #expect(await waitUntil { pane.isAtCommandLine }) #expect(await run("echo first-run", in: pane)) #expect(await run("seq 1 5", in: pane)) + // Its output is kept once the prompt after it is drawn, which can come a moment after the + // pane is back at the command line. + #expect(await waitUntil { pane.timeline.entries.last?.output != nil }) let contents = try #require(pane.savedContents()) pane.shutDown() #expect(contents.timeline.entries.map(\.command) == ["echo first-run", "seq 1 5"]) diff --git a/Tests/CodecUITests/TestSupport.swift b/Tests/CodecUITests/TestSupport.swift index 76b093b..a54609b 100644 --- a/Tests/CodecUITests/TestSupport.swift +++ b/Tests/CodecUITests/TestSupport.swift @@ -134,9 +134,15 @@ func makeTestWindow(size: NSSize = NSSize(width: 700, height: 400)) -> NSWindow return window } +/// How long `waitUntil` waits by default. CI's runners start zsh far more slowly than a Mac at a desk +/// while the whole suite runs at once: since macOS 26.7 there, a shell has taken more than 15 seconds +/// to show its first prompt. Waits end as soon as their condition holds, so the longer limit only +/// costs time when a test fails. +let defaultWait: Duration = ProcessInfo.processInfo.environment["CI"] == nil ? .seconds(15) : .seconds(60) + /// Polls `condition` while letting the main queue run, until it holds or `timeout` passes. @MainActor -func waitUntil(timeout: Duration = .seconds(15), _ condition: () -> Bool) async -> Bool { +func waitUntil(timeout: Duration = defaultWait, _ condition: () -> Bool) async -> Bool { let deadline = ContinuousClock.now + timeout while ContinuousClock.now < deadline { if condition() { return true }