From af36c25ee8a8b4d8c3aacfe8f6aacf08cd37c41a Mon Sep 17 00:00:00 2001 From: Dimitrij Denissenko Date: Tue, 16 Jun 2026 20:04:03 +0100 Subject: [PATCH 1/2] Modernise code --- .github/workflows/test.yml | 4 +-- README.md | 3 ++- README.md.tpl | 3 ++- graceful.go | 5 +++- graceful_test.go | 55 ++++++++++++++++++++++++++++++++++++++ shutdown_test.go | 15 ++++++----- 6 files changed, 73 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ddcafc2..09658ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,5 +7,5 @@ on: branches: - main jobs: - reuse: - uses: bsm/misc/.github/workflows/test-go.yaml@main + go: + uses: bsm/misc/.github/workflows/test-go.yml@main diff --git a/README.md b/README.md index f842bbc..3e42f2d 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Wait for servers to terminate gracefully. ```go import ( "context" + "errors" "log" "net/http" "time" @@ -26,7 +27,7 @@ func main() { // Wait for either SIGINT/SIGTERM or ListenAndServe to exit. // Handle errors. err := shutdown.Wait(srv.ListenAndServe) - if err != nil && err != http.ErrServerClosed { + if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalln("Server error", err) } diff --git a/README.md.tpl b/README.md.tpl index 44c017b..25b34a6 100644 --- a/README.md.tpl +++ b/README.md.tpl @@ -10,6 +10,7 @@ Wait for servers to terminate gracefully. ```go import ( "context" + "errors" "log" "net/http" "time" @@ -17,5 +18,5 @@ import ( "github.com/bsm/shutdown" ) -func main() {{ "Example" | code }} +func main() {{ "ExampleWait" | code }} ``` diff --git a/graceful.go b/graceful.go index d367400..a25d964 100644 --- a/graceful.go +++ b/graceful.go @@ -36,7 +36,10 @@ func GracefulContext(ctx context.Context, start StartFunc, shutdown ShutdownFunc return err } - timeout, cancel := context.WithTimeout(context.Background(), DefaultShutdownTimeout) + // Detach the parent's cancellation/deadline so shutdown always gets the full + // timeout (the parent may already be cancelled, e.g. by the signal that + // triggered the shutdown), while still preserving any values it carries. + timeout, cancel := context.WithTimeout(context.WithoutCancel(ctx), DefaultShutdownTimeout) defer cancel() if err := shutdown(timeout); err != nil { diff --git a/graceful_test.go b/graceful_test.go index 21afb4b..6fd3c66 100644 --- a/graceful_test.go +++ b/graceful_test.go @@ -1,8 +1,11 @@ package shutdown_test import ( + "context" + "errors" "log" "net/http" + "testing" "github.com/bsm/shutdown" ) @@ -17,3 +20,55 @@ func ExampleGraceful() { log.Fatalln("Server error", err) } } + +func TestGracefulContext_start_error(t *testing.T) { + boom := errors.New("boom") + + // Unexpected start errors are surfaced and shutdown is not invoked. + called := false + err := shutdown.GracefulContext(context.Background(), + func() error { return boom }, + func(context.Context) error { called = true; return nil }, + ) + if !errors.Is(err, boom) { + t.Fatalf("expected boom, got %v", err) + } + if called { + t.Fatal("shutdown should not run when start fails unexpectedly") + } + + // Expected start errors are swallowed. + err = shutdown.GracefulContext(context.Background(), + func() error { return boom }, + func(context.Context) error { return nil }, + boom, + ) + if err != nil { + t.Fatalf("expected nil, got %v", err) + } +} + +type ctxKey string + +func TestGracefulContext_shutdown_detached(t *testing.T) { + // A cancelled parent triggers shutdown; the shutdown context must remain + // live (fresh timeout) yet still carry the parent's values. + parent, cancel := context.WithCancel(context.WithValue(context.Background(), ctxKey("k"), "v")) + cancel() + + err := shutdown.GracefulContext(parent, + func() error { <-parent.Done(); return nil }, + func(ctx context.Context) error { + if ctx.Err() != nil { + t.Errorf("shutdown context already cancelled: %v", ctx.Err()) + } + if got := ctx.Value(ctxKey("k")); got != "v" { + t.Errorf("parent value not preserved, got %v", got) + } + return nil + }, + ) + if err != nil { + t.Fatalf("expected nil, got %v", err) + } +} diff --git a/shutdown_test.go b/shutdown_test.go index 4a65543..3a1c119 100644 --- a/shutdown_test.go +++ b/shutdown_test.go @@ -2,7 +2,7 @@ package shutdown_test import ( "context" - "fmt" + "errors" "log" "net/http" "testing" @@ -20,7 +20,7 @@ func ExampleWait() { // Wait for either SIGINT/SIGTERM or ListenAndServe to exit. // Handle errors. err := shutdown.Wait(srv.ListenAndServe) - if err != nil && err != http.ErrServerClosed { + if err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalln("Server error", err) } @@ -35,11 +35,12 @@ func ExampleWait() { } func TestWait_fails_immediately(t *testing.T) { - err := shutdown.Wait(func() error { return fmt.Errorf("doh!") }) + sentinel := errors.New("doh!") + err := shutdown.Wait(func() error { return sentinel }) if err == nil { t.Fatalf("expected error, got nil") - } else if err.Error() != "doh!" { - t.Fatalf("expected speficic error, got %v", err) + } else if !errors.Is(err, sentinel) { + t.Fatalf("expected specific error, got %v", err) } } @@ -56,7 +57,7 @@ func TestWaitContext_nil_callback(t *testing.T) { } if err := ctx.Err(); err == nil { t.Fatalf("expected error, got nil") - } else if err != context.Canceled { - t.Fatalf("expected speficic error, got %v", err) + } else if !errors.Is(err, context.Canceled) { + t.Fatalf("expected specific error, got %v", err) } } From c3c32316b859b9eac58e714c6ca1d24bcfadc7ce Mon Sep 17 00:00:00 2001 From: Dimitrij Denissenko Date: Tue, 16 Jun 2026 20:08:05 +0100 Subject: [PATCH 2/2] Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09658ca..1d75f9c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,8 @@ on: pull_request: branches: - main +permissions: + contents: read jobs: go: uses: bsm/misc/.github/workflows/test-go.yml@main