fix(depot): exit 64 on unset DEPOT_DIR, count push transport failures
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,6 @@ func TestIntegrationPushStatusPull(t *testing.T) {
|
|||||||
"BUNNY_STORAGE_HOST": hostPort(srv),
|
"BUNNY_STORAGE_HOST": hostPort(srv),
|
||||||
"BUNNY_STORAGE_READ_PASSWORD": "readkey",
|
"BUNNY_STORAGE_READ_PASSWORD": "readkey",
|
||||||
"BUNNY_STORAGE_PASSWORD": "writekey",
|
"BUNNY_STORAGE_PASSWORD": "writekey",
|
||||||
"CDN_UNREACHABLE": "1", // unused; CDNBase defaults to unreachable in test cfg path via storageURL fallback
|
|
||||||
}
|
}
|
||||||
getenv := testGetenv(env)
|
getenv := testGetenv(env)
|
||||||
|
|
||||||
|
|||||||
+17
-3
@@ -3,6 +3,7 @@ package depot
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -60,18 +61,30 @@ func printRunUsage(w io.Writer) {
|
|||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errUsage marks errors that are the caller's fault (usage, exit 64) rather
|
||||||
|
// than internal/backend failures (exit 70).
|
||||||
|
var errUsage = errors.New("usage error")
|
||||||
|
|
||||||
// resolveBackend builds the named backend, resolving "local" against DEPOT_DIR.
|
// resolveBackend builds the named backend, resolving "local" against DEPOT_DIR.
|
||||||
func resolveBackend(target string, getenv func(string) string, cfg Config) (Backend, error) {
|
func resolveBackend(target string, getenv func(string) string, cfg Config) (Backend, error) {
|
||||||
root := ""
|
root := ""
|
||||||
if target == "local" {
|
if target == "local" {
|
||||||
root = getenv("DEPOT_DIR")
|
root = getenv("DEPOT_DIR")
|
||||||
if root == "" {
|
if root == "" {
|
||||||
return nil, fmt.Errorf("--target local requires DEPOT_DIR to be set")
|
return nil, fmt.Errorf("--target local requires DEPOT_DIR to be set: %w", errUsage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return NewBackend(target, root, cfg)
|
return NewBackend(target, root, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// backendErrExit maps a resolveBackend error onto the exit contract.
|
||||||
|
func backendErrExit(err error) int {
|
||||||
|
if errors.Is(err, errUsage) {
|
||||||
|
return exitUsage
|
||||||
|
}
|
||||||
|
return exitInternal
|
||||||
|
}
|
||||||
|
|
||||||
func printSweepStatus(stdout io.Writer, referenced int, res SweepResult) {
|
func printSweepStatus(stdout io.Writer, referenced int, res SweepResult) {
|
||||||
fmt.Fprintf(stdout, "referenced=%d present=%d missing=%d unconfirmed=%d\n",
|
fmt.Fprintf(stdout, "referenced=%d present=%d missing=%d unconfirmed=%d\n",
|
||||||
referenced, len(res.Present), len(res.Missing), len(res.Unconfirmed))
|
referenced, len(res.Present), len(res.Missing), len(res.Unconfirmed))
|
||||||
@@ -120,7 +133,7 @@ func runStatus(args []string, stdout, stderr io.Writer, getenv func(string) stri
|
|||||||
backend, err := resolveBackend(*target, getenv, cfg)
|
backend, err := resolveBackend(*target, getenv, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, "depot status:", err)
|
fmt.Fprintln(stderr, "depot status:", err)
|
||||||
return exitInternal
|
return backendErrExit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
shaSizes, err := ReferencedSHAs(*manifests)
|
shaSizes, err := ReferencedSHAs(*manifests)
|
||||||
@@ -199,6 +212,7 @@ func runPush(args []string, stdout, stderr io.Writer, getenv func(string) string
|
|||||||
}
|
}
|
||||||
if err := backend.Put(context.Background(), sha, srcPath); err != nil {
|
if err := backend.Put(context.Background(), sha, srcPath); err != nil {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
|
failed++
|
||||||
if transportErr == nil {
|
if transportErr == nil {
|
||||||
transportErr = err
|
transportErr = err
|
||||||
}
|
}
|
||||||
@@ -318,7 +332,7 @@ func runGet(args []string, stdout, stderr io.Writer, getenv func(string) string)
|
|||||||
backend, err := resolveBackend(*target, getenv, cfg)
|
backend, err := resolveBackend(*target, getenv, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(stderr, "depot get:", err)
|
fmt.Fprintln(stderr, "depot get:", err)
|
||||||
return exitInternal
|
return backendErrExit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := backend.Get(context.Background(), sha, dest); err != nil {
|
if err := backend.Get(context.Background(), sha, dest); err != nil {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package depot
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -30,6 +31,19 @@ func TestRunUsage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("status --target local without DEPOT_DIR", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
writeManifest(t, dir, shaOf("blob-a"), 5)
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
code := Run([]string{"status", "--manifests", dir, "--target", "local"}, &out, &errb, testGetenv(nil))
|
||||||
|
if code != 64 {
|
||||||
|
t.Fatalf("expected 64, got %d (stderr=%s)", code, errb.String())
|
||||||
|
}
|
||||||
|
if !bytesContains(errb.String(), "DEPOT_DIR") {
|
||||||
|
t.Fatalf("expected stderr to mention DEPOT_DIR, got %s", errb.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("push --target cdn rejected", func(t *testing.T) {
|
t.Run("push --target cdn rejected", func(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
var out, errb bytes.Buffer
|
var out, errb bytes.Buffer
|
||||||
@@ -116,6 +130,43 @@ func TestStatusExitCodes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPushTransportErrorCountsFailed(t *testing.T) {
|
||||||
|
// Probes 404 (blob absent), PUTs 500 (transport-level upload failure).
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPut {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(404)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
sha := shaOf("push-fail-blob")
|
||||||
|
manifestsDir := t.TempDir()
|
||||||
|
writeManifest(t, manifestsDir, sha, 14)
|
||||||
|
sourceDir := t.TempDir()
|
||||||
|
blobPath := filepath.Join(sourceDir, BlobKey(sha))
|
||||||
|
if err := os.MkdirAll(filepath.Dir(blobPath), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(blobPath, []byte("push-fail-blob"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out, errb bytes.Buffer
|
||||||
|
code := Run([]string{"push", "--manifests", manifestsDir, "--source", sourceDir, "--target", "bunny"}, &out, &errb,
|
||||||
|
testGetenv(map[string]string{
|
||||||
|
"BUNNY_STORAGE_HOST": hostPort(srv),
|
||||||
|
"BUNNY_STORAGE_PASSWORD": "writekey",
|
||||||
|
}))
|
||||||
|
if code != 70 {
|
||||||
|
t.Fatalf("expected 70, got %d (stdout=%s stderr=%s)", code, out.String(), errb.String())
|
||||||
|
}
|
||||||
|
if !bytesContains(out.String(), "uploaded=0 failed=1") {
|
||||||
|
t.Fatalf("expected uploaded=0 failed=1, got %s", out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func bytesContains(s, substr string) bool {
|
func bytesContains(s, substr string) bool {
|
||||||
return bytes.Contains([]byte(s), []byte(substr))
|
return bytes.Contains([]byte(s), []byte(substr))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user