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" ) }