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
70 changes: 70 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Publishes a GitHub release when a version tag (v0.1.0, ...) is pushed:
# the ad-hoc signed app as NetScanner-<version>.zip, with that version's
# CHANGELOG section as the notes. The Homebrew cask in
# Criseda/homebrew-tap points at this zip; bump its version and sha256
# (printed in the job log and the release notes) after each release.
name: Release

on:
push:
tags: ["v*"]

permissions:
contents: write

jobs:
release:
name: release
runs-on: macos-26
steps:
- name: Check out the repo
uses: actions/checkout@v7

# The tag must name the version the app reports, and the CHANGELOG
# must have a section for it, or the release would mislead.
- name: Check the tag against the version and CHANGELOG
id: version
run: |
version="$(sed -n 's/^[[:space:]]*MARKETING_VERSION[[:space:]]*=[[:space:]]*\([^[:space:]]*\).*/\1/p' Config/Version.xcconfig)"
if [[ "$GITHUB_REF_NAME" != "v$version" ]]; then
echo "::error::Tag $GITHUB_REF_NAME does not match MARKETING_VERSION $version"
exit 1
fi
awk -v heading="## $version" '
$0 == heading { found = 1; next }
found && /^## / { exit }
found { print }
' CHANGELOG.md > release-notes.md
if [[ ! -s release-notes.md ]]; then
echo "::error::CHANGELOG.md has no \"## $version\" section"
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Run NetScannerKit tests
run: swift test --package-path Packages/NetScannerKit

- name: Fetch the scan engine
run: Scripts/fetch-engine.sh

- name: Build and package the app
id: package
run: |
output="$(Scripts/package-release.sh)"
echo "$output"
echo "zip=$(echo "$output" | tail -n 2 | head -n 1)" >> "$GITHUB_OUTPUT"
echo "sha256=$(echo "$output" | tail -n 1)" >> "$GITHUB_OUTPUT"

- name: Publish the release
env:
GH_TOKEN: ${{ github.token }}
ZIP: ${{ steps.package.outputs.zip }}
SHA256: ${{ steps.package.outputs.sha256 }}
run: |
{
echo
echo "SHA-256 of \`$(basename "$ZIP")\`: \`$SHA256\`"
} >> release-notes.md
gh release create "$GITHUB_REF_NAME" "$ZIP" \
--title "NetScanner for macOS ${{ steps.version.outputs.version }}" \
--notes-file release-notes.md
19 changes: 16 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ and the app target stays thin views plus `@Observable` models.
Scripts/fetch-engine.sh # download the pinned ns (universal), checksum-verified
swift test --package-path Packages/NetScannerKit
xcodebuild -project NetScanner.xcodeproj -scheme NetScanner -configuration Debug build
Scripts/package-release.sh # Release build zipped for GitHub Releases; prints its SHA-256
```

`fetch-engine.sh --local ../NetScanner/zig-out/bin/ns` installs a locally
Expand Down Expand Up @@ -226,9 +227,21 @@ Like the engine repo, every user-facing change gets an entry under

## Distribution

- **Homebrew cask** (first target): Developer ID signed, hardened
runtime, notarized and stapled `.dmg` or `.zip`. Homebrew requires
signed and notarized apps for new casks.
- **Homebrew** (now): the `netscanner-app` cask in
[Criseda/homebrew-tap](https://github.com/Criseda/homebrew-tap), next
to the `netscanner` formula for the `ns` CLI. Pushing a `v<version>`
tag runs `.github/workflows/release.yml`, which checks the tag
against `MARKETING_VERSION` and a `## <version>` CHANGELOG section,
builds with `Scripts/package-release.sh` and publishes
`NetScanner-<version>.zip`; then bump the cask's `version` and
`sha256`. The app is ad-hoc signed, not notarized (no paid developer
account yet), so the official `homebrew/cask` tap will not take it
and users allow the first launch in Privacy & Security.
- **Developer ID** (later, needs a paid developer account): sign,
notarize and staple in the release workflow, which removes the
Gatekeeper prompt and keeps the Local Network grant across updates.
The official `homebrew/cask` tap also needs notability (90 forks, 90
watchers or 225 stars for a self-submission).
- **Mac App Store** (later, needs a paid developer account): requires
App Sandbox. Open questions to verify before committing to it:
whether a sandboxed child can read the ARP table via `sysctl`, open
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ First version of the native macOS app:
engine but not from the app, so the app reads it and hands it to the
engine (`--neighbor-table=parent`).
- Bundles NetScanner v1.7.0 as a universal binary.
- Released as `NetScanner-<version>.zip` on GitHub and as the
`netscanner-app` cask in `criseda/tap` for Homebrew. Not notarized
yet, so macOS asks to allow the first launch.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,23 @@ shows its results in a SwiftUI interface built for macOS 26 and later
[NetScannerDesktop](https://github.com/Criseda/NetScannerDesktop) for
Windows.

## Install

With [Homebrew](https://brew.sh):

```sh
brew install --cask criseda/tap/netscanner-app
```

Or download `NetScanner-<version>.zip` from
[Releases](https://github.com/Criseda/NetScanner-MacOS/releases). The
app is not notarized by Apple yet, so macOS blocks the first launch:
open it once, then choose **Open Anyway** in System Settings ▸ Privacy
& Security.

The `ns` command-line scanner is in the same tap:
`brew install criseda/tap/netscanner`.

## Features

- **Discovery**: TCP, mDNS, SSDP and ARP discovery of a subnet (or ICMP
Expand Down
47 changes: 47 additions & 0 deletions Scripts/package-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/bash
# Builds the Release app and packs it into the zip that GitHub Releases
# and the Homebrew cask (Criseda/homebrew-tap) serve.
#
# Usage:
# Scripts/package-release.sh # writes build/release/NetScanner-<version>.zip
#
# Run Scripts/fetch-engine.sh first. The app is ad-hoc signed ("Sign to
# Run Locally"), not Developer ID signed or notarized, so Gatekeeper
# asks the user to allow it on first launch. The cask's caveats say how.
#
# Prints the zip's path and SHA-256 last, for the cask.

set -euo pipefail

repo_root="$(cd "$(dirname "$0")/.." && pwd)"
config="$repo_root/Config/Version.xcconfig"
build_dir="$repo_root/build/release"

version="$(sed -n 's/^[[:space:]]*MARKETING_VERSION[[:space:]]*=[[:space:]]*\([^[:space:]]*\).*/\1/p' "$config")"
[[ -n "$version" ]] || { echo "error: MARKETING_VERSION is not set in $config" >&2; exit 1; }

[[ -x "$repo_root/Engine/ns" ]] || { echo "error: Engine/ns is missing; run Scripts/fetch-engine.sh" >&2; exit 1; }

rm -rf "$build_dir"
xcodebuild build \
-project "$repo_root/NetScanner.xcodeproj" \
-scheme NetScanner \
-configuration Release \
-derivedDataPath "$build_dir/DerivedData" \
-quiet

app="$build_dir/DerivedData/Build/Products/Release/NetScanner.app"
[[ -d "$app" ]] || { echo "error: the build produced no $app" >&2; exit 1; }

# Both the app and the embedded engine must carry a valid signature, or
# Apple silicon refuses to run them and macOS hides the ARP table.
codesign --verify --deep --strict "$app"
codesign --verify --strict "$app/Contents/Helpers/ns"

# ditto keeps the bundle's symlinks, extended attributes and signature
# intact, which zip(1) does not.
zip="$build_dir/NetScanner-$version.zip"
ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"

echo "$zip"
shasum -a 256 "$zip" | cut -d' ' -f1
Loading