ci: speed up release preparation

This commit is contained in:
Peter Steinberger
2026-07-17 13:43:12 +01:00
parent 8bd78de5da
commit cb70d7104e
10 changed files with 117 additions and 18 deletions
@@ -465,6 +465,7 @@ jobs:
ref: ${{ inputs.ref }}
path: source
fetch-depth: 0
filter: blob:none
persist-credentials: false
submodules: recursive
@@ -105,6 +105,7 @@ jobs:
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
filter: blob:none
- name: Validate Tideclaw alpha preflight target
if: startsWith(github.ref, 'refs/heads/tideclaw/alpha/')
@@ -586,6 +587,7 @@ jobs:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
filter: blob:none
persist-credentials: false
- name: Validate npm release request
@@ -203,6 +203,7 @@ jobs:
ref: ${{ inputs.ref }}
path: source
fetch-depth: 0
filter: blob:none
- name: Resolve checked-out fallback SHA
if: steps.fast_ref.outputs.fallback == 'true'
@@ -565,6 +566,7 @@ jobs:
persist-credentials: false
ref: ${{ github.sha }}
fetch-depth: 0
filter: blob:none
- name: Set artifact metadata
id: artifact
@@ -401,6 +401,7 @@ jobs:
with:
ref: refs/tags/${{ inputs.tag }}
fetch-depth: 0
filter: blob:none
persist-credentials: false
- name: Resolve checked-out release ref
+1
View File
@@ -447,6 +447,7 @@ jobs:
with:
ref: ${{ inputs.workflow_ref }}
fetch-depth: 0
filter: blob:none
persist-credentials: false
- name: Setup Node environment
@@ -68,6 +68,7 @@ jobs:
persist-credentials: false
ref: ${{ github.ref }}
fetch-depth: 0
filter: blob:none
- name: Resolve checked-out ref
id: ref
@@ -321,6 +322,7 @@ jobs:
persist-credentials: false
ref: ${{ github.ref }}
fetch-depth: 0
filter: blob:none
- name: Checkout target revision
env:
+1
View File
@@ -82,6 +82,7 @@ jobs:
persist-credentials: false
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
fetch-depth: 0
filter: blob:none
- name: Resolve checked-out ref
id: ref
+21 -18
View File
@@ -4,6 +4,7 @@ import { resolve } from "node:path";
import { truncateUtf16Safe } from "../../packages/normalization-core/src/utf16-slice.js";
import { validateExternalCodePluginPackageJson } from "../../packages/plugin-package-contract/src/index.ts";
import { retryClawHubRead } from "../../src/infra/clawhub-retry.js";
import { runTasksWithConcurrency } from "../../src/utils/run-with-concurrency.js";
import { readBoundedResponseText } from "./bounded-response.ts";
import {
assertPluginReleaseDependencyFreshness,
@@ -102,6 +103,9 @@ const CLAWHUB_REQUEST_TIMEOUT_MS = 30_000;
const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 64 * 1024;
const CLAWHUB_ERROR_BODY_MAX_BYTES = 8 * 1024;
const CLAWHUB_ERROR_BODY_MAX_CHARS = 400;
// All-publishable releases query dozens of packages. Bound registry pressure while
// allowing independent package state reads to leave the core publish critical path quickly.
const CLAWHUB_RELEASE_PLAN_CONCURRENCY = 8;
const OPENCLAW_PLUGIN_CLAWHUB_REPOSITORY = "openclaw/openclaw";
const OPENCLAW_PLUGIN_CLAWHUB_WORKFLOW_FILENAME = "plugin-clawhub-release.yml";
const SAFE_EXTENSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
@@ -652,32 +656,22 @@ export async function collectPluginClawHubReleasePlan(params?: {
params?.resolveLatestVersion,
);
const planned: PluginReleasePlanItemWithPackageState[] = [];
for (const plugin of selectedPublishable) {
const packageExists = await doesClawHubPackageExist(plugin.packageName, {
const planTasks = selectedPublishable.map((plugin) => async () => {
const queryOptions = {
registryBaseUrl: params?.registryBaseUrl,
fetchImpl: params?.fetchImpl,
requestTimeoutMs: params?.requestTimeoutMs,
sleep: params?.sleep,
});
};
const packageExists = await doesClawHubPackageExist(plugin.packageName, queryOptions);
const hasTrustedPublisher = packageExists
? await hasClawHubTrustedPublisher(plugin.packageName, {
registryBaseUrl: params?.registryBaseUrl,
fetchImpl: params?.fetchImpl,
requestTimeoutMs: params?.requestTimeoutMs,
sleep: params?.sleep,
})
? await hasClawHubTrustedPublisher(plugin.packageName, queryOptions)
: false;
const alreadyPublished = packageExists
? await isPluginVersionPublishedOnClawHub(plugin.packageName, plugin.version, {
registryBaseUrl: params?.registryBaseUrl,
fetchImpl: params?.fetchImpl,
requestTimeoutMs: params?.requestTimeoutMs,
sleep: params?.sleep,
})
? await isPluginVersionPublishedOnClawHub(plugin.packageName, plugin.version, queryOptions)
: false;
planned.push({
return {
extensionId: plugin.extensionId,
packageDir: plugin.packageDir,
packageName: plugin.packageName,
@@ -688,8 +682,17 @@ export async function collectPluginClawHubReleasePlan(params?: {
hasTrustedPublisher,
alreadyPublished,
artifactName: formatClawHubPackageArtifactName(plugin),
});
} satisfies PluginReleasePlanItemWithPackageState;
});
const planResult = await runTasksWithConcurrency({
tasks: planTasks,
limit: CLAWHUB_RELEASE_PLAN_CONCURRENCY,
errorMode: "stop",
});
if (planResult.hasError) {
throw planResult.firstError;
}
const planned = planResult.results;
const all = planned.map(stripPackageReleaseState);
return {
+53
View File
@@ -431,6 +431,59 @@ describe("resolveSelectedClawHubPublishablePluginPackages", () => {
});
describe("collectPluginClawHubReleasePlan", () => {
it("bounds parallel ClawHub package-state reads and preserves plan order", async () => {
const extraExtensionIds = Array.from({ length: 11 }, (_, index) => `demo-${index + 2}`);
const repoDir = createTempPluginRepo({ extraExtensionIds });
const packageNames = ["demo-plugin", ...extraExtensionIds].map(
(extensionId) => `@openclaw/${extensionId}`,
);
const baseFetch = createClawHubPlanFetch({
packages: Object.fromEntries(
packageNames.map((packageName) => [packageName, { status: 200 }]),
),
trustedPublishers: Object.fromEntries(
packageNames.map((packageName) => [
packageName,
{
status: 200,
body: {
trustedPublisher: {
repository: "openclaw/openclaw",
workflowFilename: "plugin-clawhub-release.yml",
},
},
},
]),
),
versions: Object.fromEntries(
packageNames.map((packageName) => [`${packageName}@2026.4.1`, 404]),
),
}).fetchImpl;
let activeRequests = 0;
let maxActiveRequests = 0;
const fetchImpl: typeof fetch = async (...args) => {
activeRequests += 1;
maxActiveRequests = Math.max(maxActiveRequests, activeRequests);
try {
await new Promise((resolve) => setTimeout(resolve, 5));
return await baseFetch(...args);
} finally {
activeRequests -= 1;
}
};
const plan = await collectPluginClawHubReleasePlan({
rootDir: repoDir,
selectionMode: "all-publishable",
fetchImpl,
registryBaseUrl: "https://clawhub.ai",
});
expect(maxActiveRequests).toBe(8);
expect(plan.all.map((plugin) => plugin.packageName)).toEqual(packageNames.toSorted());
expect(plan.candidates.map((plugin) => plugin.packageName)).toEqual(packageNames.toSorted());
});
it("rejects stale required dependencies before querying ClawHub", async () => {
const repoDir = createTempPluginRepo({
requiredLatestDependencyVersion: "1.2.3",
@@ -4603,6 +4603,39 @@ wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
expect(readFileSync(LIVE_E2E_WORKFLOW, "utf8")).toContain("live-cache attempt ${attempt}/2");
});
it("keeps release history checks blobless", () => {
const fullHistoryCheckouts: Array<[string, string, string]> = [
[RELEASE_PUBLISH_WORKFLOW, "resolve_release_target", "Checkout release tag"],
[
RELEASE_CHECKS_WORKFLOW,
"resolve_target",
"Checkout selected ref for reachability fallback",
],
[RELEASE_CHECKS_WORKFLOW, "prepare_release_package", "Checkout trusted workflow ref"],
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", "Checkout package workflow ref"],
[PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm", "Checkout"],
[PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub", "Checkout"],
[PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "pack_plugins_clawhub_artifacts", "Checkout"],
[OPENCLAW_NPM_RELEASE_WORKFLOW, "preflight_openclaw_npm", "Checkout"],
[OPENCLAW_NPM_RELEASE_WORKFLOW, "validate_publish_request", "Checkout"],
[
".github/workflows/openclaw-cross-os-release-checks-reusable.yml",
"prepare",
"Checkout public source ref",
],
];
for (const [workflowPath, jobName, stepName] of fullHistoryCheckouts) {
expect(
workflowStep(workflowJob(workflowPath, jobName), stepName).with,
workflowPath,
).toMatchObject({
"fetch-depth": 0,
filter: "blob:none",
});
}
});
it("validates the macOS release handoff before the GitHub release page exists", () => {
const macosRelease = readWorkflow(".github/workflows/macos-release.yml");
const validateJob = workflowJob(