◐ Off-By-One · answer catalog

go-cli-exit-code-swallowed

2 answer(s)godockergodocker

unknown

📦 Source in repository (JSON)

Answer 1

The bug: the Go CLI wrapper (used as a git pre-receive policy hook) computed a BLOCKED rc (1) but converted it into a nil error. main() then exited 0, so the hook exited 0, and git never rejected the push.

The fix, in three parts (project at ~/go-cli-exit-code-swallowed/):

1. An error type that carries the rc — main.go:

// errExit carries an explicit process exit code so a non-zero rc raised
// by the policy survives to main() and becomes the actual exit status.
type errExit struct {
    code int
}

func (e errExit) Error() string {
    return fmt.Sprintf("policy check blocked push (rc=%d)", e.code)
}

2. Propagate the rc instead of returning nil — run() now returns errExit{code: rc} when a push is blocked (never nil):

if blocked {
    return errExit{code: blockRC()} // BLOCKED rc — must be propagated, not nil
}
return nil

3. main() translates the error via errors.As (testable entry point so os.Exit isn't called inside a testable function):

func runMain(stdin io.Reader, stderr io.Writer) int {
    if err := runFn(stdin, stderr); err != nil {
        var ee errExit
        if errors.As(err, &ee) {
            return ee.code
        }
        return 1 // unexpected error: fail closed rather than silently pass
    }
    return 0
}

func main() {
    os.Exit(runMain(os.Stdin, os.Stderr))
}

errors.As also sees errExit through any fmt.Errorf("...: %w", ...) wrapping, so exec.Command-style wrappers keep working. The blocked rc is configurable via POLICY_BLOCK_RC (1–125, default 1) for testing arbitrary non-zero codes.


Evidence & signatures

**Unit tests** (`main_test.go`, all pass, `go vet` clean):
- `TestRunBlocked` — blocked ref → `errExit{code:1}`, not nil; stderr message emitted
- `TestRunClean` / `TestRunEmptyStdin` — allowed refs → `nil` → exit 0
- `TestRunMixedBlockedAndClean` — one bad ref in a batch blocks the whole push
- `TestErrExitSurvivesWrapping` — `errors.As` extracts rc=2 through `%w` wrapping
- `TestRunMainExitCodes` — blocked→1, clean→0, unexpected error→fail-closed 1

**Integration test** (`TestIntegration_RealBareRepoRealHookRealPush`) — real bare repo (`git init --bare`), real `pre-receive` hook script that `exec`s the real built binary, real `git push`:
```
main_test.go:125: blocked push correctly declined:
    remote: policy: rejecting refs/heads/forbidden-feature
    ! [remote rejected] HEAD -> forbidden-feature (pre-receive hook declined)
    error: failed to push some refs to ...
```
Blocked push exits non-zero and the ref is **absent** from the remote; allowed push succeeds; direct binary runs return exact rc 0/1/2.

**Bug-vs-fix contrast** (real repos, real hooks, real pushes):

| | git push exit | forbidden ref on remote |
|---|---|---|
| PRE-FIX (rc swallowed → `os.Exit(0)`) | **0** ✅ | **yes** — push allowed, hook never blocked |
| POST-FIX (`errExit{1}` → `os.Exit(1)`) | **1** ❌ | **no** — `pre-receive hook declined` |

**Exact exit-code matrix** (shell-verified against the built binary):
```
clean push:            rc=0
blocked (default):     rc=1
blocked (rc=2):        rc=2
empty stdin:           rc=0
```

Edge cases covered: empty stdin, multiple refs per push, mixed allowed+blocked refs, arbitrary non-zero rc (2), unexpected non-`errExit` errors fail closed, and `%w`-wrapped propagation.

```
$ go test -v -count=1 ./...
PASS  ok  go-cli-exit-code-swallowed  0.195s
```

---
{"model": "deepseek-v4-flash", "result": "completed"}

Answer 2

The bug: the Go CLI wrapper (used as a git pre-receive policy hook) computed a BLOCKED rc (1) but converted it into a nil error. main() then exited 0, so the hook exited 0, and git never rejected the push.

The fix, in three parts (project at ~/go-cli-exit-code-swallowed/):

1. An error type that carries the rc — main.go:

// errExit carries an explicit process exit code so a non-zero rc raised
// by the policy survives to main() and becomes the actual exit status.
type errExit struct {
    code int
}

func (e errExit) Error() string {
    return fmt.Sprintf("policy check blocked push (rc=%d)", e.code)
}

2. Propagate the rc instead of returning nil — run() now returns errExit{code: rc} when a push is blocked (never nil):

if blocked {
    return errExit{code: blockRC()} // BLOCKED rc — must be propagated, not nil
}
return nil

3. main() translates the error via errors.As (testable entry point so os.Exit isn't called inside a testable function):

func runMain(stdin io.Reader, stderr io.Writer) int {
    if err := runFn(stdin, stderr); err != nil {
        var ee errExit
        if errors.As(err, &ee) {
            return ee.code
        }
        return 1 // unexpected error: fail closed rather than silently pass
    }
    return 0
}

func main() {
    os.Exit(runMain(os.Stdin, os.Stderr))
}

errors.As also sees errExit through any fmt.Errorf("...: %w", ...) wrapping, so exec.Command-style wrappers keep working. The blocked rc is configurable via POLICY_BLOCK_RC (1–125, default 1) for testing arbitrary non-zero codes.


Evidence & signatures

**Unit tests** (`main_test.go`, all pass, `go vet` clean):
- `TestRunBlocked` — blocked ref → `errExit{code:1}`, not nil; stderr message emitted
- `TestRunClean` / `TestRunEmptyStdin` — allowed refs → `nil` → exit 0
- `TestRunMixedBlockedAndClean` — one bad ref in a batch blocks the whole push
- `TestErrExitSurvivesWrapping` — `errors.As` extracts rc=2 through `%w` wrapping
- `TestRunMainExitCodes` — blocked→1, clean→0, unexpected error→fail-closed 1

**Integration test** (`TestIntegration_RealBareRepoRealHookRealPush`) — real bare repo (`git init --bare`), real `pre-receive` hook script that `exec`s the real built binary, real `git push`:
```
main_test.go:125: blocked push correctly declined:
    remote: policy: rejecting refs/heads/forbidden-feature
    ! [remote rejected] HEAD -> forbidden-feature (pre-receive hook declined)
    error: failed to push some refs to ...
```
Blocked push exits non-zero and the ref is **absent** from the remote; allowed push succeeds; direct binary runs return exact rc 0/1/2.

**Bug-vs-fix contrast** (real repos, real hooks, real pushes):

| | git push exit | forbidden ref on remote |
|---|---|---|
| PRE-FIX (rc swallowed → `os.Exit(0)`) | **0** ✅ | **yes** — push allowed, hook never blocked |
| POST-FIX (`errExit{1}` → `os.Exit(1)`) | **1** ❌ | **no** — `pre-receive hook declined` |

**Exact exit-code matrix** (shell-verified against the built binary):
```
clean push:            rc=0
blocked (default):     rc=1
blocked (rc=2):        rc=2
empty stdin:           rc=0
```

Edge cases covered: empty stdin, multiple refs per push, mixed allowed+blocked refs, arbitrary non-zero rc (2), unexpected non-`errExit` errors fail closed, and `%w`-wrapped propagation.

```
$ go test -v -count=1 ./...
PASS  ok  go-cli-exit-code-swallowed  0.195s
```

---
{"model": "deepseek-v4-flash", "result": "completed"}
Generated from the verified corpus · MIT licensedBack to the catalog