Merge branch 'main' into feat/add-validator-submitter-ci-job

This commit is contained in:
Ahmad Kaouk 2026-03-06 14:22:14 +01:00 committed by GitHub
commit c541704b43
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 109 additions and 34 deletions

15
test/.dockerignore Normal file
View file

@ -0,0 +1,15 @@
# Keep submitter image build context minimal.
*
!package.json
!bun.lock
!tsconfig.json
!bunfig.toml
!.papi/
!.papi/**
!tools/validator-set-submitter/
!tools/validator-set-submitter/**
!contract-bindings/
!contract-bindings/**
!utils/
!utils/**

View file

@ -17,13 +17,13 @@ const SUBMITTER_READY_TIMEOUT_SECONDS = 30;
const SUBMITTER_LOG_TAIL_LINES = 200;
/**
* Builds the validator-set-submitter Docker image from the repo root.
* Builds the validator-set-submitter Docker image from the test directory.
*/
export async function buildSubmitterImage(): Promise<void> {
logger.debug("Building validator-set-submitter Docker image...");
const repoRoot = path.resolve(import.meta.dir, "../../..");
await $`docker build -f test/tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .`
.cwd(repoRoot)
const testRoot = path.resolve(import.meta.dir, "../..");
await $`docker build -f tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .`
.cwd(testRoot)
.quiet();
logger.debug("Validator-set-submitter image built successfully");
}
@ -106,9 +106,11 @@ export async function launchSubmitter(options: LaunchSubmitterOptions): Promise<
timeoutSeconds: SUBMITTER_READY_TIMEOUT_SECONDS
});
} catch (error) {
const logResult = await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}`
.nothrow()
.quiet();
const logs =
(await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}`.nothrow().text()) ||
"<no logs captured>";
`${logResult.stdout.toString()}${logResult.stderr.toString()}`.trim() || "<no logs captured>";
await stopSubmitter(containerName);
throw new Error(
`Submitter did not become ready. Expected log "${SUBMITTER_READY_LOG}". Last ${SUBMITTER_LOG_TAIL_LINES} log lines:\n${logs}`,

View file

@ -3,6 +3,7 @@ import readline from "node:readline";
import { isCI } from "launcher/network";
import { logger } from "utils";
import { launchNetwork } from "../../launcher";
import { getDefaultRelayerImageTag } from "../../launcher/network";
import type { LaunchNetworkResult } from "../../launcher/types";
import { ConnectorFactory, type TestConnectors } from "./connectors";
import { TestSuiteManager } from "./manager";
@ -57,7 +58,7 @@ export abstract class BaseTestSuite {
datahavenImageTag:
this.options.networkOptions?.datahavenImageTag || "datahavenxyz/datahaven:local",
relayerImageTag:
this.options.networkOptions?.relayerImageTag || "datahavenxyz/snowbridge-relay:latest",
this.options.networkOptions?.relayerImageTag || getDefaultRelayerImageTag(),
buildDatahaven: false, // default to false in the test suite so we can speed up the CI
...this.options.networkOptions
});

View file

@ -144,6 +144,7 @@ export const launchNetwork = async (
options: NetworkLaunchOptions
): Promise<LaunchNetworkResult> => {
const networkId = options.networkId;
const relayerImageTag = options.relayerImageTag || getDefaultRelayerImageTag();
const launchedNetwork = new LaunchedNetwork();
launchedNetwork.networkName = networkId;
let injectContracts = false;
@ -177,7 +178,7 @@ export const launchNetwork = async (
{
networkId,
datahavenImageTag: options.datahavenImageTag || "datahavenxyz/datahaven:local",
relayerImageTag: options.relayerImageTag || "datahavenxyz/snowbridge-relay:latest",
relayerImageTag,
authorityIds: TEST_AUTHORITY_IDS,
buildDatahaven: options.buildDatahaven ?? !isCI, // if not specified, default to false for CI, true for local testing
datahavenBuildExtraArgs: options.datahavenBuildExtraArgs || "--features=fast-runtime"
@ -248,14 +249,10 @@ export const launchNetwork = async (
// 7. Launch relayers
logger.info("❄️ Launching Snowbridge relayers...");
if (!options.relayerImageTag) {
throw new Error("Relayer image tag not specified");
}
await launchRelayers(
{
networkId,
relayerImageTag: options.relayerImageTag,
relayerImageTag,
kurtosisEnclaveName
},
launchedNetwork
@ -297,4 +294,13 @@ export const launchNetwork = async (
}
};
export const getDefaultRelayerImageTag = (): string => {
if (process.env.RELAYER_IMAGE_TAG) {
return process.env.RELAYER_IMAGE_TAG;
}
return process.arch === "arm64"
? "datahavenxyz/snowbridge-relay:local"
: "datahavenxyz/snowbridge-relay:latest";
};
export const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";

View file

@ -93,6 +93,47 @@ export const RELAYER_CONFIG_PATHS = {
SOLOCHAIN: path.join(RELAYER_CONFIG_DIR, "solochain-relay.json")
};
const LOCAL_RELAYER_SOURCE_DIR = path.resolve(
import.meta.dir,
"..",
"..",
"contracts",
"lib",
"snowbridge",
"relayer"
);
const isLocalRelayerImage = (relayerImageTag: string): boolean =>
relayerImageTag.endsWith(":local");
const ensureLocalRelayerImage = async (relayerImageTag: string): Promise<void> => {
if (!isLocalRelayerImage(relayerImageTag)) {
return;
}
const localImageExists = await $`docker image inspect ${relayerImageTag}`.nothrow().quiet();
if (localImageExists.exitCode === 0) {
logger.debug(`Local relayer image already available: ${relayerImageTag}`);
return;
}
const dockerfilePath = path.join(LOCAL_RELAYER_SOURCE_DIR, "Dockerfile");
const dockerfileExists = await Bun.file(dockerfilePath).exists();
invariant(
dockerfileExists,
`❌ Local relayer Dockerfile not found at ${dockerfilePath}. Cannot build ${relayerImageTag}`
);
logger.info(
`🐳 Local relayer image ${relayerImageTag} not found. Building from ${LOCAL_RELAYER_SOURCE_DIR} for ${process.arch}...`
);
await runShellCommandWithLogger(`docker build -f Dockerfile -t ${relayerImageTag} .`, {
cwd: LOCAL_RELAYER_SOURCE_DIR,
logLevel: "debug"
});
logger.success(`✅ Built local relayer image: ${relayerImageTag}`);
};
/**
* Generates configuration files for relayers.
*
@ -278,16 +319,16 @@ export const initEthClientPallet = async (
process.platform === "linux" ? "--add-host host.docker.internal:host-gateway" : "";
// Opportunistic pull - pull the image from Docker Hub only if it's not a local image
const isLocal = relayerImageTag.endsWith(":local");
const isLocal = isLocalRelayerImage(relayerImageTag);
const platformParam = isLocal ? "" : "--platform linux/amd64";
logger.debug("Generating beacon checkpoint");
const datastoreHostPath = path.resolve(datastorePath);
const command = `docker run \
const command = `docker run ${platformParam} \
-v ${beaconConfigHostPath}:${beaconConfigContainerPath}:ro \
-v ${checkpointHostPath}:${checkpointContainerPath} \
-v ${datastoreHostPath}:/data \
--name generate-beacon-checkpoint-${networkId} \
--platform linux/amd64 \
--workdir /app \
${addHostParam} \
${launchedNetwork.networkName ? `--network ${launchedNetwork.networkName}` : ""} \
@ -400,6 +441,7 @@ export const launchRelayers = async (
const { relayerImageTag, kurtosisEnclaveName } = options;
invariant(relayerImageTag, "❌ relayerImageTag is required");
await ensureLocalRelayerImage(relayerImageTag);
await killExistingContainers("snowbridge-");
@ -623,7 +665,7 @@ const launchRelayerContainers = async (
launchedNetwork: LaunchedNetwork,
networkId: string
): Promise<void> => {
const isLocal = relayerImageTag.endsWith(":local");
const isLocal = isLocalRelayerImage(relayerImageTag);
const networkName = launchedNetwork.networkName;
invariant(networkName, "❌ Docker network name not found in LaunchedNetwork instance");
const restartArgs = ["--restart", "on-failure:5"];
@ -641,8 +683,7 @@ const launchRelayerContainers = async (
"docker",
"run",
"-d",
"--platform",
"linux/amd64",
...(isLocal ? [] : ["--platform", "linux/amd64"]),
"--add-host",
"host.docker.internal:host-gateway",
"--name",

View file

@ -1,7 +1,8 @@
# Validator Set Submitter image
#
# Build from the repository root:
# docker build -f test/tools/validator-set-submitter/Dockerfile \
# Build from the test directory:
# cd test
# docker build -f tools/validator-set-submitter/Dockerfile \
# -t datahavenxyz/validator-set-submitter:local .
#
# Runtime expectations:
@ -13,8 +14,8 @@ FROM oven/bun:1.3.3-slim AS deps
WORKDIR /app
COPY test/package.json test/bun.lock test/tsconfig.json ./
COPY test/.papi ./.papi
COPY package.json bun.lock tsconfig.json ./
COPY .papi ./.papi
RUN bun install --frozen-lockfile --production
FROM oven/bun:1.3.3-slim
@ -24,10 +25,10 @@ WORKDIR /app
RUN useradd -m -u 1001 -U -s /bin/sh -d /submitter submitter
COPY --from=deps /app/node_modules ./node_modules
COPY test/tsconfig.json test/bunfig.toml ./
COPY test/tools/validator-set-submitter/ ./tools/validator-set-submitter/
COPY test/contract-bindings/ ./contract-bindings/
COPY test/utils/ ./utils/
COPY tsconfig.json bunfig.toml ./
COPY tools/validator-set-submitter/ ./tools/validator-set-submitter/
COPY contract-bindings/ ./contract-bindings/
COPY utils/ ./utils/
ENV NODE_ENV=production

View file

@ -1,4 +1,3 @@
import { parseDeploymentsFile } from "utils";
import { parseEther } from "viem";
import { parse as parseYaml } from "yaml";
@ -37,6 +36,7 @@ export async function loadConfig(
let serviceManagerAddress = optionalHexString(raw, "service_manager_address");
if (!serviceManagerAddress) {
const { parseDeploymentsFile } = await import("../../utils/contracts.ts");
const deployments = await parseDeploymentsFile(networkId);
serviceManagerAddress = deployments.ServiceManager;
}

View file

@ -178,6 +178,13 @@ export async function waitForLog(opts: {
const { readable } = Transform.toWeb(pass);
const decoder = new TextDecoder();
let bufferedLogs = "";
const hasHit = (text: string): boolean => {
if (typeof opts.search === "string") return text.includes(opts.search);
// Avoid stateful regex surprises with /g or /y across multiple checks.
opts.search.lastIndex = 0;
return opts.search.test(text);
};
const timer = setTimeout(
() =>
pass.destroy(
@ -190,14 +197,16 @@ export async function waitForLog(opts: {
try {
for await (const chunk of readable) {
const text = decoder.decode(chunk as Uint8Array, { stream: false });
const hit =
typeof opts.search === "string" ? text.includes(opts.search) : opts.search.test(text);
if (hit) return text.trim();
bufferedLogs += decoder.decode(chunk as Uint8Array, { stream: true });
if (hasHit(bufferedLogs)) return bufferedLogs.trim();
if (bufferedLogs.length > 64_000) {
bufferedLogs = bufferedLogs.slice(-64_000);
}
}
bufferedLogs += decoder.decode();
if (hasHit(bufferedLogs)) return bufferedLogs.trim();
throw new Error(
`Log stream ended before "${opts.search}" appeared for container ${opts.containerName}`
);