Files
gastownhall__beads/scripts/pull_dolt_image_test.go
Jim Wordelman 30f30ca95b ci: PR-CI lane runs the full internal/storage/dolt suite against a live Dolt server (#5836)
* test(ci): red — gastownhall/beads: no PR-CI lane runs TestBenchDBPurgeDoesNotLeak against a live Dolt server (checkDolt SKIP) (refs be-aiy5)

* feat(ci): green — gastownhall/beads: no PR-CI lane runs TestBenchDBPurgeDoesNotLeak against a live Dolt server (checkDolt SKIP) (refs be-aiy5)

Add test-server-storage-full as a sibling job to test-server-storage in
pr-risk.yml. build-embedded already compiles every top-level test in
internal/storage/dolt into one binary (embedded-test-binaries); the
existing job only ever runs -test.run '^TestConformance$' against it.
The new job reuses that same artifact and live-server setup with no new
build step, and runs everything else via -test.skip '^TestConformance$'
so previously-stranded tests (TestCreateGuard_*, federation/concurrent
suites, and diff-owned tests such as TestBenchDBPurgeDoesNotLeak on PR
#5792) get a real PASS/FAIL instead of a silent SKIP. Sets
BEADS_TEST_ENV_RUN_DOLT=1 so federation_test.go's own live-server check
fails loudly rather than skipping if this job's setup ever regresses.
Wired into ci-gate: added to needs, CI_GATE_REQUIRED, and the
tier-gated skipped_ok fallback (mirrors test-server-storage's existing
treatment in each).

Also updates two pre-existing meta-tests over pr-risk.yml that pinned
exact snapshots of the file this job necessarily changes:
scripts/ci_capability_selector_test.go's ci-gate authority snapshot,
and scripts/pull_dolt_image_test.go's retry-helper call count
(pr-risk.yml: 2 -> 3, the new job's own Dolt-image-pull step).

* chore: release gate PASS for be-pp7e (PR-CI lane for TestBenchDBPurgeDoesNotLeak)

Deploy gate record for be-pp7e, gating be-aiy5's PR-CI lane addition
(runs TestBenchDBPurgeDoesNotLeak against a live Dolt server instead of
letting checkDolt SKIP it). Review: be-w90n, verdict pass. All 7 gate
criteria PASS; two pre-existing/unrelated failures encountered during
evaluation (be-jy56, be-vf95) were attributed and excluded from the
verdict.

* test(ci): red — shard test-server-storage-full across 16 matrix jobs (refs be-yy25)

TestPRRiskGateReachesFullServerDoltStorageSuite now asserts
pr-risk.yml's test-server-storage-full job is a fail-fast:false matrix
over shard 1..16, with the Test step invoking
server-storage-test-shard.sh, instead of pinning the old single-job
unsharded command. Fails against the current workflow, which still
runs all 1126 tests unsharded in one 15m job (be-extn).

* feat(ci): green — shard test-server-storage-full across 16 matrix jobs (refs be-yy25)

Adds .github/scripts/server-storage-test-shard.sh (mirrors
embedded-storage-test-shard.sh: discovers internal/storage/dolt/*_test.go,
excludes TestConformance, manifest-plus-hash-fallback shard assignment) and
its shard manifest .github/scripts/server-storage-test-shards.txt, pinning
TestCloudAuthCLIRouting (~9.5m) alone on shard 1 and spreading the other
known 10-20s-heavy tests across the rest.

pr-risk.yml's test-server-storage-full job becomes a fail-fast:false matrix
over shard 1..16, Test step now runs server-storage-test-shard.sh instead of
the old unsharded single-job command that died at 256/1126 tests after
15m28s with zero failures (job 95616282508).

Documents the new lane in engdocs/CI_REQUIRED_CHECK_TOPOLOGY.md alongside the
existing embedded matrix description.

TestPRRiskGateReachesFullServerDoltStorageSuite now passes. Offline shard
partition verified disjoint and complete across all 1125 discovered tests
(BEADS_TEST_SHARD_LIST_ONLY=1 for i=1..16). (be-extn)

* test(feat): red — Fix: sharded lane runs prebuilt test binary from repo root, breaking ../schema/migrations and ../issueops (6 tests) (refs be-gepv)

* feat: green — Fix: sharded lane runs prebuilt test binary from repo root, breaking ../schema/migrations and ../issueops (6 tests) (refs be-gepv)

* chore: release gate PASS for be-23f3

Extends deploy/be-pp7e-gate (PR gastownhall/beads#5836) with be-gepv's
fix rather than cutting a fresh isolated branch: be-gepv's own bead
text and be-extn's precedent both establish this fix belongs on the
already-open PR. All 7 gate criteria PASS; two repo-wide lint/policy
failures attributed to already-tracked pre-existing issues (be-vf95,
be-jy56), confirmed untouched by this PR lineage.

be-gepv, be-6yo1, be-23f3

* fix(dolt): federation test isolation + panic (be-3c78s)

TestFederationDatabaseIsolation and TestFederationVersionControlAPIs are
federation-prototype tests that predate the suite's shared-server,
branch-per-test isolation architecture (setupTestStore/StartTestBranch)
and were never updated to account for it.

- setupFederationStore hardcoded Database: "beads" for both towns. New()
  always connects in server mode against one shared Dolt server for the
  whole test binary (TestMain), so Path alone doesn't isolate two stores
  — Database does. Both towns silently shared one database, so Alpha
  could read Beta's issues and vice versa. Give each town its own
  database (beads_test_federation_<prefix>, matching the beads_test AD-01
  cleanup-firewall prefix).

- TestFederationDatabaseIsolation's not-found assertions assumed GetIssue
  returns (nil, nil) on a miss. It never does — issueops.GetIssueInTx
  always returns a wrapped storage.ErrNotFound. Switched to
  errors.Is(err, storage.ErrNotFound).

- TestFederationVersionControlAPIs hardcoded Checkout(ctx, "main") to
  return to its "starting" branch, but setupTestStore already checks out
  a private branch (test-<name>-<hex>) — "main" is a branch shared by
  every parallel test in the binary and never has vc-001. That mismatch
  is what produced the nil-pointer panic (GetIssue's error was discarded
  via `_`, and the resulting nil issue was dereferenced). Capture the
  real starting branch via CurrentBranch() right after setupTestStore()
  and check both previously-discarded GetIssue errors before
  dereferencing.

Neither bug is a production security issue: New(), Checkout(), and
GetIssue() all behave exactly as specified. Both bugs are confined to
this prototype test file's own setup/assertions, which hadn't caught up
to the isolation model the rest of the suite already uses correctly.

Verified via `go build`/`go vet`/`go test -c` (all clean) and running the
compiled binary, which now reaches and cleanly SKIPs both tests (no
Docker locally) instead of the previous isolation failure / panic
surfacing at compile-adjacent runtime. Full GREEN confirmation is via
CI, which has a working Dolt/Docker environment.

Refs be-3c78s

* chore: release gate PASS for be-42tlj
2026-08-19 20:39:38 -07:00

204 lines
5.4 KiB
Go

package scripts_test
import (
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
)
const doltSQLServerImage = "dolthub/dolt-sql-server:2.2.0"
func TestPullDoltImageRetriesTransientFailures(t *testing.T) {
tests := []struct {
name string
failures int
wantExit int
wantPulls int
wantSleeps int
}{
{name: "immediate success", failures: 0, wantExit: 0, wantPulls: 1, wantSleeps: 0},
{name: "eventual success", failures: 2, wantExit: 0, wantPulls: 3, wantSleeps: 2},
{name: "exhausted retries", failures: 3, wantExit: 42, wantPulls: 3, wantSleeps: 2},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
run := runPullDoltImage(t, test.failures)
if got := pullDoltExitCode(run.err); got != test.wantExit {
t.Fatalf("exit = %d, want %d; error=%v\n%s", got, test.wantExit, run.err, run.output)
}
if len(run.dockerCalls) != test.wantPulls {
t.Fatalf("docker pull count = %d, want %d; calls=%q\n%s", len(run.dockerCalls), test.wantPulls, run.dockerCalls, run.output)
}
for _, call := range run.dockerCalls {
if call != "pull "+doltSQLServerImage {
t.Fatalf("docker call = %q, want %q", call, "pull "+doltSQLServerImage)
}
}
if len(run.sleepCalls) != test.wantSleeps {
t.Fatalf("sleep count = %d, want %d; calls=%q\n%s", len(run.sleepCalls), test.wantSleeps, run.sleepCalls, run.output)
}
for _, call := range run.sleepCalls {
if call != "5" {
t.Fatalf("sleep call = %q, want 5", call)
}
}
})
}
}
func TestDoltImagePullWorkflowsUseRetryHelper(t *testing.T) {
wantCalls := map[string]int{
"main.yml": 2,
"pr.yml": 2,
"pr-risk.yml": 3,
"regression.yml": 1,
}
workflowsDir := filepath.Join(sourceRepoRoot(t), ".github", "workflows")
for name, want := range wantCalls {
data, err := os.ReadFile(filepath.Join(workflowsDir, name))
if err != nil {
t.Fatal(err)
}
text := string(data)
if got := strings.Count(text, "run: ./scripts/ci/pull-dolt-image.sh"); got != want {
t.Errorf("%s retry-helper calls = %d, want %d", name, got, want)
}
}
workflowPaths, err := filepath.Glob(filepath.Join(workflowsDir, "*.y*ml"))
if err != nil {
t.Fatal(err)
}
for _, path := range workflowPaths {
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(data), "docker pull "+doltSQLServerImage) {
t.Errorf("%s still pulls the Dolt image without retries", filepath.Base(path))
}
}
}
func TestPullDoltImageHelperIsExecutable(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows does not preserve Unix executable bits")
}
path := filepath.Join(sourceRepoRoot(t), "scripts", "ci", "pull-dolt-image.sh")
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm()&0o111 == 0 {
t.Fatalf("%s is not executable", path)
}
}
type pullDoltRun struct {
output string
dockerCalls []string
sleepCalls []string
err error
}
func runPullDoltImage(t *testing.T, failures int) pullDoltRun {
t.Helper()
bash, err := exec.LookPath("bash")
if err != nil {
t.Skipf("bash is required to test pull-dolt-image.sh: %v", err)
}
bin := t.TempDir()
stateDir := t.TempDir()
dockerLog := filepath.Join(stateDir, "docker-calls")
sleepLog := filepath.Join(stateDir, "sleep-calls")
for _, path := range []string{dockerLog, sleepLog} {
if err := os.WriteFile(path, nil, 0o600); err != nil {
t.Fatal(err)
}
}
writeExecutable(t, filepath.Join(bin, "docker"), `#!/bin/sh
set -eu
count=0
if [ -s "$DOLT_PULL_CALL_COUNT" ]; then
IFS= read -r count <"$DOLT_PULL_CALL_COUNT"
fi
count=$((count + 1))
printf '%s\n' "$count" >"$DOLT_PULL_CALL_COUNT"
printf '%s\n' "$*" >>"$DOLT_PULL_DOCKER_LOG"
if [ "$count" -le "$DOLT_PULL_FAILURES" ]; then
exit 42
fi
`)
writeExecutable(t, filepath.Join(bin, "sleep"), `#!/bin/sh
set -eu
printf '%s\n' "$*" >>"$DOLT_PULL_SLEEP_LOG"
`)
pathEnv := shellPathEnv()
binPath := shellPathUnderEnv(t, bash, bin, pathEnv)
statePath := shellPathUnderEnv(t, bash, stateDir, pathEnv)
commandPath := binPath + ":" + os.Getenv("PATH") + ":/usr/bin:/bin"
if runtime.GOOS == "windows" {
commandPath = binPath + ":/usr/bin:/bin"
}
root := sourceRepoRoot(t)
env := []string{
"PATH=" + os.Getenv("PATH"),
"BEADS_TEST_COMMAND_PATH=" + commandPath,
"LC_ALL=C",
"LANG=C",
"BASH_ENV=",
"ENV=",
"DOLT_PULL_CALL_COUNT=" + statePath + "/docker-call-count",
"DOLT_PULL_DOCKER_LOG=" + statePath + "/docker-calls",
"DOLT_PULL_SLEEP_LOG=" + statePath + "/sleep-calls",
"DOLT_PULL_FAILURES=" + strconv.Itoa(failures),
}
requireShellCommandPath(t, bash, root, env, "docker", binPath+"/docker")
requireShellCommandPath(t, bash, root, env, "sleep", binPath+"/sleep")
cmd := bashScriptCommand(bash, "scripts/ci/pull-dolt-image.sh")
cmd.Dir = root
cmd.Env = env
output, runErr := cmd.CombinedOutput()
dockerCalls := readCallLines(t, dockerLog)
sleepCalls := readCallLines(t, sleepLog)
return pullDoltRun{output: string(output), dockerCalls: dockerCalls, sleepCalls: sleepCalls, err: runErr}
}
func readCallLines(t *testing.T, path string) []string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
text := strings.TrimSpace(string(data))
if text == "" {
return nil
}
return strings.Split(text, "\n")
}
func pullDoltExitCode(err error) int {
if err == nil {
return 0
}
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return exitErr.ExitCode()
}
return -1
}