Refactor release hardening follow-ups (#39959)

* build: fail fast on stale host-env swift policy

* build: sync generated host env swift policy

* build: guard bundled extension root dependency gaps

* refactor: centralize provider capability quirks

* test: table-drive provider regression coverage

* fix: block merge when prep branch has unpushed commits

* refactor: simplify models config merge preservation
This commit is contained in:
Peter Steinberger
2026-03-08 14:49:58 +00:00
committed by GitHub
parent 27558806b5
commit eba9dcc67a
13 changed files with 425 additions and 110 deletions

View File

@@ -229,6 +229,30 @@ checkout_prep_branch() {
git checkout "$prep_branch"
}
verify_prep_branch_matches_prepared_head() {
local pr="$1"
local prepared_head_sha="$2"
require_artifact .local/prep-context.env
checkout_prep_branch "$pr"
local prep_branch_head_sha
prep_branch_head_sha=$(git rev-parse HEAD)
if [ "$prep_branch_head_sha" = "$prepared_head_sha" ]; then
return 0
fi
echo "Local prep branch moved after prepare-push (expected $prepared_head_sha, got $prep_branch_head_sha)."
if git merge-base --is-ancestor "$prepared_head_sha" "$prep_branch_head_sha" 2>/dev/null; then
echo "Unpushed local commits on prep branch:"
git log --oneline "${prepared_head_sha}..${prep_branch_head_sha}" | sed 's/^/ /' || true
echo "Run scripts/pr prepare-sync-head $pr to push them before merge."
else
echo "Prep branch no longer contains the prepared head. Re-run prepare-init."
fi
exit 1
}
resolve_head_push_url() {
# shellcheck disable=SC1091
source .local/pr-meta.env
@@ -1667,6 +1691,7 @@ merge_verify() {
require_artifact .local/prep.env
# shellcheck disable=SC1091
source .local/prep.env
verify_prep_branch_matches_prepared_head "$pr" "$PREP_HEAD_SHA"
local json
json=$(pr_meta_json "$pr")

View File

@@ -8,6 +8,17 @@ import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./s
type PackFile = { path: string };
type PackResult = { files?: PackFile[] };
type PackageJson = {
name?: string;
version?: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
openclaw?: {
install?: {
npmSpec?: string;
};
};
};
const requiredPathGroups = [
["dist/index.js", "dist/index.mjs"],
@@ -108,11 +119,6 @@ const appcastPath = resolve("appcast.xml");
const laneBuildMin = 1_000_000_000;
const laneFloorAdoptionDateKey = 20260227;
type PackageJson = {
name?: string;
version?: string;
};
function normalizePluginSyncVersion(version: string): string {
const normalized = version.trim().replace(/^v/, "");
const base = /^([0-9]+\.[0-9]+\.[0-9]+)/.exec(normalized)?.[1];
@@ -122,6 +128,92 @@ function normalizePluginSyncVersion(version: string): string {
return normalized.replace(/[-+].*$/, "");
}
const ALLOWLISTED_BUNDLED_EXTENSION_ROOT_DEP_GAPS: Record<string, string[]> = {
googlechat: ["google-auth-library"],
matrix: ["@matrix-org/matrix-sdk-crypto-nodejs", "@vector-im/matrix-bot-sdk", "music-metadata"],
msteams: ["@microsoft/agents-hosting"],
nostr: ["nostr-tools"],
tlon: ["@tloncorp/api", "@tloncorp/tlon-skill", "@urbit/aura"],
zalouser: ["zca-js"],
};
export function collectBundledExtensionRootDependencyGapErrors(params: {
rootPackage: PackageJson;
extensions: Array<{ id: string; packageJson: PackageJson }>;
}): string[] {
const rootDeps = {
...params.rootPackage.dependencies,
...params.rootPackage.optionalDependencies,
};
const errors: string[] = [];
for (const extension of params.extensions) {
if (!extension.packageJson.openclaw?.install?.npmSpec) {
continue;
}
const missing = Object.keys(extension.packageJson.dependencies ?? {})
.filter((dep) => dep !== "openclaw" && !rootDeps[dep])
.toSorted();
const allowlisted = [
...(ALLOWLISTED_BUNDLED_EXTENSION_ROOT_DEP_GAPS[extension.id] ?? []),
].toSorted();
if (missing.join("\n") !== allowlisted.join("\n")) {
const unexpected = missing.filter((dep) => !allowlisted.includes(dep));
const resolved = allowlisted.filter((dep) => !missing.includes(dep));
const parts = [
`bundled extension '${extension.id}' root dependency mirror drift`,
`missing in root package: ${missing.length > 0 ? missing.join(", ") : "(none)"}`,
];
if (unexpected.length > 0) {
parts.push(`new gaps: ${unexpected.join(", ")}`);
}
if (resolved.length > 0) {
parts.push(`remove stale allowlist entries: ${resolved.join(", ")}`);
}
errors.push(parts.join(" | "));
}
}
return errors;
}
function collectBundledExtensions(): Array<{ id: string; packageJson: PackageJson }> {
const extensionsDir = resolve("extensions");
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
entry.isDirectory(),
);
return entries.flatMap((entry) => {
const packagePath = join(extensionsDir, entry.name, "package.json");
try {
return [
{
id: entry.name,
packageJson: JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson,
},
];
} catch {
return [];
}
});
}
function checkBundledExtensionRootDependencyMirrors() {
const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as PackageJson;
const errors = collectBundledExtensionRootDependencyGapErrors({
rootPackage,
extensions: collectBundledExtensions(),
});
if (errors.length > 0) {
console.error("release-check: bundled extension root dependency mirror validation failed:");
for (const error of errors) {
console.error(` - ${error}`);
}
process.exit(1);
}
}
function runPackDry(): PackResult[] {
const raw = execSync("npm pack --dry-run --json --ignore-scripts", {
encoding: "utf8",
@@ -321,6 +413,7 @@ function main() {
checkPluginVersions();
checkAppcastSparkleVersions();
checkPluginSdkExports();
checkBundledExtensionRootDependencyMirrors();
const results = runPackDry();
const files = results.flatMap((entry) => entry.files ?? []);