mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
1ece62a4d8
* fix(release): preflight package source metadata * fix(ci): gate source package producers * fix(ci): preflight canonical package producers * fix(ci): skip no-package source setup * fix(ci): normalize source package validation * fix(ci): normalize Docker package reporting * fix(ci): avoid generic dependency guard * test(ci): cover gated package identity
750 lines
27 KiB
TypeScript
750 lines
27 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { parse } from "yaml";
|
|
import { validateBundledPackageDependencyAlignment } from "../../scripts/package-source-dependencies.mjs";
|
|
import {
|
|
validatePackageSource,
|
|
validatePackageSourceDir,
|
|
validatePackageSourceRef,
|
|
} from "../../scripts/package-source-preflight.mjs";
|
|
import { writeRunSummary } from "../../scripts/test-docker-all.mts";
|
|
|
|
const changelog = `# Changelog
|
|
|
|
## Unreleased
|
|
|
|
- Package source preflight notes with enough detail.
|
|
`;
|
|
|
|
function rootManifest(overrides: Record<string, unknown> = {}) {
|
|
return JSON.stringify({
|
|
name: "openclaw",
|
|
version: "2026.8.1",
|
|
dependencies: {
|
|
"@openclaw/ai": "workspace:*",
|
|
openai: "6.49.0",
|
|
},
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function aiManifest(overrides: Record<string, unknown> = {}) {
|
|
return JSON.stringify({
|
|
name: "@openclaw/ai",
|
|
version: "2026.8.1",
|
|
dependencies: {
|
|
openai: "6.49.0",
|
|
},
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
type WorkflowStep = {
|
|
env?: Record<string, string>;
|
|
id?: string;
|
|
if?: string;
|
|
name?: string;
|
|
run?: string;
|
|
uses?: string;
|
|
with?: Record<string, unknown>;
|
|
};
|
|
|
|
type Workflow = {
|
|
jobs: Record<
|
|
string,
|
|
{
|
|
env?: Record<string, string>;
|
|
outputs?: Record<string, unknown>;
|
|
steps: WorkflowStep[];
|
|
with?: Record<string, unknown>;
|
|
}
|
|
>;
|
|
};
|
|
|
|
function readWorkflow(file: string): Workflow {
|
|
return parse(readFileSync(file, "utf8")) as Workflow;
|
|
}
|
|
|
|
function workflowStep(workflow: Workflow, job: string, name: string): WorkflowStep {
|
|
const found = workflow.jobs[job]?.steps.find((step) => step.name === name);
|
|
expect(found, `${job}: ${name}`).toBeDefined();
|
|
return found!;
|
|
}
|
|
|
|
function runSourceRequirement(step: WorkflowStep, env: Record<string, string>) {
|
|
const tempDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-package-source-workflow-"));
|
|
const outputPath = path.join(tempDir, "output");
|
|
try {
|
|
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", step.run ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
...env,
|
|
GITHUB_OUTPUT: outputPath,
|
|
},
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
return readFileSync(outputPath, "utf8").trim();
|
|
} finally {
|
|
rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
function runLiveArtifactTupleValidation(packageEnv: Record<string, string>) {
|
|
const workflow = readWorkflow(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml");
|
|
const step = workflowStep(workflow, "validate_selected_ref", "Validate selected ref");
|
|
const tempDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-live-artifact-tuple-"));
|
|
const fakeBin = path.join(tempDir, "bin");
|
|
const outputPath = path.join(tempDir, "output");
|
|
const summaryPath = path.join(tempDir, "summary");
|
|
const selectedSha = "a".repeat(40);
|
|
mkdirSync(fakeBin);
|
|
writeFileSync(
|
|
path.join(fakeBin, "git"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$1" == "rev-parse" && "$2" == "--verify" ]]; then
|
|
printf '%s\\n' "$SELECTED_SHA"
|
|
exit 0
|
|
fi
|
|
if [[ "$1" == "merge-base" && "$2" == "--is-ancestor" ]]; then
|
|
exit 0
|
|
fi
|
|
exit 64
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const stepEnv = Object.fromEntries(Object.keys(step.env ?? {}).map((name) => [name, ""]));
|
|
try {
|
|
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", step.run ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
...stepEnv,
|
|
GITHUB_OUTPUT: outputPath,
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
INPUT_REF: "main",
|
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
PROVIDED_BARE_IMAGE: "ghcr.io/openclaw/openclaw:test",
|
|
SELECTED_SHA: selectedSha,
|
|
SHARED_IMAGE_POLICY: "existing-only",
|
|
...packageEnv,
|
|
},
|
|
});
|
|
const output =
|
|
result.status === 0
|
|
? Object.fromEntries(
|
|
readFileSync(outputPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => {
|
|
const separator = line.indexOf("=");
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
)
|
|
: {};
|
|
return { output, result };
|
|
} finally {
|
|
rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
function runLiveSourcePackageBuildAndValidation(packageEnv: Record<string, string>) {
|
|
const workflow = readWorkflow(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml");
|
|
const pack = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Pack OpenClaw package for Docker E2E",
|
|
);
|
|
const validate = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Validate OpenClaw Docker E2E package",
|
|
);
|
|
const artifactTuple = runLiveArtifactTupleValidation(packageEnv);
|
|
const tempDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-live-source-package-"));
|
|
const fakeBin = path.join(tempDir, "bin");
|
|
const callsPath = path.join(tempDir, "calls");
|
|
const outputPath = path.join(tempDir, "output");
|
|
const summaryPath = path.join(tempDir, "summary");
|
|
const selectedSha = "a".repeat(40);
|
|
mkdirSync(fakeBin);
|
|
writeFileSync(
|
|
path.join(fakeBin, "node"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%s\\n' "$*" >> "$CALLS_PATH"
|
|
if [[ "$1" == "scripts/package-openclaw-for-docker.mjs" ]]; then
|
|
shift
|
|
output_dir=""
|
|
output_name=""
|
|
while (( "$#" )); do
|
|
case "$1" in
|
|
--output-dir) output_dir="$2"; shift 2 ;;
|
|
--output-name) output_name="$2"; shift 2 ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
fixture="$(mktemp -d)"
|
|
mkdir -p "$fixture/package/dist" "$output_dir"
|
|
printf '%s\\n' '{"name":"openclaw","version":"2026.8.1"}' > "$fixture/package/package.json"
|
|
printf '{"commit":"%s"}\\n' "$SELECTED_SHA" > "$fixture/package/dist/build-info.json"
|
|
tar -czf "$output_dir/$output_name" -C "$fixture" package
|
|
rm -rf "$fixture"
|
|
exit 0
|
|
fi
|
|
if [[ "$1" == "scripts/check-openclaw-package-tarball.mjs" ]]; then
|
|
exit 0
|
|
fi
|
|
exit 64
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(
|
|
path.join(fakeBin, "timeout"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
shift 2
|
|
exec "$@"
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(
|
|
path.join(fakeBin, "sha256sum"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%064d %s\\n' 0 "$1"
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const commonEnv = {
|
|
...process.env,
|
|
ALLOW_UNRELEASED_CHANGELOG: "false",
|
|
CALLS_PATH: callsPath,
|
|
GITHUB_OUTPUT: outputPath,
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
GITHUB_WORKSPACE: tempDir,
|
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
SELECTED_SHA: selectedSha,
|
|
SHARED_IMAGE_POLICY: "existing-only",
|
|
};
|
|
try {
|
|
const buildResult = spawnSync("bash", ["--noprofile", "--norc", "-c", pack.run ?? ""], {
|
|
cwd: tempDir,
|
|
encoding: "utf8",
|
|
env: commonEnv,
|
|
});
|
|
const artifactPresent = artifactTuple.output.package_artifact_present === "true";
|
|
const validationResult = spawnSync(
|
|
"bash",
|
|
["--noprofile", "--norc", "-c", validate.run ?? ""],
|
|
{
|
|
cwd: tempDir,
|
|
encoding: "utf8",
|
|
env: {
|
|
...commonEnv,
|
|
EXPECTED_PACKAGE_FILE_NAME: artifactPresent ? packageEnv.PACKAGE_FILE_NAME : "",
|
|
EXPECTED_PACKAGE_SHA256: artifactPresent ? packageEnv.PACKAGE_SHA256 : "",
|
|
EXPECTED_PACKAGE_SOURCE_SHA: artifactPresent ? packageEnv.PACKAGE_SOURCE_SHA : "",
|
|
EXPECTED_PACKAGE_VERSION: artifactPresent ? packageEnv.PACKAGE_VERSION : "",
|
|
},
|
|
},
|
|
);
|
|
const output =
|
|
validationResult.status === 0
|
|
? Object.fromEntries(
|
|
readFileSync(outputPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => {
|
|
const separator = line.indexOf("=");
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
)
|
|
: {};
|
|
const calls = readFileSync(callsPath, "utf8").trim().split("\n");
|
|
return { artifactTuple, buildResult, calls, output, validationResult, validate };
|
|
} finally {
|
|
rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
function runReleaseInputCapture(params: {
|
|
candidateArtifactJson?: string;
|
|
releasePackageSpec?: string;
|
|
}) {
|
|
const workflow = readWorkflow(".github/workflows/openclaw-release-checks.yml");
|
|
const step = workflowStep(workflow, "resolve_target", "Capture selected inputs");
|
|
const tempDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-release-inputs-"));
|
|
const outputPath = path.join(tempDir, "output");
|
|
const stepEnv = Object.fromEntries(Object.keys(step.env ?? {}).map((name) => [name, ""]));
|
|
try {
|
|
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", step.run ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
...stepEnv,
|
|
CANDIDATE_ARTIFACT_JSON_INPUT: params.candidateArtifactJson ?? "",
|
|
GITHUB_OUTPUT: outputPath,
|
|
RELEASE_ALLOW_UNRELEASED_CHANGELOG_INPUT: "false",
|
|
RELEASE_CROSS_OS_SUITE_FILTER_INPUT: "",
|
|
RELEASE_FAIL_FAST_INPUT: "false",
|
|
RELEASE_FILTER_VALIDATOR: path.resolve("scripts/github/validate-release-suite-filters.sh"),
|
|
RELEASE_LIVE_SUITE_FILTER_INPUT: "",
|
|
RELEASE_MODE_INPUT: "both",
|
|
RELEASE_PACKAGE_SPEC_INPUT: params.releasePackageSpec ?? "",
|
|
RELEASE_PROFILE_INPUT: "beta",
|
|
RELEASE_PROVIDER_INPUT: "openai",
|
|
RELEASE_QA_DISCORD_LIVE_CI_ENABLED: "false",
|
|
RELEASE_QA_SLACK_LIVE_CI_ENABLED: "false",
|
|
RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED: "false",
|
|
RELEASE_REF_INPUT: "main",
|
|
RELEASE_RERUN_GROUP_INPUT: "package",
|
|
RELEASE_RUN_MATURITY_SCORECARD_INPUT: "false",
|
|
RELEASE_RUN_RELEASE_SOAK_INPUT: "false",
|
|
RELEASE_SKIP_PACKAGE_TELEGRAM_E2E_INPUT: "false",
|
|
},
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
return Object.fromEntries(
|
|
readFileSync(outputPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => {
|
|
const separator = line.indexOf("=");
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
);
|
|
} finally {
|
|
rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
}
|
|
|
|
describe("package source preflight", () => {
|
|
it("accepts aligned source manifests and the explicitly allowed Unreleased section", () => {
|
|
expect(
|
|
validatePackageSource({
|
|
aiManifestContent: aiManifest(),
|
|
allowUnreleasedChangelog: true,
|
|
changelogContent: changelog,
|
|
rootManifestContent: rootManifest(),
|
|
}),
|
|
).toBe("2026.8.1");
|
|
});
|
|
|
|
it("uses canonical package changelog validation", () => {
|
|
expect(() =>
|
|
validatePackageSource({
|
|
aiManifestContent: aiManifest(),
|
|
changelogContent: changelog,
|
|
rootManifestContent: rootManifest(),
|
|
}),
|
|
).toThrow("CHANGELOG.md does not contain a release section for 2026.8.1.");
|
|
});
|
|
|
|
it("rejects source package version drift", () => {
|
|
expect(() =>
|
|
validatePackageSource({
|
|
aiManifestContent: aiManifest({ version: "2026.8.2" }),
|
|
allowUnreleasedChangelog: true,
|
|
changelogContent: changelog,
|
|
rootManifestContent: rootManifest(),
|
|
}),
|
|
).toThrow("packages/ai/package.json version must match package.json");
|
|
});
|
|
|
|
it("rejects @openclaw/ai dependency drift before packing", () => {
|
|
expect(() =>
|
|
validatePackageSource({
|
|
aiManifestContent: aiManifest({
|
|
dependencies: {
|
|
openai: "6.50.0",
|
|
},
|
|
}),
|
|
allowUnreleasedChangelog: true,
|
|
changelogContent: changelog,
|
|
rootManifestContent: rootManifest(),
|
|
}),
|
|
).toThrow(
|
|
"package.json must declare openai@6.50.0 to bundle @openclaw/ai without duplicate dependencies",
|
|
);
|
|
});
|
|
|
|
it("shares exact, workspace, private, and value-type dependency semantics with packaging", () => {
|
|
expect(
|
|
validateBundledPackageDependencyAlignment({
|
|
bundledDependencies: {
|
|
exact: "1.2.3",
|
|
private: "0.0.0-private",
|
|
workspace: "4.5.6",
|
|
},
|
|
bundledPackageLabel: "packed @openclaw/ai",
|
|
rootDependencies: {
|
|
exact: "1.2.3",
|
|
workspace: "workspace:4.5.6",
|
|
},
|
|
}),
|
|
).toEqual([
|
|
["exact", "1.2.3"],
|
|
["workspace", "4.5.6"],
|
|
]);
|
|
|
|
expect(() =>
|
|
validateBundledPackageDependencyAlignment({
|
|
bundledDependencies: { invalid: 123 },
|
|
bundledPackageLabel: "packed @openclaw/ai",
|
|
rootDependencies: { invalid: "123" },
|
|
}),
|
|
).toThrow("packed @openclaw/ai dependency invalid must declare a string version");
|
|
expect(() =>
|
|
validateBundledPackageDependencyAlignment({
|
|
bundledDependencies: { invalid: "1.2.3" },
|
|
bundledPackageLabel: "packed @openclaw/ai",
|
|
rootDependencies: { invalid: 123 },
|
|
}),
|
|
).toThrow("root package.json dependency invalid must declare a string version");
|
|
});
|
|
|
|
it("rejects real partial-json source manifest drift", () => {
|
|
const root = JSON.parse(readFileSync("package.json", "utf8")) as {
|
|
dependencies: Record<string, string>;
|
|
};
|
|
root.dependencies["partial-json"] = "0.1.8";
|
|
expect(() =>
|
|
validatePackageSource({
|
|
aiManifestContent: readFileSync("packages/ai/package.json", "utf8"),
|
|
allowUnreleasedChangelog: true,
|
|
changelogContent: readFileSync("CHANGELOG.md", "utf8"),
|
|
rootManifestContent: JSON.stringify(root),
|
|
}),
|
|
).toThrow(
|
|
"package.json must declare partial-json@0.1.7 to bundle @openclaw/ai without duplicate dependencies",
|
|
);
|
|
});
|
|
|
|
it("preserves historical sources from before the @openclaw/ai workspace split", () => {
|
|
expect(
|
|
validatePackageSource({
|
|
aiManifestContent: null,
|
|
allowUnreleasedChangelog: true,
|
|
changelogContent: changelog,
|
|
rootManifestContent: rootManifest({ dependencies: {} }),
|
|
}),
|
|
).toBe("2026.8.1");
|
|
});
|
|
|
|
it("validates the current source ref without modifying the checkout", () => {
|
|
expect(
|
|
validatePackageSourceRef("HEAD", {
|
|
allowUnreleasedChangelog: true,
|
|
}),
|
|
).toBe("2026.8.1");
|
|
expect(
|
|
validatePackageSourceDir(process.cwd(), {
|
|
allowUnreleasedChangelog: true,
|
|
}),
|
|
).toBe("2026.8.1");
|
|
});
|
|
|
|
it("normalizes release-check package mode and guards the source resolver", () => {
|
|
const workflow = readWorkflow(".github/workflows/openclaw-release-checks.yml");
|
|
const steps = workflow.jobs.prepare_release_package!.steps;
|
|
const preflightIndex = steps.findIndex(
|
|
(step) => step.name === "Validate release package source metadata",
|
|
);
|
|
const setupIndex = steps.findIndex((step) => step.name === "Setup Node environment");
|
|
const packageIndex = steps.findIndex(
|
|
(step) => step.name === "Resolve release package artifact",
|
|
);
|
|
const preflight = steps[preflightIndex]!;
|
|
const packageStep = steps[packageIndex]!;
|
|
const setup = workflowStep(workflow, "prepare_release_package", "Setup Node environment");
|
|
const upload = workflowStep(
|
|
workflow,
|
|
"prepare_release_package",
|
|
"Upload release package artifact",
|
|
);
|
|
const artifactIdentity = workflowStep(
|
|
workflow,
|
|
"prepare_release_package",
|
|
"Validate shared release candidate identity",
|
|
);
|
|
|
|
expect(preflightIndex).toBeGreaterThan(-1);
|
|
expect(preflightIndex).toBeLessThan(setupIndex);
|
|
expect(preflightIndex).toBeLessThan(packageIndex);
|
|
expect(runReleaseInputCapture({ releasePackageSpec: " \t " })).toMatchObject({
|
|
candidate_artifact_json: "",
|
|
package_mode: "source",
|
|
release_package_spec: "",
|
|
});
|
|
expect(runReleaseInputCapture({ releasePackageSpec: "openclaw@beta" })).toMatchObject({
|
|
candidate_artifact_json: "",
|
|
package_mode: "published",
|
|
release_package_spec: "openclaw@beta",
|
|
});
|
|
expect(runReleaseInputCapture({ candidateArtifactJson: " \t " })).toMatchObject({
|
|
candidate_artifact_json: "",
|
|
package_mode: "source",
|
|
release_package_spec: "",
|
|
});
|
|
expect(
|
|
runReleaseInputCapture({ candidateArtifactJson: '{"packageArtifactId":"1"}' }),
|
|
).toMatchObject({
|
|
candidate_artifact_json: '{"packageArtifactId":"1"}',
|
|
package_mode: "artifact",
|
|
release_package_spec: "",
|
|
});
|
|
expect(preflight.if).toBe("needs.resolve_target.outputs.package_mode == 'source'");
|
|
expect(preflight.env?.PACKAGE_REF).toBe("${{ needs.resolve_target.outputs.revision }}");
|
|
expect(preflight.run).toContain("node scripts/package-source-preflight.mjs");
|
|
expect(packageStep.env?.PACKAGE_MODE).toBe("${{ needs.resolve_target.outputs.package_mode }}");
|
|
expect(setup.if).toBe("needs.resolve_target.outputs.package_mode != 'artifact'");
|
|
expect(packageStep.if).toBe("needs.resolve_target.outputs.package_mode != 'artifact'");
|
|
expect(packageStep.run).toContain('if [[ "$PACKAGE_MODE" == "published" ]]');
|
|
expect(upload.if).toBe("needs.resolve_target.outputs.package_mode != 'artifact'");
|
|
expect(artifactIdentity.if).toBe("needs.resolve_target.outputs.package_mode == 'artifact'");
|
|
expect(workflow.jobs.docker_e2e_release_checks?.with).toMatchObject({
|
|
enable_prepublish_plugin_registry:
|
|
"${{ needs.resolve_target.outputs.package_mode != 'published' }}",
|
|
});
|
|
expect(workflow.jobs.package_acceptance_release_checks?.with).toMatchObject({
|
|
candidate_artifact_json: "${{ needs.resolve_target.outputs.candidate_artifact_json }}",
|
|
source:
|
|
"${{ (needs.resolve_target.outputs.package_acceptance_package_spec != '' || needs.resolve_target.outputs.package_mode == 'published') && 'npm' || 'artifact' }}",
|
|
});
|
|
const workflowSource = readFileSync(".github/workflows/openclaw-release-checks.yml", "utf8");
|
|
expect(workflowSource.match(/\$\{\{ inputs\.candidate_artifact_json \}\}/gu)).toHaveLength(1);
|
|
expect(workflowSource.match(/\$\{\{ inputs\.release_package_spec \}\}/gu)).toHaveLength(1);
|
|
});
|
|
|
|
it("guards prepare-only source before harness setup and skips no-package lane setup", () => {
|
|
const workflow = readWorkflow(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml");
|
|
const steps = workflow.jobs.prepare_docker_e2e_image!.steps;
|
|
const sourceRequirement = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Resolve source package requirement",
|
|
);
|
|
const prepareOnlyPreflight = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Validate prepare-only Docker E2E package source metadata",
|
|
);
|
|
const plannedPreflight = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Validate Docker E2E package source metadata",
|
|
);
|
|
const harnessSetup = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Setup trusted release harness",
|
|
);
|
|
const setup = workflowStep(workflow, "prepare_docker_e2e_image", "Setup Node environment");
|
|
const pack = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Pack OpenClaw package for Docker E2E",
|
|
);
|
|
|
|
expect(
|
|
runSourceRequirement(sourceRequirement, {
|
|
PACKAGE_ARTIFACT_PRESENT: "false",
|
|
}),
|
|
).toBe("required=true");
|
|
expect(
|
|
runSourceRequirement(sourceRequirement, {
|
|
PACKAGE_ARTIFACT_PRESENT: "true",
|
|
}),
|
|
).toBe("required=false");
|
|
expect(steps.indexOf(prepareOnlyPreflight)).toBeLessThan(steps.indexOf(harnessSetup));
|
|
expect(harnessSetup.uses).toBe("./.release-harness/.github/actions/setup-release-harness");
|
|
expect(steps.indexOf(plannedPreflight)).toBeGreaterThan(
|
|
steps.findIndex((step) => step.name === "Plan Docker E2E images"),
|
|
);
|
|
expect(steps.indexOf(plannedPreflight)).toBeLessThan(steps.indexOf(setup));
|
|
expect(steps.indexOf(plannedPreflight)).toBeLessThan(steps.indexOf(pack));
|
|
expect(prepareOnlyPreflight.run).toContain(
|
|
"node .release-harness/scripts/package-source-preflight.mjs",
|
|
);
|
|
expect(prepareOnlyPreflight.if).toBe(
|
|
"inputs.prepare_only && steps.package_source.outputs.required == 'true'",
|
|
);
|
|
expect(plannedPreflight.if).toBe(
|
|
"(!inputs.prepare_only) && steps.plan.outputs.needs_package == '1' && steps.package_source.outputs.required == 'true'",
|
|
);
|
|
expect(setup.if).toContain(
|
|
"steps.plan.outputs.needs_package == '1' && steps.package_source.outputs.required == 'true'",
|
|
);
|
|
expect(pack.if).toBe(
|
|
"steps.plan.outputs.needs_package == '1' && steps.package_source.outputs.required == 'true'",
|
|
);
|
|
});
|
|
|
|
it("treats tab and newline-only package artifact tuples as absent", () => {
|
|
const whitespace = " \t\n ";
|
|
const { output, result } = runLiveArtifactTupleValidation({
|
|
PACKAGE_ARTIFACT_DIGEST: whitespace,
|
|
PACKAGE_ARTIFACT_ID: whitespace,
|
|
PACKAGE_ARTIFACT_NAME: whitespace,
|
|
PACKAGE_ARTIFACT_RUN_ATTEMPT: whitespace,
|
|
PACKAGE_ARTIFACT_RUN_ID: whitespace,
|
|
PACKAGE_FILE_NAME: whitespace,
|
|
PACKAGE_SHA256: whitespace,
|
|
PACKAGE_SOURCE_SHA: whitespace,
|
|
PACKAGE_VERSION: whitespace,
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(output.package_artifact_present).toBe("false");
|
|
});
|
|
|
|
it("keeps a whitespace-only artifact tuple in source mode through Docker reports", async () => {
|
|
const whitespace = " \t\n ";
|
|
const result = runLiveSourcePackageBuildAndValidation({
|
|
PACKAGE_ARTIFACT_DIGEST: whitespace,
|
|
PACKAGE_ARTIFACT_ID: whitespace,
|
|
PACKAGE_ARTIFACT_NAME: whitespace,
|
|
PACKAGE_ARTIFACT_RUN_ATTEMPT: whitespace,
|
|
PACKAGE_ARTIFACT_RUN_ID: whitespace,
|
|
PACKAGE_FILE_NAME: whitespace,
|
|
PACKAGE_SHA256: whitespace,
|
|
PACKAGE_SOURCE_SHA: whitespace,
|
|
PACKAGE_VERSION: whitespace,
|
|
});
|
|
|
|
expect(result.artifactTuple.result.status, result.artifactTuple.result.stderr).toBe(0);
|
|
expect(result.artifactTuple.output.package_artifact_present).toBe("false");
|
|
expect(result.validate.env).toMatchObject({
|
|
EXPECTED_PACKAGE_FILE_NAME:
|
|
"${{ needs.validate_selected_ref.outputs.package_artifact_present == 'true' && inputs.package_file_name || '' }}",
|
|
EXPECTED_PACKAGE_SHA256:
|
|
"${{ needs.validate_selected_ref.outputs.package_artifact_present == 'true' && inputs.package_sha256 || '' }}",
|
|
EXPECTED_PACKAGE_SOURCE_SHA:
|
|
"${{ needs.validate_selected_ref.outputs.package_artifact_present == 'true' && inputs.package_source_sha || '' }}",
|
|
EXPECTED_PACKAGE_VERSION:
|
|
"${{ needs.validate_selected_ref.outputs.package_artifact_present == 'true' && inputs.package_version || '' }}",
|
|
});
|
|
expect(result.buildResult.status, result.buildResult.stderr).toBe(0);
|
|
expect(result.validationResult.status, result.validationResult.stderr).toBe(0);
|
|
expect(result.calls).toEqual([
|
|
expect.stringContaining("scripts/package-openclaw-for-docker.mjs"),
|
|
expect.stringContaining("scripts/check-openclaw-package-tarball.mjs"),
|
|
]);
|
|
expect(result.output).toMatchObject({
|
|
file_name: "openclaw-current.tgz",
|
|
source_sha: "a".repeat(40),
|
|
version: "2026.8.1",
|
|
});
|
|
|
|
const workflow = readWorkflow(".github/workflows/openclaw-live-and-e2e-checks-reusable.yml");
|
|
const prepared = workflow.jobs.prepare_docker_e2e_image!;
|
|
expect(prepared.outputs).toMatchObject({
|
|
package_artifact_id:
|
|
"${{ steps.upload_package.outputs.artifact-id || (needs.validate_selected_ref.outputs.package_artifact_present == 'true' && inputs.package_artifact_id || '') }}",
|
|
package_artifact_name:
|
|
"${{ steps.upload_package.outputs.artifact-id && format('docker-e2e-package-{0}-{1}', github.run_id, github.run_attempt) || (needs.validate_selected_ref.outputs.package_artifact_present == 'true' && inputs.package_artifact_name || '') }}",
|
|
});
|
|
const reportArtifactName =
|
|
"${{ needs.prepare_docker_e2e_image.outputs.package_artifact_name || 'docker-e2e-package' }}";
|
|
for (const jobId of [
|
|
"validate_docker_e2e",
|
|
"validate_docker_lanes",
|
|
"validate_docker_openwebui",
|
|
]) {
|
|
expect(workflow.jobs[jobId]!.env?.OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME).toBe(
|
|
reportArtifactName,
|
|
);
|
|
}
|
|
const candidateManifest = workflowStep(
|
|
workflow,
|
|
"prepare_docker_e2e_image",
|
|
"Emit immutable release candidate tuple",
|
|
);
|
|
for (const name of [
|
|
"PACKAGE_ARTIFACT_DIGEST",
|
|
"PACKAGE_ARTIFACT_ID",
|
|
"PACKAGE_ARTIFACT_NAME",
|
|
"PACKAGE_ARTIFACT_RUN_ATTEMPT",
|
|
"PACKAGE_ARTIFACT_RUN_ID",
|
|
]) {
|
|
expect(candidateManifest.env?.[name]).toContain(
|
|
"needs.validate_selected_ref.outputs.package_artifact_present == 'true'",
|
|
);
|
|
}
|
|
|
|
const noPackageArtifactName =
|
|
result.artifactTuple.output.package_artifact_present === "true" ? whitespace : "";
|
|
expect(noPackageArtifactName).toBe("");
|
|
const reportDir = mkdtempSync(path.join(os.tmpdir(), "openclaw-live-source-report-"));
|
|
try {
|
|
await writeRunSummary(
|
|
reportDir,
|
|
{
|
|
failures: [{ name: "live-models", status: 1 }],
|
|
lanes: [],
|
|
status: "failed",
|
|
},
|
|
{
|
|
OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME: noPackageArtifactName || "docker-e2e-package",
|
|
OPENCLAW_DOCKER_E2E_SELECTED_SHA: "a".repeat(40),
|
|
},
|
|
);
|
|
const summary = JSON.parse(readFileSync(path.join(reportDir, "summary.json"), "utf8"));
|
|
const failures = JSON.parse(readFileSync(path.join(reportDir, "failures.json"), "utf8"));
|
|
expect(summary.packageArtifactName).toBe("docker-e2e-package");
|
|
expect(failures.packageArtifactName).toBe("docker-e2e-package");
|
|
expect(JSON.stringify({ failures, summary })).not.toContain(whitespace);
|
|
} finally {
|
|
rmSync(reportDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it("guards install-smoke candidate packaging before its dependency install", () => {
|
|
const workflow = readWorkflow(".github/workflows/install-smoke-reusable.yml");
|
|
const packageCandidate = workflowStep(
|
|
workflow,
|
|
"installer_smoke_candidate_payload",
|
|
"Package candidate only inside pinned harness",
|
|
);
|
|
expect(packageCandidate.run).toContain('-v "$PWD/.release-harness:/harness:ro"');
|
|
expect(packageCandidate.run).toContain(
|
|
'node /harness/scripts/package-source-preflight.mjs "${preflight_args[@]}"',
|
|
);
|
|
expect(packageCandidate.run!.indexOf("package-source-preflight.mjs")).toBeLessThan(
|
|
packageCandidate.run!.indexOf("pnpm install --frozen-lockfile"),
|
|
);
|
|
});
|
|
|
|
it("guards npm source producers with trusted tooling before Node setup", () => {
|
|
const workflow = readWorkflow(".github/workflows/openclaw-npm-release.yml");
|
|
const steps = workflow.jobs.preflight_openclaw_npm!.steps;
|
|
const checkout = workflowStep(
|
|
workflow,
|
|
"preflight_openclaw_npm",
|
|
"Checkout trusted package source preflight",
|
|
);
|
|
const preflight = workflowStep(
|
|
workflow,
|
|
"preflight_openclaw_npm",
|
|
"Validate npm package source metadata",
|
|
);
|
|
const setup = workflowStep(workflow, "preflight_openclaw_npm", "Setup Node environment");
|
|
const build = workflowStep(workflow, "preflight_openclaw_npm", "Build");
|
|
|
|
expect(checkout.with).toMatchObject({
|
|
ref: "${{ github.workflow_sha }}",
|
|
path: ".release-harness",
|
|
});
|
|
expect(preflight.run).toContain("node .release-harness/scripts/package-source-preflight.mjs");
|
|
expect(preflight.run).toContain('if [[ "$RELEASE_REF" =~ ^[0-9a-fA-F]{40}$ ]]');
|
|
expect(steps.indexOf(checkout)).toBeLessThan(steps.indexOf(preflight));
|
|
expect(steps.indexOf(preflight)).toBeLessThan(steps.indexOf(setup));
|
|
expect(steps.indexOf(preflight)).toBeLessThan(steps.indexOf(build));
|
|
});
|
|
});
|