mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
Merge branch 'main' into feat/add-validator-submitter-ci-job
This commit is contained in:
commit
c541704b43
8 changed files with 109 additions and 34 deletions
15
test/.dockerignore
Normal file
15
test/.dockerignore
Normal 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/**
|
||||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue