diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 93bd2cb..2ec0080 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,8 +11,20 @@ permissions:
jobs:
macos:
- name: macOS helper
- runs-on: macos-latest
+ name: macOS helper (${{ matrix.os }})
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ # GitHub retired the macos-13 runner label in December 2025, so
+ # we can't continuously verify the documented macOS 13 support
+ # floor on CI. The Swift helper is still pinned to
+ # `-target arm64-apple-macos13.0` in the build script, which
+ # makes the shipped Mach-O run on macOS 13. macos-14 covers the
+ # older Swift 5.10 toolchain (catches Swift 6.x-only syntax);
+ # macos-latest covers the current SDK + Swift toolchain.
+ os: [macos-14, macos-latest]
steps:
- name: Check out repository
@@ -48,3 +60,21 @@ jobs:
- name: Build ad-hoc signed helper
run: make scanner-ci
+
+ # Aggregator job pinned in branch protection. Its name stays stable
+ # ("macOS helper") even as the matrix expands, so adding new OS rows
+ # doesn't require editing the required-status-checks contract.
+ macos-helper:
+ name: macOS helper
+ runs-on: ubuntu-latest
+ needs: macos
+ if: always()
+ steps:
+ - name: Require macOS matrix to pass
+ env:
+ MATRIX_RESULT: ${{ needs.macos.result }}
+ run: |
+ if [ "$MATRIX_RESULT" != "success" ]; then
+ echo "macOS matrix concluded with: $MATRIX_RESULT" >&2
+ exit 1
+ fi
diff --git a/README.md b/README.md
index 92c615f..09f1944 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,19 @@ Permissions requested:
- Location Services for WiFi discovery.
- macOS Keychain for password retrieval.
+### Versioning
+
+`macwifi` follows [Semantic Versioning](https://semver.org/). The exported
+Go API of the `macwifi` package — types, functions, and option helpers —
+is stable across the v1.x line: no breaking changes will land without a
+major-version bump. The wire protocol between the Go client and the
+embedded helper is an internal implementation detail and is not covered
+by this commitment.
+
+Versions before v1.0.0 are retracted in `go.mod` when they cannot meet
+the macOS support floor; `go get` and tooling like Dependabot will steer
+you to the latest non-retracted release.
+
### Reuse One Helper Session
When scanning for networks and then fetching a password in one run, create a
@@ -163,39 +176,41 @@ MACWIFI_APP=$PWD/WifiScanner.app go run ./examples/scan
`MACWIFI_APP` tells the Go package to use a local helper bundle instead of the
embedded one.
-## Releasing the Helper
-
-The embedded helper is part of the Go module via `go:embed`. To cut a release
-with an updated helper:
-
-```sh
-make release
-# commit, tag, and push
-```
-
-`make release` builds, signs with Developer ID, notarizes, staples, and copies
-the bundle into `embedded/WifiScanner.app`.
-
-Release prerequisites:
+## Releases
+
+Releases are fully automated. Day-to-day:
+
+1. Land changes on `main` using [Conventional Commits](https://www.conventionalcommits.org/).
+2. [Release Please](https://github.com/googleapis/release-please) keeps a
+ rolling Release PR open with the next `CHANGELOG.md` entry, the
+ `go.mod`-aware version bump, and the `embeddedVersion` constant in
+ `embed.go`.
+3. Merging the Release PR tags the version, publishes a GitHub Release,
+ primes `proxy.golang.org`, and attaches the signed `WifiScanner.app`
+ zip to the release.
+
+The signed companion workflow handles the helper bundle. Pull requests
+that touch `scanner/Sources/`, `scanner/Info.plist`,
+`scanner/entitlements.plist`, or the helper build scripts trigger a
+macOS runner that builds, codesigns with Developer ID, notarizes,
+staples, and commits the regenerated `embedded/WifiScanner.app` back to
+the PR branch before merge. The workflow expects the environment
+secrets documented in
+[`.github/SIGNING_SECRETS.md`](.github/SIGNING_SECRETS.md).
-- A **Developer ID Application** certificate in your login keychain.
-- A `xcrun notarytool` credential profile named `macwifi-notary`, or another
- profile set through `$NOTARY_PROFILE`.
+### Local helper rebuild (development only)
-Set up the default notary profile once:
+When iterating on Swift code, you don't need notarization — just an
+ad-hoc signed bundle pointed at via `MACWIFI_APP`:
```sh
-xcrun notarytool store-credentials macwifi-notary \
- --apple-id YOUR_APPLE_ID \
- --team-id YOUR_TEAM_ID \
- --password YOUR_APP_SPECIFIC_PASSWORD
+make scanner
+MACWIFI_APP=$PWD/WifiScanner.app go run ./examples/scan
```
-On GitHub, pull requests to `main` that touch the Swift companion run the
-signed companion workflow. The workflow signs, notarizes, staples, and commits
-the updated `embedded/WifiScanner.app` back to the pull request branch before
-merge. The workflow expects the environment secrets documented in
-[`.github/SIGNING_SECRETS.md`](.github/SIGNING_SECRETS.md).
+A full Developer ID + notarized release build can be produced locally
+with `make release`, but it is not required to ship — CI handles that
+end-to-end.
## Contributing
diff --git a/client_smoke_test.go b/client_smoke_test.go
new file mode 100644
index 0000000..d9f0c44
--- /dev/null
+++ b/client_smoke_test.go
@@ -0,0 +1,42 @@
+//go:build darwin
+
+package macwifi_test
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/jaisonerick/macwifi"
+)
+
+// TestClientLifecycle exercises the Go↔helper launch path end-to-end:
+// extract the embedded bundle, launch it via `open -W`, accept the
+// helper's loopback callback, send a close_request, and wait for a
+// clean process exit.
+//
+// Deliberately avoids Scan/Password so no Location Services or
+// Keychain prompt can appear on interactive machines. Skipped in
+// -short mode because it spawns a real GUI app and round-trips through
+// macOS launch services.
+func TestClientLifecycle(t *testing.T) {
+ if testing.Short() {
+ t.Skip("launches the signed helper app; skipped under -short")
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ c, err := macwifi.New(ctx)
+ if err != nil {
+ t.Fatalf("New: %v", err)
+ }
+ if err := c.Close(); err != nil {
+ t.Fatalf("Close: %v", err)
+ }
+
+ // Close is documented as idempotent; a second call must not error.
+ if err := c.Close(); err != nil {
+ t.Fatalf("second Close: %v", err)
+ }
+}
diff --git a/embedded/WifiScanner.app/Contents/CodeResources b/embedded/WifiScanner.app/Contents/CodeResources
index 26f364f..06a7dbd 100644
Binary files a/embedded/WifiScanner.app/Contents/CodeResources and b/embedded/WifiScanner.app/Contents/CodeResources differ
diff --git a/embedded/WifiScanner.app/Contents/Info.plist b/embedded/WifiScanner.app/Contents/Info.plist
index 95c28c3..3c01da5 100644
--- a/embedded/WifiScanner.app/Contents/Info.plist
+++ b/embedded/WifiScanner.app/Contents/Info.plist
@@ -11,9 +11,9 @@
CFBundleExecutable
wifi-scanner
CFBundleVersion
- 1
+ 1.0.0
CFBundleShortVersionString
- 1.0
+ 1.0.0
CFBundlePackageType
APPL
CFBundleInfoDictionaryVersion
diff --git a/embedded/WifiScanner.app/Contents/MacOS/wifi-scanner b/embedded/WifiScanner.app/Contents/MacOS/wifi-scanner
index 0ef8d4c..ba7e543 100755
Binary files a/embedded/WifiScanner.app/Contents/MacOS/wifi-scanner and b/embedded/WifiScanner.app/Contents/MacOS/wifi-scanner differ
diff --git a/embedded/WifiScanner.app/Contents/Resources/macwifi-build.json b/embedded/WifiScanner.app/Contents/Resources/macwifi-build.json
index 8415d40..f256c9c 100644
--- a/embedded/WifiScanner.app/Contents/Resources/macwifi-build.json
+++ b/embedded/WifiScanner.app/Contents/Resources/macwifi-build.json
@@ -1,3 +1,3 @@
{
- "sourceDigest": "fa7747c57327c80db8d945b689b5831781c274e4c6c3b8347d7c7488567d745e"
+ "sourceDigest": "823f2847f331e0810002d8916bebf9afd868fc45feb238fa14dd9d42a17a5fab"
}
diff --git a/embedded/WifiScanner.app/Contents/_CodeSignature/CodeResources b/embedded/WifiScanner.app/Contents/_CodeSignature/CodeResources
index 275a9f1..beab298 100644
--- a/embedded/WifiScanner.app/Contents/_CodeSignature/CodeResources
+++ b/embedded/WifiScanner.app/Contents/_CodeSignature/CodeResources
@@ -6,7 +6,7 @@
Resources/macwifi-build.json
- ZhY/eeOnXMNmm2MrPMmgYCdEmAg=
+ qkQqjLkwtpbFQkhZy2/mZg17G5E=
files2
@@ -15,7 +15,7 @@
hash2
- UNqC9tSATdJqoAMeZu5mlwn/kDC8aW8yN2VoXFfGKxk=
+ 0fQ/3mmE2Q94bir6m6DHI38yMOBig+/RBuDrEeMmGLA=
diff --git a/go.mod b/go.mod
index 305dd1a..540378a 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,14 @@ module github.com/jaisonerick/macwifi
go 1.26.1
-// v0.1.0 was tagged from an off-branch local snapshot that never landed on
-// main and is missing the CI, tests, and Swift-target pin that shipped in
-// v0.1.1 onward. Use v0.1.1 or later.
-retract v0.1.0
+// v0.1.0 was tagged from an off-branch local snapshot that never landed
+// on main. v0.1.1 and v0.1.2 shipped a helper compiled without an
+// explicit Swift target; the resulting bundle's Mach-O LC_BUILD_VERSION
+// pins macOS 15.0 minimum, so the helper refuses to launch on macOS 13
+// and 14 even though the README and Info.plist advertise macOS 13+. Use
+// v0.1.3 or later.
+retract (
+ v0.1.2
+ v0.1.1
+ v0.1.0
+)
diff --git a/scanner/Info.plist b/scanner/Info.plist
index 95c28c3..3c01da5 100644
--- a/scanner/Info.plist
+++ b/scanner/Info.plist
@@ -11,9 +11,9 @@
CFBundleExecutable
wifi-scanner
CFBundleVersion
- 1
+ 1.0.0
CFBundleShortVersionString
- 1.0
+ 1.0.0
CFBundlePackageType
APPL
CFBundleInfoDictionaryVersion
diff --git a/scanner/Sources/WifiScanner.swift b/scanner/Sources/WifiScanner.swift
index 21632b1..f58d911 100644
--- a/scanner/Sources/WifiScanner.swift
+++ b/scanner/Sources/WifiScanner.swift
@@ -357,7 +357,7 @@ func watchParentPID(_ pid: pid_t) {
let source = DispatchSource.makeProcessSource(
identifier: pid,
eventMask: .exit,
- queue: .global(qos: .utility),
+ queue: .global(qos: .utility)
)
source.setEventHandler { exit(0) }
source.resume()
diff --git a/scanner/Tests/WifiScannerTests.swift b/scanner/Tests/WifiScannerTests.swift
index 423e3c5..b3ea3ff 100644
--- a/scanner/Tests/WifiScannerTests.swift
+++ b/scanner/Tests/WifiScannerTests.swift
@@ -50,7 +50,7 @@ func testBinaryWriterUsesLittleEndianAndLengthPrefixes() throws {
0x04, 0x00, 0x77, 0x69, 0x66, 0x69,
0x02, 0xAA, 0xBB,
],
- "BinaryWriter should encode values in the helper wire format",
+ "BinaryWriter should encode values in the helper wire format"
)
}
@@ -97,7 +97,7 @@ func testParseBSSIDAcceptsHexOctets() throws {
try expectEqual(
parseBSSID("01:23:45:ab:CD:ef"),
Data([0x01, 0x23, 0x45, 0xAB, 0xCD, 0xEF]),
- "parseBSSID should accept six hex octets",
+ "parseBSSID should accept six hex octets"
)
}
@@ -119,7 +119,7 @@ func testEncodePasswordResponseMatchesWireFormat() throws {
0x06, 0x00, 0x64, 0x65, 0x6E, 0x69, 0x65, 0x64,
0x06, 0x00, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74,
],
- "password response should match the Go protocol wire format",
+ "password response should match the Go protocol wire format"
)
}
@@ -136,7 +136,7 @@ func testEncodeScanResponseMatchesWireFormat() throws {
phyMode: "ax",
password: "pw",
current: true,
- saved: true,
+ saved: true
)
let response = encodeScanResponse(networks: [network], error: "")
@@ -161,7 +161,7 @@ func testEncodeScanResponseMatchesWireFormat() throws {
0x02, 0x00, 0x70, 0x77,
0x03,
],
- "scan response should match the Go protocol wire format",
+ "scan response should match the Go protocol wire format"
)
}