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
80 changes: 80 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
name: Release

on:
pull_request:
paths:
- ".github/workflows/release.yml"
- "netmon.sh"
- "tests/**"
push:
tags:
- "v*"

permissions:
contents: read

jobs:
verify:
name: Verify release contract
runs-on: macos-15
timeout-minutes: 10

steps:
- name: Check out source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Check Bash syntax
run: /bin/bash -n netmon.sh tests/netmon_test.sh

- name: Run deterministic behavior tests
run: /bin/bash tests/netmon_test.sh

- name: Verify declared version
run: |
declared="$(/bin/bash netmon.sh --version)"
test "$declared" = "MacNetStatsTerm 1.0.0"

publish:
name: Publish tagged script
if: startsWith(github.ref, 'refs/tags/')
needs: verify
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write

steps:
- name: Check out tagged source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Verify tag matches script version
run: |
grep --quiet --extended-regexp '^v[0-9]+\.[0-9]+\.[0-9]+$' <<<"$GITHUB_REF_NAME"
version="$(bash netmon.sh --version)"
test "$version" = "MacNetStatsTerm ${GITHUB_REF_NAME#v}"

- name: Build single-file distribution
run: |
mkdir dist
install -m 0755 netmon.sh dist/macnetstats
(
cd dist
sha256sum macnetstats > macnetstats.sha256
)
sha256sum --check dist/macnetstats.sha256
test "$(dist/macnetstats --version)" = "MacNetStatsTerm ${GITHUB_REF_NAME#v}"

- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
dist/macnetstats \
dist/macnetstats.sha256 \
--verify-tag \
--title "MacNetStatsTerm ${GITHUB_REF_NAME#v}" \
--generate-notes
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,28 @@ No package manager or third-party runtime is required. The script is macOS-speci

## Install and run

### Install the v1 release

Download the exact reviewed script and its checksum from the
[v1.0.0 release](https://github.com/okturan/MacNetStatsTerm/releases/tag/v1.0.0):

```bash
mkdir -p "$HOME/.local/bin"
curl --fail --location --remote-name \
https://github.com/okturan/MacNetStatsTerm/releases/download/v1.0.0/macnetstats
curl --fail --location --remote-name \
https://github.com/okturan/MacNetStatsTerm/releases/download/v1.0.0/macnetstats.sha256
shasum -a 256 -c macnetstats.sha256
install -m 0755 macnetstats "$HOME/.local/bin/macnetstats"
"$HOME/.local/bin/macnetstats" --version
```

The checksum detects download corruption and binds the asset to the release
notes; it is published through the same GitHub release channel and is not an
independent code-signing identity.

### Run from source

```bash
git clone https://github.com/okturan/MacNetStatsTerm.git
cd MacNetStatsTerm
Expand All @@ -35,7 +57,7 @@ cd MacNetStatsTerm

The repository tracks `netmon.sh` as executable. Press <kbd>Control</kbd>+<kbd>C</kbd> to stop; the original terminal screen and cursor are restored.

### Install for the current user
### Install the source checkout for the current user

After cloning, install the tested script under a user-owned executable directory:

Expand All @@ -51,7 +73,9 @@ Add `$HOME/.local/bin` to `PATH` if you want to run it as `macnetstats`. Remove
rm "$HOME/.local/bin/macnetstats"
```

There is no packaged release or Homebrew formula yet. The default branch and its green macOS CI are the current distribution source; a versioned release remains a separate maintainer decision.
There is no Homebrew formula. The v1.0.0 GitHub Release is the supported
single-file distribution; the default branch and its green macOS CI remain the
source of truth for later development.

### Select an interface explicitly

Expand Down Expand Up @@ -103,7 +127,10 @@ Normal `INT`, `TERM`, and shell-exit paths restore the cursor and leave the alte

## Verification

The deterministic checks source the script without starting its infinite UI loop. They cover rate scaling, counter deltas and resets, interface detection and fallback, dependency failures, and the non-interactive execution guard.
The deterministic checks source the script without starting its infinite UI
loop. They cover rate scaling, counter deltas and resets, interface detection
and fallback, dependency failures, the non-interactive execution guard, and
the release version/help/invalid-option contract.

```bash
bash -n netmon.sh tests/netmon_test.sh
Expand Down
45 changes: 45 additions & 0 deletions netmon.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/bin/bash

# Constants
readonly MACNETSTATS_VERSION="1.0.0"
readonly KB_TO_MB_THRESHOLD=1024
readonly REFRESH_INTERVAL=1

Expand All @@ -16,6 +17,26 @@ print_error() {
printf 'MacNetStatsTerm: %s\n' "$*" >&2
}

print_version() {
printf 'MacNetStatsTerm %s\n' "$MACNETSTATS_VERSION"
}

print_usage() {
cat <<'USAGE'
Usage: macnetstats [--help | --version]

Interactive macOS terminal dashboard for the active network interface.

Options:
-h, --help Show this help and exit.
-V, --version Show the installed version and exit.

Environment:
NETMON_INTERFACE Monitor this interface instead of auto-detecting.
NETMON_FALLBACK_INTERFACE Fallback interface when detection fails (default: en0).
USAGE
}

is_non_negative_integer() {
case "${1:-}" in
'' | *[!0-9]*) return 1 ;;
Expand Down Expand Up @@ -211,6 +232,30 @@ main() {
local download_rate
local upload_rate

if [ "$#" -gt 0 ]; then
if [ "$#" -ne 1 ]; then
print_error "expected at most one option"
print_usage >&2
return 2
fi

case "$1" in
-h | --help)
print_usage
return 0
;;
-V | --version)
print_version
return 0
;;
*)
print_error "unknown option: $1"
print_usage >&2
return 2
;;
esac
fi

if [ ! -t 1 ]; then
print_error "an interactive terminal is required"
return 1
Expand Down
28 changes: 27 additions & 1 deletion tests/netmon_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,29 @@ test_direct_execution_rejects_noninteractive_output() {
assert_contains "$output" "interactive terminal is required" "non-interactive error"
}

test_version_is_available_without_a_terminal() {
local actual
actual=$(bash "$ROOT_DIR/netmon.sh" --version)
assert_equal "MacNetStatsTerm 1.0.0" "$actual" "version output"
}

test_help_is_available_without_a_terminal() {
local output
output=$(bash "$ROOT_DIR/netmon.sh" --help)
assert_contains "$output" "Usage: macnetstats" "help usage" || return 1
assert_contains "$output" "NETMON_INTERFACE" "help environment"
}

test_unknown_option_fails_before_terminal_check() {
local output
local status
output=$(bash "$ROOT_DIR/netmon.sh" --unknown 2>&1)
status=$?

assert_equal "2" "$status" "unknown option status" || return 1
assert_contains "$output" "unknown option: --unknown" "unknown option error"
}

test_cleanup_restores_terminal_state() {
local expected="cnorm sgr0 rmcup"
TPUT_CALLS=""
Expand All @@ -163,7 +186,7 @@ test_cleanup_restores_terminal_state() {
assert_equal "0" "$SCREEN_ACTIVE" "terminal cleanup state"
}

printf '1..12\n'
printf '1..15\n'
run_test "formats KB/s using the sample interval" test_rate_formats_kilobytes
run_test "scales 1024 KB/s to MB/s" test_rate_scales_at_one_megabyte
run_test "calculates independent receive/transmit deltas" test_transfer_pair_uses_independent_deltas
Expand All @@ -175,6 +198,9 @@ run_test "honors an explicit interface override" test_explicit_interface_bypasse
run_test "accepts available dependencies" test_dependency_check_accepts_present_commands
run_test "reports all missing dependencies" test_dependency_check_reports_missing_commands
run_test "rejects direct non-interactive execution" test_direct_execution_rejects_noninteractive_output
run_test "prints the version without requiring a terminal" test_version_is_available_without_a_terminal
run_test "prints help without requiring a terminal" test_help_is_available_without_a_terminal
run_test "rejects an unknown option before terminal setup" test_unknown_option_fails_before_terminal_check
run_test "restores terminal state on cleanup" test_cleanup_restores_terminal_state

if [ "$TESTS_FAILED" -ne 0 ]; then
Expand Down