From 6e9e64bd307677fe5321a0c86d425b7d2aeb10c5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 10 Jul 2026 22:31:42 -0700 Subject: [PATCH] fix: current-tree package checks support unreleased versions (#104186) * fix(ci): package unreleased QA builds safely * style(ci): format QA changelog intent --- .../openclaw-live-and-e2e-checks-reusable.yml | 23 +++++++- .../openclaw-scheduled-live-checks.yml | 1 + .github/workflows/plugin-prerelease.yml | 1 + scripts/docker-e2e-rerun.mjs | 13 ++++- scripts/e2e/bun-global-install-smoke.sh | 1 + scripts/e2e/parallels/package-artifact.ts | 1 + scripts/lib/docker-e2e-package.sh | 1 + scripts/test-docker-all.mjs | 55 +++++++++++-------- scripts/test-install-sh-docker.sh | 1 + test/scripts/docker-all-scheduler.test.ts | 26 +++++++++ test/scripts/docker-build-helper.test.ts | 2 + test/scripts/docker-e2e-helper-cli.test.ts | 6 +- test/scripts/package-changelog.test.ts | 38 +++++++++++++ test/scripts/parallels-smoke-model.test.ts | 1 + .../plugin-prerelease-test-plan.test.ts | 1 + test/scripts/release-no-push-workflow.test.ts | 5 ++ .../release-workflow-matrix-plan.test.ts | 18 ++++++ test/scripts/test-install-sh-docker.test.ts | 2 + 18 files changed, 168 insertions(+), 28 deletions(-) diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index fcc4d6718f13..ed947b35bf44 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -119,6 +119,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: @@ -131,6 +136,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 @@ -378,6 +388,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: @@ -1906,13 +1917,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 diff --git a/.github/workflows/openclaw-scheduled-live-checks.yml b/.github/workflows/openclaw-scheduled-live-checks.yml index 98fe0e59734a..4a082703f5d4 100644 --- a/.github/workflows/openclaw-scheduled-live-checks.yml +++ b/.github/workflows/openclaw-scheduled-live-checks.yml @@ -32,6 +32,7 @@ jobs: include_release_path_suites: true include_openwebui: true include_live_suites: true + allow_unreleased_changelog: true shared_image_artifact_namespace: scheduled-live shared_image_policy: no-push-artifact secrets: diff --git a/.github/workflows/plugin-prerelease.yml b/.github/workflows/plugin-prerelease.yml index 070416f03de0..f992d0f55ace 100644 --- a/.github/workflows/plugin-prerelease.yml +++ b/.github/workflows/plugin-prerelease.yml @@ -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 shared_image_artifact_namespace: plugin-prerelease diff --git a/scripts/docker-e2e-rerun.mjs b/scripts/docker-e2e-rerun.mjs index 427042bd150e..dbfa891d25ca 100644 --- a/scripts/docker-e2e-rerun.mjs +++ b/scripts/docker-e2e-rerun.mjs @@ -98,6 +98,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 = [ @@ -106,6 +107,7 @@ const REUSE_INPUT_KEYS = [ "publishedUpgradeSurvivorBaseline", "publishedUpgradeSurvivorBaselines", "publishedUpgradeSurvivorScenarios", + "allowUnreleasedChangelog", ]; const WORKFLOW_INPUT_RE = /(?:^|\s)-f\s+([a-z0-9_]+)=('([^']*)'|[^\s]+)/gu; @@ -122,8 +124,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; } @@ -235,6 +241,9 @@ function ghWorkflowCommand(lanes, ref, workflow, reuseInputs = {}) { if (reuseInputs.bareImage || reuseInputs.functionalImage) { fields.push("-f", "shared_image_policy=existing-only"); } + if (reuseInputs.allowUnreleasedChangelog === "true") { + fields.push("-f", "allow_unreleased_changelog=true"); + } if (reuseInputs.publishedUpgradeSurvivorBaseline) { fields.push( "-f", diff --git a/scripts/e2e/bun-global-install-smoke.sh b/scripts/e2e/bun-global-install-smoke.sh index 18a14f2c4f73..fcbf31d6767c 100755 --- a/scripts/e2e/bun-global-install-smoke.sh +++ b/scripts/e2e/bun-global-install-smoke.sh @@ -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 diff --git a/scripts/e2e/parallels/package-artifact.ts b/scripts/e2e/parallels/package-artifact.ts index b63b2c5d6895..45bd2be29bb3 100644 --- a/scripts/e2e/parallels/package-artifact.ts +++ b/scripts/e2e/parallels/package-artifact.ts @@ -178,6 +178,7 @@ export async function packOpenClaw(input: { "node", [ "scripts/package-openclaw-for-docker.mjs", + "--allow-unreleased-changelog", "--skip-build", "--source-dir", repoRoot, diff --git a/scripts/lib/docker-e2e-package.sh b/scripts/lib/docker-e2e-package.sh index ae58ec4a1132..a46abc0113fc 100644 --- a/scripts/lib/docker-e2e-package.sh +++ b/scripts/lib/docker-e2e-package.sh @@ -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="$?" diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index 120e7a841311..84d281a292f3 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -276,6 +276,7 @@ function maybeGhcrImage(value) { 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 bareImage = maybeGhcrImage(env.OPENCLAW_DOCKER_E2E_BARE_IMAGE); const functionalImage = maybeGhcrImage(env.OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE); const fields = [ @@ -297,6 +298,9 @@ export function githubWorkflowRerunCommand(laneNames, ref, env = process.env) { "-f", "live_models_only=false", ]; + if (allowUnreleasedChangelog) { + fields.push("-f", "allow_unreleased_changelog=true"); + } if (env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC) { fields.push( "-f", @@ -417,38 +421,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 @@ -456,7 +460,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, @@ -474,18 +480,19 @@ async function writeFailureIndex(logDir, summary) { ? githubWorkflowRerunCommand( workflowRerunFailures.map((failure) => failure.name), ref, + env, ) : undefined, generatedAt: new Date().toISOString(), lanes, note: "Targeted GitHub reruns repack the exact selected ref and reuse only GHCR-backed shared images when the generated command includes 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"), @@ -981,10 +988,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"; @@ -1492,6 +1502,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"); @@ -1653,7 +1664,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, @@ -1692,7 +1703,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, @@ -1721,7 +1732,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, @@ -1739,7 +1750,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, diff --git a/scripts/test-install-sh-docker.sh b/scripts/test-install-sh-docker.sh index 52dac6a297b2..19cc9932d02a 100755 --- a/scripts/test-install-sh-docker.sh +++ b/scripts/test-install-sh-docker.sh @@ -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" diff --git a/test/scripts/docker-all-scheduler.test.ts b/test/scripts/docker-all-scheduler.test.ts index 793bbd68b45a..30c4ea62c4f9 100644 --- a/test/scripts/docker-all-scheduler.test.ts +++ b/test/scripts/docker-all-scheduler.test.ts @@ -183,6 +183,7 @@ describe("scripts/test-docker-all scheduler", () => { const registryCommand = githubWorkflowRerunCommand(["install-e2e"], "b".repeat(40), { OPENCLAW_DOCKER_E2E_BARE_IMAGE: "ghcr.io/openclaw/openclaw-docker-e2e-bare:test", OPENCLAW_DOCKER_E2E_FUNCTIONAL_IMAGE: "ghcr.io/openclaw/openclaw-docker-e2e-functional:test", + OPENCLAW_DOCKER_E2E_ALLOW_UNRELEASED_CHANGELOG: "true", OPENCLAW_DOCKER_E2E_WORKFLOW_REF: "main", OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: "openclaw@2026.5.3", OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: "openclaw@2026.5.3 openclaw@2026.5.2", @@ -196,9 +197,34 @@ describe("scripts/test-docker-all scheduler", () => { "docker_e2e_functional_image='ghcr.io/openclaw/openclaw-docker-e2e-functional:test'", ); expect(registryCommand).toContain("shared_image_policy=existing-only"); + expect(registryCommand).toContain("allow_unreleased_changelog=true"); expectDeclaredDispatchInputs(registryCommand); }); + 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 { diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 3110d01c011a..d9254732f72e 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -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)"); diff --git a/test/scripts/docker-e2e-helper-cli.test.ts b/test/scripts/docker-e2e-helper-cli.test.ts index b6f3a66cb136..f9967ba787e8 100644 --- a/test/scripts/docker-e2e-helper-cli.test.ts +++ b/test/scripts/docker-e2e-helper-cli.test.ts @@ -439,7 +439,7 @@ describe("Docker E2E helper CLIs", () => { lanes: [ { ghWorkflowCommand: - "gh workflow run 'openclaw-live-and-e2e-checks-reusable.yml' --ref 'full-release-validation-temp-deleted' -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 'full-release-validation-temp-deleted' -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, }, @@ -469,6 +469,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).not.toContain("package_artifact_run_id="); expect(result.stdout).not.toContain("package_artifact_name="); @@ -503,7 +504,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, }, @@ -542,6 +543,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 }); } diff --git a/test/scripts/package-changelog.test.ts b/test/scripts/package-changelog.test.ts index 3361f6d51aa2..0626d33430b7 100644 --- a/test/scripts/package-changelog.test.ts +++ b/test/scripts/package-changelog.test.ts @@ -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( diff --git a/test/scripts/parallels-smoke-model.test.ts b/test/scripts/parallels-smoke-model.test.ts index a499cae8819a..dbfbcee568f1 100644 --- a/test/scripts/parallels-smoke-model.test.ts +++ b/test/scripts/parallels-smoke-model.test.ts @@ -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)"); diff --git a/test/scripts/plugin-prerelease-test-plan.test.ts b/test/scripts/plugin-prerelease-test-plan.test.ts index 139395c1f015..c133c3bc06bc 100644 --- a/test/scripts/plugin-prerelease-test-plan.test.ts +++ b/test/scripts/plugin-prerelease-test-plan.test.ts @@ -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 }}", shared_image_artifact_namespace: "plugin-prerelease", shared_image_policy: "no-push-artifact", diff --git a/test/scripts/release-no-push-workflow.test.ts b/test/scripts/release-no-push-workflow.test.ts index 4a467ffed371..cec0dc2dbf49 100644 --- a/test/scripts/release-no-push-workflow.test.ts +++ b/test/scripts/release-no-push-workflow.test.ts @@ -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", }); diff --git a/test/scripts/release-workflow-matrix-plan.test.ts b/test/scripts/release-workflow-matrix-plan.test.ts index f80790279424..c179360edae9 100644 --- a/test/scripts/release-workflow-matrix-plan.test.ts +++ b/test/scripts/release-workflow-matrix-plan.test.ts @@ -132,6 +132,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)( diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 36ebe4658cae..e8414db6a68c 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -872,6 +872,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"); @@ -1171,6 +1172,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");