Skip to content

Commit b37fba7

Browse files
committed
ci(release): harden clawhub plugin publish
1 parent 5b528f4 commit b37fba7

6 files changed

Lines changed: 252 additions & 18 deletions

.github/workflows/plugin-clawhub-release.yml

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ env:
3232
CLAWHUB_REGISTRY: "https://clawhub.ai"
3333
CLAWHUB_REPOSITORY: "openclaw/clawhub"
3434
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
35-
CLAWHUB_REF: "199e6a0cdf32471702e0503e9899e8d24f06a527"
35+
CLAWHUB_REF: "facf20ceb6cc459e2872d941e71335a784bbc55c"
3636

3737
jobs:
3838
preview_plugins_clawhub:
@@ -50,7 +50,7 @@ jobs:
5050
uses: actions/checkout@v6
5151
with:
5252
persist-credentials: false
53-
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
53+
ref: ${{ github.ref }}
5454
fetch-depth: 0
5555

5656
- name: Setup Node environment
@@ -62,14 +62,29 @@ jobs:
6262

6363
- name: Resolve checked-out ref
6464
id: ref
65-
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
66-
67-
- name: Validate ref is on main or a release branch
65+
env:
66+
TARGET_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
6867
run: |
6968
set -euo pipefail
7069
git fetch --no-tags origin \
7170
+refs/heads/main:refs/remotes/origin/main \
7271
'+refs/heads/release/*:refs/remotes/origin/release/*'
72+
if [[ -n "${TARGET_REF}" ]]; then
73+
if git rev-parse --verify --quiet "${TARGET_REF}^{commit}" >/dev/null; then
74+
target_sha="$(git rev-parse "${TARGET_REF}^{commit}")"
75+
elif git rev-parse --verify --quiet "origin/${TARGET_REF}^{commit}" >/dev/null; then
76+
target_sha="$(git rev-parse "origin/${TARGET_REF}^{commit}")"
77+
else
78+
echo "Unable to resolve requested publish ref: ${TARGET_REF}" >&2
79+
exit 1
80+
fi
81+
git checkout --detach "${target_sha}"
82+
fi
83+
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
84+
85+
- name: Validate ref is on main or a release branch
86+
run: |
87+
set -euo pipefail
7388
if git merge-base --is-ancestor HEAD origin/main; then
7489
exit 0
7590
fi
@@ -153,6 +168,12 @@ jobs:
153168
echo "::error::One or more selected plugin versions already exist on ClawHub. Bump the version before running a real publish."
154169
exit 1
155170
171+
- name: Verify OpenClaw ClawHub package ownership
172+
if: steps.plan.outputs.has_candidates == 'true'
173+
env:
174+
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
175+
run: node --import tsx scripts/plugin-clawhub-owner-preflight.ts .local/plugin-clawhub-release-plan.json
176+
156177
preview_plugin_pack:
157178
needs: preview_plugins_clawhub
158179
if: needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
@@ -161,16 +182,26 @@ jobs:
161182
contents: read
162183
strategy:
163184
fail-fast: false
164-
max-parallel: 1
185+
max-parallel: 6
165186
matrix:
166187
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
167188
steps:
168189
- name: Checkout
169190
uses: actions/checkout@v6
170191
with:
171192
persist-credentials: false
172-
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
173-
fetch-depth: 1
193+
ref: ${{ github.ref }}
194+
fetch-depth: 0
195+
196+
- name: Checkout target revision
197+
env:
198+
TARGET_SHA: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
199+
run: |
200+
set -euo pipefail
201+
git fetch --no-tags origin \
202+
+refs/heads/main:refs/remotes/origin/main \
203+
'+refs/heads/release/*:refs/remotes/origin/release/*'
204+
git checkout --detach "${TARGET_SHA}"
174205
175206
- name: Setup Node environment
176207
uses: ./.github/actions/setup-node-env
@@ -185,9 +216,15 @@ jobs:
185216
with:
186217
persist-credentials: false
187218
repository: ${{ env.CLAWHUB_REPOSITORY }}
188-
ref: ${{ env.CLAWHUB_REF }}
219+
ref: main
189220
path: clawhub-source
190-
fetch-depth: 1
221+
fetch-depth: 0
222+
223+
- name: Checkout pinned ClawHub CLI revision
224+
working-directory: clawhub-source
225+
env:
226+
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
227+
run: git checkout --detach "${CLAWHUB_REF}"
191228

192229
- name: Install ClawHub CLI dependencies
193230
working-directory: clawhub-source
@@ -203,6 +240,9 @@ jobs:
203240
chmod +x "$RUNNER_TEMP/clawhub"
204241
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
205242
243+
- name: Verify package-local runtime build
244+
run: pnpm release:plugins:npm:runtime:check --package "${{ matrix.plugin.packageDir }}"
245+
206246
- name: Preview publish command
207247
env:
208248
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
@@ -223,15 +263,26 @@ jobs:
223263
id-token: write
224264
strategy:
225265
fail-fast: false
266+
max-parallel: 6
226267
matrix:
227268
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
228269
steps:
229270
- name: Checkout
230271
uses: actions/checkout@v6
231272
with:
232273
persist-credentials: false
233-
ref: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
234-
fetch-depth: 1
274+
ref: ${{ github.ref }}
275+
fetch-depth: 0
276+
277+
- name: Checkout target revision
278+
env:
279+
TARGET_SHA: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
280+
run: |
281+
set -euo pipefail
282+
git fetch --no-tags origin \
283+
+refs/heads/main:refs/remotes/origin/main \
284+
'+refs/heads/release/*:refs/remotes/origin/release/*'
285+
git checkout --detach "${TARGET_SHA}"
235286
236287
- name: Setup Node environment
237288
uses: ./.github/actions/setup-node-env
@@ -246,9 +297,15 @@ jobs:
246297
with:
247298
persist-credentials: false
248299
repository: ${{ env.CLAWHUB_REPOSITORY }}
249-
ref: ${{ env.CLAWHUB_REF }}
300+
ref: main
250301
path: clawhub-source
251-
fetch-depth: 1
302+
fetch-depth: 0
303+
304+
- name: Checkout pinned ClawHub CLI revision
305+
working-directory: clawhub-source
306+
env:
307+
CLAWHUB_REF: ${{ env.CLAWHUB_REF }}
308+
run: git checkout --detach "${CLAWHUB_REF}"
252309

253310
- name: Install ClawHub CLI dependencies
254311
working-directory: clawhub-source
@@ -304,7 +361,19 @@ jobs:
304361
encoded_name="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_NAME ?? ""))')"
305362
encoded_version="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_VERSION ?? ""))')"
306363
url="${CLAWHUB_REGISTRY%/}/api/v1/packages/${encoded_name}/versions/${encoded_version}"
307-
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
364+
status=""
365+
for attempt in $(seq 1 8); do
366+
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
367+
if [[ "${status}" == "404" || "${status}" =~ ^2 ]]; then
368+
break
369+
fi
370+
if [[ "${status}" == "429" || "${status}" =~ ^5 ]]; then
371+
echo "ClawHub availability check returned ${status} for ${PACKAGE_NAME}@${PACKAGE_VERSION}; retrying (${attempt}/8)."
372+
sleep 60
373+
continue
374+
fi
375+
break
376+
done
308377
if [[ "${status}" =~ ^2 ]]; then
309378
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
310379
exit 1

scripts/lib/plugin-clawhub-release.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ type PluginReleasePlan = {
6060
skippedPublished: PluginReleasePlanItem[];
6161
};
6262

63+
type ClawHubPackageOwnerDetail = {
64+
owner?: {
65+
handle?: unknown;
66+
} | null;
67+
};
68+
6369
type ClawHubPublishablePluginPackageFilters = {
6470
extensionIds?: readonly string[];
6571
packageNames?: readonly string[];
@@ -76,6 +82,7 @@ const CLAWHUB_SHARED_RELEASE_INPUT_PATHS = [
7682
"scripts/lib/npm-publish-plan.mjs",
7783
"scripts/lib/plugin-npm-release.ts",
7884
"scripts/lib/plugin-clawhub-release.ts",
85+
"scripts/plugin-clawhub-owner-preflight.ts",
7986
"scripts/openclaw-npm-release-check.ts",
8087
"scripts/plugin-clawhub-publish.sh",
8188
"scripts/plugin-clawhub-release-check.ts",
@@ -343,6 +350,59 @@ async function isPluginVersionPublishedOnClawHub(
343350
);
344351
}
345352

353+
export async function collectClawHubOpenClawOwnerErrors(params: {
354+
plugins: readonly Pick<PublishablePluginPackage, "packageName">[];
355+
requiredOwnerHandle?: string;
356+
registryBaseUrl?: string;
357+
fetchImpl?: typeof fetch;
358+
}): Promise<string[]> {
359+
const fetchImpl = params.fetchImpl ?? fetch;
360+
const requiredOwnerHandle = params.requiredOwnerHandle ?? "openclaw";
361+
const errors: string[] = [];
362+
363+
await Promise.all(
364+
params.plugins.map(async (plugin) => {
365+
if (!plugin.packageName.startsWith("@openclaw/")) {
366+
return;
367+
}
368+
369+
const url = new URL(
370+
`/api/v1/packages/${encodeURIComponent(plugin.packageName)}`,
371+
getRegistryBaseUrl(params.registryBaseUrl),
372+
);
373+
const response = await fetchImpl(url, {
374+
method: "GET",
375+
headers: {
376+
Accept: "application/json",
377+
},
378+
});
379+
380+
if (response.status === 404) {
381+
errors.push(
382+
`${plugin.packageName}: ClawHub package row must already exist under @${requiredOwnerHandle} before OpenClaw release publish.`,
383+
);
384+
return;
385+
}
386+
if (!response.ok) {
387+
errors.push(
388+
`${plugin.packageName}: failed to query ClawHub owner: ${response.status} ${response.statusText}`,
389+
);
390+
return;
391+
}
392+
393+
const detail = (await response.json()) as ClawHubPackageOwnerDetail;
394+
const ownerHandle = typeof detail.owner?.handle === "string" ? detail.owner.handle : null;
395+
if (ownerHandle !== requiredOwnerHandle) {
396+
errors.push(
397+
`${plugin.packageName}: ClawHub package owner must be @${requiredOwnerHandle}; got ${ownerHandle ? `@${ownerHandle}` : "<missing>"}.`,
398+
);
399+
}
400+
}),
401+
);
402+
403+
return errors.toSorted();
404+
}
405+
346406
export async function collectPluginClawHubReleasePlan(params?: {
347407
rootDir?: string;
348408
selection?: string[];
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env -S node --import tsx
2+
3+
import { readFileSync } from "node:fs";
4+
import { pathToFileURL } from "node:url";
5+
import { collectClawHubOpenClawOwnerErrors } from "./lib/plugin-clawhub-release.ts";
6+
7+
type ReleasePlanFile = {
8+
candidates?: Array<{
9+
packageName?: unknown;
10+
}>;
11+
};
12+
13+
export async function runClawHubOwnerPreflight(argv: string[]) {
14+
const planPath = argv[0];
15+
if (!planPath) {
16+
throw new Error("usage: plugin-clawhub-owner-preflight.ts <release-plan.json>");
17+
}
18+
19+
const parsed = JSON.parse(readFileSync(planPath, "utf8")) as ReleasePlanFile;
20+
const candidates = (parsed.candidates ?? [])
21+
.filter(
22+
(candidate): candidate is { packageName: string } =>
23+
typeof candidate.packageName === "string",
24+
)
25+
.map((candidate) => ({ packageName: candidate.packageName }));
26+
27+
const errors = await collectClawHubOpenClawOwnerErrors({ plugins: candidates });
28+
if (errors.length > 0) {
29+
throw new Error(
30+
`ClawHub OpenClaw package ownership preflight failed:\n${errors.map((error) => `- ${error}`).join("\n")}`,
31+
);
32+
}
33+
34+
console.log(`ClawHub OpenClaw owner preflight passed for ${candidates.length} candidate(s).`);
35+
}
36+
37+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
38+
try {
39+
await runClawHubOwnerPreflight(process.argv.slice(2));
40+
} catch (error) {
41+
console.error(error instanceof Error ? error.message : String(error));
42+
process.exit(1);
43+
}
44+
}

scripts/plugin-clawhub-publish.sh

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,17 @@ if [[ "${mode}" == "--dry-run" ]]; then
152152
exit 0
153153
fi
154154

155-
CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}"
155+
publish_log="${pack_dir}/publish.log"
156+
for attempt in $(seq 1 "${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8}"); do
157+
if CLAWHUB_WORKDIR="${clawhub_workdir}" "${publish_cmd[@]}" > >(tee "${publish_log}") 2>&1; then
158+
exit 0
159+
fi
160+
if ! grep -Eqi "rate limit|too many requests|\\b429\\b" "${publish_log}"; then
161+
exit 1
162+
fi
163+
echo "ClawHub publish hit a rate limit; retrying (${attempt}/${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8})." >&2
164+
sleep "${OPENCLAW_CLAWHUB_PUBLISH_RETRY_DELAY_SECONDS:-60}"
165+
done
166+
167+
echo "ClawHub publish failed after ${OPENCLAW_CLAWHUB_PUBLISH_ATTEMPTS:-8} attempts." >&2
168+
exit 1

scripts/verify-plugin-npm-published-runtime.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,18 @@ function sleep(ms) {
124124
}
125125

126126
async function packPublishedPackage(spec, destinationDir) {
127-
const attempts = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_ATTEMPTS ?? "6", 10);
128-
const delayMs = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_DELAY_MS ?? "5000", 10);
127+
const attempts = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_ATTEMPTS ?? "90", 10);
128+
const delayMs = Number.parseInt(process.env.OPENCLAW_PLUGIN_NPM_VERIFY_DELAY_MS ?? "10000", 10);
129129
let lastError;
130130
for (let attempt = 1; attempt <= attempts; attempt += 1) {
131131
try {
132132
return npmPack(spec, destinationDir);
133133
} catch (error) {
134134
lastError = error;
135135
if (attempt < attempts) {
136+
console.error(
137+
`npm pack ${spec} not visible yet (attempt ${attempt}/${attempts}); retrying in ${delayMs}ms...`,
138+
);
136139
await sleep(delayMs);
137140
}
138141
}

0 commit comments

Comments
 (0)