mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-22 13:18:09 +00:00
1291 lines
38 KiB
TypeScript
1291 lines
38 KiB
TypeScript
// Plugin ClawHub release tests validate plugin release metadata and artifacts.
|
|
import { execFileSync } from "node:child_process";
|
|
import {
|
|
chmodSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
realpathSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { delimiter, join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
buildOpenClawReleaseClawHubPlan,
|
|
buildOpenClawReleaseClawHubRuntimeState,
|
|
parseOpenClawReleaseClawHubPlanArgs,
|
|
} from "../scripts/lib/openclaw-release-clawhub-plan.ts";
|
|
import {
|
|
collectClawHubPublishablePluginPackages,
|
|
collectClawHubVersionGateErrors,
|
|
collectPluginClawHubReleasePathsFromGitRange,
|
|
collectPluginClawHubReleasePlan,
|
|
resolveChangedClawHubPublishablePluginPackages,
|
|
resolveSelectedClawHubPublishablePluginPackages,
|
|
type PublishablePluginPackage,
|
|
} from "../scripts/lib/plugin-clawhub-release.ts";
|
|
import {
|
|
collectPublishablePluginPackages,
|
|
OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
|
} from "../scripts/lib/plugin-npm-release.ts";
|
|
import { cleanupTempDirs, makeTempRepoRoot } from "./helpers/temp-repo.js";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
cleanupTempDirs(tempDirs);
|
|
});
|
|
|
|
describe("resolveChangedClawHubPublishablePluginPackages", () => {
|
|
const publishablePlugins: PublishablePluginPackage[] = [
|
|
{
|
|
extensionId: "feishu",
|
|
packageDir: "extensions/feishu",
|
|
packageName: "@openclaw/feishu",
|
|
version: "2026.4.1",
|
|
channel: "stable",
|
|
publishTag: "latest",
|
|
},
|
|
{
|
|
extensionId: "zalo",
|
|
packageDir: "extensions/zalo",
|
|
packageName: "@openclaw/zalo",
|
|
version: "2026.4.1-beta.1",
|
|
channel: "beta",
|
|
publishTag: "beta",
|
|
},
|
|
];
|
|
|
|
it("ignores shared release-tooling changes", () => {
|
|
expect(
|
|
resolveChangedClawHubPublishablePluginPackages({
|
|
plugins: publishablePlugins,
|
|
changedPaths: ["pnpm-lock.yaml"],
|
|
}),
|
|
).toStrictEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("collectClawHubPublishablePluginPackages", () => {
|
|
it("requires the ClawHub external plugin contract", () => {
|
|
const repoDir = createTempPluginRepo({
|
|
includeClawHubContract: false,
|
|
});
|
|
|
|
expect(() => collectClawHubPublishablePluginPackages(repoDir)).toThrow(
|
|
"openclaw.compat.pluginApi is required for external code plugin packages.",
|
|
);
|
|
});
|
|
|
|
it("rejects unsafe extension directory names", () => {
|
|
const repoDir = createTempPluginRepo({
|
|
extensionId: "Demo Plugin",
|
|
});
|
|
|
|
expect(() => collectClawHubPublishablePluginPackages(repoDir)).toThrow(
|
|
"Demo Plugin: extension directory name must match",
|
|
);
|
|
});
|
|
|
|
it("validates only selected package names when filters are provided", () => {
|
|
const repoDir = createTempPluginRepo({
|
|
extraExtensionIds: ["broken-plugin"],
|
|
});
|
|
writeFileSync(
|
|
join(repoDir, "extensions", "broken-plugin", "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
name: "@openclaw/broken-plugin",
|
|
version: "2026.4.1",
|
|
openclaw: {
|
|
extensions: ["./index.ts"],
|
|
release: {
|
|
publishToClawHub: true,
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
expect(
|
|
collectClawHubPublishablePluginPackages(repoDir, {
|
|
packageNames: ["@openclaw/demo-plugin"],
|
|
}).map((plugin) => plugin.packageName),
|
|
).toEqual(["@openclaw/demo-plugin"]);
|
|
});
|
|
});
|
|
|
|
describe("OpenClaw dual-published plugin metadata", () => {
|
|
const dualPublishedPlugins = [
|
|
{
|
|
extensionId: "cohere",
|
|
packageName: "@openclaw/cohere-provider",
|
|
install: {
|
|
clawhubSpec: "clawhub:@openclaw/cohere-provider",
|
|
defaultChoice: "npm",
|
|
minHostVersion: ">=2026.6.8",
|
|
npmSpec: "@openclaw/cohere-provider",
|
|
},
|
|
},
|
|
{
|
|
extensionId: "diagnostics-otel",
|
|
packageName: "@openclaw/diagnostics-otel",
|
|
install: {
|
|
clawhubSpec: "clawhub:@openclaw/diagnostics-otel",
|
|
defaultChoice: "npm",
|
|
minHostVersion: ">=2026.4.25",
|
|
npmSpec: "@openclaw/diagnostics-otel",
|
|
},
|
|
},
|
|
{
|
|
extensionId: "diagnostics-prometheus",
|
|
packageName: "@openclaw/diagnostics-prometheus",
|
|
install: {
|
|
clawhubSpec: "clawhub:@openclaw/diagnostics-prometheus",
|
|
defaultChoice: "npm",
|
|
minHostVersion: ">=2026.4.25",
|
|
npmSpec: "@openclaw/diagnostics-prometheus",
|
|
},
|
|
},
|
|
{
|
|
extensionId: "gmi",
|
|
packageName: "@openclaw/gmi-provider",
|
|
install: {
|
|
clawhubSpec: "clawhub:@openclaw/gmi-provider",
|
|
defaultChoice: "npm",
|
|
minHostVersion: ">=2026.6.8",
|
|
npmSpec: "@openclaw/gmi-provider",
|
|
},
|
|
},
|
|
] as const;
|
|
|
|
it("keeps dual-published plugins selectable through both ClawHub and npm release paths", () => {
|
|
const packageNames = dualPublishedPlugins.map((plugin) => plugin.packageName);
|
|
const clawHubPublishable = collectClawHubPublishablePluginPackages(undefined, {
|
|
packageNames,
|
|
});
|
|
const npmPublishable = collectPublishablePluginPackages(undefined, {
|
|
packageNames,
|
|
});
|
|
|
|
expect(clawHubPublishable.map((plugin) => plugin.packageName)).toEqual(packageNames);
|
|
expect(npmPublishable.map((plugin) => plugin.packageName)).toEqual(packageNames);
|
|
|
|
for (const plugin of dualPublishedPlugins) {
|
|
const packageJson = JSON.parse(
|
|
readFileSync(`extensions/${plugin.extensionId}/package.json`, "utf8"),
|
|
) as {
|
|
openclaw?: {
|
|
install?: {
|
|
clawhubSpec?: string;
|
|
defaultChoice?: string;
|
|
minHostVersion?: string;
|
|
npmSpec?: string;
|
|
};
|
|
release?: {
|
|
publishToClawHub?: boolean;
|
|
publishToNpm?: boolean;
|
|
};
|
|
};
|
|
};
|
|
|
|
expect(packageJson.openclaw?.install).toEqual(plugin.install);
|
|
expect(packageJson.openclaw?.release).toEqual({
|
|
publishToClawHub: true,
|
|
publishToNpm: true,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("collectClawHubVersionGateErrors", () => {
|
|
it("requires a version bump when a publishable plugin changes", () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const baseRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
writeFileSync(
|
|
join(repoDir, "extensions", "demo-plugin", "index.ts"),
|
|
"export const demo = 2;\n",
|
|
);
|
|
git(repoDir, ["add", "."]);
|
|
git(repoDir, [
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"commit",
|
|
"-m",
|
|
"change plugin",
|
|
]);
|
|
const headRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
const errors = collectClawHubVersionGateErrors({
|
|
rootDir: repoDir,
|
|
plugins: collectClawHubPublishablePluginPackages(repoDir),
|
|
gitRange: { baseRef, headRef },
|
|
});
|
|
|
|
expect(errors).toEqual([
|
|
"@openclaw/demo-plugin@2026.4.1: changed publishable plugin still has the same version in package.json.",
|
|
]);
|
|
});
|
|
|
|
it("does not require a version bump for the first ClawHub opt-in", () => {
|
|
const repoDir = createTempPluginRepo({
|
|
publishToClawHub: false,
|
|
});
|
|
const baseRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
writeFileSync(
|
|
join(repoDir, "extensions", "demo-plugin", "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
name: "@openclaw/demo-plugin",
|
|
version: "2026.4.1",
|
|
type: "module",
|
|
repository: {
|
|
type: "git",
|
|
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
|
},
|
|
openclaw: {
|
|
extensions: ["./index.ts"],
|
|
compat: {
|
|
pluginApi: ">=2026.4.1",
|
|
},
|
|
install: {
|
|
npmSpec: "@openclaw/demo-plugin",
|
|
},
|
|
build: {
|
|
openclawVersion: "2026.4.1",
|
|
},
|
|
release: {
|
|
publishToClawHub: true,
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
git(repoDir, ["add", "."]);
|
|
git(repoDir, [
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"commit",
|
|
"-m",
|
|
"opt in",
|
|
]);
|
|
const headRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
const errors = collectClawHubVersionGateErrors({
|
|
rootDir: repoDir,
|
|
plugins: collectClawHubPublishablePluginPackages(repoDir),
|
|
gitRange: { baseRef, headRef },
|
|
});
|
|
|
|
expect(errors).toStrictEqual([]);
|
|
});
|
|
|
|
it("does not require a version bump for shared release-tooling changes", () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const { baseRef, headRef } = commitSharedReleaseToolingChange(repoDir);
|
|
|
|
const errors = collectClawHubVersionGateErrors({
|
|
rootDir: repoDir,
|
|
plugins: collectClawHubPublishablePluginPackages(repoDir),
|
|
gitRange: { baseRef, headRef },
|
|
});
|
|
|
|
expect(errors).toStrictEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("resolveSelectedClawHubPublishablePluginPackages", () => {
|
|
it("selects all publishable plugins when shared release tooling changes", () => {
|
|
const repoDir = createTempPluginRepo({
|
|
extraExtensionIds: ["demo-two"],
|
|
});
|
|
const { baseRef, headRef } = commitSharedReleaseToolingChange(repoDir);
|
|
|
|
const selected = resolveSelectedClawHubPublishablePluginPackages({
|
|
rootDir: repoDir,
|
|
plugins: collectClawHubPublishablePluginPackages(repoDir),
|
|
gitRange: { baseRef, headRef },
|
|
});
|
|
|
|
expect(selected.map((plugin) => plugin.extensionId)).toEqual(["demo-plugin", "demo-two"]);
|
|
});
|
|
|
|
it("selects all publishable plugins when the shared setup action changes", () => {
|
|
const repoDir = createTempPluginRepo({
|
|
extraExtensionIds: ["demo-two"],
|
|
});
|
|
const baseRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
mkdirSync(join(repoDir, ".github", "actions", "setup-node-env"), { recursive: true });
|
|
writeFileSync(
|
|
join(repoDir, ".github", "actions", "setup-node-env", "action.yml"),
|
|
"name: setup-node-env\n",
|
|
);
|
|
git(repoDir, ["add", "."]);
|
|
git(repoDir, [
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"commit",
|
|
"-m",
|
|
"shared helpers",
|
|
]);
|
|
const headRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
const selected = resolveSelectedClawHubPublishablePluginPackages({
|
|
rootDir: repoDir,
|
|
plugins: collectClawHubPublishablePluginPackages(repoDir),
|
|
gitRange: { baseRef, headRef },
|
|
});
|
|
|
|
expect(selected.map((plugin) => plugin.extensionId)).toEqual(["demo-plugin", "demo-two"]);
|
|
});
|
|
});
|
|
|
|
describe("collectPluginClawHubReleasePlan", () => {
|
|
it("keeps existing trusted packages with missing versions as normal candidates", async () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const { fetchImpl, requests } = createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
},
|
|
trustedPublishers: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: {
|
|
repository: "openclaw/openclaw",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
versions: {
|
|
"@openclaw/demo-plugin@2026.4.1": 404,
|
|
},
|
|
});
|
|
|
|
const plan = await collectPluginClawHubReleasePlan({
|
|
rootDir: repoDir,
|
|
selection: ["@openclaw/demo-plugin"],
|
|
fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
});
|
|
|
|
expect(plan.candidates.map((plugin) => plugin.packageName)).toEqual(["@openclaw/demo-plugin"]);
|
|
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
|
expect(plan.missingTrustedPublisher).toStrictEqual([]);
|
|
expect(requests).toEqual([
|
|
"/api/v1/packages/%40openclaw%2Fdemo-plugin",
|
|
"/api/v1/packages/%40openclaw%2Fdemo-plugin/trusted-publisher",
|
|
"/api/v1/packages/%40openclaw%2Fdemo-plugin/versions/2026.4.1",
|
|
]);
|
|
});
|
|
|
|
it("routes missing package rows to bootstrap candidates instead of normal candidates", async () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const { fetchImpl } = createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 404,
|
|
},
|
|
},
|
|
});
|
|
|
|
const plan = await collectPluginClawHubReleasePlan({
|
|
rootDir: repoDir,
|
|
selection: ["@openclaw/demo-plugin"],
|
|
fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
});
|
|
|
|
expect(plan.candidates).toStrictEqual([]);
|
|
expect(plan.bootstrapCandidates.map((plugin) => plugin.packageName)).toEqual([
|
|
"@openclaw/demo-plugin",
|
|
]);
|
|
expect(plan.bootstrapCandidates[0]).toMatchObject({
|
|
alreadyPublished: false,
|
|
artifactName: "clawhub-package-openclaw-demo-plugin-2026.4.1",
|
|
packageName: "@openclaw/demo-plugin",
|
|
version: "2026.4.1",
|
|
});
|
|
expect(plan.missingTrustedPublisher).toStrictEqual([]);
|
|
});
|
|
|
|
it("routes existing packages without trusted publisher config out of normal candidates", async () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const { fetchImpl } = createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
},
|
|
trustedPublishers: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: null,
|
|
},
|
|
},
|
|
},
|
|
versions: {
|
|
"@openclaw/demo-plugin@2026.4.1": 404,
|
|
},
|
|
});
|
|
|
|
const plan = await collectPluginClawHubReleasePlan({
|
|
rootDir: repoDir,
|
|
selection: ["@openclaw/demo-plugin"],
|
|
fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
});
|
|
|
|
expect(plan.candidates).toStrictEqual([]);
|
|
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
|
expect(plan.missingTrustedPublisher.map((plugin) => plugin.packageName)).toEqual([
|
|
"@openclaw/demo-plugin",
|
|
]);
|
|
expect(plan.missingTrustedPublisher[0]).toMatchObject({
|
|
alreadyPublished: false,
|
|
artifactName: "clawhub-package-openclaw-demo-plugin-2026.4.1",
|
|
packageName: "@openclaw/demo-plugin",
|
|
version: "2026.4.1",
|
|
});
|
|
});
|
|
|
|
it("routes environment-pinned trusted publisher config out of normal candidates", async () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const { fetchImpl } = createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
},
|
|
trustedPublishers: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: {
|
|
repository: "openclaw/openclaw",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
environment: "clawhub-plugin-release",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
versions: {
|
|
"@openclaw/demo-plugin@2026.4.1": 404,
|
|
},
|
|
});
|
|
|
|
const plan = await collectPluginClawHubReleasePlan({
|
|
rootDir: repoDir,
|
|
selection: ["@openclaw/demo-plugin"],
|
|
fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
});
|
|
|
|
expect(plan.candidates).toStrictEqual([]);
|
|
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
|
expect(plan.missingTrustedPublisher.map((plugin) => plugin.packageName)).toEqual([
|
|
"@openclaw/demo-plugin",
|
|
]);
|
|
});
|
|
|
|
it("skips versions that already exist on ClawHub", async () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const { fetchImpl } = createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
},
|
|
trustedPublishers: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: null,
|
|
},
|
|
},
|
|
},
|
|
versions: {
|
|
"@openclaw/demo-plugin@2026.4.1": 200,
|
|
},
|
|
});
|
|
|
|
const plan = await collectPluginClawHubReleasePlan({
|
|
rootDir: repoDir,
|
|
selection: ["@openclaw/demo-plugin"],
|
|
fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
});
|
|
|
|
expect(plan.candidates).toStrictEqual([]);
|
|
expect(plan.bootstrapCandidates).toStrictEqual([]);
|
|
expect(plan.missingTrustedPublisher.map((plugin) => plugin.packageName)).toEqual([
|
|
"@openclaw/demo-plugin",
|
|
]);
|
|
expect(plan.missingTrustedPublisher[0]).toMatchObject({
|
|
alreadyPublished: true,
|
|
artifactName: "clawhub-package-openclaw-demo-plugin-2026.4.1",
|
|
packageName: "@openclaw/demo-plugin",
|
|
version: "2026.4.1",
|
|
});
|
|
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",
|
|
packageName: "@openclaw/demo-plugin",
|
|
publishTag: "latest",
|
|
version: "2026.4.1",
|
|
});
|
|
});
|
|
|
|
it("plans selected packages without validating unrelated publishable packages", async () => {
|
|
const repoDir = createTempPluginRepo({
|
|
extraExtensionIds: ["broken-plugin"],
|
|
});
|
|
writeFileSync(
|
|
join(repoDir, "extensions", "broken-plugin", "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
name: "@openclaw/broken-plugin",
|
|
version: "2026.4.1",
|
|
openclaw: {
|
|
extensions: ["./index.ts"],
|
|
release: {
|
|
publishToClawHub: true,
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
|
|
const plan = await collectPluginClawHubReleasePlan({
|
|
rootDir: repoDir,
|
|
selection: ["@openclaw/demo-plugin"],
|
|
fetchImpl: createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
},
|
|
trustedPublishers: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: {
|
|
repository: "openclaw/openclaw",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
versions: {
|
|
"@openclaw/demo-plugin@2026.4.1": 404,
|
|
},
|
|
}).fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
});
|
|
|
|
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",
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("buildOpenClawReleaseClawHubPlan", () => {
|
|
it("emits a dispatch plan that keeps ClawHub children on the release tag", async () => {
|
|
const repoDir = createTempPluginRepo({
|
|
extraExtensionIds: ["demo-two", "demo-three"],
|
|
});
|
|
const { fetchImpl } = createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
"@openclaw/demo-two": {
|
|
status: 404,
|
|
},
|
|
"@openclaw/demo-three": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
},
|
|
trustedPublishers: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: {
|
|
repository: "openclaw/openclaw",
|
|
workflowFilename: "plugin-clawhub-release.yml",
|
|
},
|
|
},
|
|
},
|
|
"@openclaw/demo-three": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: null,
|
|
},
|
|
},
|
|
},
|
|
versions: {
|
|
"@openclaw/demo-plugin@2026.4.1": 404,
|
|
"@openclaw/demo-three@2026.4.1": 404,
|
|
},
|
|
});
|
|
|
|
const plan = await buildOpenClawReleaseClawHubPlan(
|
|
{
|
|
releaseTag: "v2026.4.1-beta.1",
|
|
releasePublishBranch: "main",
|
|
releasePublishRunId: "12345",
|
|
pluginPublishScope: "all-publishable",
|
|
plugins: [],
|
|
},
|
|
{
|
|
rootDir: repoDir,
|
|
fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
},
|
|
);
|
|
|
|
expect(plan.clawHubWorkflowRef).toBe("v2026.4.1-beta.1");
|
|
expect(plan.releasePublishBranch).toBe("main");
|
|
expect(plan.normal).toEqual({
|
|
workflow: "plugin-clawhub-release.yml",
|
|
ref: "v2026.4.1-beta.1",
|
|
shouldDispatch: true,
|
|
packages: ["@openclaw/demo-plugin"],
|
|
inputs: {
|
|
publish_scope: "selected",
|
|
plugins: "@openclaw/demo-plugin",
|
|
release_publish_run_id: "12345",
|
|
release_publish_branch: "main",
|
|
},
|
|
});
|
|
expect(plan.bootstrap).toEqual({
|
|
workflow: "plugin-clawhub-new.yml",
|
|
ref: "v2026.4.1-beta.1",
|
|
shouldDispatch: true,
|
|
packages: ["@openclaw/demo-two", "@openclaw/demo-three"],
|
|
inputs: {
|
|
plugins: "@openclaw/demo-two,@openclaw/demo-three",
|
|
release_publish_run_id: "12345",
|
|
release_publish_branch: "main",
|
|
},
|
|
});
|
|
expect(new Set([...plan.normal.packages, ...plan.bootstrap.packages]).size).toBe(3);
|
|
expect(plan.summary).toEqual({
|
|
normalCount: 1,
|
|
bootstrapCount: 2,
|
|
missingTrustedPublisherCount: 1,
|
|
normalPlugins: "@openclaw/demo-plugin",
|
|
bootstrapPlugins: "@openclaw/demo-two,@openclaw/demo-three",
|
|
missingTrustedPlugins: "@openclaw/demo-three",
|
|
});
|
|
expect(plan.verifier).toEqual({
|
|
clawHubWorkflowRef: "v2026.4.1-beta.1",
|
|
});
|
|
});
|
|
|
|
it("routes already-published packages missing trusted publisher config to bootstrap repair", async () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const { fetchImpl } = createClawHubPlanFetch({
|
|
packages: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
package: {},
|
|
owner: {},
|
|
},
|
|
},
|
|
},
|
|
trustedPublishers: {
|
|
"@openclaw/demo-plugin": {
|
|
status: 200,
|
|
body: {
|
|
trustedPublisher: null,
|
|
},
|
|
},
|
|
},
|
|
versions: {
|
|
"@openclaw/demo-plugin@2026.4.1": 200,
|
|
},
|
|
});
|
|
|
|
const plan = await buildOpenClawReleaseClawHubPlan(
|
|
{
|
|
releaseTag: "v2026.4.1-beta.1",
|
|
releasePublishBranch: "release/2026.4.1",
|
|
releasePublishRunId: "12345",
|
|
pluginPublishScope: "selected",
|
|
plugins: ["@openclaw/demo-plugin"],
|
|
},
|
|
{
|
|
rootDir: repoDir,
|
|
fetchImpl,
|
|
registryBaseUrl: "https://clawhub.ai",
|
|
},
|
|
);
|
|
|
|
expect(plan.normal.shouldDispatch).toBe(false);
|
|
expect(plan.bootstrap).toMatchObject({
|
|
workflow: "plugin-clawhub-new.yml",
|
|
ref: "v2026.4.1-beta.1",
|
|
shouldDispatch: true,
|
|
packages: ["@openclaw/demo-plugin"],
|
|
inputs: {
|
|
plugins: "@openclaw/demo-plugin",
|
|
release_publish_run_id: "12345",
|
|
release_publish_branch: "release/2026.4.1",
|
|
},
|
|
});
|
|
expect(plan.summary).toMatchObject({
|
|
normalCount: 0,
|
|
bootstrapCount: 1,
|
|
missingTrustedPublisherCount: 1,
|
|
bootstrapPlugins: "@openclaw/demo-plugin",
|
|
missingTrustedPlugins: "@openclaw/demo-plugin",
|
|
});
|
|
});
|
|
|
|
it("rejects incompatible all-publishable plugin selection args", () => {
|
|
expect(() =>
|
|
parseOpenClawReleaseClawHubPlanArgs([
|
|
"--release-tag",
|
|
"v2026.4.1-beta.1",
|
|
"--release-publish-branch",
|
|
"main",
|
|
"--release-publish-run-id",
|
|
"12345",
|
|
"--plugin-publish-scope",
|
|
"all-publishable",
|
|
"--plugins",
|
|
"@openclaw/demo-plugin",
|
|
]),
|
|
).toThrow("plugin-publish-scope=all-publishable must not be combined with --plugins.");
|
|
});
|
|
});
|
|
|
|
describe("buildOpenClawReleaseClawHubRuntimeState", () => {
|
|
it("includes the normal ClawHub run in verifier args when the release waits for it", () => {
|
|
const state = buildOpenClawReleaseClawHubRuntimeState({
|
|
repository: "openclaw/openclaw",
|
|
waitForClawHub: true,
|
|
forceSkipClawHub: false,
|
|
normalRunId: "111",
|
|
bootstrapRunId: "",
|
|
bootstrapCompleted: false,
|
|
});
|
|
|
|
expect(state.verifierArgs).toEqual(["--plugin-clawhub-run", "111"]);
|
|
expect(state.proofLines.normal).toBe(
|
|
"- plugin ClawHub publish: https://github.com/openclaw/openclaw/actions/runs/111",
|
|
);
|
|
expect(state.proofLines.bootstrap).toBe("- plugin ClawHub bootstrap: not needed");
|
|
});
|
|
|
|
it("includes a completed bootstrap run even when there is no normal ClawHub run", () => {
|
|
const state = buildOpenClawReleaseClawHubRuntimeState({
|
|
repository: "openclaw/openclaw",
|
|
waitForClawHub: false,
|
|
forceSkipClawHub: false,
|
|
normalRunId: "",
|
|
bootstrapRunId: "222",
|
|
bootstrapCompleted: true,
|
|
});
|
|
|
|
expect(state.verifierArgs).toEqual(["--plugin-clawhub-bootstrap-run", "222"]);
|
|
expect(state.proofLines.normal).toBe("- plugin ClawHub publish: no normal OIDC candidates");
|
|
expect(state.proofLines.bootstrap).toBe(
|
|
"- plugin ClawHub bootstrap: https://github.com/openclaw/openclaw/actions/runs/222",
|
|
);
|
|
});
|
|
|
|
it("skips ClawHub verification for non-awaited incomplete runs while keeping proof links", () => {
|
|
const state = buildOpenClawReleaseClawHubRuntimeState({
|
|
repository: "openclaw/openclaw",
|
|
waitForClawHub: false,
|
|
forceSkipClawHub: false,
|
|
normalRunId: "111",
|
|
bootstrapRunId: "222",
|
|
bootstrapCompleted: false,
|
|
});
|
|
|
|
expect(state.verifierArgs).toEqual(["--skip-clawhub"]);
|
|
expect(state.proofLines.normal).toBe(
|
|
"- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/openclaw/openclaw/actions/runs/111",
|
|
);
|
|
expect(state.proofLines.bootstrap).toBe(
|
|
"- plugin ClawHub bootstrap: dispatched separately, not awaited by this proof: https://github.com/openclaw/openclaw/actions/runs/222",
|
|
);
|
|
});
|
|
|
|
it("keeps completed bootstrap run evidence when the normal ClawHub run is not awaited", () => {
|
|
const state = buildOpenClawReleaseClawHubRuntimeState({
|
|
repository: "openclaw/openclaw",
|
|
waitForClawHub: false,
|
|
forceSkipClawHub: false,
|
|
normalRunId: "111",
|
|
bootstrapRunId: "222",
|
|
bootstrapCompleted: true,
|
|
});
|
|
|
|
expect(state.verifierArgs).toEqual(["--skip-clawhub", "--plugin-clawhub-bootstrap-run", "222"]);
|
|
expect(state.proofLines.normal).toBe(
|
|
"- plugin ClawHub publish: dispatched separately, not awaited by this proof: https://github.com/openclaw/openclaw/actions/runs/111",
|
|
);
|
|
expect(state.proofLines.bootstrap).toBe(
|
|
"- plugin ClawHub bootstrap: https://github.com/openclaw/openclaw/actions/runs/222",
|
|
);
|
|
});
|
|
|
|
it("forces skip-clawhub after a failed child run even if ClawHub runs completed", () => {
|
|
const state = buildOpenClawReleaseClawHubRuntimeState({
|
|
repository: "openclaw/openclaw",
|
|
waitForClawHub: true,
|
|
forceSkipClawHub: true,
|
|
normalRunId: "111",
|
|
bootstrapRunId: "222",
|
|
bootstrapCompleted: true,
|
|
});
|
|
|
|
expect(state.verifierArgs).toEqual(["--skip-clawhub"]);
|
|
expect(state.proofLines.normal).toBe(
|
|
"- plugin ClawHub publish: https://github.com/openclaw/openclaw/actions/runs/111",
|
|
);
|
|
expect(state.proofLines.bootstrap).toBe(
|
|
"- plugin ClawHub bootstrap: https://github.com/openclaw/openclaw/actions/runs/222",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("plugin-clawhub-publish.sh", () => {
|
|
it("previews the publish command through the ClawHub CLI dry-run preflight", () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const binDir = join(repoDir, "bin");
|
|
const markerPath = join(repoDir, "clawhub-invoked");
|
|
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"),
|
|
"--dry-run",
|
|
"extensions/demo-plugin",
|
|
],
|
|
{
|
|
cwd: repoDir,
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
PATH: `${binDir}${delimiter}${process.env.PATH ?? ""}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(output).toContain("Publish command: CLAWHUB_WORKDIR=");
|
|
expect(output).toContain("Resolved ClawPack:");
|
|
const invocations = readFileSync(markerPath, "utf8");
|
|
const resolvedRepoDir = realpathSync(repoDir);
|
|
expect(invocations).toContain(`--workdir ${resolvedRepoDir}`);
|
|
expect(invocations).toContain(
|
|
`package pack ${join(resolvedRepoDir, "extensions/demo-plugin")}`,
|
|
);
|
|
expect(invocations).toContain("package publish ");
|
|
expect(invocations).toContain(".tgz --tags latest");
|
|
expect(invocations).toContain("--dry-run");
|
|
});
|
|
|
|
it("passes a manual override reason when trusted publisher repair requires one", () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const binDir = join(repoDir, "bin");
|
|
const markerPath = join(repoDir, "clawhub-invoked");
|
|
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);
|
|
|
|
execFileSync(
|
|
"bash",
|
|
[
|
|
join(process.cwd(), "scripts/plugin-clawhub-publish.sh"),
|
|
"--publish",
|
|
"extensions/demo-plugin",
|
|
],
|
|
{
|
|
cwd: repoDir,
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON:
|
|
"GitHub Actions trusted publisher repair before OIDC migration",
|
|
PATH: `${binDir}${delimiter}${process.env.PATH ?? ""}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
const invocations = readFileSync(markerPath, "utf8");
|
|
expect(invocations).toContain("package publish ");
|
|
expect(invocations).toContain(
|
|
"--manual-override-reason GitHub Actions trusted publisher repair before OIDC migration",
|
|
);
|
|
});
|
|
|
|
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,
|
|
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", () => {
|
|
it("rejects unsafe git refs", () => {
|
|
const repoDir = createTempPluginRepo();
|
|
const headRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
expect(() =>
|
|
collectPluginClawHubReleasePathsFromGitRange({
|
|
rootDir: repoDir,
|
|
gitRange: {
|
|
baseRef: "--not-a-ref",
|
|
headRef,
|
|
},
|
|
}),
|
|
).toThrow("baseRef must be a normal git ref or commit SHA.");
|
|
});
|
|
});
|
|
|
|
function createTempPluginRepo(
|
|
options: {
|
|
extensionId?: string;
|
|
extraExtensionIds?: string[];
|
|
publishToClawHub?: boolean;
|
|
includeClawHubContract?: boolean;
|
|
} = {},
|
|
) {
|
|
const repoDir = makeTempRepoRoot(tempDirs, "openclaw-clawhub-release-");
|
|
const extensionId = options.extensionId ?? "demo-plugin";
|
|
const extensionIds = [extensionId, ...(options.extraExtensionIds ?? [])];
|
|
|
|
writeFileSync(
|
|
join(repoDir, "package.json"),
|
|
JSON.stringify({ name: "openclaw-test-root", type: "module" }, null, 2),
|
|
);
|
|
writeFileSync(join(repoDir, "pnpm-lock.yaml"), "lockfileVersion: '9.0'\n");
|
|
for (const currentExtensionId of extensionIds) {
|
|
mkdirSync(join(repoDir, "extensions", currentExtensionId), { recursive: true });
|
|
writeFileSync(
|
|
join(repoDir, "extensions", currentExtensionId, "package.json"),
|
|
JSON.stringify(
|
|
{
|
|
name: `@openclaw/${currentExtensionId}`,
|
|
version: "2026.4.1",
|
|
type: "module",
|
|
repository: {
|
|
type: "git",
|
|
url: OPENCLAW_PLUGIN_NPM_REPOSITORY_URL,
|
|
},
|
|
openclaw: {
|
|
extensions: ["./index.ts"],
|
|
...(options.includeClawHubContract === false
|
|
? {}
|
|
: {
|
|
compat: {
|
|
pluginApi: ">=2026.4.1",
|
|
},
|
|
build: {
|
|
openclawVersion: "2026.4.1",
|
|
},
|
|
}),
|
|
install: {
|
|
npmSpec: `@openclaw/${currentExtensionId}`,
|
|
},
|
|
release: {
|
|
publishToClawHub: options.publishToClawHub ?? true,
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
writeFileSync(
|
|
join(repoDir, "extensions", currentExtensionId, "index.ts"),
|
|
`export const ${currentExtensionId.replaceAll(/[-.]/g, "_")} = 1;\n`,
|
|
);
|
|
writeFileSync(join(repoDir, "extensions", currentExtensionId, "README.md"), "# Demo plugin\n");
|
|
}
|
|
|
|
git(repoDir, ["init", "-b", "main"]);
|
|
git(repoDir, ["add", "."]);
|
|
git(repoDir, [
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"commit",
|
|
"-m",
|
|
"init",
|
|
]);
|
|
|
|
return repoDir;
|
|
}
|
|
|
|
function commitSharedReleaseToolingChange(repoDir: string) {
|
|
const baseRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
mkdirSync(join(repoDir, "scripts"), { recursive: true });
|
|
writeFileSync(join(repoDir, "scripts", "plugin-clawhub-publish.sh"), "#!/usr/bin/env bash\n");
|
|
git(repoDir, ["add", "."]);
|
|
git(repoDir, [
|
|
"-c",
|
|
"user.name=Test",
|
|
"-c",
|
|
"user.email=test@example.com",
|
|
"commit",
|
|
"-m",
|
|
"shared tooling",
|
|
]);
|
|
const headRef = git(repoDir, ["rev-parse", "HEAD"]);
|
|
|
|
return { baseRef, headRef };
|
|
}
|
|
|
|
function createClawHubPlanFetch(config: {
|
|
packages: Record<
|
|
string,
|
|
{
|
|
status: number;
|
|
body?: unknown;
|
|
}
|
|
>;
|
|
trustedPublishers?: Record<
|
|
string,
|
|
{
|
|
status: number;
|
|
body?: unknown;
|
|
}
|
|
>;
|
|
versions?: Record<string, number>;
|
|
}) {
|
|
const requests: string[] = [];
|
|
const fetchImpl: typeof fetch = async (input) => {
|
|
const requestUrl =
|
|
typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
const url = new URL(requestUrl);
|
|
requests.push(url.pathname);
|
|
|
|
const packageMatch = url.pathname.match(/^\/api\/v1\/packages\/([^/]+)$/u);
|
|
if (packageMatch) {
|
|
const packageName = decodeURIComponent(packageMatch[1]);
|
|
const packageResponse = config.packages[packageName];
|
|
if (!packageResponse) {
|
|
throw new Error(`Unexpected package detail request for ${packageName}`);
|
|
}
|
|
return new Response(JSON.stringify(packageResponse.body ?? {}), {
|
|
status: packageResponse.status,
|
|
});
|
|
}
|
|
|
|
const trustedPublisherMatch = url.pathname.match(
|
|
/^\/api\/v1\/packages\/([^/]+)\/trusted-publisher$/u,
|
|
);
|
|
if (trustedPublisherMatch) {
|
|
const packageName = decodeURIComponent(trustedPublisherMatch[1]);
|
|
const trustedPublisherResponse = config.trustedPublishers?.[packageName];
|
|
if (!trustedPublisherResponse) {
|
|
throw new Error(`Unexpected trusted-publisher request for ${packageName}`);
|
|
}
|
|
return new Response(JSON.stringify(trustedPublisherResponse.body ?? {}), {
|
|
status: trustedPublisherResponse.status,
|
|
});
|
|
}
|
|
|
|
const versionMatch = url.pathname.match(/^\/api\/v1\/packages\/([^/]+)\/versions\/([^/]+)$/u);
|
|
if (versionMatch) {
|
|
const packageName = decodeURIComponent(versionMatch[1]);
|
|
const version = decodeURIComponent(versionMatch[2]);
|
|
const status = config.versions?.[`${packageName}@${version}`];
|
|
if (!status) {
|
|
throw new Error(`Unexpected version detail request for ${packageName}@${version}`);
|
|
}
|
|
return new Response("{}", { status });
|
|
}
|
|
|
|
throw new Error(`Unexpected ClawHub request to ${url.pathname}`);
|
|
};
|
|
|
|
return { fetchImpl, requests };
|
|
}
|
|
|
|
function git(cwd: string, args: string[]) {
|
|
return execFileSync("git", ["-C", cwd, ...args], {
|
|
encoding: "utf8",
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
}).trim();
|
|
}
|