mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat: dogfood reusable ClawHub package publish
This commit is contained in:
@@ -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 }}
|
||||
|
||||
@@ -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),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user