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
25 changes: 0 additions & 25 deletions .github/workflows/lint.yml

This file was deleted.

44 changes: 29 additions & 15 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -1,66 +1,80 @@
name: Release

# Two ways in:
# - Every green Test run on main auto-tags the next v0.x.y patch release.
# - Manually pushing a v*.*.* tag (e.g. a minor bump: git tag v0.2.0 &&
# git push origin v0.2.0) releases that tag directly.
# A successful CI run on the current main commit is the only release path.
# VERSION sets the next deliberate baseline; later releases increment patch.
on:
workflow_run:
workflows: [Test]
workflows: [CI]
branches: [main]
types: [completed]
push:
tags: ["v*.*.*"]

permissions:
contents: write

concurrency:
group: release
cancel-in-progress: false

jobs:
release:
runs-on: ubuntu-latest
if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
if: ${{ github.event.workflow_run.conclusion == 'success' }}

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.ref }}
ref: ${{ github.event.workflow_run.head_sha }}

- name: Compute next patch version
- name: Compute release version
id: version
if: ${{ github.event_name == 'workflow_run' }}
run: |
git fetch origin main --tags
if [ "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]; then
echo "A newer main commit superseded this CI run; nothing to release."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git describe --tags --exact-match --match 'v*.*.*' HEAD >/dev/null 2>&1; then
echo "HEAD is already tagged; nothing to release."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi

BASE_VERSION=$(tr -d '[:space:]' < VERSION)
if ! echo "$BASE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "VERSION must contain a semantic version without a leading v." >&2
exit 1
fi
BASE_TAG="v$BASE_VERSION"
LATEST=$(git tag --list 'v*.*.*' | sort -V | tail -n 1)
if [ -z "$LATEST" ]; then
TAG="v0.1.0"
TAG="$BASE_TAG"
elif [ "$LATEST" != "$BASE_TAG" ] && [ "$(printf '%s\n%s\n' "$LATEST" "$BASE_TAG" | sort -V | tail -n 1)" = "$BASE_TAG" ]; then
TAG="$BASE_TAG"
else
TAG=$(echo "$LATEST" | awk -F '[v.]' '{printf "v%d.%d.%d", $2, $3, $4 + 1}')
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

- name: Create and push tag
if: ${{ github.event_name == 'workflow_run' && steps.version.outputs.skip != 'true' }}
if: ${{ steps.version.outputs.skip != 'true' }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag "${{ steps.version.outputs.tag }}"
git push origin "${{ steps.version.outputs.tag }}"

- name: Set up Go
if: ${{ github.event_name == 'push' || steps.version.outputs.skip != 'true' }}
if: ${{ steps.version.outputs.skip != 'true' }}
uses: actions/setup-go@v5
with:
go-version: 1.23.x
check-latest: true

- name: Run goreleaser
if: ${{ github.event_name == 'push' || steps.version.outputs.skip != 'true' }}
if: ${{ steps.version.outputs.skip != 'true' }}
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
Expand Down
34 changes: 0 additions & 34 deletions .github/workflows/security.yaml

This file was deleted.

101 changes: 98 additions & 3 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
name: Test
name: CI

on:
pull_request:
push:
branches: ["**"]
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}

steps:
- name: Checkout
Expand All @@ -22,7 +27,97 @@ jobs:
uses: extractions/setup-just@v3

- name: PGN sync check
if: runner.os == 'Linux'
run: just pgn-sync-check

- name: Local conformance evidence
if: runner.os == 'Linux'
run: just conformance-local

- name: Test
run: go test ./...

race-and-fuzz:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.23.x
check-latest: true

- name: Set up just
uses: extractions/setup-just@v3

- name: Race detector
run: just test-race

- name: Fuzz smoke tests
run: just fuzz-smoke

lint:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.23.x
check-latest: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.12.0

secure:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.23.x
check-latest: true

- name: Run govulncheck
env:
GOTOOLCHAIN: go1.25.12
run: |
go install golang.org/x/vuln/cmd/govulncheck@v1.5.0
govulncheck ./...

- name: Run gosec
env:
GOTOOLCHAIN: go1.25.12
run: |
go install github.com/securego/gosec/v2/cmd/gosec@v2.27.1
gosec -exclude-generated -exclude-dir=.claude -exclude=G115 ./...

release-gate:
if: ${{ always() }}
needs: [test, race-and-fuzz, lint, secure]
runs-on: ubuntu-latest
env:
TEST_RESULT: ${{ needs.test.result }}
RACE_FUZZ_RESULT: ${{ needs.race-and-fuzz.result }}
LINT_RESULT: ${{ needs.lint.result }}
SECURE_RESULT: ${{ needs.secure.result }}

steps:
- name: Require every release gate
run: |
test "$TEST_RESULT" = success
test "$RACE_FUZZ_RESULT" = success
test "$LINT_RESULT" = success
test "$SECURE_RESULT" = success
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,14 @@ dist/
# Scratch
scratch/*

# Licensed/certification evidence is local to the manufacturer. The tracked
# example under conformance/ documents the metadata shape without test output.
conformance-artifacts/

# Internal docs
docs/*
!docs/architecture.md
!docs/conformance.md
!docs/adr/
docs/adr/*
!docs/adr/*.md
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Agent Requirements

- Read `CONTEXT.md` before architecture or runtime changes; preserve its
invariants and update it when domain language or module ownership changes.
- Before pushing, opening a PR, or marking work complete, run and pass:
- `go test ./...`
- `just pgn-sync-check`
Expand Down
54 changes: 45 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,43 @@
## Change Log for open-ships/n2k

### Unreleased — panics in a misbehaving Bus no longer crash the process
### v0.3.0 — 2026-07-18 — bounded lifecycle, wire fidelity, and hostile-input hardening

- Commanded Address (PGN 65240) is now a first-class address-claim transition:
only an exact nine-byte broadcast ISO transport transfer for this node's
complete NAME and a claimable address is accepted, followed immediately by
a new Address Claim and contention window. Adversarial tests cover malformed,
mismatched, fast-packet, addressed, and special-address commands.
- Added `just conformance-local`, a current standards/evidence guide, and a
machine-readable certification evidence template. These make local protocol
regression evidence reproducible without misrepresenting it as the licensed
NMEA certification run.
- Live reads now use independent bounded subscriptions; slow consumers receive
`ErrReceiveOverflow` without stalling protocol handling. Writes use a bounded
queue and report `ErrWriteQueueFull`.
- Runtime bus failures propagate to readers, writers, `Client.Err`, and
`Client.Status`. Scanner, bus, scheduler, and write shutdown paths are
cancellation-safe and race-tested.
- ISO transport and fast-packet assembly validate lengths, packet ranges,
session ownership, timeouts, and resource bounds. Missing/out-of-order
packets cannot be accepted as a complete transfer.
- Unmodified decoded messages preserve their original payload exactly; field
changes encode current values. String sentinel and fixed-padding edge cases
now round-trip consistently.
- USB-CAN startup configures 250 kbit/s extended-frame operation before the
bus becomes writable, and stream parsers resynchronize after corrupt input.
- NMEA 2000 source-address claiming now uses the valid 0–251 range. New
`WithPreferredAddress` lets applications restore a persisted prior address
while retaining automatic contention handling.
- Added health/configuration hooks, adversarial and fuzz tests, cross-platform
and race CI, pinned lint/security tools, architecture context, ADRs,
contribution guidance, and private security-reporting instructions.

### v0.2.2 — 2026-07-18 — clean CLI shutdown

`n2k sniff` now handles termination signals through context cancellation and
exits successfully after its input pipeline shuts down.

### v0.2.1 — 2026-07-16 — panics in a misbehaving Bus no longer crash the process

A `Bus` implementation that panics (rather than returning an error) from `WriteFrame`, `Run`, or a
message handler previously crashed the whole host process, because n2k runs those on its own
Expand All @@ -16,7 +53,7 @@ recovers panics, logs them with a stack trace, and degrades locally instead:
skipped without tearing down the reader.
- The heartbeat and system-router dispatch goroutines are guarded the same way.

### Unreleased — writable TCP gateway buses
### v0.2.0 — 2026-07-15 — writable TCP gateway buses

`n2k.TCP(addr, format)` now works with `NewClient`, not just `Receive`/`NewScanner` — the full bus
stack over the boat's WiFi gateway:
Expand Down Expand Up @@ -44,7 +81,7 @@ stack over the boat's WiFi gateway:
`just test-race` works again.
- `UDP` and `File` sources remain read-only.

### Unreleased — semver releases, installable CLI, typed physical accessors
### v0.1.0 — 2026-07-14 — semver releases, installable CLI, typed physical accessors

**Typed physical-value accessors** — every numeric PGN struct field with a physical
interpretation (a unit, non-unity resolution, or offset) now has generated accessors, on
Expand All @@ -61,17 +98,16 @@ tick). Raw-tick fields are unchanged underneath, so round-trip fidelity is prese
with `-f` CEL filters and `-unknown`.
- `n2k version` reports the release version (set by goreleaser).

**Semver releases with prebuilt binaries** — releases are now tagged `v0.x.y`: every green Test
run on `main` auto-tags the next patch and goreleaser publishes archives for Linux/macOS/Windows
(amd64/arm64); pushing a `v0.X.0` tag manually cuts a minor. Release names carry the CalVer date.
**Semver releases with prebuilt binaries** — releases are tagged `v0.x.y`, and goreleaser
publishes archives for Linux/macOS/Windows (amd64/arm64). Release names carry the CalVer date.
A commented Homebrew-tap config is in `.goreleaser.yaml`, pending an `open-ships/homebrew-tap`
repo and token.

**No-hardware quickstart** — `testdata/sample.log` is a real six-second capture (position, AIS,
and identity PGNs removed; see `testdata/README.md`), used by new runnable `Example*` tests and
the README quickstart, with an animated sniffer demo at `.github/demo.svg`.

### Unreleased — bus citizenship, device registry, and new sources
### 2026.07.13-2 — bus citizenship, device registry, and new sources

**Bus citizenship** — bus clients now meet the protocol obligations of a real NMEA 2000 device:

Expand All @@ -93,11 +129,11 @@ the README quickstart, with an animated sniffer demo at `.github/demo.svg`.
- `TCP(addr, format)` / `UDP(listenAddr, format)` — network gateway streams: `FormatYDRaw` (Yacht Devices RAW ASCII) and `FormatActisense` (Actisense binary framing; assembled messages are re-framed through the normal decode pipeline).
- candump parsing moved from the roundtrip tool into `internal/candump` (now with timestamp extraction).

### Unreleased — candump round-trip tool
### 2026.07.12 — candump round-trip tool

- `cmd/roundtrip` — replays a candump `-L` log through fast-packet assembly, decodes each message, re-encodes it, and compares against the wire bytes. Prints a before/after hex view per message (with markers under differing bytes) and a per-PGN summary; `-report <file>` collects every issue plus the summary into a file. Always processes the whole log and exits 0. Reads a log file or stdin (`candump -L vcan0 | roundtrip -`).

### Unreleased — Architecture deepening
### 2026.07.08 — Architecture deepening

Seven refactors from an architecture review: one read pipeline, one CAN-ID codec, a public bus seam, a testable transport state machine, and a metadata-driven PGN codec with round-trip coverage for every PGN.

Expand Down
Loading
Loading