Skip to content

Commit 0f34686

Browse files
authored
Merge pull request #32 from jasonlong/release/v0.5.8
Harden Octodot for v0.5.8
2 parents 1874182 + e931003 commit 0f34686

42 files changed

Lines changed: 3768 additions & 717 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
- main
77
pull_request:
88

9+
permissions:
10+
contents: read
11+
912
concurrency:
1013
group: ci-${{ github.workflow }}-${{ github.ref }}
1114
cancel-in-progress: true
@@ -17,7 +20,9 @@ jobs:
1720

1821
steps:
1922
- name: Check out repository
20-
uses: actions/checkout@v4
23+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
24+
with:
25+
persist-credentials: false
2126

2227
- name: Show Xcode version
2328
run: xcodebuild -version

.github/workflows/release.yml

Lines changed: 78 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,35 @@ on:
55
tags:
66
- "v*"
77
workflow_dispatch:
8+
inputs:
9+
tag:
10+
description: Existing version tag to release (for example, v1.2.3)
11+
required: true
12+
type: string
813

914
permissions:
10-
contents: write
15+
contents: read
1116

1217
concurrency:
13-
group: release-${{ github.ref }}
18+
group: release
1419
cancel-in-progress: false
1520

21+
env:
22+
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
23+
1624
jobs:
1725
build-and-release:
26+
permissions:
27+
contents: write
1828
runs-on: macos-26
1929
timeout-minutes: 30
2030
env:
2131
APP_PATH: /tmp/octodot-export/Octodot.app
2232
XCARCHIVE_PATH: /tmp/Octodot.xcarchive
2333
EXPORT_PATH: /tmp/octodot-export
2434
EXPORT_OPTIONS_PLIST: /tmp/OctodotExportOptions.plist
25-
ARCHIVE_PATH: /tmp/Octodot-${{ github.ref_name }}-unsigned.zip
26-
FINAL_ARCHIVE_PATH: Octodot-${{ github.ref_name }}-macos.zip
35+
ARCHIVE_PATH: /tmp/Octodot-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}-unsigned.zip
36+
FINAL_ARCHIVE_PATH: Octodot-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}-macos.zip
2737
KEYCHAIN_PATH: /tmp/octodot-build.keychain-db
2838
KEYCHAIN_PROFILE: octodot-notary
2939

@@ -32,38 +42,36 @@ jobs:
3242
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
3343
with:
3444
fetch-depth: 0
45+
persist-credentials: false
46+
ref: ${{ env.RELEASE_TAG }}
3547

3648
- name: Verify release ref is main
49+
env:
50+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
3751
run: |
38-
default_branch="${{ github.event.repository.default_branch }}"
39-
git fetch --force origin "$default_branch:refs/remotes/origin/$default_branch"
52+
git fetch --force origin "$DEFAULT_BRANCH:refs/remotes/origin/$DEFAULT_BRANCH"
4053
41-
if [ "$GITHUB_EVENT_NAME" = "push" ]; then
42-
if [ "$GITHUB_REF_TYPE" != "tag" ]; then
43-
echo "::error::Release pushes must be tag refs"
44-
exit 1
45-
fi
54+
scripts/validate_release_tag.sh "$RELEASE_TAG"
55+
56+
if ! git rev-parse --verify --quiet "refs/tags/$RELEASE_TAG^{commit}" >/dev/null; then
57+
echo "::error::Release tag $RELEASE_TAG does not exist"
58+
exit 1
59+
fi
4660
47-
tag_commit="$(git rev-list -n 1 "$GITHUB_REF_NAME")"
48-
main_commit="$(git rev-parse "origin/$default_branch")"
61+
tag_commit="$(git rev-list -n 1 "$RELEASE_TAG")"
62+
main_commit="$(git rev-parse "origin/$DEFAULT_BRANCH")"
4963
50-
if [ "$tag_commit" != "$main_commit" ]; then
51-
echo "::error::Release tag $GITHUB_REF_NAME points at $tag_commit, not origin/$default_branch $main_commit"
52-
exit 1
53-
fi
54-
elif [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
55-
if [ "$GITHUB_REF_NAME" != "$default_branch" ]; then
56-
echo "::error::Manual releases must be dispatched from $default_branch"
57-
exit 1
58-
fi
64+
if [ "$tag_commit" != "$main_commit" ]; then
65+
echo "::error::Release tag $RELEASE_TAG points at $tag_commit, not origin/$DEFAULT_BRANCH $main_commit"
66+
exit 1
5967
fi
6068
6169
- name: Show Xcode version
6270
run: xcodebuild -version
6371

6472
- name: Resolve release version
6573
run: |
66-
echo "RELEASE_MARKETING_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
74+
echo "RELEASE_MARKETING_VERSION=${RELEASE_TAG#v}" >> "$GITHUB_ENV"
6775
echo "RELEASE_BUILD_NUMBER=${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV"
6876
6977
- name: Run test suite
@@ -159,6 +167,24 @@ jobs:
159167
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
160168
codesign --display --verbose=4 "$APP_PATH"
161169
170+
info_plist="$APP_PATH/Contents/Info.plist"
171+
actual_bundle_id="$(/usr/bin/plutil -extract CFBundleIdentifier raw -o - "$info_plist")"
172+
actual_version="$(/usr/bin/plutil -extract CFBundleShortVersionString raw -o - "$info_plist")"
173+
actual_build="$(/usr/bin/plutil -extract CFBundleVersion raw -o - "$info_plist")"
174+
175+
if [ "$actual_bundle_id" != "com.octodot.app" ]; then
176+
echo "::error::Unexpected bundle identifier: $actual_bundle_id"
177+
exit 1
178+
fi
179+
if [ "$actual_version" != "$RELEASE_MARKETING_VERSION" ]; then
180+
echo "::error::Expected version $RELEASE_MARKETING_VERSION, found $actual_version"
181+
exit 1
182+
fi
183+
if [ "$actual_build" != "$RELEASE_BUILD_NUMBER" ]; then
184+
echo "::error::Expected build $RELEASE_BUILD_NUMBER, found $actual_build"
185+
exit 1
186+
fi
187+
162188
- name: Store notarization credentials
163189
env:
164190
APPLE_ID: ${{ secrets.APPLE_ID }}
@@ -182,32 +208,32 @@ jobs:
182208
183209
- name: Submit for notarization
184210
run: |
211+
set +e
185212
xcrun notarytool submit "$ARCHIVE_PATH" \
186213
--keychain-profile "$KEYCHAIN_PROFILE" \
187214
--keychain "$KEYCHAIN_PATH" \
188215
--wait \
189216
--output-format json | tee /tmp/notary-submit-output.json
217+
submit_exit="${PIPESTATUS[0]}"
218+
set -e
190219
191-
- name: Show notarization submission output
192-
if: always()
193-
run: |
194-
cat /tmp/notary-submit-output.json
220+
submission_id="$(/usr/bin/plutil -extract id raw -o - /tmp/notary-submit-output.json 2>/dev/null || true)"
221+
status="$(/usr/bin/plutil -extract status raw -o - /tmp/notary-submit-output.json 2>/dev/null || true)"
195222
196-
- name: Fetch notarization log on rejection
197-
run: |
198-
submission_id="$(/usr/bin/plutil -extract id raw -o - /tmp/notary-submit-output.json)"
199-
status="$(/usr/bin/plutil -extract status raw -o - /tmp/notary-submit-output.json)"
200-
201-
if [ "$status" = "Accepted" ]; then
223+
if [ "$submit_exit" -eq 0 ] && [ "$status" = "Accepted" ]; then
202224
exit 0
203225
fi
204226
205-
echo "Notarization status: $status"
206-
xcrun notarytool log "$submission_id" \
207-
--keychain-profile "$KEYCHAIN_PROFILE" \
208-
--keychain "$KEYCHAIN_PATH" \
209-
/tmp/notary-log.json
210-
cat /tmp/notary-log.json
227+
echo "Notarization failed (exit $submit_exit, status: ${status:-unknown})"
228+
if [ -n "$submission_id" ]; then
229+
xcrun notarytool log "$submission_id" \
230+
--keychain-profile "$KEYCHAIN_PROFILE" \
231+
--keychain "$KEYCHAIN_PATH" \
232+
/tmp/notary-log.json || true
233+
fi
234+
if [ -f /tmp/notary-log.json ]; then
235+
cat /tmp/notary-log.json
236+
fi
211237
exit 1
212238
213239
- name: Staple notarization ticket
@@ -228,31 +254,30 @@ jobs:
228254
spctl --assess --type execute --verbose=4 "$APP_PATH"
229255
230256
- name: Package signed release app
231-
env:
232-
TAG_NAME: ${{ github.ref_name }}
233257
run: |
234258
ditto -c -k --sequesterRsrc --keepParent \
235259
"$APP_PATH" \
236260
"$FINAL_ARCHIVE_PATH"
237261
238262
- name: Generate release notes
239263
env:
240-
TAG_NAME: ${{ github.ref_name }}
241264
GITHUB_REPOSITORY: ${{ github.repository }}
242265
run: |
243266
chmod +x scripts/generate_release_notes.sh
244-
scripts/generate_release_notes.sh "${TAG_NAME}" RELEASE_NOTES.md
267+
scripts/generate_release_notes.sh "${RELEASE_TAG}" RELEASE_NOTES.md
245268
246269
- name: Publish GitHub release
247270
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65
248271
with:
249272
files: ${{ env.FINAL_ARCHIVE_PATH }}
250273
body_path: RELEASE_NOTES.md
274+
tag_name: ${{ env.RELEASE_TAG }}
251275

252276
- name: Clean up signing keychain
253277
if: always()
254278
run: |
255279
security delete-keychain "$KEYCHAIN_PATH" || true
280+
rm -f /tmp/octodot-developer-id.p12 /tmp/notary-submit-output.json /tmp/notary-log.json
256281
257282
update-homebrew:
258283
needs: build-and-release
@@ -263,9 +288,10 @@ jobs:
263288
env:
264289
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
265290
run: |
266-
VERSION="${GITHUB_REF_NAME#v}"
291+
test -n "$TAP_TOKEN"
292+
VERSION="${RELEASE_TAG#v}"
267293
URL="https://github.com/jasonlong/octodot/releases/download/v${VERSION}/Octodot-v${VERSION}-macos.zip"
268-
ARCHIVE="Octodot-${GITHUB_REF_NAME}-macos.zip"
294+
ARCHIVE="Octodot-${RELEASE_TAG}-macos.zip"
269295
curl --fail --show-error --silent --location \
270296
--retry 5 --retry-delay 2 --retry-all-errors \
271297
--output "$ARCHIVE" "$URL"
@@ -277,7 +303,7 @@ jobs:
277303
exit 1
278304
fi
279305
280-
git clone https://x-access-token:${TAP_TOKEN}@github.com/jasonlong/homebrew-tap.git tap
306+
git clone https://github.com/jasonlong/homebrew-tap.git tap
281307
cd tap
282308
283309
cat > Casks/octodot.rb << CASK
@@ -293,11 +319,6 @@ jobs:
293319
auto_updates true
294320
depends_on macos: ">= :sonoma"
295321
296-
preflight do
297-
system_command "/usr/bin/xattr",
298-
args: ["-cr", "#{staged_path}/Octodot.app"]
299-
end
300-
301322
app "Octodot.app"
302323
303324
zap trash: [
@@ -310,5 +331,10 @@ jobs:
310331
git config user.name "github-actions[bot]"
311332
git config user.email "github-actions[bot]@users.noreply.github.com"
312333
git add Casks/octodot.rb
334+
if git diff --cached --quiet; then
335+
echo "Homebrew cask already matches ${VERSION}; nothing to commit."
336+
exit 0
337+
fi
313338
git commit -m "Update octodot to ${VERSION}"
314-
git push
339+
auth_header="$(printf 'x-access-token:%s' "$TAP_TOKEN" | base64 | tr -d '\n')"
340+
git -c http.https://github.com/.extraheader="AUTHORIZATION: basic $auth_header" push

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Always rebuild after code changes so the user can test immediately. Use `killall
1515

1616
## Architecture
1717

18-
- **AppState** (`App/AppState.swift`): Central `@MainActor @Observable` model. Owns all notification data, selection, search, auth, background refresh. ~1200 lines. All UI state derives from here.
18+
- **AppState** (`App/AppState.swift`): Central `@MainActor @Observable` model. Owns all notification data, selection, search, auth, and background refresh. All UI state derives from here.
1919
- **GitHubAPIClient** (`Auth/GitHubAPIClient.swift`): `actor` for thread-safe API calls with caching, pagination, conditional polling (`If-Modified-Since`/304), and rate limit awareness.
2020
- **InboxStore** (`App/InboxStore.swift`): Manages inbox projection — tracks recent reads, pruning, security alert state. Persists to UserDefaults.
21-
- **ThreadActionStore** (`App/ThreadActionStore.swift`): Optimistic actions (done, unsubscribe) with batched dispatch. Reconciles with server on refresh.
21+
- **ThreadActionStore** (`App/ThreadActionStore.swift`): Optimistic actions (mark read, done, unsubscribe) with batched dispatch. Reconciles with server on refresh.
2222
- **StatusItemController** (`Panel/StatusItemController.swift`): Menu bar icon, panel toggle, global hotkey (Carbon Events), right-click context menu, outside-click dismiss.
2323
- **NotificationPanel** (`Panel/NotificationPanel.swift`): `NSPanel` hosting SwiftUI via `NSHostingView`. Floating, non-activating, status bar level.
2424
- **PanelInput** (`Views/PanelInput.swift`): All keyboard routing. Vim-style (`j/k/d/x/u/o/gg/G`) plus standard shortcuts (`Cmd+Up/Down`, arrows, Page Up/Down).
@@ -31,7 +31,7 @@ Always rebuild after code changes so the user can test immediately. Use `killall
3131
- **NetworkSession protocol** for testable networking. Production uses `URLSession.shared`, tests use `StubNetworkSession`.
3232
- **Race condition prevention**: UUID-based `activeLoadRequestID` pattern — generate ID before async work, check it after `await`.
3333
- **Narrow view dependencies**: Pass specific values to row views (`let notification`, `let isSelected`), not entire state objects.
34-
- **Equatable views** for list performance (`NotificationRowView: View, Equatable`).
34+
- **Narrow row inputs** for list performance; keep list rows independent from the full app state.
3535

3636
## Testing
3737

@@ -46,7 +46,7 @@ Uses Swift Testing framework (`import Testing`, `@Test`, `#expect`). Not XCTest.
4646

4747
- **Entitlements**: Network client only (no sandbox). Hardened runtime for notarization.
4848
- **Token storage**: Keychain in release, file (`~/Library/Application Support/Octodot/.debug-token`) in debug.
49-
- **Debug logging**: `DebugTrace.log()` — compiled out in release builds (`#if DEBUG`). Writes to `/tmp/octodot-debug-trace.log`.
49+
- **Debug logging**: `DebugTrace.log()` — compiled out in release builds (`#if DEBUG`). Writes to the current user's temporary directory.
5050
- **Version config**: `OCTODOT_MARKETING_VERSION` in project.pbxproj. CI overrides from git tag.
5151
- **New files**: Must be added to `project.pbxproj` manually (PBXFileReference, PBXBuildFile, PBXGroup children, PBXSourcesBuildPhase). Follow existing ID patterns (`AA00...` for refs, `BB00...` for build files).
5252
- **Commit style**: Imperative mood, concise subject line. Body explains "why" not "what".

Octodot.xcodeproj/project.pbxproj

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
BB0000000000000000000030 /* InboxStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000030 /* InboxStore.swift */; };
3434
BB0000000000000000000032 /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000032 /* SemanticVersion.swift */; };
3535
BB0000000000000000000033 /* UpdateChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000033 /* UpdateChecker.swift */; };
36+
BB0000000000000000000036 /* UpdateInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000036 /* UpdateInstaller.swift */; };
3637
BB0000000000000000000034 /* SemanticVersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000034 /* SemanticVersionTests.swift */; };
3738
BB0000000000000000000035 /* UpdateCheckerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000035 /* UpdateCheckerTests.swift */; };
3839
BB0000000000000000000020 /* AppStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000000000000000000020 /* AppStateTests.swift */; };
@@ -81,6 +82,7 @@
8182
AA0000000000000000000030 /* InboxStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxStore.swift; sourceTree = "<group>"; };
8283
AA0000000000000000000032 /* SemanticVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersion.swift; sourceTree = "<group>"; };
8384
AA0000000000000000000033 /* UpdateChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateChecker.swift; sourceTree = "<group>"; };
85+
AA0000000000000000000036 /* UpdateInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateInstaller.swift; sourceTree = "<group>"; };
8486
AA0000000000000000000034 /* SemanticVersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemanticVersionTests.swift; sourceTree = "<group>"; };
8587
AA0000000000000000000035 /* UpdateCheckerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckerTests.swift; sourceTree = "<group>"; };
8688
AA0000000000000000000010 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
@@ -136,6 +138,7 @@
136138
AA0000000000000000000032 /* SemanticVersion.swift */,
137139
AA0000000000000000000029 /* ThreadActionStore.swift */,
138140
AA0000000000000000000033 /* UpdateChecker.swift */,
141+
AA0000000000000000000036 /* UpdateInstaller.swift */,
139142
);
140143
path = App;
141144
sourceTree = "<group>";
@@ -317,6 +320,7 @@
317320
/* Begin PBXShellScriptBuildPhase section */
318321
DD0000000000000000000015 /* Remove Legacy Icon Key */ = {
319322
isa = PBXShellScriptBuildPhase;
323+
alwaysOutOfDate = 1;
320324
buildActionMask = 2147483647;
321325
files = (
322326
);
@@ -361,6 +365,7 @@
361365
BB0000000000000000000027 /* DebugTrace.swift in Sources */,
362366
BB0000000000000000000032 /* SemanticVersion.swift in Sources */,
363367
BB0000000000000000000033 /* UpdateChecker.swift in Sources */,
368+
BB0000000000000000000036 /* UpdateInstaller.swift in Sources */,
364369
);
365370
runOnlyForDeploymentPostprocessing = 0;
366371
};
@@ -446,11 +451,12 @@
446451
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
447452
MTL_FAST_MATH = YES;
448453
OCTODOT_BUILD_NUMBER = 1;
449-
OCTODOT_MARKETING_VERSION = 0.5.7;
454+
OCTODOT_MARKETING_VERSION = 0.5.8;
450455
ONLY_ACTIVE_ARCH = YES;
451456
SDKROOT = macosx;
452457
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
453458
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
459+
SWIFT_STRICT_CONCURRENCY = complete;
454460
};
455461
name = Debug;
456462
};
@@ -502,10 +508,11 @@
502508
MACOSX_DEPLOYMENT_TARGET = 14.0;
503509
MTL_FAST_MATH = YES;
504510
OCTODOT_BUILD_NUMBER = 1;
505-
OCTODOT_MARKETING_VERSION = 0.5.7;
511+
OCTODOT_MARKETING_VERSION = 0.5.8;
506512
SDKROOT = macosx;
507513
SWIFT_COMPILATION_MODE = wholemodule;
508514
SWIFT_OPTIMIZATION_LEVEL = "-O";
515+
SWIFT_STRICT_CONCURRENCY = complete;
509516
};
510517
name = Release;
511518
};

0 commit comments

Comments
 (0)