Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
GOOGLE_REDIRECT_SCHEME = com.googleusercontent.apps.dummy-client-id
EOF

- name: Stamp build metadata
run: ./scripts/stamp_build_metadata.sh .

- name: Install XcodeGen
run: brew install xcodegen

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ jobs:
printf 'DEVELOPMENT_TEAM = %s\n' "$APPLE_TEAM_ID"
} > Config.xcconfig

- name: Stamp build metadata
run: ./scripts/stamp_build_metadata.sh .

- name: Install signing certificate
shell: bash
env:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,7 @@ playground.xcworkspace
*.key
Secrets/
Config.xcconfig

# Generated build metadata (git commit stamp)
BuildMetadata.xcconfig
.wrangler/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Entry point for coding agents working on TaskMenu. Keep this file short and repo
- If you add, remove, rename, or retarget source files, update `project.yml` and run `xcodegen generate`.
- When files are added or deleted, update the corresponding folder-local `README.md` in the same change so its file map and ownership notes stay current.
- Keep `TaskMenu.xcodeproj` generated; do not hand-edit it.
- Do not commit `Config.xcconfig`; it contains local OAuth and signing values.
- Do not commit `Config.xcconfig`; it contains local OAuth and signing values. `BuildConfig.xcconfig` is the committed entry point that includes it, plus the generated `BuildMetadata.xcconfig` git-commit stamp.
- Preserve the menu-bar-only behavior for app launches.
- Update `CHANGELOG.md` before committing feature or bug-fix work, under `## Unreleased` when present.

Expand Down
9 changes: 9 additions & 0 deletions BuildConfig.xcconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Build configuration entry point for both Debug and Release.
//
// Config.xcconfig holds local OAuth and signing values and is never committed;
// copy Config.xcconfig.example to create it.
// BuildMetadata.xcconfig is generated by scripts/stamp_build_metadata.sh and
// supplies GIT_COMMIT_HASH for the version string shown in Settings.

#include? "Config.xcconfig"
#include? "BuildMetadata.xcconfig"
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

## Unreleased

### Added
- Settings now shows the git commit the build came from next to the version number, e.g. `Version 1.3.0 (a1b2c3d)`. Builds made outside a git checkout show `(dev)`.

## v1.3.0 (2026-07-25)

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Or download the signed and notarized DMG from the [latest GitHub release](https:
- XcodeGen-generated Xcode project
- Apple frameworks only, with zero third-party dependencies
- Strict concurrency enabled
- Builds are stamped with the current git commit by `scripts/stamp_build_metadata.sh` (a scheme build pre-action), and Settings shows it next to the version

Build from the command line with:

Expand Down
34 changes: 17 additions & 17 deletions TaskMenu.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
0FD718B29CF8183561A6F550 /* TaskItemModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4CD6808B2587AD2F8F9563 /* TaskItemModelTests.swift */; };
11E28771324DA02B74C9983F /* DateFormattingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 500B536CAEBC7F3B0AF40519 /* DateFormattingTests.swift */; };
16F0CFB570B7684CE136B82E /* MenuBarIcon.svg in Resources */ = {isa = PBXBuildFile; fileRef = D898FA58565B1CBB9ED2695E /* MenuBarIcon.svg */; };
199755B8E809EBE89ADC94B0 /* SettingsVersionRowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5CAE54DF55AAAB094159CE /* SettingsVersionRowTests.swift */; };
27A7DDF8EE882EE13FB18EF6 /* MetricKitPayloadStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77DBAAB7CC102C6006686981 /* MetricKitPayloadStoreTests.swift */; };
2869F2F8237B112F7767EFE6 /* TaskDetailAppKitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BE8A6C21B3086FFB9D3D86 /* TaskDetailAppKitViewController.swift */; };
3059000A80728B12B6989398 /* KeychainServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 058C00FB539FD9B1D05BCBFD /* KeychainServiceTests.swift */; };
Expand Down Expand Up @@ -78,11 +79,13 @@
220E8145372B626FB6D267C7 /* MetricKitService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricKitService.swift; sourceTree = "<group>"; };
246C75CF9B4CE92C49A9A967 /* TaskListContentAppKitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListContentAppKitView.swift; sourceTree = "<group>"; };
2667803C667BBD21DCCCD029 /* TaskListAppKitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListAppKitViewController.swift; sourceTree = "<group>"; };
2C5CAE54DF55AAAB094159CE /* SettingsVersionRowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsVersionRowTests.swift; sourceTree = "<group>"; };
2C84C4C64E2BF982920144CA /* TaskListControlsAppKitViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListControlsAppKitViews.swift; sourceTree = "<group>"; };
33B8E5D270E820D7C6070D4D /* TaskMenuActionButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskMenuActionButtonTests.swift; sourceTree = "<group>"; };
3F2D65032E1F2A015F67B6D0 /* TaskPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskPopoverViewController.swift; sourceTree = "<group>"; };
3FC8DA7EF5FEF032B81469A3 /* GitHubUpdateChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitHubUpdateChecker.swift; sourceTree = "<group>"; };
4041A68FD32907E3B89B8F8B /* MenuBarWindowGlassSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarWindowGlassSupport.swift; sourceTree = "<group>"; };
4332DD537F3F61B0B7FAD4B1 /* BuildConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildConfig.xcconfig; sourceTree = "<group>"; };
4AC00155161191D4DBBDA015 /* SearchFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterTests.swift; sourceTree = "<group>"; };
4DC959FBC4EF3DD0EF5D14D6 /* GoogleTasksAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoogleTasksAPI.swift; sourceTree = "<group>"; };
4E2E315C1C8E04FD083F0AEA /* TaskMenu.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TaskMenu.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand All @@ -96,7 +99,6 @@
68CD93FC69779BFB08340F08 /* TaskMenu.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TaskMenu.entitlements; sourceTree = "<group>"; };
6E4CD6808B2587AD2F8F9563 /* TaskItemModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskItemModelTests.swift; sourceTree = "<group>"; };
77DBAAB7CC102C6006686981 /* MetricKitPayloadStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricKitPayloadStoreTests.swift; sourceTree = "<group>"; };
7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
7DF81FBE51E0B1CC80B1A5FD /* SettingsLaunchAtLoginTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLaunchAtLoginTests.swift; sourceTree = "<group>"; };
85062E7D51749F3ED4FD688D /* AppKitTaskUIHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKitTaskUIHelpers.swift; sourceTree = "<group>"; };
8C8C52C19BDA6F8F2D3C638F /* AppIcon.svg */ = {isa = PBXFileReference; path = AppIcon.svg; sourceTree = "<group>"; };
Expand Down Expand Up @@ -124,15 +126,6 @@
/* End PBXFileReference section */

/* Begin PBXGroup section */
02C7954B6F0AE7D4B33301DC /* TaskMenu */ = {
isa = PBXGroup;
children = (
7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */,
);
name = TaskMenu;
path = .;
sourceTree = "<group>";
};
141A484087CC4923B5F2371C /* Resources */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -236,6 +229,7 @@
BB598B8D34C0A014D7826A63 /* MockURLProtocol.swift */,
4AC00155161191D4DBBDA015 /* SearchFilterTests.swift */,
7DF81FBE51E0B1CC80B1A5FD /* SettingsLaunchAtLoginTests.swift */,
2C5CAE54DF55AAAB094159CE /* SettingsVersionRowTests.swift */,
62C9B2D2EE1B7668B9245DBE /* TaskDetailViewTests.swift */,
6E4CD6808B2587AD2F8F9563 /* TaskItemModelTests.swift */,
A2191A69341B04C760925B55 /* TaskListViewTests.swift */,
Expand All @@ -249,13 +243,22 @@
E7776CF5C61A2557700CB1FE = {
isa = PBXGroup;
children = (
FBAF85847F4317CD71BEB8FB /* git-sha-in-settings */,
A34BA795C75683448AFABACA /* TaskMenu */,
02C7954B6F0AE7D4B33301DC /* TaskMenu */,
C53DB1319E1111ADB350C841 /* TaskMenuTests */,
1E011DB940E268BD3223C95C /* Products */,
);
sourceTree = "<group>";
};
FBAF85847F4317CD71BEB8FB /* git-sha-in-settings */ = {
isa = PBXGroup;
children = (
4332DD537F3F61B0B7FAD4B1 /* BuildConfig.xcconfig */,
);
name = "git-sha-in-settings";
path = .;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -304,11 +307,7 @@
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 2640;
TargetAttributes = {
2499498786EF41DCFC6BE7F6 = {
DevelopmentTeam = V82M9YX8BR;
};
54F3465CB5E85F801680719A = {
DevelopmentTeam = V82M9YX8BR;
ProvisioningStyle = Automatic;
};
};
Expand Down Expand Up @@ -397,6 +396,7 @@
D321CE95DA3DC0D6090CD619 /* MockURLProtocol.swift in Sources */,
8F3D1A4FC0600A0D85BA22E8 /* SearchFilterTests.swift in Sources */,
C3C41DC18427B14CEA020FD6 /* SettingsLaunchAtLoginTests.swift in Sources */,
199755B8E809EBE89ADC94B0 /* SettingsVersionRowTests.swift in Sources */,
F583510B7875405F768BADD3 /* TaskDetailViewTests.swift in Sources */,
0FD718B29CF8183561A6F550 /* TaskItemModelTests.swift in Sources */,
B1E9E90C4184BDD3DED97A2B /* TaskListViewTests.swift in Sources */,
Expand Down Expand Up @@ -440,7 +440,7 @@
};
6EF47391B5EF770AC931DC3F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */;
baseConfigurationReference = 4332DD537F3F61B0B7FAD4B1 /* BuildConfig.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
Expand Down Expand Up @@ -528,7 +528,7 @@
};
A734F39AA9B691088E02AEFF /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7B9B96C944BC0E6F55E3D791 /* Config.xcconfig */;
baseConfigurationReference = 4332DD537F3F61B0B7FAD4B1 /* BuildConfig.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
Expand Down
124 changes: 124 additions & 0 deletions TaskMenu.xcodeproj/xcshareddata/xcschemes/TaskMenu.xcscheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2640"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
runPostActionsOnFailure = "NO">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Stamp build metadata"
scriptText = "&quot;${SRCROOT}/scripts/stamp_build_metadata.sh&quot; &quot;${SRCROOT}&quot;&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "54F3465CB5E85F801680719A"
BuildableName = "TaskMenu.app"
BlueprintName = "TaskMenu"
ReferencedContainer = "container:TaskMenu.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "54F3465CB5E85F801680719A"
BuildableName = "TaskMenu.app"
BlueprintName = "TaskMenu"
ReferencedContainer = "container:TaskMenu.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
onlyGenerateCoverageForSpecifiedTargets = "NO">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "54F3465CB5E85F801680719A"
BuildableName = "TaskMenu.app"
BlueprintName = "TaskMenu"
ReferencedContainer = "container:TaskMenu.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2499498786EF41DCFC6BE7F6"
BuildableName = "TaskMenuTests.xctest"
BlueprintName = "TaskMenuTests"
ReferencedContainer = "container:TaskMenu.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<CommandLineArguments>
</CommandLineArguments>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "54F3465CB5E85F801680719A"
BuildableName = "TaskMenu.app"
BlueprintName = "TaskMenu"
ReferencedContainer = "container:TaskMenu.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "54F3465CB5E85F801680719A"
BuildableName = "TaskMenu.app"
BlueprintName = "TaskMenu"
ReferencedContainer = "container:TaskMenu.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
</CommandLineArguments>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
15 changes: 14 additions & 1 deletion TaskMenu/Models/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ final class AppState {
var isCheckingForUpdates = false
var updateCheckErrorMessage: String?
let currentAppVersion: String
/// Short git commit the running build was stamped with, or nil for builds
/// made outside a git checkout.
let currentBuildCommit: String?

/// Version plus build commit, e.g. `1.3.0 (a1b2c3d)`. Unstamped builds
/// show `dev` in place of the commit. Update checks compare
/// `currentAppVersion`, not this string.
var currentAppVersionDisplay: String {
"\(currentAppVersion) (\(currentBuildCommit ?? "dev"))"
}

var selectedList: TaskList? {
taskLists.first { $0.id == selectedListId }
Expand Down Expand Up @@ -234,14 +244,17 @@ final class AppState {
userDefaults: UserDefaults = .standard,
dueDateNotificationService: any DueDateNotificationServicing = DueDateNotificationService(),
updateChecker: any UpdateChecking = GitHubUpdateChecker(),
currentAppVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
currentAppVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0",
currentBuildCommit: String? = Bundle.main.infoDictionary?["GITCommitHash"] as? String
) {
self.authService = authService
self.api = api ?? GoogleTasksAPI(authService: authService)
self.userDefaults = userDefaults
self.dueDateNotificationService = dueDateNotificationService
self.updateChecker = updateChecker
self.currentAppVersion = currentAppVersion
let trimmedBuildCommit = currentBuildCommit?.trimmingCharacters(in: .whitespacesAndNewlines)
self.currentBuildCommit = (trimmedBuildCommit?.isEmpty ?? true) ? nil : trimmedBuildCommit
self.dueDateNotificationsEnabled = userDefaults.object(
forKey: Constants.UserDefaults.dueDateNotificationsEnabledKey
) as? Bool ?? true
Expand Down
3 changes: 2 additions & 1 deletion TaskMenu/Models/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Models hold the app's main state container and Google Tasks data shapes. Keep th

## Files

- `AppState.swift` - `@MainActor @Observable` source of truth for auth state, signed-in Google account profile, task lists, selected list, visible tasks, caches, row disclosure, search, due-date notification preference, update-check state, and task mutations.
- `AppState.swift` - `@MainActor @Observable` source of truth for auth state, signed-in Google account profile, task lists, selected list, visible tasks, caches, row disclosure, search, due-date notification preference, update-check state, build version identity, and task mutations.
- `TaskItem.swift` - Google Task model, completion helpers, parent/subtask fields, Google due-date conversion, and paged task-list response model.
- `TaskList.swift` - Google Task List model and collection response model.

Expand All @@ -15,6 +15,7 @@ Models hold the app's main state container and Google Tasks data shapes. Keep th
- Preserve `toggleTask(_:)` optimistic-update rollback behavior. If an optimistic API call fails, restore the prior local task state and set `errorMessage`.
- Keep the per-list cache in sync when adding, adding subtasks, completing, updating, deleting, or selecting lists.
- Use `taskLoadRequestID` guards when introducing async task-loading work so stale responses cannot overwrite the active list. Fetches also capture `taskStateGeneration`, which every committed mutation bumps, so a fetch snapshot taken before a local change is discarded rather than applied.
- Keep update checks comparing `currentAppVersion` (marketing version only). `currentBuildCommit` and `currentAppVersionDisplay` are display-only; the latter renders `1.3.0 (a1b2c3d)`, or `(dev)` when the build carries no commit stamp.
- Route post-await mutation writes through `commitTaskChange(to:_:)`: it re-checks the captured list against the current selection and writes to the live array or `taskCacheByListID` accordingly. Re-check `isSignedIn` after every await before touching state.

## Task Ordering And Search
Expand Down
2 changes: 2 additions & 0 deletions TaskMenu/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
<string>public.app-category.productivity</string>
<key>LSUIElement</key>
<true/>
<key>GITCommitHash</key>
<string>$(GIT_COMMIT_HASH)</string>
<key>GOOGLE_CLIENT_ID</key>
<string>$(GOOGLE_CLIENT_ID)</string>
<key>GOOGLE_REDIRECT_SCHEME</key>
Expand Down
Loading
Loading