From 2a6bbf413eeb85617ad83810c6e5cd7ba57f5ffb Mon Sep 17 00:00:00 2001 From: okturan Date: Sat, 18 Jul 2026 22:33:27 +0200 Subject: [PATCH] release: add verified MacNetStatsTerm v1 distribution --- .github/workflows/release.yml | 80 +++++++++++++++++++++++++++++++++++ README.md | 33 +++++++++++++-- netmon.sh | 45 ++++++++++++++++++++ tests/netmon_test.sh | 28 +++++++++++- 4 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..02cb5ee --- /dev/null +++ b/.github/workflows/release.yml @@ -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 diff --git a/README.md b/README.md index 5ee19bb..48d9a2d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -35,7 +57,7 @@ cd MacNetStatsTerm The repository tracks `netmon.sh` as executable. Press Control+C 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: @@ -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 @@ -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 diff --git a/netmon.sh b/netmon.sh index 162fcfb..67df4cd 100755 --- a/netmon.sh +++ b/netmon.sh @@ -1,6 +1,7 @@ #!/bin/bash # Constants +readonly MACNETSTATS_VERSION="1.0.0" readonly KB_TO_MB_THRESHOLD=1024 readonly REFRESH_INTERVAL=1 @@ -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 ;; @@ -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 diff --git a/tests/netmon_test.sh b/tests/netmon_test.sh index aa4401c..392298f 100755 --- a/tests/netmon_test.sh +++ b/tests/netmon_test.sh @@ -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="" @@ -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 @@ -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