From a3fac51a4a4ea948f1a691489733f776606eec37 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 14 Apr 2026 17:25:24 +0100 Subject: [PATCH 1/3] fix(sui,aptos): skip tarring cwd when ContractsDir is empty newSui and newAptos unconditionally called filepath.Abs(in.ContractsDir) and added the result as a testcontainers ContainerFile. When the caller leaves ContractsDir empty, filepath.Abs("") resolves to the process working directory, causing testcontainers-go to tar the entire cwd via archive/tar and copy it into the container. In integration-tests the cwd is a live test working directory (logs, db dumps, artifact collectors). Any file that grows between tar.FileInfoHeader's stat-derived size and the subsequent io.Copy trips the stdlib tar writer's ErrWriteTooLong guard, which testcontainers surfaces as: create container: created hook: can't copy to container: error compressing file: archive/tar: write too long This manifests as a Sui CCIP smoke-test flake in downstream consumers (chainlink CCIP in-memory tests), typically hidden by the provider's retry loop but occasionally exhausting retries and always adding noise. Gate the ContainerFile entry on a non-empty ContractsDir. Callers that explicitly pass a directory continue to get the prior copy-to-container behaviour; callers that need no host files no longer tar a busy cwd. --- framework/components/blockchain/aptos.go | 20 ++++++++++++------ framework/components/blockchain/sui.go | 27 ++++++++++++++++-------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/framework/components/blockchain/aptos.go b/framework/components/blockchain/aptos.go index 42b66ee1a..8d52bb3aa 100644 --- a/framework/components/blockchain/aptos.go +++ b/framework/components/blockchain/aptos.go @@ -55,11 +55,6 @@ func newAptos(ctx context.Context, in *Input) (*Output, error) { defaultAptos(in) containerName := framework.DefaultTCName("blockchain-node") - absPath, err := filepath.Abs(in.ContractsDir) - if err != nil { - return nil, err - } - exposedPorts, bindings, err := framework.GenerateCustomPortsData(in.CustomPorts) if err != nil { return nil, err @@ -108,12 +103,23 @@ func newAptos(ctx context.Context, in *Input) (*Output, error) { }, ImagePlatform: imagePlatform, Cmd: cmd, - Files: []testcontainers.ContainerFile{ + } + + // Only copy host contracts into the container when the caller explicitly + // provides a ContractsDir. See sui.go for the full rationale — in short, + // filepath.Abs("") resolves to cwd, and tarring a live test working + // directory can flake with archive/tar: write too long. + if in.ContractsDir != "" { + absPath, err := filepath.Abs(in.ContractsDir) + if err != nil { + return nil, err + } + req.Files = []testcontainers.ContainerFile{ { HostFilePath: absPath, ContainerFilePath: "/", }, - }, + } } c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ diff --git a/framework/components/blockchain/sui.go b/framework/components/blockchain/sui.go index e3debe6ef..33388232c 100644 --- a/framework/components/blockchain/sui.go +++ b/framework/components/blockchain/sui.go @@ -95,11 +95,6 @@ func newSui(ctx context.Context, in *Input) (*Output, error) { defaultSui(in) containerName := framework.DefaultTCName("blockchain-node") - absPath, err := filepath.Abs(in.ContractsDir) - if err != nil { - return nil, err - } - // Sui container always listens on port 9000 internally containerPort := fmt.Sprintf("%s/tcp", DefaultSuiNodePort) @@ -150,14 +145,28 @@ func newSui(ctx context.Context, in *Input) (*Output, error) { "--force-regenesis", "--with-faucet", }, - Files: []testcontainers.ContainerFile{ + // we need faucet for funding + WaitingFor: wait.ForListeningPort(DefaultFaucetPort).WithStartupTimeout(1 * time.Minute).WithPollInterval(200 * time.Millisecond), + } + + // Only copy host contracts into the container when the caller explicitly + // provides a ContractsDir. When empty, filepath.Abs("") resolves to the + // process working directory, which in integration tests is an actively + // written test working directory (logs, db dumps, artifacts). testcontainers + // tars that whole directory via archive/tar, and any file that grows + // between os.Stat (header size) and io.Copy (body) triggers + // archive/tar: write too long, surfacing as a flaky test. + if in.ContractsDir != "" { + absPath, err := filepath.Abs(in.ContractsDir) + if err != nil { + return nil, err + } + req.Files = []testcontainers.ContainerFile{ { HostFilePath: absPath, ContainerFilePath: "/", }, - }, - // we need faucet for funding - WaitingFor: wait.ForListeningPort(DefaultFaucetPort).WithStartupTimeout(1 * time.Minute).WithPollInterval(200 * time.Millisecond), + } } c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ From 14664092839ccc436d9d947f2f8a75f86ef16dc7 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 14 Apr 2026 18:23:20 +0100 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20minimize=20diff=20=E2=80=94=20p?= =?UTF-8?q?reserve=20field=20order=20and=20trailing=20commas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same logical change (gate Files on ContractsDir != ""), restructured to reduce diff noise: compute files slice before req literal, assign via Files: files — req struct keeps original field order, trailing commas, and WaitingFor placement untouched. --- framework/components/blockchain/aptos.go | 32 ++++++++++------------ framework/components/blockchain/sui.go | 35 ++++++++++-------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/framework/components/blockchain/aptos.go b/framework/components/blockchain/aptos.go index 8d52bb3aa..46c475efd 100644 --- a/framework/components/blockchain/aptos.go +++ b/framework/components/blockchain/aptos.go @@ -55,6 +55,20 @@ func newAptos(ctx context.Context, in *Input) (*Output, error) { defaultAptos(in) containerName := framework.DefaultTCName("blockchain-node") + var files []testcontainers.ContainerFile + if in.ContractsDir != "" { + absPath, err := filepath.Abs(in.ContractsDir) + if err != nil { + return nil, err + } + files = []testcontainers.ContainerFile{ + { + HostFilePath: absPath, + ContainerFilePath: "/", + }, + } + } + exposedPorts, bindings, err := framework.GenerateCustomPortsData(in.CustomPorts) if err != nil { return nil, err @@ -103,23 +117,7 @@ func newAptos(ctx context.Context, in *Input) (*Output, error) { }, ImagePlatform: imagePlatform, Cmd: cmd, - } - - // Only copy host contracts into the container when the caller explicitly - // provides a ContractsDir. See sui.go for the full rationale — in short, - // filepath.Abs("") resolves to cwd, and tarring a live test working - // directory can flake with archive/tar: write too long. - if in.ContractsDir != "" { - absPath, err := filepath.Abs(in.ContractsDir) - if err != nil { - return nil, err - } - req.Files = []testcontainers.ContainerFile{ - { - HostFilePath: absPath, - ContainerFilePath: "/", - }, - } + Files: files, } c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ diff --git a/framework/components/blockchain/sui.go b/framework/components/blockchain/sui.go index 33388232c..f326c6917 100644 --- a/framework/components/blockchain/sui.go +++ b/framework/components/blockchain/sui.go @@ -95,6 +95,20 @@ func newSui(ctx context.Context, in *Input) (*Output, error) { defaultSui(in) containerName := framework.DefaultTCName("blockchain-node") + var files []testcontainers.ContainerFile + if in.ContractsDir != "" { + absPath, err := filepath.Abs(in.ContractsDir) + if err != nil { + return nil, err + } + files = []testcontainers.ContainerFile{ + { + HostFilePath: absPath, + ContainerFilePath: "/", + }, + } + } + // Sui container always listens on port 9000 internally containerPort := fmt.Sprintf("%s/tcp", DefaultSuiNodePort) @@ -145,30 +159,11 @@ func newSui(ctx context.Context, in *Input) (*Output, error) { "--force-regenesis", "--with-faucet", }, + Files: files, // we need faucet for funding WaitingFor: wait.ForListeningPort(DefaultFaucetPort).WithStartupTimeout(1 * time.Minute).WithPollInterval(200 * time.Millisecond), } - // Only copy host contracts into the container when the caller explicitly - // provides a ContractsDir. When empty, filepath.Abs("") resolves to the - // process working directory, which in integration tests is an actively - // written test working directory (logs, db dumps, artifacts). testcontainers - // tars that whole directory via archive/tar, and any file that grows - // between os.Stat (header size) and io.Copy (body) triggers - // archive/tar: write too long, surfacing as a flaky test. - if in.ContractsDir != "" { - absPath, err := filepath.Abs(in.ContractsDir) - if err != nil { - return nil, err - } - req.Files = []testcontainers.ContainerFile{ - { - HostFilePath: absPath, - ContainerFilePath: "/", - }, - } - } - c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, From ff6206f6ac9ff0b66c596df13f8448f28774606c Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Tue, 14 Apr 2026 19:28:13 +0100 Subject: [PATCH 3/3] chore: add v0.15.16 changeset --- framework/.changeset/v0.15.16.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 framework/.changeset/v0.15.16.md diff --git a/framework/.changeset/v0.15.16.md b/framework/.changeset/v0.15.16.md new file mode 100644 index 000000000..2a4e86268 --- /dev/null +++ b/framework/.changeset/v0.15.16.md @@ -0,0 +1 @@ +- Fix Sui/Aptos CTF providers tarring the process working directory when `ContractsDir` is empty, eliminating an `archive/tar: write too long` flake in downstream CCIP smoke tests \ No newline at end of file