Skip to content

Commit eba9dcc

Browse files
authored
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
1 parent 2755880 commit eba9dcc

13 files changed

+425
-110
lines changed

apps/macos/Sources/OpenClaw/HostEnvSecurityPolicy.generated.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ enum HostEnvSecurityPolicy {
2222
"PS4",
2323
"GCONV_PATH",
2424
"IFS",
25-
"SSLKEYLOGFILE",
25+
"SSLKEYLOGFILE"
2626
]
2727

2828
static let blockedOverrideKeys: Set<String> = [
@@ -50,17 +50,17 @@ enum HostEnvSecurityPolicy {
5050
"OPENSSL_ENGINES",
5151
"PYTHONSTARTUP",
5252
"WGETRC",
53-
"CURL_HOME",
53+
"CURL_HOME"
5454
]
5555

5656
static let blockedOverridePrefixes: [String] = [
5757
"GIT_CONFIG_",
58-
"NPM_CONFIG_",
58+
"NPM_CONFIG_"
5959
]
6060

6161
static let blockedPrefixes: [String] = [
6262
"DYLD_",
6363
"LD_",
64-
"BASH_FUNC_",
64+
"BASH_FUNC_"
6565
]
6666
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@
227227
"build:plugin-sdk:dts": "tsc -p tsconfig.plugin-sdk.dts.json",
228228
"build:strict-smoke": "pnpm canvas:a2ui:bundle && node scripts/tsdown-build.mjs && node scripts/copy-plugin-sdk-root-alias.mjs && pnpm build:plugin-sdk:dts",
229229
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
230-
"check": "pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope && pnpm check:host-env-policy:swift",
230+
"check": "pnpm check:host-env-policy:swift && pnpm format:check && pnpm tsgo && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
231231
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-links",
232232
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
233233
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",

scripts/pr

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,30 @@ checkout_prep_branch() {
229229
git checkout "$prep_branch"
230230
}
231231

232+
verify_prep_branch_matches_prepared_head() {
233+
local pr="$1"
234+
local prepared_head_sha="$2"
235+
236+
require_artifact .local/prep-context.env
237+
checkout_prep_branch "$pr"
238+
239+
local prep_branch_head_sha
240+
prep_branch_head_sha=$(git rev-parse HEAD)
241+
if [ "$prep_branch_head_sha" = "$prepared_head_sha" ]; then
242+
return 0
243+
fi
244+
245+
echo "Local prep branch moved after prepare-push (expected $prepared_head_sha, got $prep_branch_head_sha)."
246+
if git merge-base --is-ancestor "$prepared_head_sha" "$prep_branch_head_sha" 2>/dev/null; then
247+
echo "Unpushed local commits on prep branch:"
248+
git log --oneline "${prepared_head_sha}..${prep_branch_head_sha}" | sed 's/^/ /' || true
249+
echo "Run scripts/pr prepare-sync-head $pr to push them before merge."
250+
else
251+
echo "Prep branch no longer contains the prepared head. Re-run prepare-init."
252+
fi
253+
exit 1
254+
}
255+
232256
resolve_head_push_url() {
233257
# shellcheck disable=SC1091
234258
source .local/pr-meta.env
@@ -1667,6 +1691,7 @@ merge_verify() {
16671691
require_artifact .local/prep.env
16681692
# shellcheck disable=SC1091
16691693
source .local/prep.env
1694+
verify_prep_branch_matches_prepared_head "$pr" "$PREP_HEAD_SHA"
16701695

16711696
local json
16721697
json=$(pr_meta_json "$pr")

scripts/release-check.ts

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./s
88

99
type PackFile = { path: string };
1010
type PackResult = { files?: PackFile[] };
11+
type PackageJson = {
12+
name?: string;
13+
version?: string;
14+
dependencies?: Record<string, string>;
15+
optionalDependencies?: Record<string, string>;
16+
openclaw?: {
17+
install?: {
18+
npmSpec?: string;
19+
};
20+
};
21+
};
1122

1223
const requiredPathGroups = [
1324
["dist/index.js", "dist/index.mjs"],
@@ -108,11 +119,6 @@ const appcastPath = resolve("appcast.xml");
108119
const laneBuildMin = 1_000_000_000;
109120
const laneFloorAdoptionDateKey = 20260227;
110121

111-
type PackageJson = {
112-
name?: string;
113-
version?: string;
114-
};
115-
116122
function normalizePluginSyncVersion(version: string): string {
117123
const normalized = version.trim().replace(/^v/, "");
118124
const base = /^([0-9]+\.[0-9]+\.[0-9]+)/.exec(normalized)?.[1];
@@ -122,6 +128,92 @@ function normalizePluginSyncVersion(version: string): string {
122128
return normalized.replace(/[-+].*$/, "");
123129
}
124130

131+
const ALLOWLISTED_BUNDLED_EXTENSION_ROOT_DEP_GAPS: Record<string, string[]> = {
132+
googlechat: ["google-auth-library"],
133+
matrix: ["@matrix-org/matrix-sdk-crypto-nodejs", "@vector-im/matrix-bot-sdk", "music-metadata"],
134+
msteams: ["@microsoft/agents-hosting"],
135+
nostr: ["nostr-tools"],
136+
tlon: ["@tloncorp/api", "@tloncorp/tlon-skill", "@urbit/aura"],
137+
zalouser: ["zca-js"],
138+
};
139+
140+
export function collectBundledExtensionRootDependencyGapErrors(params: {
141+
rootPackage: PackageJson;
142+
extensions: Array<{ id: string; packageJson: PackageJson }>;
143+
}): string[] {
144+
const rootDeps = {
145+
...params.rootPackage.dependencies,
146+
...params.rootPackage.optionalDependencies,
147+
};
148+
const errors: string[] = [];
149+
150+
for (const extension of params.extensions) {
151+
if (!extension.packageJson.openclaw?.install?.npmSpec) {
152+
continue;
153+
}
154+
155+
const missing = Object.keys(extension.packageJson.dependencies ?? {})
156+
.filter((dep) => dep !== "openclaw" && !rootDeps[dep])
157+
.toSorted();
158+
const allowlisted = [
159+
...(ALLOWLISTED_BUNDLED_EXTENSION_ROOT_DEP_GAPS[extension.id] ?? []),
160+
].toSorted();
161+
if (missing.join("\n") !== allowlisted.join("\n")) {
162+
const unexpected = missing.filter((dep) => !allowlisted.includes(dep));
163+
const resolved = allowlisted.filter((dep) => !missing.includes(dep));
164+
const parts = [
165+
`bundled extension '${extension.id}' root dependency mirror drift`,
166+
`missing in root package: ${missing.length > 0 ? missing.join(", ") : "(none)"}`,
167+
];
168+
if (unexpected.length > 0) {
169+
parts.push(`new gaps: ${unexpected.join(", ")}`);
170+
}
171+
if (resolved.length > 0) {
172+
parts.push(`remove stale allowlist entries: ${resolved.join(", ")}`);
173+
}
174+
errors.push(parts.join(" | "));
175+
}
176+
}
177+
178+
return errors;
179+
}
180+
181+
function collectBundledExtensions(): Array<{ id: string; packageJson: PackageJson }> {
182+
const extensionsDir = resolve("extensions");
183+
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
184+
entry.isDirectory(),
185+
);
186+
187+
return entries.flatMap((entry) => {
188+
const packagePath = join(extensionsDir, entry.name, "package.json");
189+
try {
190+
return [
191+
{
192+
id: entry.name,
193+
packageJson: JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson,
194+
},
195+
];
196+
} catch {
197+
return [];
198+
}
199+
});
200+
}
201+
202+
function checkBundledExtensionRootDependencyMirrors() {
203+
const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as PackageJson;
204+
const errors = collectBundledExtensionRootDependencyGapErrors({
205+
rootPackage,
206+
extensions: collectBundledExtensions(),
207+
});
208+
if (errors.length > 0) {
209+
console.error("release-check: bundled extension root dependency mirror validation failed:");
210+
for (const error of errors) {
211+
console.error(` - ${error}`);
212+
}
213+
process.exit(1);
214+
}
215+
}
216+
125217
function runPackDry(): PackResult[] {
126218
const raw = execSync("npm pack --dry-run --json --ignore-scripts", {
127219
encoding: "utf8",
@@ -321,6 +413,7 @@ function main() {
321413
checkPluginVersions();
322414
checkAppcastSparkleVersions();
323415
checkPluginSdkExports();
416+
checkBundledExtensionRootDependencyMirrors();
324417

325418
const results = runPackDry();
326419
const files = results.flatMap((entry) => entry.files ?? []);

src/agents/models-config.fills-missing-provider-apikey-from-env-var.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,21 @@ describe("models-config", () => {
246246
});
247247
});
248248

249+
it("replaces stale merged baseUrl when the provider api changes", async () => {
250+
await withTempHome(async () => {
251+
const parsed = await runCustomProviderMergeTest({
252+
seedProvider: {
253+
baseUrl: "https://agent.example/v1",
254+
apiKey: "AGENT_KEY", // pragma: allowlist secret
255+
api: "openai-completions",
256+
models: [{ id: "agent-model", name: "Agent model", input: ["text"] }],
257+
},
258+
});
259+
expect(parsed.providers.custom?.apiKey).toBe("AGENT_KEY");
260+
expect(parsed.providers.custom?.baseUrl).toBe("https://config.example/v1");
261+
});
262+
});
263+
249264
it("replaces stale merged apiKey when provider is SecretRef-managed in current config", async () => {
250265
await withTempHome(async () => {
251266
await writeAgentModelsJson({

0 commit comments

Comments
 (0)