fix(release): qualify frozen candidates with current tooling (#159505)

This commit is contained in:
Dallin Romney 2026-09-27 00:49:03 -07:00 • committed by GitHub
parent 82fbdbf3db
commit ea3c488f4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 204 additions and 7 deletions

View file

@ -2056,6 +2056,25 @@ jobs:
});
}
}
// The trusted planner may name owners added after a frozen checkout.
// Project them out here so the runner never receives impossible work.
const projectFrozenNodeTestPlan = (plan) => {
const configs = plan.configs?.filter((config) => existsSync(config));
if (plan.configs?.length && !configs?.length) {
return null;
}
return { ...plan, configs };
};
const targetNodeTestShards = compatibilityTarget
? rawNodeTestShards.flatMap((shard) => {
const groups = shard.groups
?.map(projectFrozenNodeTestPlan)
.filter((group) => group !== null);
if (shard.groups?.length && !groups?.length) return [];
const projected = projectFrozenNodeTestPlan({ ...shard, groups });
return projected ? [projected] : [];
})
: rawNodeTestShards;
// Node rows list every striped test file. Current targets pack the
// projected runner contract; older targets keep their flat fields or
// projected legacy groups because their shard runner predates the codec.
@ -2069,7 +2088,7 @@ jobs:
})
: null;
const nodeTestGroupsCodec =
(rawNodeTestShards.length > 0 || uiTestGroups !== null) &&
(targetNodeTestShards.length > 0 || uiTestGroups !== null) &&
existsSync(nodeTestGroupsCodecPath)
? await importTargetPlan(nodeTestGroupsCodecPath)
: null;
@ -2120,7 +2139,7 @@ jobs:
"test/vitest/vitest.tooling-docker.config.ts",
]);
// The same capped matrix owns compact and plugin work; admit its longest rows first.
const nodeTestShards = rawNodeTestShards
const nodeTestShards = targetNodeTestShards
.toSorted((a, b) =>
Number(b.runner === "runson-c8i-8xlarge") - Number(a.runner === "runson-c8i-8xlarge") ||
(b.predictedSeconds ?? 0) - (a.predictedSeconds ?? 0))
@ -2200,7 +2219,7 @@ jobs:
!frozenTarget && !compatibilityTarget && !releaseGate &&
/^[0-9a-f]{40}$/u.test(checkoutRevision) && checkoutRevision === workflowRevision &&
typeof nodeTestPlan.hasCompleteStartupCorpusCoverage === "function" &&
nodeTestPlan.hasCompleteStartupCorpusCoverage(rawNodeTestShards, startupCorpusTestFiles)
nodeTestPlan.hasCompleteStartupCorpusCoverage(targetNodeTestShards, startupCorpusTestFiles)
? checkoutRevision : "";
if (startupCorpusNodeRevision) {
// The exact-tree Node receipt makes this row's only test step a no-op.

View file

@ -13,7 +13,7 @@ import { packFutureUpdateFixture } from "../update-first-hop-package-fixtures.mj
import { observePostCoreCommand } from "./process-observer.mjs";
// Without a core tarball, run only the plugin reinstall boundary against the supplied CLI.
export async function runConsentScenario(entry, coreTarball) {
export async function runConsentScenario(entry, coreTarball, options = {}) {
assert(entry, "expected CLI entry");
let coreTarballSha256;
if (coreTarball) {
@ -280,10 +280,27 @@ export async function runConsentScenario(entry, coreTarball) {
"accepted forced reinstall replaces package and acceptance while remaining disabled",
"explicit enable activates the reviewed replacement",
];
if (!coreTarball) {
if (!coreTarball || options.coreUpdateConsent === false) {
console.log(
JSON.stringify(
{ status: "passed", root, assertions: reinstallAssertions, runs, snapshots },
{
status: "passed",
root,
assertions: reinstallAssertions,
...(coreTarball && options.coreUpdateConsent === false
? {
omissions: [
{
scenario: "core-update-consent",
reason:
"selected frozen target predates the recorded update compatibility contract",
},
],
}
: {}),
runs,
snapshots,
},
null,
2,
),

View file

@ -303,7 +303,10 @@ function assertCorruptPluginPolicyPreserved(configPath, pluginId) {
const [command, arg, arg2] = process.argv.slice(2);
const commands = {
consent: () => runConsentScenario(arg, arg2),
consent: () =>
runConsentScenario(arg, arg2, {
coreUpdateConsent: process.env.OPENCLAW_E2E_CORE_UPDATE_CONSENT !== "0",
}),
seed: seedInstallState,
"wait-registry": waitRegistry,
snapshot: () => process.stdout.write(JSON.stringify(pluginRecordSnapshot(), null, 2)),

View file

@ -22,12 +22,33 @@ PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz plugin-update "${OPENCLAW_CURRENT_
# Bare lanes mount the package artifact instead of baking app sources into the image.
docker_e2e_package_mount_args "$PACKAGE_TGZ"
CORE_UPDATE_CONSENT=1
source "$ROOT_DIR/scripts/lib/frozen-target-compat.sh"
TARGET_ROOT_DIR="$(cd "${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$ROOT_DIR}" && pwd)"
context_status=0
openclaw_prepare_frozen_target_context "$TARGET_ROOT_DIR" || context_status=$?
case "$context_status" in
0)
source_status=0
openclaw_frozen_target_source_has_path \
"$TARGET_ROOT_DIR" scripts/lib/update-compat-contract.mjs || source_status=$?
case "$source_status" in
0) ;;
1) CORE_UPDATE_CONSENT=0 ;;
*) exit "$source_status" ;;
esac
;;
1) ;;
*) exit "$context_status" ;;
esac
docker_e2e_build_or_reuse "$IMAGE_NAME" plugin-update "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD"
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 plugin-update empty)"
echo "Running unchanged plugin update and capability consent smoke..."
docker_e2e_run_with_harness \
-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
-e "OPENCLAW_E2E_CORE_UPDATE_CONSENT=$CORE_UPDATE_CONSENT" \
-e OPENCLAW_SKIP_CHANNELS=1 \
-e OPENCLAW_SKIP_PROVIDERS=1 \
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \

View file

@ -71,6 +71,8 @@ export function runCiManifestFixture(options: {
nodeRunnerBackend?: "blacksmith" | "github" | "hybrid" | "runson";
runnerProfile?: "blacksmith" | "github" | "hybrid";
targetHostedRunnerProfileContract?: boolean;
targetFiles?: string[];
missingTargetFiles?: string[];
uiE2eProjectsCapability?: boolean;
uiReleaseTier?: boolean;
uiRealGatewayShards?: boolean;
@ -81,6 +83,45 @@ export function runCiManifestFixture(options: {
try {
const scriptsDir = path.join(root, "scripts", "lib");
mkdirSync(scriptsDir, { recursive: true });
const selectedTargetFiles = new Set(options.targetFiles ?? []);
if (!options.nodeTestShards) {
selectedTargetFiles.add(
options.bundledPlanner
? options.runnerBackend === "runson"
? "test/vitest/vitest.cron.config.ts"
: "test/vitest/bundled.config.ts"
: "test/vitest/legacy.config.ts",
);
}
const collectSelectedTargetFiles = (plan: Record<string, unknown>) => {
const configs = plan.configs;
if (Array.isArray(configs)) {
for (const config of configs) {
if (typeof config === "string") {
selectedTargetFiles.add(config);
}
}
}
const groups = plan.groups;
if (Array.isArray(groups)) {
for (const group of groups) {
if (group && typeof group === "object") {
collectSelectedTargetFiles(group as Record<string, unknown>);
}
}
}
};
for (const shard of options.nodeTestShards ?? []) {
collectSelectedTargetFiles(shard);
}
for (const missing of options.missingTargetFiles ?? []) {
selectedTargetFiles.delete(missing);
}
for (const file of selectedTargetFiles) {
const target = path.join(root, file);
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, "export {};\n");
}
if (options.bunTestRuntime) {
writeFileSync(
path.join(scriptsDir, "ci-test-runtime.mts"),

View file

@ -9512,6 +9512,93 @@ describe("ci workflow guards", () => {
).toEqual(row.groups);
});
it.each([
{ label: "verified release candidate", releaseCandidateCompatibility: true },
{ label: "unverified manual target", releaseCandidateCompatibility: false },
])("projects current-only test owners only for $label", ({ releaseCandidateCompatibility }) => {
const currentConfig = "test/vitest/vitest.current.config.ts";
const currentTest = "src/current.test.ts";
const missingConfig = "test/vitest/vitest.future.config.ts";
const missingTest = "src/future.test.ts";
const result = runCiManifestFixture({
bundledPlanner: true,
historicalCompatibility: false,
missingTargetFiles: [missingConfig],
nodeTestGroupsCodec: false,
releaseCandidateCompatibility,
targetFiles: [currentConfig],
nodeTestShards: [
{
checkName: "mixed-flat",
configs: [currentConfig, missingConfig],
includePatterns: [currentTest, missingTest, "src/**/*.integration.test.ts"],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "mixed-flat",
},
{
checkName: "missing-flat",
configs: [missingConfig],
includePatterns: [missingTest],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "missing-flat",
},
{
checkName: "mixed-groups",
groups: [
{
configs: [currentConfig, missingConfig],
includePatterns: [currentTest, missingTest],
shard_name: "current-group",
},
{
configs: [missingConfig],
includePatterns: [missingTest],
shard_name: "missing-group",
},
],
requiresDist: false,
runner: "ubuntu-24.04",
shardName: "mixed-groups",
},
],
});
expect(result.status, result.output).toBe(0);
const rows = JSON.parse(
expectDefined(result.outputs.checks_node_core_nondist_matrix, "frozen Node matrix"),
).include;
expect(rows.map((row: { check_name: string }) => row.check_name)).toEqual(
releaseCandidateCompatibility
? ["mixed-flat", "mixed-groups"]
: ["mixed-flat", "missing-flat", "mixed-groups"],
);
expect(rows[0].configs).toEqual(
releaseCandidateCompatibility ? [currentConfig] : [currentConfig, missingConfig],
);
expect(rows[0].includePatterns).toEqual([
currentTest,
missingTest,
"src/**/*.integration.test.ts",
]);
const groupedRow = releaseCandidateCompatibility ? rows[1] : rows[2];
expect(groupedRow.groups).toEqual([
expect.objectContaining({
configs: releaseCandidateCompatibility ? [currentConfig] : [currentConfig, missingConfig],
includePatterns: [currentTest, missingTest],
shard_name: "current-group",
}),
...(releaseCandidateCompatibility
? []
: [
expect.objectContaining({
configs: [missingConfig],
shard_name: "missing-group",
}),
]),
]);
});
it("provisions ripgrep for real filesystem contract selections", () => {
const contract = "src/agents/filesystem-tools-output-contract.test.ts";
const nativeTools = "src/agents/sessions/tools/index.test.ts";

View file

@ -288,6 +288,15 @@ describe("installed-CLI consent scenario report contract", () => {
expect(fs.existsSync(path.join(installPath, "index.js"))).toBe(true);
});
it("retains plugin consent proof when a frozen target predates core update consent", async () => {
await expect(
runConsentScenario(entry, coreTarball, { coreUpdateConsent: false }),
).resolves.toBeUndefined();
expect(installed).toBe(2);
expect(adapters.future).not.toHaveBeenCalled();
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('"core-update-consent"'));
});
it.each([
[
"missing plugin warning",