ci: forward-port release validation fixes

This commit is contained in:
Peter Steinberger
2026-05-10 20:38:29 +01:00
parent 4e22cdf2f5
commit 522f3296a7
6 changed files with 35 additions and 19 deletions

View File

@@ -783,6 +783,7 @@ jobs:
RELEASE_CHECKS_RESULT: ${{ needs.release_checks.result }}
NPM_TELEGRAM_RESULT: ${{ needs.npm_telegram.result }}
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
@@ -809,7 +810,7 @@ jobs:
head_sha="$(jq -r '.headSha // ""' <<< "$run_json")"
echo "${label}: ${status}/${conclusion} attempt ${attempt} head ${head_sha}: ${url}"
if [[ -n "${TARGET_SHA// }" && "$head_sha" != "$TARGET_SHA" ]]; then
if [[ "$CHILD_WORKFLOW_REF" == release-ci/* && -n "${TARGET_SHA// }" && "$head_sha" != "$TARGET_SHA" ]]; then
echo "::error::${label} child run used ${head_sha}, expected ${TARGET_SHA}. Dispatch Full Release Validation from a ref pinned to the target SHA, not a moving branch."
return 1
fi

View File

@@ -126,7 +126,7 @@ jobs:
uses: actions/download-artifact@v8
with:
name: openclaw-npm-preflight-${{ inputs.tag }}
path: preflight-manifest
path: ${{ runner.temp }}/openclaw-npm-preflight-manifest
repository: ${{ github.repository }}
run-id: ${{ inputs.preflight_run_id }}
github-token: ${{ github.token }}
@@ -151,10 +151,11 @@ jobs:
EXPECTED_SHA: ${{ steps.ref.outputs.sha }}
run: |
set -euo pipefail
manifest="preflight-manifest/preflight-manifest.json"
preflight_dir="${RUNNER_TEMP}/openclaw-npm-preflight-manifest"
manifest="${preflight_dir}/preflight-manifest.json"
if [[ ! -f "$manifest" ]]; then
echo "OpenClaw npm preflight manifest is missing." >&2
ls -la preflight-manifest >&2 || true
ls -la "$preflight_dir" >&2 || true
exit 1
fi
release_tag="$(jq -r '.releaseTag // ""' "$manifest")"
@@ -174,11 +175,11 @@ jobs:
echo "Preflight manifest npm dist-tag mismatch: expected $RELEASE_NPM_DIST_TAG, got $npm_dist_tag" >&2
exit 1
fi
if [[ -z "$tarball_name" || ! -f "preflight-manifest/$tarball_name" ]]; then
if [[ -z "$tarball_name" || ! -f "${preflight_dir}/${tarball_name}" ]]; then
echo "Preflight manifest tarball is missing: $tarball_name" >&2
exit 1
fi
actual_tarball_sha256="$(sha256sum "preflight-manifest/$tarball_name" | awk '{print $1}')"
actual_tarball_sha256="$(sha256sum "${preflight_dir}/${tarball_name}" | awk '{print $1}')"
if [[ "$actual_tarball_sha256" != "$tarball_sha256" ]]; then
echo "Preflight manifest tarball digest mismatch." >&2
exit 1
@@ -222,6 +223,13 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout release SHA
uses: actions/checkout@v6
with:
ref: ${{ needs.resolve_release_target.outputs.sha }}
fetch-depth: 1
persist-credentials: false
- name: Dispatch publish workflows
env:
GH_TOKEN: ${{ github.token }}
@@ -336,8 +344,7 @@ jobs:
changelog_file="${RUNNER_TEMP}/CHANGELOG.md"
notes_file="${RUNNER_TEMP}/release-notes.md"
gh api "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md?ref=${TARGET_SHA}" \
--jq '.content' | base64 --decode > "${changelog_file}"
git show "${TARGET_SHA}:CHANGELOG.md" > "${changelog_file}"
awk -v version="${notes_version}" '
$0 == "## " version { in_section = 1; next }
/^## / && in_section { exit }

View File

@@ -716,7 +716,6 @@ public struct AgentParams: Codable, Sendable {
public let bootstrapcontextmode: AnyCodable?
public let bootstrapcontextrunkind: AnyCodable?
public let acpturnsource: String?
public let internalruntimehandoffid: String?
public let internalevents: [[String: AnyCodable]]?
public let inputprovenance: [String: AnyCodable]?
public let voicewaketrigger: String?
@@ -753,7 +752,6 @@ public struct AgentParams: Codable, Sendable {
bootstrapcontextmode: AnyCodable?,
bootstrapcontextrunkind: AnyCodable?,
acpturnsource: String?,
internalruntimehandoffid: String?,
internalevents: [[String: AnyCodable]]?,
inputprovenance: [String: AnyCodable]?,
voicewaketrigger: String?,
@@ -789,7 +787,6 @@ public struct AgentParams: Codable, Sendable {
self.bootstrapcontextmode = bootstrapcontextmode
self.bootstrapcontextrunkind = bootstrapcontextrunkind
self.acpturnsource = acpturnsource
self.internalruntimehandoffid = internalruntimehandoffid
self.internalevents = internalevents
self.inputprovenance = inputprovenance
self.voicewaketrigger = voicewaketrigger
@@ -827,7 +824,6 @@ public struct AgentParams: Codable, Sendable {
case bootstrapcontextmode = "bootstrapContextMode"
case bootstrapcontextrunkind = "bootstrapContextRunKind"
case acpturnsource = "acpTurnSource"
case internalruntimehandoffid = "internalRuntimeHandoffId"
case internalevents = "internalEvents"
case inputprovenance = "inputProvenance"
case voicewaketrigger = "voiceWakeTrigger"

View File

@@ -50,7 +50,7 @@ const PUBLISHED_BUNDLED_RUNTIME_SIDECAR_PATHS = BUNDLED_RUNTIME_SIDECAR_PATHS.fi
);
const NODE_BUILTIN_MODULES = new Set(builtinModules.map((name) => name.replace(/^node:/u, "")));
const MAX_INSTALLED_ROOT_PACKAGE_JSON_BYTES = 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_BYTES = 4 * 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_BYTES = 6 * 1024 * 1024;
const MAX_INSTALLED_ROOT_DIST_JS_FILES = 5000;
const ROOT_DIST_JAVASCRIPT_MODULE_FILE_RE = /\.(?:c|m)?js$/u;
const OPTIONAL_OR_EXTERNALIZED_RUNTIME_IMPORTS = new Set([
@@ -69,6 +69,9 @@ const OPTIONAL_OR_EXTERNALIZED_RUNTIME_IMPORTS = new Set([
// Discord voice decoder fallback. The root chunk catches missing decoders and the owning
// Discord plugin remains externalized from the root package.
"opusscript",
// Public plugin SDK contract helpers are intentionally test-only entrypoints.
// Consumers importing them run under their own Vitest dev dependency.
"vitest",
]);
const require = createRequire(import.meta.url);
const acorn = require("acorn") as typeof import("acorn");

View File

@@ -250,6 +250,12 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
'import * as lark from "@larksuiteoapi/node-sdk";\nexport { lark };\n',
"utf8",
);
mkdirSync(join(packageRoot, "dist", "plugin-sdk"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "plugin-sdk/channel-test-helpers.js"),
'import { expect, it } from "vitest";\nexport { expect, it };\n',
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toStrictEqual([]);
} finally {
@@ -363,12 +369,12 @@ describe("collectInstalledRootDependencyManifestErrors", () => {
mkdirSync(join(packageRoot, "dist"), { recursive: true });
writeFileSync(
join(packageRoot, "dist", "oversized.js"),
"x".repeat(4 * 1024 * 1024 + 1),
"x".repeat(6 * 1024 * 1024 + 1),
"utf8",
);
expect(collectInstalledRootDependencyManifestErrors(packageRoot)).toEqual([
"installed package root dist file 'oversized.js' is invalid or exceeds 4194304 bytes.",
"installed package root dist file 'oversized.js' is invalid or exceeds 6291456 bytes.",
]);
} finally {
rmSync(packageRoot, { recursive: true, force: true });

View File

@@ -150,16 +150,20 @@ describe("package acceptance workflow", () => {
expect(workflow).toContain("Published upgrade survivor scenarios:");
});
it("requires full release child workflows to run at the resolved target SHA", () => {
it("requires pinned full release child workflows to run at the resolved target SHA", () => {
const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8");
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
expect(workflow).toContain("TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}");
expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}");
expect(workflow).toContain("package_acceptance_package_spec:");
expect(workflow).toContain(
'args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC")',
);
expect(workflow).toContain("--json status,conclusion,url,attempt,headSha,jobs");
expect(workflow).toContain(
'[[ "$CHILD_WORKFLOW_REF" == release-ci/* && -n "${TARGET_SHA// }" && "$head_sha" != "$TARGET_SHA" ]]',
);
expect(workflow).toContain("child run used ${head_sha}, expected ${TARGET_SHA}");
expect(workflow).toContain(
"Dispatch Full Release Validation from a ref pinned to the target SHA",
@@ -842,9 +846,8 @@ describe("package artifact reuse", () => {
expect(workflow).toContain("preflight-manifest.json");
expect(npmWorkflow).toContain("preflight-manifest.json");
expect(npmWorkflow).toContain("tarballSha256");
expect(workflow).toContain(
'gh api "repos/${GITHUB_REPOSITORY}/contents/CHANGELOG.md?ref=${TARGET_SHA}"',
);
expect(workflow).toContain("Checkout release SHA");
expect(workflow).toContain('git show "${TARGET_SHA}:CHANGELOG.md" > "${changelog_file}"');
expect(workflow).toContain('$0 == "## Unreleased" { in_section = 1; next }');
expect(workflow).toContain("Unreleased prerelease fallback");
expect(workflow).not.toContain("gh api --repo");