1 Commits
Author SHA1 Message Date
sneak 746a433ef3 Keep Go's build cache in check builds: re-vendor sneak/prompts e3f79c4 (closes #129)
check / check (push) Canceled after 0s
The vendored files are copies from sneak/prompts e3f79c4, with this
repository's own entries kept. The script/ builds pass a new CACHEBUST
value instead of --no-cache, which emptied Go's build cache mount, so
every make test compiled the standard library and every dependency with
-race. Every Dockerfile stage declares ARG CACHEBUST first, so every RUN
step still reruns on an unchanged tree. The steps that compile mount
Go's build cache, and both go test runs keep -count=1. The lint and test
builds write no image. script/bootstrap, script/fmt and script/fmt-check
are the canonical files plus this repository's Go steps.

Model: opus-5-5
2026-10-15 16:26:27 +00:00
5 changed files with 20 additions and 75 deletions
-14
View File
@@ -17,11 +17,6 @@ https://git.eeqj.de/sneak/secret/milestone/12
# Completed Steps # Completed Steps
- 2026-10-15: The comment on `commandWait` in `internal/cli/integration_test.go`
no longer says `script/test` gives the package 30 seconds
(https://git.eeqj.de/sneak/secret/issues/195). `script/test` sets no time
limit; the test phase of the `Dockerfile` sets one with `go test -timeout`.
The comment now names that place instead of a number.
- 2026-10-15: The canonical files are re-vendored from `sneak/prompts` commit - 2026-10-15: The canonical files are re-vendored from `sneak/prompts` commit
`e3f79c4` (https://git.eeqj.de/sneak/secret/issues/129), so Go's build cache `e3f79c4` (https://git.eeqj.de/sneak/secret/issues/129), so Go's build cache
survives between check builds and `make test` no longer compiles the standard survives between check builds and `make test` no longer compiles the standard
@@ -34,15 +29,6 @@ https://git.eeqj.de/sneak/secret/milestone/12
no image. The CI workflow cancels a branch's older run, stops after 20 no image. The CI workflow cancels a branch's older run, stops after 20
minutes, fetches the tags and leaves no token behind. `script/bootstrap` uses minutes, fetches the tags and leaves no token behind. `script/bootstrap` uses
the installed node only when its major version is the pinned one. the installed node only when its major version is the pinned one.
- 2026-10-15: `make test` no longer fails on some runs in `internal/cli`
(https://git.eeqj.de/sneak/secret/issues/192). `cli.Entry()` set `time.Local`
to UTC and ended by wiping every memguard buffer, and a test ran it in the
same process as the other tests. A timer left by an earlier test could read
`time.Local` as it changed, which the race detector reported, and the wipe
could destroy a buffer still in use, the likeliest cause of a crash of the
test process on another run. `main()` in `cmd/secret` now sets `time.Local`
before it calls `cli.Entry()`, and that test runs `cli.Entry()` in a process
of its own.
- 2026-10-15: `secret mv` from one vault to another unlocks both vaults before - 2026-10-15: `secret mv` from one vault to another unlocks both vaults before
it takes the state directory lock it takes the state directory lock
(https://git.eeqj.de/sneak/secret/issues/189). It unlocked them while holding (https://git.eeqj.de/sneak/secret/issues/189). It unlocked them while holding
-6
View File
@@ -3,16 +3,10 @@ package main
import ( import (
"os" "os"
"time"
"sneak.berlin/go/secret/internal/cli" "sneak.berlin/go/secret/internal/cli"
) )
func main() { func main() {
// Every time secret prints or writes is in UTC, whatever TZ or the
// system's time zone is set to. It is set here, before cli.Entry starts
// any goroutine that could read time.Local while it changes.
time.Local = time.UTC //nolint:gosmopolitan // this makes local time UTC
os.Exit(cli.Entry()) os.Exit(cli.Entry())
} }
+12 -51
View File
@@ -3,7 +3,6 @@ package cli_test
import ( import (
"bufio" "bufio"
"context" "context"
"fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -17,67 +16,29 @@ import (
"sneak.berlin/go/secret/internal/secret" "sneak.berlin/go/secret/internal/secret"
) )
const (
// envRunEntry, when set, makes this test binary run cli.Entry with its
// own arguments and exit, instead of running the tests.
envRunEntry = "SECRET_TEST_RUN_ENTRY"
// bufferWiped is what the test binary prints, run with envRunEntry set,
// when cli.Entry wiped the buffer made before it ran.
bufferWiped = "Entry wiped the buffer made before it ran"
)
// With envRunEntry set, the test binary makes a buffer, runs cli.Entry,
// says whether the buffer is wiped, and exits with Entry's exit code. This
// is done in init, not in a test, so that TestMain does not run in that
// process: it would build the secret binary again, and remove it at the end,
// while the other tests run it.
//
//nolint:gochecknoinits // must run before TestMain, as above
func init() {
if os.Getenv(envRunEntry) == "" {
return
}
buf := memguard.NewBufferFromBytes([]byte("key material"))
code := cli.Entry()
if !buf.IsAlive() {
fmt.Fprintln(os.Stderr, bufferWiped)
}
os.Exit(code)
}
// Entry must return its exit code rather than exit, so that its deferred // Entry must return its exit code rather than exit, so that its deferred
// memguard purge runs on the success and the error path alike. Entry wipes // memguard purge runs on the success and the error path alike.
// every memguard buffer in the process, including those other tests are //
// using, so it runs in a process of its own: this test binary, run again //nolint:paralleltest // sets os.Args, and Entry wipes every buffer in the process
// with envRunEntry set.
func TestEntryWipesBuffersAndReturnsExitCode(t *testing.T) { func TestEntryWipesBuffersAndReturnsExitCode(t *testing.T) {
t.Parallel() savedArgs := os.Args
t.Cleanup(func() { os.Args = savedArgs })
tests := []struct { tests := []struct {
args []string args []string
exitCode int exitCode int
}{ }{
{args: []string{"--help"}, exitCode: 0}, {args: []string{"secret", "--help"}, exitCode: 0},
{args: []string{"no-such-command"}, exitCode: 1}, {args: []string{"secret", "no-such-command"}, exitCode: 1},
} }
for _, tt := range tests { for _, tt := range tests {
//nolint:gosec // G204: test runs its own test binary buf := memguard.NewBufferFromBytes([]byte("key material"))
cmd := exec.CommandContext(t.Context(), os.Args[0], tt.args...) os.Args = tt.args
cmd.Env = append(os.Environ(), envRunEntry+"=1") assert.Equal(t, tt.exitCode, cli.Entry(), "exit code for %v", tt.args)
assert.False(t, buf.IsAlive(), "Entry left a buffer unwiped for %v", tt.args)
output, err := cmd.CombinedOutput()
require.NotNil(t, cmd.ProcessState, "%v did not run: %v", tt.args, err)
assert.Equal(t, tt.exitCode, cmd.ProcessState.ExitCode(),
"exit code for %v: %s", tt.args, output)
assert.Contains(t, string(output), bufferWiped,
"Entry left a buffer unwiped for %v", tt.args)
} }
} }
+3 -4
View File
@@ -34,10 +34,9 @@ const (
testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" testMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
// commandWait is how long a test lets the secret binary run before it // commandWait is how long a test lets the secret binary run before it
// kills it and fails. It stays well under the time limit the test phase // kills it and fails. It stays well under the 30 seconds script/test
// of the Dockerfile gives the whole package (go test -timeout), so a // gives the whole package, so a command that hangs fails the test with
// command that hangs fails the test with the test's own message instead // the test's own message instead of Go's timeout panic.
// of Go's timeout panic.
commandWait = 10 * time.Second commandWait = 10 * time.Second
) )
+5
View File
@@ -2,6 +2,7 @@ package cli
import ( import (
"os" "os"
"time"
"github.com/awnumar/memguard" "github.com/awnumar/memguard"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -14,6 +15,10 @@ import (
// every memguard buffer before it returns, so the caller must do nothing // every memguard buffer before it returns, so the caller must do nothing
// but exit with the code. // but exit with the code.
func Entry() int { func Entry() int {
// Every time secret prints or writes is in UTC, whatever TZ or the
// system's time zone is set to.
time.Local = time.UTC //nolint:gosmopolitan // this makes local time UTC
raiseLockedMemoryLimit() raiseLockedMemoryLimit()
// On SIGINT or SIGTERM memguard runs this function, wipes every buffer // On SIGINT or SIGTERM memguard runs this function, wipes every buffer