Skip to content

Commit 5e850b9

Browse files
Add DMG release packaging
1 parent a7a1093 commit 5e850b9

13 files changed

Lines changed: 320 additions & 13 deletions

.github/workflows/release.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
build-release:
13+
runs-on: macos-14
14+
15+
steps:
16+
- name: Check out repository
17+
uses: actions/checkout@v4
18+
19+
- name: Show toolchain versions
20+
run: |
21+
sw_vers
22+
swift --version
23+
24+
- name: Build DMG
25+
run: |
26+
chmod +x scripts/build-release-app.sh scripts/build-release-dmg.sh
27+
VERSION="${GITHUB_REF_NAME}" ./scripts/build-release-dmg.sh
28+
29+
- name: Generate checksum
30+
run: |
31+
cd dist
32+
shasum -a 256 "AeroMux-${GITHUB_REF_NAME}.dmg" > "AeroMux-${GITHUB_REF_NAME}.dmg.sha256"
33+
34+
- name: Publish GitHub release
35+
env:
36+
GH_TOKEN: ${{ github.token }}
37+
run: |
38+
gh release create "${GITHUB_REF_NAME}" \
39+
"dist/AeroMux-${GITHUB_REF_NAME}.dmg" \
40+
"dist/AeroMux-${GITHUB_REF_NAME}.dmg.sha256" \
41+
--generate-notes \
42+
--title "${GITHUB_REF_NAME}"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.build/
22
.DS_Store
3+
dist/

Packaging/Info.plist

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>en</string>
7+
<key>CFBundleDisplayName</key>
8+
<string>AeroMux</string>
9+
<key>CFBundleExecutable</key>
10+
<string>AeroMux</string>
11+
<key>CFBundleIdentifier</key>
12+
<string>com.rtalur.aeromux</string>
13+
<key>CFBundleInfoDictionaryVersion</key>
14+
<string>6.0</string>
15+
<key>CFBundleName</key>
16+
<string>AeroMux</string>
17+
<key>CFBundlePackageType</key>
18+
<string>APPL</string>
19+
<key>CFBundleShortVersionString</key>
20+
<string>__VERSION__</string>
21+
<key>CFBundleVersion</key>
22+
<string>__VERSION__</string>
23+
<key>LSMinimumSystemVersion</key>
24+
<string>13.0</string>
25+
<key>LSUIElement</key>
26+
<true/>
27+
<key>NSHighResolutionCapable</key>
28+
<true/>
29+
</dict>
30+
</plist>

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
AeroMux gives [AeroSpace](https://github.com/nikitabobko/AeroSpace) a persistent macOS sidebar. It keeps your non-empty workspaces visible, highlights the active workspace and window, and lets you click a listed window to focus it.
44

5-
This is an early source-release MVP. It builds and runs with Swift Package Manager today, but it does not yet ship as a standalone `.app`, DMG, or Homebrew package.
5+
This is an early release MVP. It now ships as a GitHub Releases DMG and can still be built locally with Swift Package Manager.
66

77
![AeroMux screenshot](docs/assets/aeromux-screenshot.png)
88

@@ -27,8 +27,8 @@ Before you try it, the current behavior is worth stating clearly:
2727
- The sidebar is anchored to the left edge of the main monitor
2828
- The clean layout depends on an AeroSpace `outer.left` gap reservation
2929
- If the gap is missing or too small, AeroMux falls back to a floating overlay
30-
- There is no menu bar item, Preferences window, or packaged app flow yet
31-
- The simplest way to run it today is from a terminal with `swift run`
30+
- There is no menu bar item or Preferences window yet
31+
- Release DMGs are currently ad hoc signed but not notarized
3232

3333
## Requirements
3434

@@ -50,6 +50,12 @@ If `which aerospace` prints nothing, AeroMux will fail to talk to AeroSpace.
5050

5151
## Install And Run
5252

53+
### Download The DMG
54+
55+
The preferred install path is the latest DMG from [GitHub Releases](https://github.com/raghavendra-talur/aeromux/releases).
56+
57+
Current release builds are ad hoc signed but not notarized, so macOS may warn on first launch. If that happens, open the app with Finder's `Open` flow and confirm the warning once.
58+
5359
### Run Directly From Source
5460

5561
```bash
@@ -182,8 +188,8 @@ Check the basics first:
182188

183189
- Main monitor only
184190
- Left sidebar only
185-
- Source-build installation only
186191
- No menu bar control or in-app quit flow yet
192+
- Release builds are not notarized yet
187193
- No published compatibility matrix yet for Intel Macs or multiple AeroSpace versions
188194

189195
## Verified On This Machine
@@ -210,3 +216,7 @@ Issues and compatibility reports are useful, especially for:
210216
- AeroSpace version compatibility
211217
- refresh-hook integration examples
212218
- ideas for packaging and a better app lifecycle
219+
220+
## Releasing
221+
222+
If you are maintaining this repository, local packaging and tag-based GitHub Releases are documented in [docs/RELEASING.md](docs/RELEASING.md).

Sources/App/AppEnvironment.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,24 @@ final class AppEnvironment {
1313
init() {
1414
logger = AppLogger()
1515
settings = SettingsStore()
16+
let aerospaceExecutablePath = AeroSpaceExecutableResolver.resolve()
1617
let commandRunner = ProcessCommandRunner(logger: logger)
17-
let client = AeroSpaceClient(commandRunner: commandRunner, logger: logger)
18-
let configService = AeroSpaceConfigService(commandRunner: commandRunner, logger: logger)
18+
let client = AeroSpaceClient(
19+
commandRunner: commandRunner,
20+
aerospaceExecutablePath: aerospaceExecutablePath,
21+
logger: logger
22+
)
23+
let configService = AeroSpaceConfigService(
24+
commandRunner: commandRunner,
25+
aerospaceExecutablePath: aerospaceExecutablePath,
26+
logger: logger
27+
)
1928
stateStore = SidebarStateStore(settings: settings, logger: logger)
20-
focusService = FocusService(commandRunner: commandRunner, logger: logger)
29+
focusService = FocusService(
30+
commandRunner: commandRunner,
31+
aerospaceExecutablePath: aerospaceExecutablePath,
32+
logger: logger
33+
)
2134
refreshCoordinator = RefreshCoordinator(
2235
settings: settings,
2336
client: client,

Sources/Services/AeroSpaceClient.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ enum AeroSpaceClientError: Error, LocalizedError {
2727

2828
actor AeroSpaceClient {
2929
private let commandRunner: CommandRunning
30+
private let aerospaceExecutablePath: String?
3031
private let logger: AppLogger
3132

32-
init(commandRunner: CommandRunning, logger: AppLogger) {
33+
init(commandRunner: CommandRunning, aerospaceExecutablePath: String?, logger: AppLogger) {
3334
self.commandRunner = commandRunner
35+
self.aerospaceExecutablePath = aerospaceExecutablePath
3436
self.logger = logger
3537
}
3638

@@ -94,9 +96,13 @@ actor AeroSpaceClient {
9496
}
9597

9698
private func runAeroSpace(arguments: [String], allowFailure: Bool) async throws -> String? {
99+
guard let aerospaceExecutablePath else {
100+
throw AeroSpaceClientError.binaryMissing
101+
}
102+
97103
let result: CommandResult
98104
do {
99-
result = try await commandRunner.run("/usr/bin/env", arguments: ["aerospace"] + arguments)
105+
result = try await commandRunner.run(aerospaceExecutablePath, arguments: arguments)
100106
} catch {
101107
throw AeroSpaceClientError.binaryMissing
102108
}

Sources/Services/AeroSpaceConfigService.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import Foundation
22

33
actor AeroSpaceConfigService {
44
private let commandRunner: CommandRunning
5+
private let aerospaceExecutablePath: String?
56
private let logger: AppLogger
67

7-
init(commandRunner: CommandRunning, logger: AppLogger) {
8+
init(commandRunner: CommandRunning, aerospaceExecutablePath: String?, logger: AppLogger) {
89
self.commandRunner = commandRunner
10+
self.aerospaceExecutablePath = aerospaceExecutablePath
911
self.logger = logger
1012
}
1113

@@ -47,7 +49,11 @@ actor AeroSpaceConfigService {
4749
}
4850

4951
private func configPath() async throws -> URL {
50-
let result = try await commandRunner.run("/usr/bin/env", arguments: ["aerospace", "config", "--config-path"])
52+
guard let aerospaceExecutablePath else {
53+
throw CommandError.launchFailure("AeroSpace CLI not found.")
54+
}
55+
56+
let result = try await commandRunner.run(aerospaceExecutablePath, arguments: ["config", "--config-path"])
5157
guard result.exitCode == 0 else {
5258
throw CommandError.nonZeroExit(result.stderr.isEmpty ? "Unable to read AeroSpace config path." : result.stderr)
5359
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
3+
enum AeroSpaceExecutableResolver {
4+
static func resolve() -> String? {
5+
let fileManager = FileManager.default
6+
let environment = ProcessInfo.processInfo.environment
7+
8+
let pathEntries = (environment["PATH"] ?? "")
9+
.split(separator: ":")
10+
.map(String.init)
11+
12+
let fallbackDirectories = [
13+
"/opt/homebrew/bin",
14+
"/usr/local/bin",
15+
"/opt/local/bin",
16+
"/usr/bin",
17+
"/bin",
18+
"\(NSHomeDirectory())/.local/bin",
19+
"\(NSHomeDirectory())/bin",
20+
]
21+
22+
var seen = Set<String>()
23+
let candidates = (pathEntries + fallbackDirectories).compactMap { directory -> String? in
24+
guard !directory.isEmpty else { return nil }
25+
let candidate = URL(fileURLWithPath: directory)
26+
.appendingPathComponent("aerospace")
27+
.path
28+
guard seen.insert(candidate).inserted else { return nil }
29+
return candidate
30+
}
31+
32+
return candidates.first(where: { fileManager.isExecutableFile(atPath: $0) })
33+
}
34+
}

Sources/Services/FocusService.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@ enum FocusServiceError: Error, LocalizedError {
1616

1717
actor FocusService {
1818
private let commandRunner: CommandRunning
19+
private let aerospaceExecutablePath: String?
1920
private let logger: AppLogger
2021

21-
init(commandRunner: CommandRunning, logger: AppLogger) {
22+
init(commandRunner: CommandRunning, aerospaceExecutablePath: String?, logger: AppLogger) {
2223
self.commandRunner = commandRunner
24+
self.aerospaceExecutablePath = aerospaceExecutablePath
2325
self.logger = logger
2426
}
2527

2628
func focus(windowId: String) async {
2729
logger.info("focus.request \(windowId)")
2830
do {
29-
let result = try await commandRunner.run("/usr/bin/env", arguments: ["aerospace", "focus", "--window-id", windowId])
31+
guard let aerospaceExecutablePath else {
32+
throw FocusServiceError.failed("AeroSpace CLI not found.")
33+
}
34+
35+
let result = try await commandRunner.run(aerospaceExecutablePath, arguments: ["focus", "--window-id", windowId])
3036
guard result.exitCode == 0 else {
3137
if result.stderr.contains("unknown") || result.stderr.contains("Usage") {
3238
throw FocusServiceError.unsupported

docs/RELEASE_CHECKLIST.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Release Checklist
2+
3+
Use this before posting AeroMux publicly outside your own network.
4+
5+
## Must Have
6+
7+
- Verify the README from a clean shell on macOS
8+
- Confirm `swift build` and `swift run` work without local hacks
9+
- Replace SSH clone URLs with HTTPS in any public-facing post if you want friction-free copy/paste
10+
- Add at least one screenshot to the repository or release post
11+
- State clearly that this is an early DMG release and not notarized yet
12+
- State clearly that the current layout is main-monitor-only and left-sidebar-only
13+
- State clearly that a reserved AeroSpace left gap is recommended
14+
15+
## Strongly Recommended
16+
17+
- Create a GitHub release for `v0.1`
18+
- Add a short changelog section or release notes
19+
- Test against at least one more AeroSpace version
20+
- Test on a second machine or user account
21+
- Verify behavior when `aerospace` is missing from `PATH`
22+
- Verify behavior when `outer.left` is absent
23+
- Verify behavior when `focus --window-id` is unsupported
24+
25+
## Nice To Have
26+
27+
- A menu bar item with Quit and Relaunch
28+
- A compatibility matrix in the README
29+
- A short demo GIF in the repo
30+
- CI that at least runs `swift build`
31+
32+
## Suggested Public Positioning
33+
34+
Use language like:
35+
36+
`AeroMux is an early DMG release for AeroSpace users on macOS.`
37+
38+
Avoid language like:
39+
40+
`This is a polished Mac app`
41+
42+
until you add notarization and a more complete app lifecycle.

0 commit comments

Comments
 (0)