fix: repair stale package latest pointers (#3303)

This commit is contained in:
Vincent Koc
2026-07-30 18:19:59 +08:00
committed by GitHub
parent be94d781ae
commit 5a1d2fe7bc
4 changed files with 386 additions and 40 deletions
+38
View File
@@ -1,3 +1,4 @@
import type { PackageArtifactSummary } from "clawhub-schema";
import type { Doc } from "../_generated/dataModel";
type PackageReleaseArtifactHashFields = Pick<
@@ -5,6 +6,20 @@ 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 {
@@ -13,3 +28,26 @@ 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",
};
}
+174
View File
@@ -83,6 +83,7 @@ const {
applySkillLineageCycleRepairInternalHandler,
inspectSkillLineageCycleInternalHandler,
nominateEmptySkillSpammersInternalHandler,
repairPackageLatestPointerHandler,
repairLegacyPluginSkillSpectorBatchInternalHandler,
repairLegacyPublisherOwnershipForUserHandler,
repairSkillLineageCyclesInternalHandler,
@@ -2006,3 +2007,176 @@ 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,
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("repoints the package, normalizes release tags, and audits the repair", async () => {
const fixture = makePackageLatestPointerRepairDb();
const result = await repairPackageLatestPointerHandler(fixture as never, {
name: "@openclaw/kitchen-sink",
dryRun: false,
confirm: "repair-package-latest-pointer-2026-07-30",
});
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",
}),
}),
);
});
});
+166
View File
@@ -1,4 +1,5 @@
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";
@@ -10,7 +11,10 @@ import {
syncPackageSearchDigestForPackageId,
} from "./functions";
import { assertRole, requireUserFromAction } from "./lib/access";
import { summarizePackageReleaseArtifact } from "./lib/packageArtifacts";
import { normalizePackageName } from "./lib/packageRegistry";
import { extractPackageDigestFields } from "./lib/packageSearchDigest";
import { resolvePackageReleaseScanStatus } from "./lib/packageSecurity";
import {
derivePersonalPublisherHandle,
ensurePersonalPublisherForUser,
@@ -46,6 +50,7 @@ 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"),
@@ -3273,6 +3278,167 @@ 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);
}
export async function repairPackageLatestPointerHandler(
ctx: Pick<MutationCtx, "db">,
args: {
name: string;
dryRun?: boolean;
confirm?: 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 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,
};
if (dryRun) return result;
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()),
},
handler: repairPackageLatestPointerHandler,
});
function clampInt(value: number, min: number, max: number) {
const rounded = Math.trunc(value);
if (!Number.isFinite(rounded)) return min;
+8 -40
View File
@@ -68,7 +68,10 @@ import { normalizeGitHubRepository } from "./lib/githubActionsOidc";
import { readGlobalPublicPluginsCount } from "./lib/globalStats";
import { toDayKey } from "./lib/leaderboards";
import { isOfficialPublisher } from "./lib/officialPublishers";
import { getPackageReleaseArtifactSha256 } from "./lib/packageArtifacts";
import {
getPackageReleaseArtifactSha256,
summarizePackageReleaseArtifact,
} from "./lib/packageArtifacts";
import {
assertPackageVersion,
derivePluginManifestSummary,
@@ -1216,7 +1219,7 @@ function toPublicPackage(
latestRelease === undefined
? pkg.latestVersionSummary?.artifact
: isPublishedPackageRelease(latestRelease)
? packageArtifactSummary(latestRelease)
? summarizePackageReleaseArtifact(latestRelease)
: undefined,
clawManifestSummary:
pkg.family === "claw" && isPublishedPackageRelease(latestRelease)
@@ -1345,41 +1348,6 @@ 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: {
@@ -5772,7 +5740,7 @@ function packageLatestSummaryFromRelease(release: Doc<"packageReleases"> | null)
icon: release.icon,
compatibility: release.compatibility,
verification: release.verification,
artifact: packageArtifactSummary(release),
artifact: summarizePackageReleaseArtifact(release),
}
: undefined;
}
@@ -5873,7 +5841,7 @@ async function restorePackageDoc(
icon: nextLatest.icon,
compatibility: nextLatest.compatibility,
verification: nextLatest.verification,
artifact: packageArtifactSummary(nextLatest),
artifact: summarizePackageReleaseArtifact(nextLatest),
}
: undefined,
summary: nextLatest?.summary,
@@ -10921,7 +10889,7 @@ export const insertReleaseInternal = internalMutation({
icon: args.icon,
compatibility: args.compatibility,
verification: releaseVerification,
artifact: packageArtifactSummary(args),
artifact: summarizePackageReleaseArtifact(args),
}
: pkg.latestVersionSummary,
tags: nextTags,