mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
ci(skills): hide retired skills on ClawHub once the sync has run (#5656)
* ci(skills): hide retired skills on ClawHub once the sync has run clawhub sync stops publishing a retired skill but leaves its last version listed. A new step inspects each RETIRED_SKILLS name under heygen-com and runs `clawhub hide --yes` with its retirement notice for the ones still listed. A hidden skill reads as not found, so later runs hide nothing; a dry run only names the skills still listed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * ci(skills): import the retirement manifest at the top of the unlist script Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
14e4772b36
commit
f5873b8089
@@ -3,7 +3,8 @@ name: Sync skills to ClawHub
|
||||
# Publishes changed skills under skills/ to ClawHub (https://clawhub.ai/heygen-com)
|
||||
# whenever they land on main. `clawhub sync` only publishes skills whose content
|
||||
# changed vs. the registry, auto-bumping the patch version — unchanged skills are
|
||||
# a no-op, so this is safe to run on every push.
|
||||
# a no-op, so this is safe to run on every push. Skills listed in RETIRED_SKILLS
|
||||
# (packages/cli/src/utils/skillsManifest.ts) are hidden once, then left alone.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -11,10 +12,12 @@ on:
|
||||
paths:
|
||||
- "skills/**"
|
||||
- "scripts/clawhub-sync.mjs"
|
||||
- "scripts/clawhub-unlist-retired.mjs"
|
||||
- "packages/cli/src/utils/skillsManifest.ts"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: "Preview what would publish without publishing"
|
||||
description: "Preview what would publish or be hidden, without changing ClawHub"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
@@ -52,3 +55,8 @@ jobs:
|
||||
env:
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
run: node scripts/clawhub-sync.mjs
|
||||
|
||||
- name: Hide retired skills
|
||||
env:
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
run: node scripts/clawhub-unlist-retired.mjs
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@
|
||||
"player:perf": "bun run --filter @hyperframes/player perf",
|
||||
"format:check": "oxfmt --check .",
|
||||
"knip": "knip",
|
||||
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-cli-lazy-deps.test.mjs scripts/file-url-paths.test.mjs scripts/check-studio-copy-dashes.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/check-edit-accuracy-filter.test.mjs scripts/edit-accuracy-retry.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/rewrite-relative-import-extensions.test.mjs scripts/lint-skills.test.mjs scripts/check-core-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-theme-fallbacks.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs registry/components/filmed-screen/filmed-screen.test.mjs registry/components/echo-smear/echo-smear.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
|
||||
"test:scripts": "node --import tsx --test scripts/animejs-v4-guidance.test.mjs scripts/check-tracked-artifacts.test.mjs scripts/check-registry-set-delta.test.mjs scripts/check-no-main-deletions.test.mjs scripts/check-pr-captures.test.mjs scripts/check-comment-citations.test.mjs scripts/comments-workflow.test.mjs scripts/comment-ratchet.test.mjs scripts/check-docs-snippet-motion.test.mjs scripts/registry-target-paths.test.mjs scripts/check-workspace-contracts.test.mjs scripts/check-media-use-copy-parity.test.mjs scripts/check-svg-sanitize-parity.test.mjs scripts/check-media-use-svg-sanitize-generated.test.mjs scripts/check-package-cycles.test.mjs scripts/check-cli-process-ownership.test.mjs scripts/check-cli-lazy-deps.test.mjs scripts/file-url-paths.test.mjs scripts/check-studio-copy-dashes.test.mjs scripts/check-large-files.test.mjs scripts/package-subpaths.test.mjs scripts/validate-release-channel.test.mjs scripts/clawhub-sync.test.mjs scripts/clawhub-unlist-retired.test.mjs scripts/publish-workflow.test.mjs scripts/pr-edit-concurrency.test.mjs scripts/merge-queue-workflows.test.mjs scripts/install-workspace-dependencies.test.mjs scripts/check-edit-accuracy-filter.test.mjs scripts/edit-accuracy-retry.test.mjs scripts/draft-changelog.test.ts scripts/set-version.test.ts scripts/release-prepare.test.ts scripts/cli-options.test.ts scripts/changelog-weekly.test.ts scripts/claude-plugin-compression.test.ts scripts/catalog-payload-assets.test.ts scripts/host-registry-assets.test.ts scripts/catalog-preview-temp.test.ts scripts/backfill-block-previews.test.ts scripts/catalog-hosted-files.test.ts scripts/player-cdn-pin.test.ts scripts/studio-runtime-smoke.test.mjs scripts/verify-packed-manifests.test.mjs scripts/rewrite-relative-import-extensions.test.mjs scripts/lint-skills.test.mjs scripts/check-core-skills.test.mjs scripts/creator-editing-recipes.test.mjs packages/gcp-cloud-run/check-dockerfile-workspaces.test.mjs packages/core/scripts/writeGeneratedFile.test.ts scripts/catalog-publication.test.mjs scripts/ci/resolve-workflow-pr.test.mjs scripts/ci/base-file.test.mjs scripts/check-catalog-source-pr.test.mjs scripts/generate-registry-items.test.ts scripts/catalog-drift.test.ts scripts/catalog-fetch-mirror.test.ts scripts/catalog-script-inlining.test.ts scripts/catalog-detail.test.ts scripts/generate-catalog-pages.test.ts scripts/verify-catalog-payloads.test.ts scripts/registry-theme-fallbacks.test.ts scripts/registry-skill-files.test.ts scripts/creator-editing-capabilities.test.mjs scripts/generate-catalog-previews.test.ts scripts/registry-primitive-payloads.test.ts registry/components/pan-stations/pan-stations.test.mjs registry/components/filmed-screen/filmed-screen.test.mjs registry/components/echo-smear/echo-smear.test.mjs && vitest run scripts/catalog/ scripts/contrastRatchet.test.ts scripts/generate-catalog-payloads.test.ts",
|
||||
"typecheck:scripts": "tsc --noEmit -p scripts/tsconfig.json",
|
||||
"test:skills": "HYPERFRAMES_MEDIA_HOME_REQUIRED=1 node --import ./packages/cli/scripts/test-home.mjs --test 'skills/**/*.test.mjs' 'packages/cli/src/media-use/**/*.test.mjs'",
|
||||
"generate:previews": "tsx scripts/generate-template-previews.ts",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
// `clawhub sync` never republishes a retired skill, but its last version stays listed until
|
||||
// the owner hides it. Hiding is ClawHub's owner soft delete (`clawhub unhide` restores it), and
|
||||
// the registry answers "Skill not found" for a hidden skill, which is what makes a rerun a no-op.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { RETIRED_SKILLS, retiredSkillNotice } from "../packages/cli/src/utils/skillsManifest.ts";
|
||||
|
||||
const OWNER_HANDLE = "heygen-com";
|
||||
const NOT_FOUND = /Skill not found/;
|
||||
|
||||
function parsedOutput(result) {
|
||||
try {
|
||||
return JSON.parse(result.output);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** "listed", "unlisted", or the inspect failure that settles neither. */
|
||||
function listing(run, slug) {
|
||||
const inspected = run(["inspect", `${OWNER_HANDLE}/${slug}`, "--json"]);
|
||||
if (!inspected.ok) return NOT_FOUND.test(inspected.message) ? "unlisted" : inspected.message;
|
||||
const parsed = parsedOutput(inspected);
|
||||
if (!parsed) return `inspect printed no JSON: ${inspected.message}`;
|
||||
return parsed.skill ? "listed" : "unlisted";
|
||||
}
|
||||
|
||||
/** Hides every retired skill ClawHub still lists; a dry run only names them. */
|
||||
export function unlistRetired({ run, names, reasonFor, dryRun }) {
|
||||
const hidden = [];
|
||||
const pending = [];
|
||||
const errors = [];
|
||||
for (const slug of names) {
|
||||
const state = listing(run, slug);
|
||||
if (state === "unlisted") continue;
|
||||
if (state !== "listed") {
|
||||
errors.push(`${slug}: ${state}`);
|
||||
continue;
|
||||
}
|
||||
if (dryRun) {
|
||||
pending.push(slug);
|
||||
continue;
|
||||
}
|
||||
const hide = run(["hide", `${OWNER_HANDLE}/${slug}`, "--yes", "--reason", reasonFor(slug)]);
|
||||
if (hide.ok) hidden.push(slug);
|
||||
else errors.push(`${slug}: ${hide.message}`);
|
||||
}
|
||||
return { hidden, pending, errors };
|
||||
}
|
||||
|
||||
function runClawhub(args) {
|
||||
const result = spawnSync("clawhub", args, { encoding: "utf8" });
|
||||
if (args[0] !== "inspect") {
|
||||
process.stdout.write(result.stdout);
|
||||
process.stderr.write(result.stderr);
|
||||
}
|
||||
return { ok: result.status === 0, output: result.stdout, message: result.stdout + result.stderr };
|
||||
}
|
||||
|
||||
function main() {
|
||||
const dryRun = process.env.DRY_RUN === "true";
|
||||
const { hidden, pending, errors } = unlistRetired({
|
||||
run: runClawhub,
|
||||
names: RETIRED_SKILLS,
|
||||
reasonFor: retiredSkillNotice,
|
||||
dryRun,
|
||||
});
|
||||
if (hidden.length > 0) console.log(`Hid retired skills on ClawHub:\n${hidden.join("\n")}`);
|
||||
if (pending.length > 0)
|
||||
console.log(`Retired skills still listed on ClawHub (dry run):\n${pending.join("\n")}`);
|
||||
if (hidden.length + pending.length + errors.length === 0)
|
||||
console.log(`No retired skill is listed on ClawHub (${RETIRED_SKILLS.length} checked).`);
|
||||
for (const error of errors) console.error(`::error::${error}`);
|
||||
if (errors.length > 0) process.exitCode = 1;
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main();
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { unlistRetired } from "./clawhub-unlist-retired.mjs";
|
||||
import { RETIRED_SKILLS } from "../packages/cli/src/utils/skillsManifest.ts";
|
||||
|
||||
const NOT_FOUND = "Error: Skill not found or unavailable to this account.";
|
||||
|
||||
function fakeClawhub({ listed = [], moderationOnly = [], inspectError = null, refuse = null }) {
|
||||
const calls = [];
|
||||
const visible = new Set(listed);
|
||||
const inspect = (args) => {
|
||||
const slug = args[1].split("/")[1];
|
||||
if (inspectError) return { ok: false, output: "", message: inspectError };
|
||||
if (moderationOnly.includes(slug))
|
||||
return { ok: true, output: JSON.stringify({ skill: null, moderation: {} }), message: "" };
|
||||
if (!visible.has(slug)) return { ok: false, output: "", message: NOT_FOUND };
|
||||
const output = JSON.stringify({ skill: { slug, tags: { latest: "1.0.3" } } });
|
||||
return { ok: true, output, message: output };
|
||||
};
|
||||
const hide = (args) => {
|
||||
if (refuse) return { ok: false, output: "", message: refuse };
|
||||
visible.delete(args[1].split("/")[1]);
|
||||
return { ok: true, output: "", message: "" };
|
||||
};
|
||||
const commands = { inspect, hide };
|
||||
const run = (args) => {
|
||||
calls.push(args);
|
||||
return commands[args[0]](args);
|
||||
};
|
||||
return { run, calls };
|
||||
}
|
||||
|
||||
const reasonFor = (slug) => `${slug} was retired`;
|
||||
const unlist = (clawhub, names, dryRun = false) =>
|
||||
unlistRetired({ run: clawhub.run, names, reasonFor, dryRun });
|
||||
const hideCalls = (clawhub) => clawhub.calls.filter((args) => args[0] === "hide");
|
||||
|
||||
describe("ClawHub retired-skill unlisting", () => {
|
||||
it("hides each retired skill still listed under the owner, with its retirement note", () => {
|
||||
const clawhub = fakeClawhub({ listed: ["general-video"] });
|
||||
|
||||
assert.deepEqual(unlist(clawhub, ["general-video", "pr-to-video"]), {
|
||||
hidden: ["general-video"],
|
||||
pending: [],
|
||||
errors: [],
|
||||
});
|
||||
assert.deepEqual(clawhub.calls, [
|
||||
["inspect", "heygen-com/general-video", "--json"],
|
||||
["hide", "heygen-com/general-video", "--yes", "--reason", "general-video was retired"],
|
||||
["inspect", "heygen-com/pr-to-video", "--json"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("hides nothing on a second run", () => {
|
||||
const clawhub = fakeClawhub({ listed: ["general-video", "pr-to-video"] });
|
||||
unlist(clawhub, ["general-video", "pr-to-video"]);
|
||||
clawhub.calls.length = 0;
|
||||
|
||||
assert.deepEqual(unlist(clawhub, ["general-video", "pr-to-video"]), {
|
||||
hidden: [],
|
||||
pending: [],
|
||||
errors: [],
|
||||
});
|
||||
assert.equal(hideCalls(clawhub).length, 0);
|
||||
});
|
||||
|
||||
it("counts a skill the registry shows only by its moderation record as unlisted", () => {
|
||||
const clawhub = fakeClawhub({ moderationOnly: ["hyperframes-core"] });
|
||||
|
||||
assert.deepEqual(unlist(clawhub, ["hyperframes-core"]).hidden, []);
|
||||
assert.equal(hideCalls(clawhub).length, 0);
|
||||
});
|
||||
|
||||
it("only names the listed skills in a dry run", () => {
|
||||
const clawhub = fakeClawhub({ listed: ["general-video", "hyperframes-audio"] });
|
||||
|
||||
assert.deepEqual(unlist(clawhub, ["general-video", "pr-to-video", "hyperframes-audio"], true), {
|
||||
hidden: [],
|
||||
pending: ["general-video", "hyperframes-audio"],
|
||||
errors: [],
|
||||
});
|
||||
assert.equal(hideCalls(clawhub).length, 0);
|
||||
});
|
||||
|
||||
it("reports an inspect failure other than not-found, without hiding", () => {
|
||||
const clawhub = fakeClawhub({ listed: ["general-video"], inspectError: "fetch failed" });
|
||||
|
||||
assert.deepEqual(unlist(clawhub, ["general-video"]).errors, ["general-video: fetch failed"]);
|
||||
assert.equal(hideCalls(clawhub).length, 0);
|
||||
});
|
||||
|
||||
it("reports a hide the registry refuses", () => {
|
||||
const clawhub = fakeClawhub({ listed: ["general-video"], refuse: "Forbidden" });
|
||||
|
||||
assert.deepEqual(unlist(clawhub, ["general-video"]), {
|
||||
hidden: [],
|
||||
pending: [],
|
||||
errors: ["general-video: Forbidden"],
|
||||
});
|
||||
});
|
||||
|
||||
it("reads the retired names from the CLI manifest and fails the workflow on a refusal", () => {
|
||||
const bin = mkdtempSync(join(tmpdir(), "clawhub-fake-"));
|
||||
try {
|
||||
writeFileSync(
|
||||
join(bin, "clawhub"),
|
||||
[
|
||||
"#!/bin/sh",
|
||||
'if [ "$1" = inspect ]; then echo \'{"skill":{"slug":"x"}}\'; exit 0; fi',
|
||||
"echo 'Forbidden' >&2",
|
||||
"exit 1",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(join(bin, "clawhub"), 0o755);
|
||||
const script = new URL("./clawhub-unlist-retired.mjs", import.meta.url).pathname;
|
||||
const run = (env) =>
|
||||
spawnSync(process.execPath, [script], { encoding: "utf8", env: { PATH: bin, ...env } });
|
||||
|
||||
const preview = run({ DRY_RUN: "true" });
|
||||
assert.equal(preview.status, 0, preview.stderr);
|
||||
for (const name of RETIRED_SKILLS) assert.match(preview.stdout, new RegExp(`^${name}$`, "m"));
|
||||
|
||||
const refused = run({});
|
||||
assert.equal(refused.status, 1);
|
||||
assert.match(refused.stderr, /::error::general-video: Forbidden/);
|
||||
} finally {
|
||||
rmSync(bin, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user