With file modes normalized, the four tarballs reproduced byte for byte across
hosts but the Windows zip still did not. Two host properties were leaking into
it:
- Entry order. The archive is fed by `find | sort`, and sort honours the
locale. A ru_RU.UTF-8 host emits docs/ before LICENSE; a C locale emits the
reverse. Same files, different archive.
- Timestamps. zip records DOS local time with no zone attached, so building at
UTC+08 embedded 19:06 where ubuntu-latest embedded 11:06 for the same commit.
Pin LC_ALL=C and TZ=UTC for the packaging subshell. Building the same commit
under ru_RU.UTF-8/Asia-Shanghai and under C/UTC now yields one hash.
The tarballs never had either problem: tar sorts internally by byte value and
stores Unix epochs, so neither locale nor zone reaches the output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
release.sh normalized entry order, ownership and mtimes, but not permissions,
so the archives inherited the builder's umask. A host with umask 002 packaged
664/775 while ubuntu-latest packaged 644/755, and the two archives hashed
differently even though every file inside was byte-identical:
CI -rw-r--r-- README.md local -rw-rw-r-- README.md
CI -rwxr-xr-x sshkeeper local -rwxrwxr-x sshkeeper
Force 755 on directories and the program, 644 on everything else. Building the
same commit under umask 002 and umask 022 now yields identical checksums.
Also correct the reproducibility claim in the release docs. What is reproducible
is the binary, given the same commit and Go version; the archive hash still
depends on the host tar and gzip, so the documented verification step now
compares the extracted binary instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository had no automation at all: every release was packaged and
published by hand, and nothing ran tests on a pull request.
- ci.yml runs gofmt, go vet and go test on Linux and macOS, and cross-builds
all five release targets. macOS is a stated release target but was never
actually exercised, only cross-compiled.
- release.yml publishes on a v* tag. It gates on `make release-check` so a red
suite cannot ship, and builds through release.sh rather than duplicating the
packaging rules, so CI archives stay byte-identical to local ones. A
hand-written docs/releases/<tag>.md becomes the release body when present,
otherwise notes are generated from history.
- nightly.yml rebuilds the tip of main on every push and replaces a rolling
`nightly` prerelease. Prerelease is deliberate: it keeps GitHub's `Latest`
badge on the newest real release rather than on an untested build.
The rolling tag is why version discovery was pinned to v* in the previous
commit; nightly.yml depends on that filter already being in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nightly builds will move a rolling `nightly` tag across main. A plain
`git describe --tags` returns whichever tag is nearest, so once that tag exists
every build — including a real release build — would report its version as
"nightly" and lose the release lineage entirely.
Restrict discovery to `v*` so the rolling tag is invisible to versioning:
with a nightly tag ahead of v0.3.1
git describe --tags → nightly
git describe --tags --match 'v*' → v0.3.1-1-gf940087
Land this before the nightly workflow exists, so no build is ever stamped from
the rolling tag.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`sshkeeper forward add` could never succeed. RunE read --local-port and init()
marked it required, but the flag was never registered on forwardAddCmd. Cobra
ignores MarkFlagRequired for an unknown flag, and GetInt returns 0 for one, so
every invocation failed validation with "invalid local port 0: must be
1-65535". There was no argument combination that worked.
Register the flag so both the read and the required annotation bind to a real
option. README and the guide already documented --local-port, so the intent was
there from the start; only the registration was missing.
The existing tests missed this because they build a throwaway cobra.Command,
register the flags on it themselves, and pass it to forwardAddCmd.RunE — the
real command's flag set was never exercised. Add a test that parses argv into
forwardAddCmd's own flags, plus one pinning the required annotation. Both fail
against the unfixed command with "unknown flag: --local-port".
Present since c2edaa4, so v0.2.0 and v0.3.0 both ship it broken.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Complete the final task of the unified TUI shell plan, which had been left
undone: the code dropped F1 in favour of Ctrl+H, but README and the guide
still documented F1 as the full-help key in nine places, and the repository
screenshots still showed the pre-shell UI.
- Replace every F1 reference in README.md and docs/guide.md with Ctrl+H,
including the two ASCII help blocks that mirror runtime output.
- Document the BS/DEL terminal constraint: Ctrl+H is BS (0x08) while xterm
sends DEL (0x7F) for Backspace, so help and text editing do not collide.
- Recapture all five screenshots from the current binary in a real xterm at
120x40, 80x24 and 60x16 using an isolated XDG profile.
Runtime verification in xterm confirms Ctrl+H opens full help while seven
Backspace presses correctly trim typed text, and that every captured screen
carries the header, framed content and bottom-anchored footer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1. Forward type selector: visible radio items (1.Local 2.Remote 3.SOCKS) with descriptions
2. Forward list: column header row (NAME/TYPE/LISTEN/TARGET/ON)
3. Forward delete: confirmation dialog before deletion
4. Server route column: → icon for via/chain, spaces for direct
- Forward form: type selector (Local/Remote/SOCKS) with radio items
- Dynamic fields: listen addr/port, target addr/port based on type
- Default listen: 127.0.0.1, warning for 0.0.0.0
- Forward list: table view NAME/TYPE/LISTEN/TARGET/ENABLED
- Forward edit: Enter/Ctrl+E opens pre-filled edit form
- Human explanation and OpenSSH preview for selected forward
- Tunnel state manager: PID tracking, start/stop, state file
- Tunnel manager screen: list running tunnels, stop, refresh
- Action menu: Connect/Connect with tunnels/Start tunnels only/Start tunnels in background/Manage port forwards/Manage tunnels/Manage route/Test/Edit/Delete
- Help screen: updated shortcuts
- CLI: tunnel --background for detached tunnel process
- README: updated with forward vs tunnel examples, new hotkeys
- Forward model: add Name, Description, Enabled fields
- DB migration 003: add name/description/enabled columns to forwards
- Forward type: radio selector (Local/Remote/SOCKS) instead of free text
- Forward form: dynamic fields based on type, 127.0.0.1 default, 0.0.0.0 warning
- Forward list: table view with NAME/TYPE/LISTEN/TARGET/ENABLED columns
- Forward edit: Enter/Ctrl+E opens edit form
- Human explanation and OpenSSH preview for selected forward
- Tunnel state manager: PID tracking, start/stop, state file
- Tunnel manager screen: list running tunnels, stop, refresh
- Action menu: reworked with Connect/Connect with tunnels/Start tunnels only/Start tunnels in background/Manage port forwards/Manage tunnels/Manage route/Test/Edit/Delete
- Help screen: updated with all shortcuts
- CLI: tunnel --background for detached tunnel process
- Default listen address: 127.0.0.1 instead of 0.0.0.0
- Validation: type required, ports 1-65535, target required for local/remote