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
34 changes: 32 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
69 changes: 42 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
42 changes: 42 additions & 0 deletions client_smoke_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Binary file modified embedded/WifiScanner.app/Contents/CodeResources
Binary file not shown.
4 changes: 2 additions & 2 deletions embedded/WifiScanner.app/Contents/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<key>CFBundleExecutable</key>
<string>wifi-scanner</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.0.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
Expand Down
Binary file modified embedded/WifiScanner.app/Contents/MacOS/wifi-scanner
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"sourceDigest": "fa7747c57327c80db8d945b689b5831781c274e4c6c3b8347d7c7488567d745e"
"sourceDigest": "823f2847f331e0810002d8916bebf9afd868fc45feb238fa14dd9d42a17a5fab"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<dict>
<key>Resources/macwifi-build.json</key>
<data>
ZhY/eeOnXMNmm2MrPMmgYCdEmAg=
qkQqjLkwtpbFQkhZy2/mZg17G5E=
</data>
</dict>
<key>files2</key>
Expand All @@ -15,7 +15,7 @@
<dict>
<key>hash2</key>
<data>
UNqC9tSATdJqoAMeZu5mlwn/kDC8aW8yN2VoXFfGKxk=
0fQ/3mmE2Q94bir6m6DHI38yMOBig+/RBuDrEeMmGLA=
</data>
</dict>
</dict>
Expand Down
15 changes: 11 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
4 changes: 2 additions & 2 deletions scanner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<key>CFBundleExecutable</key>
<string>wifi-scanner</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>1.0.0</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.0.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
Expand Down
2 changes: 1 addition & 1 deletion scanner/Sources/WifiScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
10 changes: 5 additions & 5 deletions scanner/Tests/WifiScannerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
}

Expand Down Expand Up @@ -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"
)
}

Expand All @@ -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"
)
}

Expand All @@ -136,7 +136,7 @@ func testEncodeScanResponseMatchesWireFormat() throws {
phyMode: "ax",
password: "pw",
current: true,
saved: true,
saved: true
)

let response = encodeScanResponse(networks: [network], error: "")
Expand All @@ -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"
)
}

Expand Down