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:
WaterrrForever
2026-10-22 08:49:29 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 14e4772b36
commit f5873b8089
4 changed files with 226 additions and 3 deletions
+10 -2
View File
@@ -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
View File
@@ -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",
+80
View File
@@ -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();
}
+135
View File
@@ -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 });
}
});
});