fix: current-tree package checks support unreleased versions (#104186)

* fix(ci): package unreleased QA builds safely

* style(ci): format QA changelog intent

(cherry picked from commit 6e9e64bd30)
This commit is contained in:
Peter Steinberger
2026-07-10 22:31:42 -07:00
committed by Vincent Koc
parent 544d35595e
commit 2571ae3d9f
18 changed files with 207 additions and 65 deletions
@@ -117,6 +117,11 @@ on:
required: false
default: false
type: boolean
allow_unreleased_changelog:
description: Allow current-tree QA packaging to use Unreleased notes when no exact version section exists
required: false
default: false
type: boolean
workflow_call:
inputs:
advisory:
@@ -129,6 +134,11 @@ on:
required: false
default: false
type: boolean
allow_unreleased_changelog:
description: Allow current-tree QA packaging to use Unreleased notes when no exact version section exists
required: false
default: false
type: boolean
ref:
description: Ref, tag, or SHA to validate
required: true
@@ -330,6 +340,7 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
NODE_VERSION: "24.15.0"
OPENCLAW_DOCKER_E2E_ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog }}
jobs:
validate_selected_ref:
@@ -1467,13 +1478,21 @@ jobs:
- name: Pack OpenClaw package for Docker E2E
if: steps.plan.outputs.needs_package == '1' && inputs.package_artifact_name == '' && inputs.package_artifact_run_id == ''
env:
ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog }}
shell: bash
run: |
set -euo pipefail
mkdir -p .artifacts/docker-e2e-package
node scripts/package-openclaw-for-docker.mjs \
--output-dir .artifacts/docker-e2e-package \
package_args=(
--output-dir .artifacts/docker-e2e-package
--output-name openclaw-current.tgz
)
# Only current-tree QA callers opt in; release and package-candidate callers stay strict.
if [[ "$ALLOW_UNRELEASED_CHANGELOG" == "true" ]]; then
package_args+=(--allow-unreleased-changelog)
fi
node scripts/package-openclaw-for-docker.mjs "${package_args[@]}"
- name: Validate OpenClaw Docker E2E package
id: package
@@ -32,6 +32,7 @@ jobs:
include_release_path_suites: true
include_openwebui: true
include_live_suites: true
allow_unreleased_changelog: true
secrets:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
+1
View File
@@ -549,6 +549,7 @@ jobs:
include_openwebui: false
docker_lanes: ${{ needs.preflight.outputs.plugin_prerelease_docker_lanes }}
targeted_docker_lane_group_size: 4
allow_unreleased_changelog: true
include_live_suites: false
live_models_only: false
+11 -2
View File
@@ -100,6 +100,7 @@ const TRUSTED_WORKFLOW_INPUTS = new Map([
["published_upgrade_survivor_baseline", "publishedUpgradeSurvivorBaseline"],
["published_upgrade_survivor_baselines", "publishedUpgradeSurvivorBaselines"],
["published_upgrade_survivor_scenarios", "publishedUpgradeSurvivorScenarios"],
["allow_unreleased_changelog", "allowUnreleasedChangelog"],
]);
const REUSE_INPUT_KEYS = [
@@ -111,6 +112,7 @@ const REUSE_INPUT_KEYS = [
"publishedUpgradeSurvivorBaseline",
"publishedUpgradeSurvivorBaselines",
"publishedUpgradeSurvivorScenarios",
"allowUnreleasedChangelog",
];
const WORKFLOW_INPUT_RE = /(?:^|\s)-f\s+([a-z0-9_]+)=('([^']*)'|[^\s]+)/gu;
@@ -132,8 +134,12 @@ function trustedReuseInputsFromCommand(command) {
if (!target || !value) {
continue;
}
const normalized =
target === "bareImage" || target === "functionalImage" ? maybeGhcrImage(value) : value;
let normalized = value;
if (target === "bareImage" || target === "functionalImage") {
normalized = maybeGhcrImage(value);
} else if (target === "allowUnreleasedChangelog" && value !== "true") {
normalized = "";
}
if (normalized) {
inputs[target] = normalized;
}
@@ -224,6 +230,9 @@ function ghWorkflowCommand(lanes, ref, workflow, reuseInputs = {}) {
if (reuseInputs.functionalImage) {
fields.push("-f", `docker_e2e_functional_image=${shellQuote(reuseInputs.functionalImage)}`);
}
if (reuseInputs.allowUnreleasedChangelog === "true") {
fields.push("-f", "allow_unreleased_changelog=true");
}
if (reuseInputs.publishedUpgradeSurvivorBaseline) {
fields.push(
"-f",
+1
View File
@@ -107,6 +107,7 @@ resolve_package_tgz() {
echo "==> Pack OpenClaw tarball"
PACKAGE_TGZ="$(
node scripts/package-openclaw-for-docker.mjs \
--allow-unreleased-changelog \
--skip-build \
--output-dir "$PACK_DIR" \
--output-name openclaw-current.tgz
+21 -16
View File
@@ -158,24 +158,29 @@ export async function packOpenClaw(input: {
checkDirty: true,
requireControlUi: input.requireControlUi,
});
run("node", [
"--import",
"tsx",
"--input-type=module",
"--eval",
"import { writePackageDistInventory } from './src/infra/package-dist-inventory.ts'; await writePackageDistInventory(process.cwd());",
]);
const shortHead = run("git", ["rev-parse", "--short", "HEAD"], { quiet: true }).stdout.trim();
const output = run(
"npm",
["pack", "--ignore-scripts", "--json", "--pack-destination", input.destination],
{
quiet: true,
},
).stdout;
const packed = resolveNpmPackTarballFilename(JSON.parse(output).at(-1)?.filename);
const tgzPath = path.join(input.destination, `openclaw-main-${shortHead}.tgz`);
await copyFile(path.join(input.destination, packed), tgzPath);
// The canonical helper inventories the package, bundles private workspace runtime code,
// and rejects tarballs that still depend on unpublished workspace packages.
const packedPath = run(
"node",
[
"scripts/package-openclaw-for-docker.mjs",
"--allow-unreleased-changelog",
"--skip-build",
"--source-dir",
repoRoot,
"--output-dir",
input.destination,
"--output-name",
path.basename(tgzPath),
"--pnpm-pack",
],
{ quiet: true },
).stdout.trim();
if (path.resolve(packedPath) !== path.resolve(tgzPath)) {
die(`package helper wrote an unexpected tarball: ${packedPath}`);
}
const buildCommit = await packageBuildCommitFromTgz(tgzPath);
if (!buildCommit) {
die(`failed to read packed build commit from ${tgzPath}`);
+1
View File
@@ -315,6 +315,7 @@ docker_e2e_prepare_package_tgz() {
local pack_status=0
package_tgz="$(
node "$ROOT_DIR/scripts/package-openclaw-for-docker.mjs" \
--allow-unreleased-changelog \
--output-dir "$pack_dir" \
--output-name openclaw-current.tgz
)" || pack_status="$?"
+54 -43
View File
@@ -265,16 +265,16 @@ function shellQuote(value) {
return `'${String(value).replaceAll("'", "'\\''")}'`;
}
function githubWorkflowRef() {
const explicit = process.env.OPENCLAW_DOCKER_E2E_WORKFLOW_REF;
function githubWorkflowRef(env = process.env) {
const explicit = env.OPENCLAW_DOCKER_E2E_WORKFLOW_REF;
if (explicit) {
return explicit;
}
const refName = process.env.GITHUB_REF_NAME;
const refName = env.GITHUB_REF_NAME;
if (refName) {
return refName;
}
const ref = process.env.GITHUB_REF;
const ref = env.GITHUB_REF;
if (ref?.startsWith("refs/heads/")) {
return ref.slice("refs/heads/".length);
}
@@ -284,12 +284,13 @@ function githubWorkflowRef() {
return undefined;
}
function githubWorkflowRerunCommand(laneNames, ref) {
const workflowRef = githubWorkflowRef();
const releasePath = process.env.OPENCLAW_DOCKER_ALL_PROFILE === RELEASE_PATH_PROFILE;
export function githubWorkflowRerunCommand(laneNames, ref, env = process.env) {
const workflowRef = githubWorkflowRef(env);
const releasePath = env.OPENCLAW_DOCKER_ALL_PROFILE === RELEASE_PATH_PROFILE;
const allowUnreleasedChangelog = env.OPENCLAW_DOCKER_E2E_ALLOW_UNRELEASED_CHANGELOG === "true";
const fields = [
"gh workflow run",
shellQuote(process.env.OPENCLAW_DOCKER_E2E_WORKFLOW || DEFAULT_GITHUB_WORKFLOW),
shellQuote(env.OPENCLAW_DOCKER_E2E_WORKFLOW || DEFAULT_GITHUB_WORKFLOW),
...(workflowRef ? ["--ref", shellQuote(workflowRef)] : []),
"-f",
`ref=${shellQuote(ref)}`,
@@ -306,43 +307,46 @@ function githubWorkflowRerunCommand(laneNames, ref) {
"-f",
"live_models_only=false",
];
if (process.env.GITHUB_RUN_ID) {
fields.push("-f", `package_artifact_run_id=${shellQuote(process.env.GITHUB_RUN_ID)}`);
if (allowUnreleasedChangelog) {
fields.push("-f", "allow_unreleased_changelog=true");
}
if (env.GITHUB_RUN_ID) {
fields.push("-f", `package_artifact_run_id=${shellQuote(env.GITHUB_RUN_ID)}`);
fields.push(
"-f",
`package_artifact_name=${shellQuote(
process.env.OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME || "docker-e2e-package",
env.OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME || "docker-e2e-package",
)}`,
);
}
if (process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC) {
if (env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC) {
fields.push(
"-f",
`published_upgrade_survivor_baseline=${shellQuote(process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC)}`,
`published_upgrade_survivor_baseline=${shellQuote(env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC)}`,
);
}
if (process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS) {
if (env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS) {
fields.push(
"-f",
`published_upgrade_survivor_baselines=${shellQuote(process.env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS)}`,
`published_upgrade_survivor_baselines=${shellQuote(env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS)}`,
);
}
if (process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS) {
if (env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS) {
fields.push(
"-f",
`published_upgrade_survivor_scenarios=${shellQuote(process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS)}`,
`published_upgrade_survivor_scenarios=${shellQuote(env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS)}`,
);
}
if (process.env.OPENCLAW_DOCKER_E2E_BARE_IMAGE) {
if (env.OPENCLAW_DOCKER_E2E_BARE_IMAGE) {
fields.push(
"-f",
`docker_e2e_bare_image=${shellQuote(process.env.OPENCLAW_DOCKER_E2E_BARE_IMAGE)}`,
`docker_e2e_bare_image=${shellQuote(env.OPENCLAW_DOCKER_E2E_BARE_IMAGE)}`,
);
}
if (process.env.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE) {
if (env.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE) {
fields.push(
"-f",
`docker_e2e_functional_image=${shellQuote(process.env.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE)}`,
`docker_e2e_functional_image=${shellQuote(env.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE)}`,
);
}
return fields.join(" ");
@@ -438,38 +442,38 @@ async function writeTimingStore(timingStore, results) {
console.log(`==> Docker lane timings: ${timingStore.file}`);
}
export async function writeRunSummary(logDir, summary) {
export async function writeRunSummary(logDir, summary, env = process.env) {
const file = path.join(logDir, "summary.json");
const payload = {
...summary,
packageArtifactName: process.env.OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME || undefined,
packageArtifactName: env.OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME || undefined,
finishedAt: new Date().toISOString(),
github: {
ref: process.env.GITHUB_REF_NAME || undefined,
repository: process.env.GITHUB_REPOSITORY || undefined,
runId: process.env.GITHUB_RUN_ID || undefined,
ref: env.GITHUB_REF_NAME || undefined,
repository: env.GITHUB_REPOSITORY || undefined,
runId: env.GITHUB_RUN_ID || undefined,
runUrl:
process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID
? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID
? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`
: undefined,
selectedSha: process.env.OPENCLAW_DOCKER_E2E_SELECTED_SHA || undefined,
sha: process.env.GITHUB_SHA || undefined,
workflow: process.env.GITHUB_WORKFLOW || undefined,
selectedSha: env.OPENCLAW_DOCKER_E2E_SELECTED_SHA || undefined,
sha: env.GITHUB_SHA || undefined,
workflow: env.GITHUB_WORKFLOW || undefined,
},
version: 1,
};
await fs.promises.writeFile(file, `${JSON.stringify(payload, null, 2)}\n`);
await writeFailureIndex(logDir, payload);
await writeFailureIndex(logDir, payload, env);
console.log(`==> Docker run summary: ${file}`);
}
async function writeFailureIndex(logDir, summary) {
async function writeFailureIndex(logDir, summary, env) {
const ref =
summary.github?.selectedSha ||
process.env.OPENCLAW_DOCKER_E2E_SELECTED_SHA ||
env.OPENCLAW_DOCKER_E2E_SELECTED_SHA ||
summary.github?.sha ||
summary.github?.ref ||
process.env.GITHUB_SHA ||
env.GITHUB_SHA ||
"HEAD";
const failures = Array.isArray(summary.failures)
? summary.failures
@@ -477,7 +481,9 @@ async function writeFailureIndex(logDir, summary) {
const workflowRerunFailures = failures.filter((failure) => failure.targetable !== false);
const lanes = failures.map((failure) => ({
ghWorkflowCommand:
failure.targetable === false ? undefined : githubWorkflowRerunCommand([failure.name], ref),
failure.targetable === false
? undefined
: githubWorkflowRerunCommand([failure.name], ref, env),
image: failure.image,
imageKind: failure.imageKind,
lane: failure.name,
@@ -495,18 +501,19 @@ async function writeFailureIndex(logDir, summary) {
? githubWorkflowRerunCommand(
workflowRerunFailures.map((failure) => failure.name),
ref,
env,
)
: undefined,
generatedAt: new Date().toISOString(),
lanes,
note: "Targeted GitHub reruns reuse this run's package artifact and shared Docker images when the generated command includes package_artifact_run_id and docker_e2e_*_image inputs.",
images: summary.images,
packageArtifactName: process.env.OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME || undefined,
packageArtifactName: env.OPENCLAW_DOCKER_E2E_PACKAGE_ARTIFACT_NAME || undefined,
ref,
runUrl: summary.github?.runUrl,
status: summary.status,
version: 1,
workflow: process.env.OPENCLAW_DOCKER_E2E_WORKFLOW || DEFAULT_GITHUB_WORKFLOW,
workflow: env.OPENCLAW_DOCKER_E2E_WORKFLOW || DEFAULT_GITHUB_WORKFLOW,
};
await fs.promises.writeFile(
path.join(logDir, "failures.json"),
@@ -1002,10 +1009,13 @@ async function prepareOpenClawPackage(baseEnv, logDir) {
const packageTgz = path.join(packDir, "openclaw-current.tgz");
await runForeground(
"Prepare OpenClaw package once",
`node scripts/package-openclaw-for-docker.mjs --output-dir ${shellQuote(packDir)} --output-name openclaw-current.tgz`,
`node scripts/package-openclaw-for-docker.mjs --allow-unreleased-changelog --output-dir ${shellQuote(packDir)} --output-name openclaw-current.tgz`,
baseEnv,
);
await fs.promises.access(packageTgz);
// Preserve current-tree package intent in generated GitHub reruns; otherwise a local QA
// failure would retry through the strict release path and fail before reaching its lane.
baseEnv.OPENCLAW_DOCKER_E2E_ALLOW_UNRELEASED_CHANGELOG = "true";
baseEnv.OPENCLAW_CURRENT_PACKAGE_TGZ = packageTgz;
baseEnv.OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD = "0";
baseEnv.OPENCLAW_NPM_ONBOARD_HOST_BUILD = "0";
@@ -1513,6 +1523,7 @@ async function main() {
});
baseEnv.OPENCLAW_DOCKER_E2E_IMAGE =
process.env.OPENCLAW_DOCKER_E2E_IMAGE || baseEnv.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE;
const writeSummary = (summary) => writeRunSummary(logDir, summary, baseEnv);
appendExtension(baseEnv, "matrix");
appendExtension(baseEnv, "acpx");
appendExtension(baseEnv, "codex");
@@ -1674,7 +1685,7 @@ async function main() {
const allResults = [...mainResult.results];
await writeTimingStore(timingStore, mainResult.results);
if (failFast && failures.length > 0) {
await writeRunSummary(logDir, {
await writeSummary({
chunk: releaseChunk || undefined,
failures,
image: baseEnv.OPENCLAW_DOCKER_E2E_IMAGE,
@@ -1713,7 +1724,7 @@ async function main() {
console.log("==> Provider-sensitive Docker tail lanes: none");
}
if (failures.length > 0) {
await writeRunSummary(logDir, {
await writeSummary({
chunk: releaseChunk || undefined,
failures,
image: baseEnv.OPENCLAW_DOCKER_E2E_IMAGE,
@@ -1742,7 +1753,7 @@ async function main() {
}
await writeTimingStore(timingStore, allResults);
if (failures.length > 0) {
await writeRunSummary(logDir, {
await writeSummary({
chunk: releaseChunk || undefined,
failures,
image: baseEnv.OPENCLAW_DOCKER_E2E_IMAGE,
@@ -1760,7 +1771,7 @@ async function main() {
await printFailureSummary(failures, tailLines);
process.exit(1);
}
await writeRunSummary(logDir, {
await writeSummary({
chunk: releaseChunk || undefined,
failures,
image: baseEnv.OPENCLAW_DOCKER_E2E_IMAGE,
+1
View File
@@ -324,6 +324,7 @@ prepare_update_tarball() {
fi
UPDATE_EXPECT_VERSION="$(read_candidate_version)"
package_args=(
--allow-unreleased-changelog
--source-dir "$ROOT_DIR"
--output-dir "$UPDATE_DIR"
--pack-json "$pack_json_file"
+24
View File
@@ -149,6 +149,30 @@ describe("scripts/test-docker-all scheduler", () => {
expect(result.stderr).not.toContain("at ");
});
it("preserves ephemeral package intent in generated failure reruns", async () => {
const logDir = createTempDir("openclaw-docker-all-rerun-intent-");
try {
await writeRunSummary(
logDir,
{
failures: [{ name: "install-e2e", status: 1 }],
lanes: [],
status: "failed",
},
{
...process.env,
GITHUB_SHA: "c".repeat(40),
OPENCLAW_DOCKER_E2E_ALLOW_UNRELEASED_CHANGELOG: "true",
},
);
const failureIndex = JSON.parse(readFileSync(path.join(logDir, "failures.json"), "utf8"));
expect(failureIndex.combinedGhWorkflowCommand).toContain("allow_unreleased_changelog=true");
} finally {
rmSync(logDir, { force: true, recursive: true });
}
});
it("rejects loose numeric resource limit env vars before scheduling lanes", () => {
const logDir = mkdtempSync(`${tmpdir()}/openclaw-docker-all-`);
try {
+2
View File
@@ -3881,6 +3881,7 @@ heartbeat_elapsed="\${BASH_REMATCH[1]}"
it("mounts root helper modules imported by bare Docker E2E scripts", () => {
const helper = readFileSync(DOCKER_E2E_PACKAGE_HELPER_PATH, "utf8");
expect(helper).toContain("--allow-unreleased-changelog");
expect(helper).toContain(
'-v "$ROOT_DIR/scripts/windows-cmd-helpers.mjs:/app/scripts/windows-cmd-helpers.mjs:ro"',
);
@@ -3891,6 +3892,7 @@ heartbeat_elapsed="\${BASH_REMATCH[1]}"
it("preserves pnpm lookup paths for scheduled Docker child lanes", () => {
const scheduler = readFileSync(DOCKER_ALL_SCHEDULER_PATH, "utf8");
expect(scheduler).toContain("--allow-unreleased-changelog");
expect(scheduler).toContain("env.PNPM_HOME");
expect(scheduler).toContain("env.npm_execpath ? path.dirname(env.npm_execpath)");
expect(scheduler).toContain("path.dirname(process.execPath)");
+4 -2
View File
@@ -259,7 +259,7 @@ describe("Docker E2E helper CLIs", () => {
lanes: [
{
ghWorkflowCommand:
"gh workflow run 'openclaw-live-and-e2e-checks-reusable.yml' --ref 'release/2026.6' -f package_artifact_run_id='12345' -f package_artifact_name='docker-e2e-package' -f docker_e2e_bare_image='ghcr.io/openclaw/openclaw-bare:test' -f published_upgrade_survivor_baselines='openclaw@2026.5.3' -f published_upgrade_survivor_scenarios='plugin-dependency-cleanup' -f unsafe_input='do-not-copy'",
"gh workflow run 'openclaw-live-and-e2e-checks-reusable.yml' --ref 'release/2026.6' -f package_artifact_run_id='12345' -f package_artifact_name='docker-e2e-package' -f docker_e2e_bare_image='ghcr.io/openclaw/openclaw-bare:test' -f published_upgrade_survivor_baselines='openclaw@2026.5.3' -f published_upgrade_survivor_scenarios='plugin-dependency-cleanup' -f allow_unreleased_changelog=true -f unsafe_input='do-not-copy'",
name: "published-upgrade-survivor-openclaw-2026-5-3",
status: 1,
},
@@ -288,6 +288,7 @@ describe("Docker E2E helper CLIs", () => {
expect(combinedCommand).toContain(
"published_upgrade_survivor_scenarios='plugin-dependency-cleanup'",
);
expect(combinedCommand).toContain("allow_unreleased_changelog=true");
expect(combinedCommand).not.toContain("unsafe_input");
expect(result.stdout).toContain("package_artifact_run_id='12345'");
expect(result.stdout).toContain(
@@ -317,7 +318,7 @@ describe("Docker E2E helper CLIs", () => {
lanes: [
{
ghWorkflowCommand:
"gh workflow run 'openclaw-live-and-e2e-checks-reusable.yml' --ref 'release/2026.6' -f published_upgrade_survivor_baselines='openclaw@2026.5.3'",
"gh workflow run 'openclaw-live-and-e2e-checks-reusable.yml' --ref 'release/2026.6' -f published_upgrade_survivor_baselines='openclaw@2026.5.3' -f allow_unreleased_changelog=1",
name: "published-upgrade-survivor-openclaw-2026-5-3",
status: 1,
},
@@ -356,6 +357,7 @@ describe("Docker E2E helper CLIs", () => {
expect(result.stdout).not.toContain(
"docker_lanes='published-upgrade-survivor-openclaw-2026-5-3 published-upgrade-survivor-openclaw-2026-5-2'",
);
expect(result.stdout).not.toContain("allow_unreleased_changelog");
} finally {
rmSync(root, { force: true, recursive: true });
}
+38
View File
@@ -48,6 +48,10 @@ describe("package-changelog", () => {
"2026.5.28",
"Unreleased",
]);
expect(resolvePackageChangelogVersions("2026.5.29", { allowUnreleased: true })).toEqual([
"2026.5.29",
"Unreleased",
]);
});
it("extracts only the package version stable release section", () => {
@@ -143,6 +147,22 @@ Docs: https://docs.openclaw.ai
`);
});
it("does not fall back when exact non-publish notes fail safety checks", () => {
const source = changelog`
# Changelog
## Unreleased
- Pending development package notes with enough release detail.
## 2026.5.29
- Tiny.
## 2026.5.28
- Older stable release notes with enough detail.
`;
expect(() =>
extractCurrentPackageChangelog(source, "2026.5.29", { allowUnreleased: true }),
).toThrow("Packaged changelog section for 2026.5.29 is only 7 body bytes");
});
it("fails closed when the packaged changelog is unexpectedly large", () => {
const source = changelog`
# Changelog
@@ -188,6 +208,24 @@ Docs: https://docs.openclaw.ai
}
});
it("recovers an interrupted ephemeral QA package with the default restore path", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-package-changelog-"));
const unreleasedChangelog = cumulativeChangelog.replace(
"- Pending note.",
"- Pending release note with enough detail.",
);
try {
writeFileSync(path.join(root, "package.json"), '{"version":"2026.5.29"}\n', "utf8");
writeFileSync(path.join(root, "CHANGELOG.md"), unreleasedChangelog, "utf8");
await expect(preparePackageChangelog(root, { allowUnreleased: true })).resolves.toBe(true);
await expect(restorePackageChangelog(root)).resolves.toBe(true);
expect(readFileSync(path.join(root, "CHANGELOG.md"), "utf8")).toBe(unreleasedChangelog);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("refuses to restore stale backups over current changelog edits", async () => {
const root = mkdtempSync(path.join(os.tmpdir(), "openclaw-package-changelog-"));
const backupPath = path.join(
@@ -402,6 +402,7 @@ describe("Parallels smoke model selection", () => {
expect(packageArtifact).toContain("Wait for Parallels package lock");
expect(packageArtifact).toContain("export async function packageVersionFromTgz");
expect(packageArtifact).toContain("export async function packOpenClaw");
expect(packageArtifact).toContain('"--allow-unreleased-changelog"');
expect(packageArtifact).toContain("function resolveNpmPackTarballFilename");
expect(packageArtifact).toContain("filename !== path.basename(filename)");
expect(packageArtifact).toContain("filename !== path.win32.basename(filename)");
@@ -498,6 +498,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mjs", () => {
include_release_path_suites: false,
include_repo_e2e: false,
live_models_only: false,
allow_unreleased_changelog: true,
ref: "${{ needs.preflight.outputs.checkout_revision }}",
targeted_docker_lane_group_size: 4,
},
@@ -345,6 +345,7 @@ describe("release validation no-push transport", () => {
shared_image_artifact_namespace: "release-live",
shared_image_policy: "no-push-artifact",
});
expect(live.with).not.toHaveProperty("allow_unreleased_changelog");
expect(docker.with).toMatchObject({
package_artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}",
package_artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}",
@@ -359,6 +360,7 @@ describe("release validation no-push transport", () => {
shared_image_artifact_namespace: "release-docker",
shared_image_policy: "no-push-artifact",
});
expect(docker.with).not.toHaveProperty("allow_unreleased_changelog");
expect(acceptance.with).toMatchObject({
artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}",
artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}",
@@ -399,6 +401,8 @@ describe("release validation no-push transport", () => {
package_source_sha: "${{ needs.resolve_package.outputs.package_source_sha }}",
package_version: "${{ needs.resolve_package.outputs.package_version }}",
});
expect(standardAcceptance.with).not.toHaveProperty("allow_unreleased_changelog");
expect(registryAcceptance.with).not.toHaveProperty("allow_unreleased_changelog");
expect(standardAcceptance.if).toContain("shared_image_policy == 'no-push-artifact'");
expectReadOnlyPackagePermission(standardAcceptance);
expect(registryAcceptance.if).toContain("shared_image_policy == 'existing-only'");
@@ -832,6 +836,7 @@ describe("release validation no-push transport", () => {
expect(permissionAt(scheduled.permissions, "packages", "none")).toBe("read");
expectReadOnlyPackagePermission(scheduledValidation);
expect(scheduledValidation.with).toMatchObject({
allow_unreleased_changelog: true,
shared_image_artifact_namespace: "scheduled-live",
shared_image_policy: "no-push-artifact",
});
@@ -111,6 +111,24 @@ describe("scripts/plan-release-workflow-matrix.mjs", () => {
required: false,
type: "boolean",
});
expect(definition.on.workflow_dispatch.inputs.allow_unreleased_changelog).toEqual(
definition.on.workflow_call.inputs.allow_unreleased_changelog,
);
expect(definition.on.workflow_call.inputs.allow_unreleased_changelog).toMatchObject({
default: false,
required: false,
type: "boolean",
});
expect(definition.env.OPENCLAW_DOCKER_E2E_ALLOW_UNRELEASED_CHANGELOG).toBe(
"${{ inputs.allow_unreleased_changelog }}",
);
const packageStep = definition.jobs.prepare_docker_e2e_image.steps.find(
(step) => step.name === "Pack OpenClaw package for Docker E2E",
);
expect(packageStep.env.ALLOW_UNRELEASED_CHANGELOG).toBe(
"${{ inputs.allow_unreleased_changelog }}",
);
expect(packageStep.run).toContain("package_args+=(--allow-unreleased-changelog)");
});
it.each(PROFILE_EXPECTATIONS)(
@@ -858,6 +858,7 @@ printf 'status=%s\\n' "$status"
const script = readFileSync(SCRIPT_PATH, "utf8");
expect(script).toContain('node "$HARNESS_ROOT/scripts/package-openclaw-for-docker.mjs"');
expect(script).toContain("--allow-unreleased-changelog");
expect(script).toContain('--source-dir "$ROOT_DIR"');
expect(script).toContain('--pack-json "$pack_json_file"');
expect(script).toContain("--skip-build");
@@ -1157,6 +1158,7 @@ describe("bun global install smoke", () => {
const packageHelper = readFileSync(DOCKER_E2E_PACKAGE_HELPER_PATH, "utf8");
expect(script).toContain("node scripts/package-openclaw-for-docker.mjs");
expect(script).toContain("--allow-unreleased-changelog");
expect(script).toContain("--skip-build");
expect(script).toContain("--output-name openclaw-current.tgz");
expect(script).not.toContain("npm pack --ignore-scripts --json --pack-destination");