chore: remove kitchen sink repair tooling (#3307)

This commit is contained in:
Vincent Koc
2026-07-30 19:16:58 +08:00
committed by GitHub
parent d890dfefe8
commit 23af0934e4
5 changed files with 40 additions and 587 deletions
@@ -1,142 +0,0 @@
name: Repair Kitchen Sink Latest Pointer
on:
workflow_dispatch:
inputs:
expected_sha:
description: "Exact main SHA containing the repair mutation"
required: true
type: string
apply:
description: "Apply the repair after the dry-run"
required: true
default: false
type: boolean
confirm:
description: "Required confirmation token when apply is true"
required: false
type: string
concurrency:
group: repair-kitchen-sink-latest
cancel-in-progress: false
permissions:
contents: read
jobs:
repair:
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: Production
url: https://clawhub.ai/packages/%40openclaw%2Fkitchen-sink
steps:
- name: Require exact main revision
env:
EXPECTED_SHA: ${{ inputs.expected_sha }}
run: |
set -euo pipefail
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
echo "::error::The repair must run from main."
exit 1
fi
if [[ "$GITHUB_SHA" != "$EXPECTED_SHA" ]]; then
echo "::error::Main moved: expected $EXPECTED_SHA, got $GITHUB_SHA."
exit 1
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
with:
bun-version: 1.3.10
- name: Install
run: bun install --frozen-lockfile
- name: Dry-run repair
id: preflight
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
run: |
set -euo pipefail
if [[ -z "$CONVEX_DEPLOY_KEY" ]]; then
echo "::error::Missing Production environment secret CONVEX_DEPLOY_KEY"
exit 1
fi
result="$(bunx convex run maintenance:repairPackageLatestPointer \
'{"name":"@openclaw/kitchen-sink"}' --prod)"
if jq -e '
.dryRun == true and
.previousLatestVersion == "0.2.11" and
.selectedVersion == "0.2.12" and
.eligibleReleaseCount >= 2
' <<< "$result" >/dev/null; then
{
echo "already_repaired=false"
echo "plan_token=$(jq -r '.planToken' <<< "$result")"
} >> "$GITHUB_OUTPUT"
exit 0
fi
if jq -e '
.dryRun == true and
.previousLatestVersion == "0.2.12" and
.selectedVersion == "0.2.12" and
(.releaseTagChanges | length) == 0
' <<< "$result" >/dev/null; then
echo "already_repaired=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "::error::Production package state did not match the expected pre- or post-repair state."
exit 1
- name: Require apply confirmation
if: inputs.apply
env:
CONFIRM: ${{ inputs.confirm }}
run: |
set -euo pipefail
if [[ "$CONFIRM" != "repair-package-latest-pointer-2026-07-30" ]]; then
echo "::error::The exact repair confirmation token is required."
exit 1
fi
- name: Apply repair
if: inputs.apply && steps.preflight.outputs.already_repaired != 'true'
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
EXPECTED_PLAN_TOKEN: ${{ steps.preflight.outputs.plan_token }}
run: |
set -euo pipefail
required_confirm="repair-package-latest-pointer-2026-07-30"
args="$(jq -nc \
--arg name "@openclaw/kitchen-sink" \
--arg confirm "$required_confirm" \
--arg planToken "$EXPECTED_PLAN_TOKEN" \
'{
name: $name,
dryRun: false,
confirm: $confirm,
expectedPlanToken: $planToken
}')"
result="$(bunx convex run maintenance:repairPackageLatestPointer "$args" --prod)"
jq -e '
.dryRun == false and
.selectedVersion == "0.2.12"
' <<< "$result"
- name: Verify repaired state
if: inputs.apply
env:
CONVEX_DEPLOY_KEY: ${{ secrets.CONVEX_DEPLOY_KEY }}
run: |
set -euo pipefail
readback="$(bunx convex run maintenance:repairPackageLatestPointer \
'{"name":"@openclaw/kitchen-sink"}' --prod)"
jq -e '
.dryRun == true and
.previousLatestVersion == "0.2.12" and
.selectedVersion == "0.2.12" and
(.releaseTagChanges | length) == 0
' <<< "$readback"
-38
View File
@@ -1,4 +1,3 @@
import type { PackageArtifactSummary } from "clawhub-schema";
import type { Doc } from "../_generated/dataModel";
type PackageReleaseArtifactHashFields = Pick<
@@ -6,20 +5,6 @@ type PackageReleaseArtifactHashFields = Pick<
"artifactKind" | "clawpackSha256" | "sha256hash"
>;
type PackageReleaseArtifactSummaryFields = Pick<
Doc<"packageReleases">,
| "artifactKind"
| "clawpackSha256"
| "sha256hash"
| "clawpackSize"
| "clawpackFormat"
| "npmIntegrity"
| "npmShasum"
| "npmTarballName"
| "npmUnpackedSize"
| "npmFileCount"
>;
export function getPackageReleaseArtifactSha256(
release: PackageReleaseArtifactHashFields,
): string | null {
@@ -28,26 +13,3 @@ export function getPackageReleaseArtifactSha256(
}
return release.sha256hash ?? null;
}
export function summarizePackageReleaseArtifact(
release: PackageReleaseArtifactSummaryFields,
): PackageArtifactSummary {
if (release.artifactKind === "npm-pack") {
return {
kind: "npm-pack",
sha256: getPackageReleaseArtifactSha256(release) ?? undefined,
size: release.clawpackSize,
format: release.clawpackFormat ?? "tgz",
npmIntegrity: release.npmIntegrity,
npmShasum: release.npmShasum,
npmTarballName: release.npmTarballName,
npmUnpackedSize: release.npmUnpackedSize,
npmFileCount: release.npmFileCount,
};
}
return {
kind: "legacy-zip",
sha256: getPackageReleaseArtifactSha256(release) ?? undefined,
format: "zip",
};
}
-196
View File
@@ -83,7 +83,6 @@ const {
applySkillLineageCycleRepairInternalHandler,
inspectSkillLineageCycleInternalHandler,
nominateEmptySkillSpammersInternalHandler,
repairPackageLatestPointerHandler,
repairLegacyPluginSkillSpectorBatchInternalHandler,
repairLegacyPublisherOwnershipForUserHandler,
repairSkillLineageCyclesInternalHandler,
@@ -2007,198 +2006,3 @@ describe("maintenance empty skill nominations", () => {
]);
});
});
function makePackageLatestPointerRepairDb() {
const pkg = {
_id: "packages:kitchen-sink",
name: "@openclaw/kitchen-sink",
normalizedName: "@openclaw/kitchen-sink",
family: "code-plugin",
tags: { latest: "packageReleases:old" },
latestReleaseId: "packageReleases:old",
};
const releases = [
{
_id: "packageReleases:old",
packageId: pkg._id,
version: "0.2.11",
publicationStatus: "published",
changelog: "old",
summary: "old summary",
icon: "old-icon",
distTags: ["latest"],
compatibility: {},
verification: {},
runtimeId: "old-runtime",
sourceRepo: "https://github.com/openclaw/kitchen-sink",
artifactKind: "npm-pack",
clawpackSha256: "old-sha",
clawpackSize: 10,
clawpackFormat: "tgz",
createdAt: 100,
},
{
_id: "packageReleases:new",
packageId: pkg._id,
version: "0.2.12",
publicationStatus: "published",
changelog: "new",
summary: "new summary",
icon: "new-icon",
distTags: [],
compatibility: {},
verification: {},
runtimeId: "new-runtime",
sourceRepo: "https://github.com/openclaw/kitchen-sink",
artifactKind: "npm-pack",
clawpackSha256: "new-sha",
clawpackSize: 20,
clawpackFormat: "tgz",
createdAt: 200,
},
{
_id: "packageReleases:pending",
packageId: pkg._id,
version: "0.2.13",
publicationStatus: "pending",
changelog: "pending",
distTags: [],
compatibility: {},
verification: {},
artifactKind: "npm-pack",
clawpackSha256: "pending-sha",
createdAt: 300,
},
];
const patch = vi.fn();
const insert = vi.fn();
const query = vi.fn((table: string) => ({
withIndex: (_index: string, applyIndex: (q: QueryEq) => unknown) => {
const q = { eq: vi.fn() } as unknown as QueryEq;
vi.mocked(q.eq).mockReturnValue(q);
applyIndex(q);
if (table === "packages") {
return { unique: async () => pkg };
}
if (table === "packageReleases") {
return { collect: async () => releases };
}
throw new Error(`Unexpected table: ${table}`);
},
}));
return { db: { query, patch, insert }, pkg, releases, patch, insert };
}
describe("maintenance package latest pointer repair", () => {
it("dry-runs the canonical published release without writing", async () => {
const fixture = makePackageLatestPointerRepairDb();
const result = await repairPackageLatestPointerHandler(fixture as never, {
name: "@openclaw/kitchen-sink",
});
expect(result).toMatchObject({
dryRun: true,
confirmRequired: "repair-package-latest-pointer-2026-07-30",
previousLatestVersion: "0.2.11",
selectedVersion: "0.2.12",
eligibleReleaseCount: 2,
planToken: expect.stringMatching(/^[a-f\d]{64}$/),
releaseTagChanges: [
{
releaseId: "packageReleases:old",
version: "0.2.11",
action: "remove",
},
{
releaseId: "packageReleases:new",
version: "0.2.12",
action: "add",
},
],
});
expect(fixture.patch).not.toHaveBeenCalled();
expect(fixture.insert).not.toHaveBeenCalled();
});
it("requires the exact confirmation token before applying", async () => {
const fixture = makePackageLatestPointerRepairDb();
await expect(
repairPackageLatestPointerHandler(fixture as never, {
name: "@openclaw/kitchen-sink",
dryRun: false,
confirm: "wrong",
}),
).rejects.toThrow('Pass confirm="repair-package-latest-pointer-2026-07-30" to apply.');
expect(fixture.patch).not.toHaveBeenCalled();
});
it("requires the applied repair to match the dry-run plan", async () => {
const fixture = makePackageLatestPointerRepairDb();
await expect(
repairPackageLatestPointerHandler(fixture as never, {
name: "@openclaw/kitchen-sink",
dryRun: false,
confirm: "repair-package-latest-pointer-2026-07-30",
expectedPlanToken: "wrong",
}),
).rejects.toThrow(
"Package latest repair plan changed after dry-run; rerun the dry-run before applying.",
);
expect(fixture.patch).not.toHaveBeenCalled();
expect(fixture.insert).not.toHaveBeenCalled();
});
it("repoints the package, normalizes release tags, and audits the repair", async () => {
const fixture = makePackageLatestPointerRepairDb();
const preflight = await repairPackageLatestPointerHandler(fixture as never, {
name: "@openclaw/kitchen-sink",
});
const result = await repairPackageLatestPointerHandler(fixture as never, {
name: "@openclaw/kitchen-sink",
dryRun: false,
confirm: "repair-package-latest-pointer-2026-07-30",
expectedPlanToken: preflight.planToken,
});
expect(result.selectedVersion).toBe("0.2.12");
expect(fixture.patch).toHaveBeenCalledWith("packageReleases:old", { distTags: [] });
expect(fixture.patch).toHaveBeenCalledWith("packageReleases:new", {
distTags: ["latest"],
});
expect(fixture.patch).toHaveBeenCalledWith(
fixture.pkg._id,
expect.objectContaining({
tags: { latest: "packageReleases:new" },
latestReleaseId: "packageReleases:new",
summary: "new summary",
icon: "new-icon",
runtimeId: "new-runtime",
scanStatus: "not-run",
latestVersionSummary: expect.objectContaining({
version: "0.2.12",
artifact: expect.objectContaining({
kind: "npm-pack",
sha256: "new-sha",
}),
}),
}),
);
expect(fixture.insert).toHaveBeenCalledWith(
"auditLogs",
expect.objectContaining({
action: "package.latest_pointer.repair",
targetType: "package",
targetId: fixture.pkg._id,
metadata: expect.objectContaining({
previousLatestVersion: "0.2.11",
selectedVersion: "0.2.12",
}),
}),
);
});
});
-203
View File
@@ -1,5 +1,4 @@
import { ConvexError, v } from "convex/values";
import semver from "semver";
import { internal } from "./_generated/api";
import type { Doc, Id } from "./_generated/dataModel";
import type { ActionCtx, MutationCtx, QueryCtx } from "./_generated/server";
@@ -11,11 +10,7 @@ import {
syncPackageSearchDigestForPackageId,
} from "./functions";
import { assertRole, requireUserFromAction } from "./lib/access";
import { sha256Hex } from "./lib/clawpack";
import { summarizePackageReleaseArtifact } from "./lib/packageArtifacts";
import { normalizePackageName } from "./lib/packageRegistry";
import { extractPackageDigestFields } from "./lib/packageSearchDigest";
import { resolvePackageReleaseScanStatus } from "./lib/packageSecurity";
import {
derivePersonalPublisherHandle,
ensurePersonalPublisherForUser,
@@ -51,7 +46,6 @@ const PUBLISHER_ABUSE_SIGNAL_SMOKE_OWNER_KEY =
const PUBLISHER_ABUSE_SIGNAL_SMOKE_CONFIRM =
"create-publisher-abuse-hermit-digest-smoke-2026-07-03" as const;
const SKILL_LINEAGE_CYCLE_REPAIR_CONFIRM = "repair-skill-lineage-cycles-2026-07-23" as const;
const PACKAGE_LATEST_POINTER_REPAIR_CONFIRM = "repair-package-latest-pointer-2026-07-30" as const;
const legacyPluginSkillSpectorRepairFamilyValidator = v.union(
v.literal("code-plugin"),
v.literal("bundle-plugin"),
@@ -3279,203 +3273,6 @@ export const repairLegacyPublisherOwnershipForUser = internalMutation({
handler: repairLegacyPublisherOwnershipForUserHandler,
});
function isPublishedPackageRelease(release: Doc<"packageReleases">) {
return (
!release.softDeletedAt &&
release.ownerDeletedAt === undefined &&
(release.publicationStatus === undefined || release.publicationStatus === "published")
);
}
function comparePackageLatestCandidates(
family: Doc<"packages">["family"],
a: Doc<"packageReleases">,
b: Doc<"packageReleases">,
) {
if (family === "bundle-plugin") {
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
return a._id.localeCompare(b._id);
}
const aSemver = semver.valid(a.version);
const bSemver = semver.valid(b.version);
if (aSemver && bSemver) return semver.compare(aSemver, bSemver);
if (aSemver) return 1;
if (bSemver) return -1;
if (a.createdAt !== b.createdAt) return a.createdAt - b.createdAt;
return a._id.localeCompare(b._id);
}
async function buildPackageLatestPointerPlanToken(params: {
pkg: Doc<"packages">;
releases: Doc<"packageReleases">[];
selectedRelease: Doc<"packageReleases">;
releaseTagChanges: Array<{
releaseId: Id<"packageReleases">;
version: string;
action: "add" | "remove";
}>;
}) {
const payload = JSON.stringify({
packageLatestReleaseId: params.pkg.latestReleaseId ?? null,
packageTags: params.pkg.tags,
selectedRelease: params.selectedRelease,
releaseTags: params.releases
.map((release) => ({ releaseId: release._id, distTags: release.distTags }))
.sort((a, b) => a.releaseId.localeCompare(b.releaseId)),
releaseTagChanges: params.releaseTagChanges,
});
return await sha256Hex(new TextEncoder().encode(payload));
}
export async function repairPackageLatestPointerHandler(
ctx: Pick<MutationCtx, "db">,
args: {
name: string;
dryRun?: boolean;
confirm?: string;
expectedPlanToken?: string;
},
) {
const dryRun = args.dryRun !== false;
if (!dryRun && args.confirm !== PACKAGE_LATEST_POINTER_REPAIR_CONFIRM) {
throw new ConvexError(`Pass confirm="${PACKAGE_LATEST_POINTER_REPAIR_CONFIRM}" to apply.`);
}
const normalizedName = normalizePackageName(args.name);
const pkg = await ctx.db
.query("packages")
.withIndex("by_name", (q) => q.eq("normalizedName", normalizedName))
.unique();
if (!pkg || pkg.softDeletedAt) {
throw new ConvexError(`Active package not found: ${normalizedName}`);
}
const releases = await ctx.db
.query("packageReleases")
.withIndex("by_package", (q) => q.eq("packageId", pkg._id))
.collect();
const eligibleReleases = releases.filter(isPublishedPackageRelease);
const selectedRelease = eligibleReleases.reduce<Doc<"packageReleases"> | null>(
(best, release) =>
!best || comparePackageLatestCandidates(pkg.family, best, release) < 0 ? release : best,
null,
);
if (!selectedRelease) {
throw new ConvexError(`No published releases found for package: ${normalizedName}`);
}
const releaseTagChanges = releases.flatMap((release) => {
const hadLatest = release.distTags.includes("latest");
const shouldHaveLatest = release._id === selectedRelease._id;
return hadLatest === shouldHaveLatest
? []
: [
{
releaseId: release._id,
version: release.version,
action: shouldHaveLatest ? ("add" as const) : ("remove" as const),
},
];
});
const previousLatestRelease = pkg.latestReleaseId
? (releases.find((release) => release._id === pkg.latestReleaseId) ?? null)
: null;
const planToken = await buildPackageLatestPointerPlanToken({
pkg,
releases,
selectedRelease,
releaseTagChanges,
});
const result = {
dryRun,
confirmRequired: dryRun ? PACKAGE_LATEST_POINTER_REPAIR_CONFIRM : undefined,
packageId: pkg._id,
packageName: pkg.name,
normalizedName,
family: pkg.family,
releasesScanned: releases.length,
eligibleReleaseCount: eligibleReleases.length,
previousLatestReleaseId: pkg.latestReleaseId,
previousLatestVersion: previousLatestRelease?.version,
selectedReleaseId: selectedRelease._id,
selectedVersion: selectedRelease.version,
releaseTagChanges,
planToken,
};
if (dryRun) return result;
if (!args.expectedPlanToken || args.expectedPlanToken !== planToken) {
throw new ConvexError(
"Package latest repair plan changed after dry-run; rerun the dry-run before applying.",
);
}
for (const change of releaseTagChanges) {
const release = releases.find((candidate) => candidate._id === change.releaseId);
if (!release) continue;
await ctx.db.patch(release._id, {
distTags:
change.action === "add"
? [...release.distTags, "latest"]
: release.distTags.filter((tag) => tag !== "latest"),
});
}
const now = Date.now();
const packagePatch: Partial<Doc<"packages">> = {
tags: { ...pkg.tags, latest: selectedRelease._id },
latestReleaseId: selectedRelease._id,
latestVersionSummary: {
version: selectedRelease.version,
createdAt: selectedRelease.createdAt,
changelog: selectedRelease.changelog,
icon: selectedRelease.icon,
compatibility: selectedRelease.compatibility,
verification: selectedRelease.verification,
artifact: summarizePackageReleaseArtifact(selectedRelease),
},
summary: selectedRelease.summary,
icon: selectedRelease.icon,
sourceRepo: selectedRelease.sourceRepo ?? selectedRelease.verification?.sourceRepo,
runtimeId: selectedRelease.runtimeId,
compatibility: selectedRelease.compatibility,
verification: selectedRelease.verification,
scanStatus: resolvePackageReleaseScanStatus(selectedRelease),
updatedAt: now,
};
// The wrapped internal mutation DB runs the package trigger in functions.ts,
// which refreshes package search digests from this patched document.
await ctx.db.patch(pkg._id, packagePatch);
await ctx.db.insert("auditLogs", {
action: "package.latest_pointer.repair",
targetType: "package",
targetId: pkg._id,
metadata: {
normalizedName,
previousLatestReleaseId: pkg.latestReleaseId,
previousLatestVersion: previousLatestRelease?.version,
selectedReleaseId: selectedRelease._id,
selectedVersion: selectedRelease.version,
releaseTagChanges,
},
createdAt: now,
});
return result;
}
// One-off production repair. Run dry first, then pass the exact confirmation token.
// npx convex run maintenance:repairPackageLatestPointer '{"name":"@scope/package"}' --prod
export const repairPackageLatestPointer = internalMutation({
args: {
name: v.string(),
dryRun: v.optional(v.boolean()),
confirm: v.optional(v.string()),
expectedPlanToken: v.optional(v.string()),
},
handler: repairPackageLatestPointerHandler,
});
function clampInt(value: number, min: number, max: number) {
const rounded = Math.trunc(value);
if (!Number.isFinite(rounded)) return min;
+40 -8
View File
@@ -68,10 +68,7 @@ import { normalizeGitHubRepository } from "./lib/githubActionsOidc";
import { readGlobalPublicPluginsCount } from "./lib/globalStats";
import { toDayKey } from "./lib/leaderboards";
import { isOfficialPublisher } from "./lib/officialPublishers";
import {
getPackageReleaseArtifactSha256,
summarizePackageReleaseArtifact,
} from "./lib/packageArtifacts";
import { getPackageReleaseArtifactSha256 } from "./lib/packageArtifacts";
import {
assertPackageVersion,
derivePluginManifestSummary,
@@ -1219,7 +1216,7 @@ function toPublicPackage(
latestRelease === undefined
? pkg.latestVersionSummary?.artifact
: isPublishedPackageRelease(latestRelease)
? summarizePackageReleaseArtifact(latestRelease)
? packageArtifactSummary(latestRelease)
: undefined,
clawManifestSummary:
pkg.family === "claw" && isPublishedPackageRelease(latestRelease)
@@ -1348,6 +1345,41 @@ async function paginatePublishedPackageReleases(
return { page, isDone, continueCursor: isDone ? "" : continueCursor };
}
function packageArtifactSummary(
release: Pick<
Doc<"packageReleases">,
| "artifactKind"
| "clawpackSha256"
| "sha256hash"
| "clawpackSize"
| "clawpackFormat"
| "npmIntegrity"
| "npmShasum"
| "npmTarballName"
| "npmUnpackedSize"
| "npmFileCount"
>,
): PackageArtifactSummary {
if (release.artifactKind === "npm-pack") {
return {
kind: "npm-pack",
sha256: getPackageReleaseArtifactSha256(release) ?? undefined,
size: release.clawpackSize,
format: release.clawpackFormat ?? "tgz",
npmIntegrity: release.npmIntegrity,
npmShasum: release.npmShasum,
npmTarballName: release.npmTarballName,
npmUnpackedSize: release.npmUnpackedSize,
npmFileCount: release.npmFileCount,
};
}
return {
kind: "legacy-zip",
sha256: getPackageReleaseArtifactSha256(release) ?? undefined,
format: "zip",
};
}
function digestMatchesFilters(
digest: PackageDigestLike,
args: {
@@ -5740,7 +5772,7 @@ function packageLatestSummaryFromRelease(release: Doc<"packageReleases"> | null)
icon: release.icon,
compatibility: release.compatibility,
verification: release.verification,
artifact: summarizePackageReleaseArtifact(release),
artifact: packageArtifactSummary(release),
}
: undefined;
}
@@ -5841,7 +5873,7 @@ async function restorePackageDoc(
icon: nextLatest.icon,
compatibility: nextLatest.compatibility,
verification: nextLatest.verification,
artifact: summarizePackageReleaseArtifact(nextLatest),
artifact: packageArtifactSummary(nextLatest),
}
: undefined,
summary: nextLatest?.summary,
@@ -10889,7 +10921,7 @@ export const insertReleaseInternal = internalMutation({
icon: args.icon,
compatibility: args.compatibility,
verification: releaseVerification,
artifact: summarizePackageReleaseArtifact(args),
artifact: packageArtifactSummary(args),
}
: pkg.latestVersionSummary,
tags: nextTags,