From 472f77b5f0acefaee87f502abd7dc2b3e9150032 Mon Sep 17 00:00:00 2001 From: Gale W Date: Thu, 4 Jun 2026 22:38:50 -0400 Subject: [PATCH 1/2] helper: add installed app validation diagnostics --- Docs/Architecture/RealAppTestingPlan.md | 10 +- README.md | 8 + ROADMAP.md | 4 +- Sirious.xcodeproj/project.pbxproj | 10 +- Sources/App/App.swift | 4 + Sources/App/AutomationHelperAgentState.swift | 2 +- Sources/App/AutomationHelperDiagnostics.swift | 202 ++++++++++++++++++ .../AutomationHelperXPC.swift | 1 + ...alewilliams.Sirious.AutomationHelper.plist | 2 +- .../AutomationHelperAgentStateTests.swift | 2 +- .../AutomationHelperDiagnosticsTests.swift | 25 +++ project.yml | 2 +- .../validate-installed-automation-helper.sh | 169 +++++++++++++++ 13 files changed, 432 insertions(+), 9 deletions(-) create mode 100644 Sources/App/AutomationHelperDiagnostics.swift create mode 100644 Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift create mode 100755 scripts/validate-installed-automation-helper.sh diff --git a/Docs/Architecture/RealAppTestingPlan.md b/Docs/Architecture/RealAppTestingPlan.md index 0924507..cf6260f 100644 --- a/Docs/Architecture/RealAppTestingPlan.md +++ b/Docs/Architecture/RealAppTestingPlan.md @@ -29,6 +29,7 @@ This plan keeps those two tracks separate. Normal Xcode tests should remain repe - [XCUITest](https://developer.apple.com/documentation/xctest) remains the default UI automation surface for Sirious-owned app windows and ordinary app-launch assertions. - [XCUIElement.waitForExistence(timeout:)](https://developer.apple.com/documentation/xctest/xcuielement/2879412-waitforexistence) is the preferred wait primitive for UI automation. Tests should avoid fixed sleeps except where a real external app or audio route has no observable readiness signal. - [Accessibility AXUIElement](https://developer.apple.com/documentation/applicationservices/axuielement_h?language=objc) remains the primary real-app inspection and editing surface for focused controls. +- [Apple's Service Management package-installer sample](https://developer.apple.com/documentation/ServiceManagement/updating-your-app-package-installer-to-use-the-new-service-management-api) is the next reference shape for package-style LaunchAgent validation when copied `.app` probes still report `.notFound`. - [Audio Hijack scripting](https://rogueamoeba.com/support/manuals/audiohijack/?page=scripting) is the first documented automation candidate for supervised audio-session setup. - [Loopback](https://rogueamoeba.com/support/manuals/loopback/?print=true) is the first documented virtual-device candidate for routing generated audio into Sirious as microphone-like input. - [SoundSource](https://www.rogueamoeba.com/support/manuals/soundsource/) is a documented per-app audio control candidate for supervised local routing checks. @@ -164,13 +165,15 @@ Sandbox and helper direction: - Keep the main app sandboxed for ordinary app behavior. Put assistive automation behind a separate helper boundary so the permissioned process is narrow and operator-visible. - Prefer a bundled LaunchAgent registered with `SMAppService.agent(plistName:)` before considering a LaunchDaemon, because these automation flows run in the logged-in user session and need user-facing Accessibility context. The first checked-in helper is `SiriousAutomationHelper`, a hardened-runtime command-line tool copied into the app bundle with its LaunchAgent plist at `Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist`. - `SiriousAutomationHelper` now embeds generated Info.plist metadata into the executable with `CREATE_INFOPLIST_SECTION_IN_BINARY` and `GENERATE_INFOPLIST_FILE`, keeping the helper identity visible to codesigning and Service Management. -- The helper LaunchAgent plist declares `BundleProgram` as `Contents/MacOS/SiriousAutomationHelper` and advertises `MachServices` for `com.galewilliams.Sirious.AutomationHelper`. +- The helper LaunchAgent plist declares `BundleProgram` as `Contents/Resources/SiriousAutomationHelper` and advertises `MachServices` for `com.galewilliams.Sirious.AutomationHelper`. - Use the XPC command channel as the normal app-to-helper path. The app sends command arguments to the helper's Mach service, and the helper returns termination status plus output/error strings over a small shared protocol. The helper still accepts direct CLI commands such as `--status` for diagnostics. - Gale's local Apple signing team identifier is `BC73766F69`. Some certificate common names include `AMRC3N39SQ`, but the certificate subject's organizational unit is the Team ID Xcode uses for `DEVELOPMENT_TEAM`. Keep `DEVELOPMENT_TEAM` aligned with `BC73766F69` in `project.yml` so macOS TCC can associate prompts with a stable development identity during real-app testing. Installed-app validation note: -The next helper slice should install or copy the built app into a stable local app location before treating Service Management status as authoritative. The validated bundle checks so far are: the LaunchAgent plist is copied to `Contents/Library/LaunchAgents`, the helper direct CLI answers `--status`, and the helper binary contains an embedded `__TEXT,__info_plist` section. The remaining check is whether `SMAppService.agent(plistName:)` moves from `.notFound` to `.notRegistered`, `.requiresApproval`, or `.enabled` after running from that stable installed location. +`scripts/validate-installed-automation-helper.sh` is the local installed-app probe. It builds Sirious, copies the app to a stable temporary path, verifies the app and helper signatures, confirms the LaunchAgent plist exists, runs the helper direct CLI, and asks the installed app for `SMAppService.agent(plistName:)` status. With `--register`, it can also request registration, check the XPC `--status` path when the agent is enabled, and unregister afterward. + +Current local result: the copied app reports the expected app bundle path and bundle identifier, the signatures verify, the helper direct CLI answers `--status`, and the LaunchAgent plist is present at `Contents/Library/LaunchAgents`, but `SMAppService.agent(plistName:)` still reports `.notFound` from both the repo-local `.build` copy and a `~/Applications/SiriousInstalledAppValidation/Sirious.app` copy. The next helper slice should follow Apple's package-style sample more closely by installing the controller app under an Application Support location through a package or equivalent install step, then rerun the same status/register/XPC checks from that installed path. ## Initial Scenario Matrix @@ -217,6 +220,7 @@ Cleanup failures should be reported as first-class test diagnostics. They should 11. Add supervised routed-audio scenarios that play generated command audio through the virtual microphone path. 12. Add Computer Use notes and recovery hooks only for scenarios where app automation or audio tooling leaves a real gap. 13. Add a local installed-app helper validation script or manual runbook that copies the built app into a stable test location, checks `SMAppService.agent(plistName:)` status, registers the LaunchAgent, verifies the XPC `--status` equivalent path, prompts for helper Accessibility trust, and unregisters or removes the test install cleanly. +14. Add a package-style installed-helper probe when copied app validation still reports `.notFound`, matching Apple's sample install shape before treating Service Management registration as blocked by code structure. ## Non-Goals @@ -233,4 +237,4 @@ Cleanup failures should be reported as first-class test diagnostics. They should - Which app versions and target apps should be treated as required for the first real-app validation pass? - How much audio-route setup should Sirious automate versus only detect and document? - Should routed-audio runs produce a local report artifact separate from `.xcresult` so route, app, and permission state can be inspected outside Xcode? -- Should the first installed-app helper validation use a plain copied `.app` under a local build-products or Applications-style directory, or should it use a `.pkg` path matching Apple's Service Management sample before broader real-app automation depends on it? +- Does a package-style install under Application Support move `SMAppService.agent(plistName:)` from `.notFound` to `.notRegistered`, `.requiresApproval`, or `.enabled` for the current helper layout? diff --git a/README.md b/README.md index ac36887..981f448 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,14 @@ Real-app and routed-audio testing is planned in [Real App Testing Plan](./Docs/A Automation-helper testing is part of that local-only path. Sirious bundles `SiriousAutomationHelper` as a LaunchAgent-backed helper with an XPC command channel for Accessibility-owned text insertion. The current code validates the built bundle shape, but Service Management registration should be checked from a stable local app install rather than only the DerivedData build product. +Run the installed-app helper validation probe with: + +```sh +scripts/validate-installed-automation-helper.sh --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" --keep-installed +``` + +The script builds Sirious, installs a temporary app copy, verifies the app and helper signatures, checks the bundled LaunchAgent plist, runs the helper directly, and asks the installed app for `SMAppService.agent(plistName:)` status. Add `--register` only when that status is no longer `notFound`; registration can add a Login Items entry and may require local macOS approval. + Install the local SwiftFormat pre-commit hook: ```sh diff --git a/ROADMAP.md b/ROADMAP.md index f6d718c..1bd4be4 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -183,7 +183,8 @@ Planned - [ ] Add Loopback and Audio Hijack route detection before attempting any automatic audio setup. - [ ] Add supervised routed-audio scenarios that play generated command audio through a virtual microphone into Sirious. - [ ] Add Computer Use setup, observation, and recovery notes for scenarios where Accessibility or app automation leaves a real gap. -- [ ] Add an installed-app helper validation slice that copies or packages Sirious into a stable local app location before checking `SMAppService.agent(plistName:)` status, registration, XPC connection, and helper Accessibility prompting. +- [x] Add an installed-app helper validation slice that copies Sirious into a stable local app location before checking bundle shape, signatures, direct helper status, and `SMAppService.agent(plistName:)` status. +- [ ] Add a package-style installed-helper probe to determine whether Apple's Application Support install shape moves Service Management out of `.notFound` before registration, XPC connection, and helper Accessibility prompting. - [ ] Decide which scenarios belong in a local `.xctestplan`, which should be manifest-gated, and which should remain manual supervised checks. ### Exit Criteria @@ -220,3 +221,4 @@ Planned - Added a repo-local SpeakSwiftlyServer fixture generation command and refreshed the paired MP3 corpus through the live service. - Added the local-only real-app scenario model for gated setup, expectations, cleanup, and artifact reporting. - Added a bundled automation helper LaunchAgent with embedded helper Info.plist metadata, a `MachServices` plist entry, and an XPC command channel for helper-owned Accessibility commands. +- Added an installed-app helper validation script and captured that copied app installs still report `.notFound` from Service Management despite valid bundle shape and signatures. diff --git a/Sirious.xcodeproj/project.pbxproj b/Sirious.xcodeproj/project.pbxproj index 6603e65..38b7f69 100644 --- a/Sirious.xcodeproj/project.pbxproj +++ b/Sirious.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 186B2F4188A61F1837AB0663 /* WorkspaceStateProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EC8789017EFD204AFA940AF /* WorkspaceStateProviding.swift */; }; 188C565FF6D5248D8D1347DC /* InstalledApplicationCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA94681BB3770374ED64F0E /* InstalledApplicationCandidate.swift */; }; 1A4930B07DFE71B984524BBB /* RoutingMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90148173640BEF7E4606F92 /* RoutingMode.swift */; }; + 1F910B2AD1BE2155E2B93A88 /* AutomationHelperDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D38CDDDF3EA5F3EA1F7DDF /* AutomationHelperDiagnostics.swift */; }; 2160056700C4D8EBDC9C045B /* TextCommandExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB4F9D0CEB18279D1874CB15 /* TextCommandExecutor.swift */; }; 2707C2AA585F5AFEF3044D17 /* FirstStageContextResolverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B21CE25C80990889763EA0 /* FirstStageContextResolverTests.swift */; }; 28B0A35DABBF66D3FF0FF1BE /* TranscriptStability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651F0FD88128DF3A97143425 /* TranscriptStability.swift */; }; @@ -53,6 +54,7 @@ 4933BD4D831E07B912B7F90D /* FocusedControlContextTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40EACC58DBF817A1563ED62B /* FocusedControlContextTests.swift */; }; 4A3B40B9AD2193BB729B47C3 /* AlwaysOnTopWindowModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F25AD60A1982BAEEEC94A25 /* AlwaysOnTopWindowModifier.swift */; }; 4ABB56139DD6C016C8399376 /* HomeDirectoryAccessStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3F4C28FBAE9F3A38B521B /* HomeDirectoryAccessStateTests.swift */; }; + 4CEB0E91424FDC6DC42F47F7 /* AutomationHelperDiagnosticsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD012BB90FBBFF5C7BBB9421 /* AutomationHelperDiagnosticsTests.swift */; }; 4DBF8FFECBD83AF60203FC1D /* LoginItemState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E234BBC3AAAF94F9508106 /* LoginItemState.swift */; }; 4E34D5CB0D601B3CDC02BF1E /* CommandCenterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD4EBE52C0BFB14673B01E /* CommandCenterView.swift */; }; 50099EFFB3EE7D840F7A8CC9 /* ServiceCommandPatterns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12033D743AA63617DB2BDF13 /* ServiceCommandPatterns.swift */; }; @@ -169,7 +171,7 @@ isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; - dstSubfolderSpec = 6; + dstSubfolderSpec = 7; files = ( 8315DA30FD65517F9BC0D94C /* SiriousAutomationHelper in Embed Dependencies */, ); @@ -202,6 +204,7 @@ 13F1634F0993DB7160D620AA /* SiriousRuntimeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriousRuntimeTests.swift; sourceTree = ""; }; 14215BF76E17A6633AC975DF /* CommandExecutionDispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandExecutionDispatcherTests.swift; sourceTree = ""; }; 162110AD8307759C8420ADCA /* AppleSpeechAudioFileTranscriptSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleSpeechAudioFileTranscriptSourceTests.swift; sourceTree = ""; }; + 17D38CDDDF3EA5F3EA1F7DDF /* AutomationHelperDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomationHelperDiagnostics.swift; sourceTree = ""; }; 17E2C4728C5F48A7751164F4 /* FocusedControlProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedControlProviding.swift; sourceTree = ""; }; 19F29D22576FD82326A5026A /* Sirious.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Sirious.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1A885F28380DE16550CD5B98 /* WindowCommandExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCommandExecutor.swift; sourceTree = ""; }; @@ -284,6 +287,7 @@ B8E9BFD6ED449FBB8CA4C292 /* AutomationHelperXPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomationHelperXPC.swift; sourceTree = ""; }; B8EA67D9801A1BF625CB20F1 /* StaticSystemContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticSystemContextProvider.swift; sourceTree = ""; }; B92B42F8B22ED5FEB646DB3C /* SystemCommandCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemCommandCatalog.swift; sourceTree = ""; }; + BD012BB90FBBFF5C7BBB9421 /* AutomationHelperDiagnosticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomationHelperDiagnosticsTests.swift; sourceTree = ""; }; BF632DE15C3368A00F28A886 /* TranscriptionRuntimeState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptionRuntimeState.swift; sourceTree = ""; }; C2A6CF196BEB44DD84ABE401 /* AutomationHelperAgentStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomationHelperAgentStateTests.swift; sourceTree = ""; }; C4C3F4C28FBAE9F3A38B521B /* HomeDirectoryAccessStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeDirectoryAccessStateTests.swift; sourceTree = ""; }; @@ -335,6 +339,7 @@ 43560F72CDBA2EA76A53C277 /* AppleSpeechAudioFixtureManifestTests.swift */, C2A6CF196BEB44DD84ABE401 /* AutomationHelperAgentStateTests.swift */, ED530957FFB4911E7B8A30B7 /* AutomationHelperCommandRunnerTests.swift */, + BD012BB90FBBFF5C7BBB9421 /* AutomationHelperDiagnosticsTests.swift */, 14215BF76E17A6633AC975DF /* CommandExecutionDispatcherTests.swift */, 5BA62AC9B866BCC81D86D71E /* CommandExecutionRequestResolverTests.swift */, B54A7393D86AF7BB9BF5AF29 /* FirstStageAppWindowRoutingTests.swift */, @@ -528,6 +533,7 @@ 076D6E1313CFACB95E4771C6 /* AppWindowID.swift */, 25644243BE990A3B2DF456E8 /* AutomationHelperAgentState.swift */, EA118D63B32331E2697D4EC0 /* AutomationHelperCommandRunner.swift */, + 17D38CDDDF3EA5F3EA1F7DDF /* AutomationHelperDiagnostics.swift */, 5CBD4EBE52C0BFB14673B01E /* CommandCenterView.swift */, 5352FD80637F096E940491CE /* DebugView.swift */, D5E234BBC3AAAF94F9508106 /* LoginItemState.swift */, @@ -865,6 +871,7 @@ CA0E871BC79B92B10294CA0E /* AudioStateProviding.swift in Sources */, ABF7C1C2A441CADB7693A35E /* AutomationHelperAgentState.swift in Sources */, E8C146D6F028BAE2B009778F /* AutomationHelperCommandRunner.swift in Sources */, + 1F910B2AD1BE2155E2B93A88 /* AutomationHelperDiagnostics.swift in Sources */, 87710B2712D729F4D6DD8447 /* AutomationHelperXPC.swift in Sources */, 4E34D5CB0D601B3CDC02BF1E /* CommandCenterView.swift in Sources */, 9EAC0DD3373858B219B00E86 /* CommandComplexity.swift in Sources */, @@ -952,6 +959,7 @@ 098CDB9100377D9E39375590 /* AppleSpeechAudioFixtureManifestTests.swift in Sources */, 2B494155510D7962D58B6CB8 /* AutomationHelperAgentStateTests.swift in Sources */, E384475FB82B008258CFA04C /* AutomationHelperCommandRunnerTests.swift in Sources */, + 4CEB0E91424FDC6DC42F47F7 /* AutomationHelperDiagnosticsTests.swift in Sources */, 365D61CB718CC9F9CF0EA6D8 /* CommandExecutionDispatcherTests.swift in Sources */, B94C28C850A93DEFC53BB948 /* CommandExecutionRequestResolverTests.swift in Sources */, 7BAEFD42BADA77C080573354 /* FirstStageAppWindowRoutingTests.swift in Sources */, diff --git a/Sources/App/App.swift b/Sources/App/App.swift index 83b4062..85254d8 100644 --- a/Sources/App/App.swift +++ b/Sources/App/App.swift @@ -4,6 +4,10 @@ import SwiftUI struct SiriousApp: App { @State private var runtime = SiriousRuntime() + init() { + AutomationHelperDiagnostics.exitIfRequested() + } + var body: some Scene { WindowGroup { CommandCenterView() diff --git a/Sources/App/AutomationHelperAgentState.swift b/Sources/App/AutomationHelperAgentState.swift index 3299dd6..e72de64 100644 --- a/Sources/App/AutomationHelperAgentState.swift +++ b/Sources/App/AutomationHelperAgentState.swift @@ -33,7 +33,7 @@ protocol AutomationHelperAgentServiceProviding { } struct AutomationHelperAgentService: AutomationHelperAgentServiceProviding { - static let plistName = "com.galewilliams.Sirious.AutomationHelper.plist" + static let plistName = AutomationHelperXPC.launchAgentPlistName private var service: SMAppService { SMAppService.agent(plistName: Self.plistName) diff --git a/Sources/App/AutomationHelperDiagnostics.swift b/Sources/App/AutomationHelperDiagnostics.swift new file mode 100644 index 0000000..e2e65fe --- /dev/null +++ b/Sources/App/AutomationHelperDiagnostics.swift @@ -0,0 +1,202 @@ +import Foundation +import ServiceManagement + +enum AutomationHelperDiagnosticCommand: String, CaseIterable { + case status = "--automation-helper-status" + case register = "--automation-helper-register" + case unregister = "--automation-helper-unregister" + case xpcStatus = "--automation-helper-xpc-status" + + static var usage: String { + allCases.map(\.rawValue).joined(separator: ", ") + } +} + +enum AutomationHelperDiagnostics { + static func exitIfRequested(arguments: [String] = CommandLine.arguments) { + guard let command = command(from: arguments) else { + return + } + + let result = run(command) + print(result.message) + Foundation.exit(result.exitCode) + } + + static func command(from arguments: [String]) -> AutomationHelperDiagnosticCommand? { + let diagnosticArguments = arguments.dropFirst() + + guard let firstArgument = diagnosticArguments.first else { + return nil + } + + return AutomationHelperDiagnosticCommand(rawValue: firstArgument) + } + + static func run(_ command: AutomationHelperDiagnosticCommand) -> AutomationHelperDiagnosticResult { + let service = SMAppService.agent(plistName: AutomationHelperXPC.launchAgentPlistName) + + switch command { + case .status: + return .success(""" + Automation helper status: \(service.status.diagnosticDescription). + App bundle path: \(Bundle.main.bundlePath). + App bundle identifier: \(Bundle.main.bundleIdentifier ?? "unknown"). + """) + + case .register: + do { + try service.register() + return .success("Automation helper registration requested. Current status: \(service.status.diagnosticDescription).") + } catch { + return .failure("Sirious could not register the automation helper. macOS reported: \(error.localizedDescription). Current status: \(service.status.diagnosticDescription).") + } + + case .unregister: + do { + try service.unregister() + return .success("Automation helper unregistration requested. Current status: \(service.status.diagnosticDescription).") + } catch { + return .failure("Sirious could not unregister the automation helper. macOS reported: \(error.localizedDescription). Current status: \(service.status.diagnosticDescription).") + } + + case .xpcStatus: + let result = AutomationHelperXPCDiagnostics.run(arguments: AutomationHelperCommand.status.arguments) + + if result.succeeded { + return .success(result.trimmedMessage) + } + + return .failure(result.trimmedMessage) + } + } +} + +struct AutomationHelperDiagnosticResult: Equatable { + var exitCode: Int32 + var message: String + + static func success(_ message: String) -> Self { + Self(exitCode: 0, message: message) + } + + static func failure(_ message: String) -> Self { + Self(exitCode: 1, message: message) + } +} + +private enum AutomationHelperXPCDiagnostics { + static func run(arguments: [String]) -> AutomationHelperCommandResult { + let completion = AutomationHelperXPCDiagnosticCompletion(arguments: arguments) + let connection = NSXPCConnection( + machServiceName: AutomationHelperXPC.machServiceName, + options: [] + ) + + connection.remoteObjectInterface = NSXPCInterface(with: AutomationHelperXPCProtocol.self) + connection.invalidationHandler = { + completion.finishWithConnectionError( + "Sirious lost its XPC connection to the automation helper before the helper returned a response." + ) + } + connection.interruptionHandler = { + completion.finishWithConnectionError( + "Sirious had its XPC connection to the automation helper interrupted before the helper returned a response." + ) + } + connection.resume() + + let proxy = connection.remoteObjectProxyWithErrorHandler { error in + completion.finishWithConnectionError( + "Sirious could not connect to the automation helper XPC service named \(AutomationHelperXPC.machServiceName). macOS reported: \(error.localizedDescription)" + ) + connection.invalidate() + } + + guard let helper = proxy as? AutomationHelperXPCProtocol else { + connection.invalidate() + return AutomationHelperCommandResult( + terminationStatus: 126, + standardOutput: "", + standardError: "Sirious could not create an XPC proxy for the automation helper command protocol." + ) + } + + helper.runCommand(arguments) { reply in + completion.finish(with: reply) + connection.invalidate() + } + + return completion.wait() + } +} + +private final class AutomationHelperXPCDiagnosticCompletion: @unchecked Sendable { + private let arguments: [String] + private let semaphore = DispatchSemaphore(value: 0) + private let lock = NSLock() + private var result: AutomationHelperCommandResult? + + init(arguments: [String]) { + self.arguments = arguments + } + + func finish(with reply: NSDictionary) { + finish(AutomationHelperCommandResult(xpcReply: reply)) + } + + func finishWithConnectionError(_ message: String) { + finish(AutomationHelperCommandResult( + terminationStatus: 126, + standardOutput: "", + standardError: "\(message) Command: \(arguments.joined(separator: " "))." + )) + } + + func wait() -> AutomationHelperCommandResult { + let deadline = DispatchTime.now() + .seconds(10) + + if semaphore.wait(timeout: deadline) == .success, + let result + { + return result + } + + return AutomationHelperCommandResult( + terminationStatus: 124, + standardOutput: "", + standardError: "Sirious timed out waiting for the automation helper XPC service named \(AutomationHelperXPC.machServiceName). Command: \(arguments.joined(separator: " "))." + ) + } + + private func finish(_ result: AutomationHelperCommandResult) { + lock.lock() + defer { + lock.unlock() + } + + guard self.result == nil else { + return + } + + self.result = result + semaphore.signal() + } +} + +private extension SMAppService.Status { + var diagnosticDescription: String { + switch self { + case .notRegistered: + "notRegistered(rawValue: \(rawValue))" + case .enabled: + "enabled(rawValue: \(rawValue))" + case .requiresApproval: + "requiresApproval(rawValue: \(rawValue))" + case .notFound: + "notFound(rawValue: \(rawValue))" + @unknown default: + "unknown(rawValue: \(rawValue))" + } + } +} diff --git a/Sources/AutomationHelperSupport/AutomationHelperXPC.swift b/Sources/AutomationHelperSupport/AutomationHelperXPC.swift index 4b100b1..06444fe 100644 --- a/Sources/AutomationHelperSupport/AutomationHelperXPC.swift +++ b/Sources/AutomationHelperSupport/AutomationHelperXPC.swift @@ -1,6 +1,7 @@ import Foundation enum AutomationHelperXPC { + static let launchAgentPlistName = "com.galewilliams.Sirious.AutomationHelper.plist" static let machServiceName = "com.galewilliams.Sirious.AutomationHelper" static let terminationStatusKey = "terminationStatus" static let standardOutputKey = "standardOutput" diff --git a/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist b/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist index e39cdfd..64af66c 100644 --- a/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist +++ b/Sources/Support/com.galewilliams.Sirious.AutomationHelper.plist @@ -7,7 +7,7 @@ com.galewilliams.Sirious BundleProgram - Contents/MacOS/SiriousAutomationHelper + Contents/Resources/SiriousAutomationHelper Label com.galewilliams.Sirious.AutomationHelper MachServices diff --git a/Tests/SiriousTests/AutomationHelperAgentStateTests.swift b/Tests/SiriousTests/AutomationHelperAgentStateTests.swift index 2b574dc..9070a2f 100644 --- a/Tests/SiriousTests/AutomationHelperAgentStateTests.swift +++ b/Tests/SiriousTests/AutomationHelperAgentStateTests.swift @@ -18,7 +18,7 @@ struct AutomationHelperAgentStateTests { let plist = try #require(PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any]) #expect(plist["Label"] as? String == "com.galewilliams.Sirious.AutomationHelper") - #expect(plist["BundleProgram"] as? String == "Contents/MacOS/SiriousAutomationHelper") + #expect(plist["BundleProgram"] as? String == "Contents/Resources/SiriousAutomationHelper") #expect(plist["AssociatedBundleIdentifiers"] as? [String] == ["com.galewilliams.Sirious"]) #expect(plist["MachServices"] as? [String: Bool] == [ "com.galewilliams.Sirious.AutomationHelper": true, diff --git a/Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift b/Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift new file mode 100644 index 0000000..6eb8881 --- /dev/null +++ b/Tests/SiriousTests/AutomationHelperDiagnosticsTests.swift @@ -0,0 +1,25 @@ +@testable import Sirious +import Testing + +struct AutomationHelperDiagnosticsTests { + @Test("automation helper diagnostics ignore ordinary app launch") + func automationHelperDiagnosticsIgnoreOrdinaryAppLaunch() { + let command = AutomationHelperDiagnostics.command(from: ["/Applications/Sirious.app/Contents/MacOS/Sirious"]) + + #expect(command == nil) + } + + @Test("automation helper diagnostics parse helper flags") + func automationHelperDiagnosticsParseHelperFlags() { + #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--automation-helper-status"]) == .status) + #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--automation-helper-register"]) == .register) + #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--automation-helper-unregister"]) == .unregister) + #expect(AutomationHelperDiagnostics.command(from: ["Sirious", "--automation-helper-xpc-status"]) == .xpcStatus) + } + + @Test("automation helper diagnostic result factories map exit codes") + func automationHelperDiagnosticResultFactoriesMapExitCodes() { + #expect(AutomationHelperDiagnosticResult.success("ok").exitCode == 0) + #expect(AutomationHelperDiagnosticResult.failure("nope").exitCode == 1) + } +} diff --git a/project.yml b/project.yml index 996bcae..9ee2cad 100644 --- a/project.yml +++ b/project.yml @@ -44,7 +44,7 @@ targets: - target: SiriousAutomationHelper embed: true copy: - destination: executables + destination: resources info: path: Sources/Support/Info.plist properties: diff --git a/scripts/validate-installed-automation-helper.sh b/scripts/validate-installed-automation-helper.sh new file mode 100755 index 0000000..d450c82 --- /dev/null +++ b/scripts/validate-installed-automation-helper.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env sh +set -eu + +SELF_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +REPO_ROOT=$(CDPATH= cd -- "$SELF_DIR/.." && pwd) + +CONFIGURATION=Debug +DERIVED_DATA_PATH="$REPO_ROOT/.build/InstalledAppValidation/DerivedData" +INSTALL_APP_PATH="$REPO_ROOT/.build/InstalledAppValidation/Sirious.app" +RUN_REGISTRATION=0 +KEEP_INSTALLED=0 +SKIP_BUILD=0 + +usage() { + cat <<'USAGE' +Usage: scripts/validate-installed-automation-helper.sh [options] + +Build Sirious, copy the app to a stable local test path, and validate the bundled +SiriousAutomationHelper LaunchAgent shape. + +Options: + --configuration NAME Xcode configuration to build. Default: Debug. + --derived-data PATH DerivedData path for the validation build. + --install-app PATH Destination .app path for the test install. + --register Register, XPC-check, and unregister the LaunchAgent. + --skip-build Reuse the app already built under the DerivedData path. + --keep-installed Leave the copied .app in place after validation. + -h, --help Show this help. + +The default install path stays inside the repository's .build directory. For a +closer local install test, pass a stable user location such as: + + --install-app "$HOME/Applications/SiriousInstalledAppValidation/Sirious.app" +USAGE +} + +log() { + printf '%s\n' "$*" +} + +fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +while [ "$#" -gt 0 ]; do + case "$1" in + --configuration) + [ "$#" -ge 2 ] || fail "--configuration requires a value." + CONFIGURATION=$2 + shift 2 + ;; + --derived-data) + [ "$#" -ge 2 ] || fail "--derived-data requires a value." + DERIVED_DATA_PATH=$2 + shift 2 + ;; + --install-app) + [ "$#" -ge 2 ] || fail "--install-app requires a value." + INSTALL_APP_PATH=$2 + shift 2 + ;; + --register) + RUN_REGISTRATION=1 + shift + ;; + --skip-build) + SKIP_BUILD=1 + shift + ;; + --keep-installed) + KEEP_INSTALLED=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "Unsupported argument '$1'. Run with --help for usage." + ;; + esac +done + +BUILT_APP_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/Sirious.app" +INSTALLED_HELPER_PATH="$INSTALL_APP_PATH/Contents/Resources/SiriousAutomationHelper" +INSTALLED_AGENT_PLIST="$INSTALL_APP_PATH/Contents/Library/LaunchAgents/com.galewilliams.Sirious.AutomationHelper.plist" +INSTALLED_APP_EXECUTABLE="$INSTALL_APP_PATH/Contents/MacOS/Sirious" + +cleanup() { + if [ "$KEEP_INSTALLED" -eq 0 ]; then + rm -rf "$INSTALL_APP_PATH" + fi +} + +trap cleanup EXIT + +cd "$REPO_ROOT" + +if [ "$SKIP_BUILD" -eq 0 ]; then + log "Building Sirious $CONFIGURATION into $DERIVED_DATA_PATH." + xcodebuild build \ + -project Sirious.xcodeproj \ + -scheme Sirious \ + -configuration "$CONFIGURATION" \ + -destination "platform=macOS,arch=arm64" \ + -derivedDataPath "$DERIVED_DATA_PATH" +fi + +[ -d "$BUILT_APP_PATH" ] || fail "Expected built app at $BUILT_APP_PATH, but it does not exist." + +log "Installing validation copy at $INSTALL_APP_PATH." +mkdir -p "$(dirname -- "$INSTALL_APP_PATH")" +rm -rf "$INSTALL_APP_PATH" +cp -R "$BUILT_APP_PATH" "$INSTALL_APP_PATH" + +[ -x "$INSTALLED_APP_EXECUTABLE" ] || fail "Installed app executable is missing or not executable at $INSTALLED_APP_EXECUTABLE." +[ -x "$INSTALLED_HELPER_PATH" ] || fail "Installed helper executable is missing or not executable at $INSTALLED_HELPER_PATH." +[ -f "$INSTALLED_AGENT_PLIST" ] || fail "Installed LaunchAgent plist is missing at $INSTALLED_AGENT_PLIST." + +log "Installed app signature:" +codesign --verify --deep --strict --verbose=2 "$INSTALL_APP_PATH" + +log "Installed helper signature:" +codesign --verify --strict --verbose=2 "$INSTALLED_HELPER_PATH" + +log "Installed LaunchAgent plist:" +plutil -p "$INSTALLED_AGENT_PLIST" + +log "Direct helper diagnostic:" +"$INSTALLED_HELPER_PATH" --status + +log "ServiceManagement status from installed app:" +STATUS_OUTPUT=$("$INSTALLED_APP_EXECUTABLE" --automation-helper-status) +printf '%s\n' "$STATUS_OUTPUT" + +case "$STATUS_OUTPUT" in + *notFound*) + fail "ServiceManagement still reports the automation helper as notFound from the installed app at $INSTALL_APP_PATH." + ;; +esac + +if [ "$RUN_REGISTRATION" -eq 0 ]; then + log "Skipping registration because --register was not provided." + exit 0 +fi + +log "Registering installed automation helper." +"$INSTALLED_APP_EXECUTABLE" --automation-helper-register + +log "ServiceManagement status after registration:" +REGISTERED_STATUS_OUTPUT=$("$INSTALLED_APP_EXECUTABLE" --automation-helper-status) +printf '%s\n' "$REGISTERED_STATUS_OUTPUT" + +case "$REGISTERED_STATUS_OUTPUT" in + *enabled*) + log "Checking installed automation helper XPC status." + "$INSTALLED_APP_EXECUTABLE" --automation-helper-xpc-status + ;; + *requiresApproval*) + log "macOS requires Login Items approval before the LaunchAgent can run; skipping XPC status until approval is granted." + ;; + *) + fail "Expected enabled or requiresApproval after registration, but got: $REGISTERED_STATUS_OUTPUT" + ;; +esac + +log "Unregistering installed automation helper." +"$INSTALLED_APP_EXECUTABLE" --automation-helper-unregister From 8f0ba23460ea7a7c1c72794e3a9726d143e45239 Mon Sep 17 00:00:00 2001 From: Gale W Date: Sun, 7 Jun 2026 17:14:15 -0400 Subject: [PATCH 2/2] build: sync xcode guidance and maintenance Why: Refresh this XcodeGen repo's local guidance and managed repo-maintenance surfaces to the current Apple/Xcode baseline. Verification: sh scripts/repo-maintenance/validate-all.sh --- .codex/environments/xcode-project.toml | 22 ++++++++++ .../workflows/validate-repo-maintenance.yml | 1 + .swiftformat | 1 - AGENTS.md | 13 +++++- scripts/repo-maintenance/lib/common.sh | 44 +++++++++++++++++++ scripts/repo-maintenance/release.sh | 9 +++- .../release/40-github-release.sh | 9 +++- 7 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 .codex/environments/xcode-project.toml diff --git a/.codex/environments/xcode-project.toml b/.codex/environments/xcode-project.toml new file mode 100644 index 0000000..fcca4c0 --- /dev/null +++ b/.codex/environments/xcode-project.toml @@ -0,0 +1,22 @@ +# Copy to .codex/environments/xcode-project.toml and replace SCHEME_NAME. +# Keep DerivedData inside the worktree so Codex GUI worktree validation is isolated. +version = 1 +name = "xcode-project" + +[setup] +script = "xcodebuild -scheme Sirious -derivedDataPath ./DerivedData -resolvePackageDependencies" + +[[actions]] +name = "resolve packages" +icon = "tool" +command = "xcodebuild -scheme Sirious -derivedDataPath ./DerivedData -resolvePackageDependencies" + +[[actions]] +name = "build" +icon = "tool" +command = "xcodebuild -scheme Sirious -derivedDataPath ./DerivedData build" + +[[actions]] +name = "test" +icon = "tool" +command = "xcodebuild -scheme Sirious -derivedDataPath ./DerivedData test" diff --git a/.github/workflows/validate-repo-maintenance.yml b/.github/workflows/validate-repo-maintenance.yml index afd62d7..5e05590 100644 --- a/.github/workflows/validate-repo-maintenance.yml +++ b/.github/workflows/validate-repo-maintenance.yml @@ -14,6 +14,7 @@ jobs: name: validate runs-on: macos-26 steps: + # This is a validated floor, not a ceiling; update to newer stable official versions when validated. - uses: actions/checkout@v6.0.2 - name: Report selected Xcode run: xcode-select --print-path diff --git a/.swiftformat b/.swiftformat index 5d3d2b7..62ecf88 100644 --- a/.swiftformat +++ b/.swiftformat @@ -5,7 +5,6 @@ --rules andOperator,anyObjectProtocol,applicationMain,assertionFailures,blankLineAfterImports,blankLinesAfterGuardStatements,blankLinesAroundMark,blankLinesAtEndOfScope,blankLinesAtStartOfScope,blankLinesBetweenChainedFunctions,blankLinesBetweenImports,blankLinesBetweenScopes,braces,conditionalAssignment,consecutiveBlankLines,consecutiveSpaces,consistentSwitchCaseSpacing,docComments,docCommentsBeforeModifiers,duplicateImports,elseOnSameLine,emptyBraces,emptyExtensions,enumNamespaces,environmentEntry,extensionAccessControl,fileMacro,genericExtensions,headerFileName,hoistAwait,hoistPatternLet,hoistTry,indent,initCoderUnavailable,isEmpty,leadingDelimiters,linebreakAtEndOfFile,linebreaks,modifierOrder,noForceTryInTests,noForceUnwrapInTests,noGuardInTests,numberFormatting,opaqueGenericParameters,organizeDeclarations,preferFinalClasses,privateStateVariables,redundantAsync,redundantBackticks,redundantBreak,redundantClosure,redundantEquatable,redundantExtensionACL,redundantFileprivate,redundantGet,redundantInit,redundantInternal,redundantLet,redundantLetError,redundantMemberwiseInit,redundantNilInit,redundantObjc,redundantOptionalBinding,redundantParens,redundantPattern,redundantPublic,redundantRawValues,redundantReturn,redundantSelf,redundantSendable,redundantStaticSelf,redundantSwiftTestingSuite,redundantThrows,redundantType,redundantTypedThrows,redundantVariable,redundantViewBuilder,semicolons,simplifyGenericConstraints,sortDeclarations,sortImports,sortTypealiases,spaceAroundBraces,spaceAroundBrackets,spaceAroundComments,spaceAroundGenerics,spaceAroundOperators,spaceAroundParens,spaceInsideBrackets,spaceInsideComments,spaceInsideGenerics,spaceInsideParens,strongOutlets,strongifiedSelf,swiftTestingTestCaseNames,todos,trailingClosures,trailingCommas,trailingSpace,typeSugar,validateTestCases,void,wrap,wrapArguments,wrapAttributes,wrapLoopBodies,wrapMultilineFunctionChains,wrapSingleLineComments,yodaConditions ---swift-version 6.0 --acronyms ID,URL,UUID --allow-partial-wrapping true --anonymous-for-each convert diff --git a/AGENTS.md b/AGENTS.md index cf24aa6..19149c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,15 +1,17 @@ # AGENTS.md +## Apple / Xcode Project Workflow + - Use `xcode-build-run-workflow` for normal Xcode build, run, diagnostics, preview, file-membership, and guarded mutation work inside this existing project. - Use `xcode-testing-workflow` when the task is primarily about Swift Testing, XCTest, XCUITest, `.xctestplan`, flaky tests, retries, or test diagnosis. - Use `apple-ui-accessibility-workflow` when the task is primarily about SwiftUI accessibility semantics, Apple UI accessibility review, accessibility tree shaping, or UIKit/AppKit accessibility bridge behavior. -- Use `sync-xcode-project-guidance` when this repo's local workflow guidance drifts and should be refreshed or merged forward. +- Use `sync-xcode-project-guidance` when the repo guidance for this project drifts and needs to be refreshed or merged forward. - Re-run `sync-xcode-project-guidance` after substantial Xcode-workflow or plugin updates so local guidance stays aligned. - Use `scripts/repo-maintenance/validate-all.sh` for local maintainer validation and `scripts/repo-maintenance/sync-shared.sh` for repo-local sync steps. - Use `scripts/repo-maintenance/release.sh --mode standard --version vX.Y.Z` from a feature branch or worktree only when the task is actually a protected-main release, publish, merge, tag, or release-PR preparation. - Do not run the standard release workflow from `main`; when a protected-main release is explicitly requested, let it validate, bump versions, tag, push the branch and tag, open the release PR, watch CI, address valid PR comments or record out-of-scope concerns in `ROADMAP.md`, merge to protected `main`, fast-forward local `main`, and clean up stale branches. - Treat `scripts/repo-maintenance/config/profile.env` as the installed `maintain-project-repo` profile marker, and keep it on the `xcode-app` profile for native Apple app repos. -- Read relevant Apple documentation before proposing or making Xcode, SwiftUI, lifecycle, or architecture changes. +- Read relevant Apple documentation before proposing or making Xcode, SwiftUI, lifecycle, architecture, or build-configuration changes. - Prefer Dash or local Apple docs first, then official Apple docs when local docs are insufficient. - Prefer the simplest correct Swift that is easiest to read and reason about. - Prefer synthesized and framework-provided behavior over extra wrappers and boilerplate. @@ -19,8 +21,15 @@ - Keep data flow straight and dependency direction unidirectional. - Treat the `.xcworkspace` or `.xcodeproj` as the source of truth for app integration, schemes, and build settings. - Prefer Xcode-aware tooling or `xcodebuild` over ad hoc filesystem assumptions when project structure or target membership is involved. +- For new Xcode app, framework, and workspace repositories, prefer XcodeGen plus checked-in `.xcconfig` files by default unless there is a concrete reason to avoid that generator dependency. +- If this repo is XcodeGen-backed, treat `project.yml`, `project.yaml`, and any included XcodeGen specs as the source of truth for generated targets, schemes, build settings, build configurations, packages, and file membership. +- For XcodeGen-backed projects, edit the spec set and rerun `xcodegen generate` instead of hand-editing generated `.pbxproj` files. +- Prefer external `.xcconfig` files for nontrivial build settings, wire them from the XcodeGen spec, keep secrets out of committed configs, and review config diffs with the spec and generated project diff. +- After regenerating an XcodeGen project, review the spec diff, `.xcconfig` diff, and generated `.xcodeproj` diff, then validate the affected scheme with explicit `xcodebuild` commands. - Prefer Swift Testing for modern unit-style tests, keep XCTest where Apple tooling or dependencies still require it, and use XCUITest with explicit element wait APIs instead of fixed sleeps. - Keep `.xctestplan` files versioned when the project depends on repeatable test-plan configurations, and inspect or run them explicitly with `xcodebuild -showTestPlans` and `xcodebuild -testPlan ...`. +- Prefer normal Xcode and XCTest parallel execution for ordinary Swift Testing, XCTest, and XCUITest runs when the project, scheme, destination, and test plan support it. Do not serialize regular tests just because they use Swift, XCTest, async tests, UI automation, or `.xctestplan` matrices. +- Treat tests that load large local AI or ML models, especially models over 500 million parameters, as heavy system-resource tests. Run those tests sequentially, one at a time, and call `unload_models` on Gale's live TTS service before the heavy run and `reload_models` after it ends, even when the run fails or is interrupted. - Prefer a checked-in repo-root `.swiftformat` file as the Swift formatting source of truth. - Prefer a pre-commit hook such as `scripts/repo-maintenance/hooks/pre-commit.sample` that formats staged Swift sources and then verifies them with `swiftformat --lint` before commit. - Treat SwiftLint as an optional complementary signal layer for clarity, safety, and maintainability after SwiftFormat owns formatting shape. diff --git a/scripts/repo-maintenance/lib/common.sh b/scripts/repo-maintenance/lib/common.sh index b456429..b0afa95 100755 --- a/scripts/repo-maintenance/lib/common.sh +++ b/scripts/repo-maintenance/lib/common.sh @@ -55,6 +55,50 @@ positive_integer_or_default() { esac } +is_semver_prerelease_tag() { + tag_name="$1" + case "$tag_name" in + v[0-9]*.[0-9]*.[0-9]*-*) + return 0 + ;; + *) + return 1 + ;; + esac +} + +expected_github_prerelease_value() { + tag_name="$1" + if is_semver_prerelease_tag "$tag_name"; then + printf '%s\n' "true" + else + printf '%s\n' "false" + fi +} + +github_release_create_prerelease_flag() { + tag_name="$1" + if is_semver_prerelease_tag "$tag_name"; then + printf '%s\n' "--prerelease" + fi +} + +verify_github_release_prerelease_metadata() { + tag_name="$1" + expected_value="$(expected_github_prerelease_value "$tag_name")" + + actual_value="$(gh release view "$tag_name" --json isPrerelease --jq .isPrerelease 2>/dev/null || true)" + case "$actual_value" in + true|false) + ;; + *) + die "GitHub release $tag_name exists, but its prerelease metadata was not readable. Confirm gh can read release JSON metadata before rerunning release.sh." + ;; + esac + + [ "$actual_value" = "$expected_value" ] || die "GitHub release $tag_name prerelease metadata mismatch: tag implies isPrerelease=$expected_value but GitHub reports isPrerelease=$actual_value. Update the release metadata or delete and recreate the release before rerunning release.sh." +} + github_wait_timeout() { value="$1" default_timeout="$(positive_integer_or_default "${REPO_MAINTENANCE_GH_WAIT_TIMEOUT_SECONDS:-120}" 120)" diff --git a/scripts/repo-maintenance/release.sh b/scripts/repo-maintenance/release.sh index 2d3149b..7151e19 100755 --- a/scripts/repo-maintenance/release.sh +++ b/scripts/repo-maintenance/release.sh @@ -398,18 +398,23 @@ create_github_release() { fi if [ "$REPO_MAINTENANCE_DRY_RUN" = "true" ]; then - log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag." + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag${prerelease_flag:+ $prerelease_flag}." return 0 fi if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + verify_github_release_prerelease_metadata "$RELEASE_TAG" log "GitHub release $RELEASE_TAG already exists." return 0 fi - gh release create "$RELEASE_TAG" --verify-tag --generate-notes + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + # shellcheck disable=SC2086 + gh release create "$RELEASE_TAG" --verify-tag --generate-notes $prerelease_flag log "Created GitHub release $RELEASE_TAG." wait_for_github_release "$RELEASE_TAG" + verify_github_release_prerelease_metadata "$RELEASE_TAG" } cleanup_merged_branches() { diff --git a/scripts/repo-maintenance/release/40-github-release.sh b/scripts/repo-maintenance/release/40-github-release.sh index 0be674d..2220e09 100755 --- a/scripts/repo-maintenance/release/40-github-release.sh +++ b/scripts/repo-maintenance/release/40-github-release.sh @@ -16,15 +16,20 @@ if ! command -v gh >/dev/null 2>&1; then fi if [ "${REPO_MAINTENANCE_DRY_RUN:-false}" = "true" ]; then - log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag." + prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" + log "Would create a GitHub release for $RELEASE_TAG with gh release create --verify-tag${prerelease_flag:+ $prerelease_flag}." exit 0 fi if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + verify_github_release_prerelease_metadata "$RELEASE_TAG" log "GitHub release $RELEASE_TAG already exists." exit 0 fi -gh release create "$RELEASE_TAG" --verify-tag --generate-notes +prerelease_flag="$(github_release_create_prerelease_flag "$RELEASE_TAG")" +# shellcheck disable=SC2086 +gh release create "$RELEASE_TAG" --verify-tag --generate-notes $prerelease_flag log "Created GitHub release $RELEASE_TAG." wait_for_github_release "$RELEASE_TAG" +verify_github_release_prerelease_metadata "$RELEASE_TAG"