feat: dogfood reusable ClawHub package publish

This commit is contained in:
Patrick Erichsen
2026-06-08 20:19:21 -07:00
committed by GitHub
parent 9210d8f7d9
commit e8cf6df3a3
5 changed files with 189 additions and 83 deletions
+66 -73
View File
@@ -24,6 +24,11 @@ on:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
dry_run:
description: Validate the full ClawHub artifact handoff without publishing.
required: false
default: false
type: boolean
concurrency:
group: plugin-clawhub-release-${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
@@ -35,7 +40,7 @@ env:
CLAWHUB_REGISTRY: "https://clawhub.ai"
CLAWHUB_REPOSITORY: "openclaw/clawhub"
# Pinned to a reviewed ClawHub commit so release behavior stays reproducible.
CLAWHUB_REF: "facf20ceb6cc459e2872d941e71335a784bbc55c"
CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"
jobs:
preview_plugins_clawhub:
@@ -326,15 +331,12 @@ jobs:
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --dry-run "${PACKAGE_DIR}"
publish_plugins_clawhub:
pack_plugins_clawhub_artifacts:
needs: [preview_plugins_clawhub, preview_plugin_pack, validate_release_publish_approval]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions:
actions: read
contents: read
id-token: write
strategy:
fail-fast: false
max-parallel: 32
@@ -407,73 +409,7 @@ jobs:
chmod +x "$RUNNER_TEMP/clawhub"
echo "$RUNNER_TEMP" >> "$GITHUB_PATH"
- name: Write ClawHub token config
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
if [[ -z "${CLAWHUB_TOKEN}" ]]; then
echo "No CLAWHUB_TOKEN secret configured; publish will rely on GitHub OIDC trusted publishing."
exit 0
fi
node --input-type=module <<'EOF'
import { writeFileSync } from "node:fs";
import { join } from "node:path";
const path = join(process.env.RUNNER_TEMP, "clawhub-config.json");
writeFileSync(
path,
`${JSON.stringify(
{
registry: process.env.CLAWHUB_REGISTRY,
token: process.env.CLAWHUB_TOKEN,
},
null,
2,
)}\n`,
);
console.log(path);
EOF
echo "CLAWHUB_CONFIG_PATH=${RUNNER_TEMP}/clawhub-config.json" >> "$GITHUB_ENV"
- name: Check ClawHub package version
id: clawhub_package_version
env:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
run: |
set -euo pipefail
encoded_name="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_NAME ?? ""))')"
encoded_version="$(node -e 'console.log(encodeURIComponent(process.env.PACKAGE_VERSION ?? ""))')"
url="${CLAWHUB_REGISTRY%/}/api/v1/packages/${encoded_name}/versions/${encoded_version}"
status=""
for attempt in $(seq 1 8); do
status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' "${url}")"
if [[ "${status}" == "404" || "${status}" =~ ^2 ]]; then
break
fi
if [[ "${status}" == "429" || "${status}" =~ ^5 ]]; then
echo "ClawHub availability check returned ${status} for ${PACKAGE_NAME}@${PACKAGE_VERSION}; retrying (${attempt}/8)."
sleep 60
continue
fi
break
done
if [[ "${status}" =~ ^2 ]]; then
echo "${PACKAGE_NAME}@${PACKAGE_VERSION} is already published on ClawHub."
echo "already_published=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${status}" != "404" ]]; then
echo "Unexpected ClawHub response (${status}) for ${PACKAGE_NAME}@${PACKAGE_VERSION}."
exit 1
fi
echo "already_published=false" >> "$GITHUB_OUTPUT"
- name: Publish
if: steps.clawhub_package_version.outputs.already_published != 'true'
- name: Pack ClawHub package artifact
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
SOURCE_REPO: ${{ github.repository }}
@@ -481,8 +417,65 @@ jobs:
SOURCE_REF: ${{ github.ref }}
PACKAGE_TAG: ${{ matrix.plugin.publishTag }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
run: bash scripts/plugin-clawhub-publish.sh --publish "${PACKAGE_DIR}"
OPENCLAW_CLAWHUB_PACK_OUTPUT_DIR: ${{ runner.temp }}/clawhub-package-artifact
run: bash scripts/plugin-clawhub-publish.sh --pack "${PACKAGE_DIR}"
- name: Upload ClawHub package artifact
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.plugin.artifactName }}
path: ${{ runner.temp }}/clawhub-package-artifact/*.tgz
if-no-files-found: error
retention-days: 7
approve_plugin_clawhub_release:
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
environment: clawhub-plugin-release
permissions: {}
steps:
- name: Approve ClawHub package publish
run: echo "ClawHub package publish approved."
publish_plugins_clawhub:
needs: [preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugin_clawhub_release]
if: github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
permissions:
actions: read
contents: read
id-token: write
strategy:
fail-fast: false
max-parallel: 32
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854
with:
dry_run: ${{ inputs.dry_run }}
json: true
package_artifact_name: ${{ matrix.plugin.artifactName }}
registry: https://clawhub.ai
site: https://clawhub.ai
source_repo: ${{ github.repository }}
source_commit: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}
source_ref: ${{ github.ref }}
tags: ${{ matrix.plugin.publishTag }}
secrets:
clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}
verify_published_clawhub_package:
needs: [preview_plugins_clawhub, publish_plugins_clawhub]
if: github.event_name == 'workflow_dispatch' && inputs.dry_run != true && needs.preview_plugins_clawhub.outputs.has_candidates == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
max-parallel: 32
matrix:
plugin: ${{ fromJson(needs.preview_plugins_clawhub.outputs.matrix) }}
steps:
- name: Verify published ClawHub package
env:
CLAWHUB_REGISTRY: ${{ env.CLAWHUB_REGISTRY }}
+12
View File
@@ -55,6 +55,7 @@ export type PublishablePluginPackage = {
type PluginReleasePlanItem = PublishablePluginPackage & {
alreadyPublished: boolean;
artifactName: string;
};
type PluginReleasePlan = {
@@ -103,6 +104,16 @@ function getRegistryBaseUrl(explicit?: string) {
);
}
function formatClawHubPackageArtifactName(
plugin: Pick<PublishablePluginPackage, "packageName" | "version">,
) {
const safeName = plugin.packageName
.replace(/^@/u, "")
.replace(/[^A-Za-z0-9_.-]+/gu, "-")
.replace(/^-+|-+$/gu, "");
return `clawhub-package-${safeName}-${plugin.version}`;
}
async function readClawHubPackageOwnerDetail(
response: Response,
packageName: string,
@@ -467,6 +478,7 @@ export async function collectPluginClawHubReleasePlan(params?: {
plugin.version,
{ registryBaseUrl: params?.registryBaseUrl, fetchImpl: params?.fetchImpl },
),
artifactName: formatClawHubPackageArtifactName(plugin),
}),
),
);
+17 -4
View File
@@ -8,8 +8,8 @@ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/.." && pwd)"
invocation_root="$(pwd)"
if [[ "${mode}" != "--dry-run" && "${mode}" != "--publish" ]]; then
echo "usage: bash scripts/plugin-clawhub-publish.sh [--dry-run|--publish] <package-dir>" >&2
if [[ "${mode}" != "--dry-run" && "${mode}" != "--publish" && "${mode}" != "--pack" ]]; then
echo "usage: bash scripts/plugin-clawhub-publish.sh [--dry-run|--publish|--pack] <package-dir>" >&2
exit 2
fi
@@ -119,6 +119,21 @@ if [[ ! -f "${pack_path}" ]]; then
exit 1
fi
echo "Resolved ClawPack: ${pack_path}"
if [[ "${mode}" == "--pack" ]]; then
output_dir="${OPENCLAW_CLAWHUB_PACK_OUTPUT_DIR:-}"
if [[ -z "${output_dir}" ]]; then
echo "OPENCLAW_CLAWHUB_PACK_OUTPUT_DIR is required for --pack" >&2
exit 2
fi
mkdir -p "${output_dir}"
output_path="${output_dir}/$(basename "${pack_path}")"
cp "${pack_path}" "${output_path}"
echo "Packed ClawPack: ${output_path}"
exit 0
fi
publish_cmd=(
clawhub
--workdir
@@ -143,8 +158,6 @@ if [[ -n "${source_ref}" ]]; then
)
fi
echo "Resolved ClawPack: ${pack_path}"
printf 'Publish command: CLAWHUB_WORKDIR=%q' "${clawhub_workdir}"
printf ' %q' "${publish_cmd[@]}"
printf '\n'
+76 -1
View File
@@ -1,6 +1,13 @@
// Plugin ClawHub release tests validate plugin release metadata and artifacts.
import { execFileSync } from "node:child_process";
import { chmodSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
realpathSync,
writeFileSync,
} from "node:fs";
import { delimiter, join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import {
@@ -328,6 +335,7 @@ describe("collectPluginClawHubReleasePlan", () => {
expect(plan.skippedPublished).toHaveLength(1);
expect(plan.skippedPublished[0]).toEqual({
alreadyPublished: true,
artifactName: "clawhub-package-openclaw-demo-plugin-2026.4.1",
channel: "stable",
extensionId: "demo-plugin",
packageDir: "extensions/demo-plugin",
@@ -367,6 +375,9 @@ describe("collectPluginClawHubReleasePlan", () => {
});
expect(plan.candidates.map((plugin) => plugin.packageName)).toEqual(["@openclaw/demo-plugin"]);
expect(plan.candidates.map((plugin) => plugin.artifactName)).toEqual([
"clawhub-package-openclaw-demo-plugin-2026.4.1",
]);
});
});
@@ -510,6 +521,70 @@ exit 0
expect(invocations).toContain(".tgz --tags latest");
expect(invocations).toContain("--dry-run");
});
it("packs a reusable workflow artifact without publishing", () => {
const repoDir = createTempPluginRepo();
const binDir = join(repoDir, "bin");
const markerPath = join(repoDir, "clawhub-invoked");
const outputDir = join(repoDir, "clawhub-artifacts");
mkdirSync(binDir, { recursive: true });
const clawhubPath = join(binDir, "clawhub");
writeFileSync(
clawhubPath,
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> ${JSON.stringify(markerPath)}
if [[ "\${1:-}" == "--workdir" ]]; then
shift 2
fi
if [[ "\${1:-}" == "package" && "\${2:-}" == "pack" ]]; then
pack_destination=""
while [[ "$#" -gt 0 ]]; do
case "$1" in
--pack-destination)
pack_destination="\${2:-}"
shift 2
;;
*)
shift
;;
esac
done
mkdir -p "$pack_destination"
pack_path="$pack_destination/openclaw-demo-plugin-2026.4.1.tgz"
printf 'fake tgz\\n' > "$pack_path"
printf '{"path":"%s","name":"@openclaw/demo-plugin","version":"2026.4.1"}\\n' "$pack_path"
fi
exit 0
`,
);
chmodSync(clawhubPath, 0o755);
const output = execFileSync(
"bash",
[
join(process.cwd(), "scripts/plugin-clawhub-publish.sh"),
"--pack",
"extensions/demo-plugin",
],
{
cwd: repoDir,
encoding: "utf8",
env: {
...process.env,
OPENCLAW_CLAWHUB_PACK_OUTPUT_DIR: outputDir,
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0",
PATH: `${binDir}${delimiter}${process.env.PATH ?? ""}`,
},
},
);
expect(output).toContain("Packed ClawPack:");
expect(existsSync(join(outputDir, "openclaw-demo-plugin-2026.4.1.tgz"))).toBe(true);
const invocations = readFileSync(markerPath, "utf8");
expect(invocations).toContain("package pack ");
expect(invocations).not.toContain("package publish ");
});
});
describe("collectPluginClawHubReleasePathsFromGitRange", () => {
@@ -1564,7 +1564,25 @@ describe("package artifact reuse", () => {
expect(packageJson.scripts?.["release:fast-pretag-check"]).toBe(
"bash scripts/release-fast-pretag-check.sh",
);
expect(clawHubWorkflow).toContain('CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"');
expect(clawHubWorkflow).toContain("pack_plugins_clawhub_artifacts:");
expect(clawHubWorkflow).toContain("Pack ClawHub package artifact");
expect(clawHubWorkflow).toContain("Upload ClawHub package artifact");
expect(clawHubWorkflow).toContain("dry_run:");
expect(clawHubWorkflow).toContain("default: false");
expect(clawHubWorkflow).toContain("approve_plugin_clawhub_release:");
expect(clawHubWorkflow).toContain("Approve ClawHub package publish");
expect(clawHubWorkflow).toContain(
"uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854",
);
expect(clawHubWorkflow).toContain("dry_run: ${{ inputs.dry_run }}");
expect(clawHubWorkflow).toContain("package_artifact_name: ${{ matrix.plugin.artifactName }}");
expect(clawHubWorkflow).toContain("clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}");
expect(clawHubWorkflow).toContain("verify_published_clawhub_package:");
expect(clawHubWorkflow).toContain("inputs.dry_run != true");
expect(clawHubWorkflow).toContain("Verify published ClawHub package");
expect(clawHubWorkflow).not.toContain("bash scripts/plugin-clawhub-publish.sh --publish");
expect(clawHubWorkflow).not.toContain("Write ClawHub token config");
expect(clawHubWorkflow).toContain("bun install failed while preparing ClawHub CLI; retrying");
expect(clawHubWorkflow).toContain("max-parallel: 32");
expect(clawHubResolveRefIndex).toBeGreaterThanOrEqual(0);
@@ -1601,11 +1619,6 @@ describe("package artifact reuse", () => {
expect(pluginNpmWorkflow).toContain(
"steps.npm_package_version.outputs.already_published != 'true'",
);
expect(clawHubWorkflow).toContain("Check ClawHub package version");
expect(clawHubWorkflow).toContain("already_published=true");
expect(clawHubWorkflow).toContain(
"steps.clawhub_package_version.outputs.already_published != 'true'",
);
expect(pluginNpmWorkflow).toContain("Direct Plugin NPM Release dispatch");
expect(clawHubWorkflow).toContain("Direct Plugin ClawHub Release dispatch");
expect(openclawNpmWorkflow).toContain("Direct OpenClaw npm publish");