mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix(api): preserve latest across package backports (#3302)
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
|
||||
### Fixes
|
||||
|
||||
- API: keep older code-plugin and Claw backports from replacing the highest-semver `latest` release while preserving custom distribution tags.
|
||||
- Integrations: truncate publisher-controlled Discord webhook titles to the platform's 256-character embed limit.
|
||||
- Security: recover scheduled temporal publisher-abuse scans from strict Convex payload validation failures without leaving zombie running runs (thanks @jesse-merhi).
|
||||
- CI: retry transient Convex preview provisioning failures under fresh deployment names during Vercel preview builds.
|
||||
|
||||
@@ -7915,6 +7915,7 @@ describe("packages public queries", () => {
|
||||
expect(ctx.patch).toHaveBeenCalledWith("packageReleases:pending", {
|
||||
publicationStatus: "published",
|
||||
pendingPublication: undefined,
|
||||
distTags: ["latest"],
|
||||
verification: { scanStatus: "clean" },
|
||||
});
|
||||
await flushScheduledPackageReleaseTagCleanup(ctx);
|
||||
@@ -7931,6 +7932,74 @@ describe("packages public queries", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the current latest when finalizing an older package release", async () => {
|
||||
const existingPackage = makePackageDoc({
|
||||
latestReleaseId: "packageReleases:latest",
|
||||
latestVersionSummary: { version: "2.0.0" },
|
||||
tags: { latest: "packageReleases:latest" },
|
||||
summary: "current summary",
|
||||
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
||||
});
|
||||
const latestRelease = makeReleaseDoc({
|
||||
_id: "packageReleases:latest",
|
||||
version: "2.0.0",
|
||||
distTags: ["latest"],
|
||||
publicationStatus: "published",
|
||||
});
|
||||
const pendingRelease = makeReleaseDoc({
|
||||
_id: "packageReleases:pending",
|
||||
version: "1.0.1",
|
||||
publicationStatus: "pending",
|
||||
pendingPublication: {
|
||||
displayName: "Demo Plugin v1",
|
||||
tags: ["latest", "v1"],
|
||||
channel: "community",
|
||||
isOfficial: false,
|
||||
},
|
||||
distTags: ["latest", "v1"],
|
||||
summary: "backport summary",
|
||||
changelog: "backport",
|
||||
integritySha256: "backport-sha",
|
||||
verification: { scanStatus: "pending" },
|
||||
llmAnalysis: {
|
||||
status: "clean",
|
||||
verdict: "clean",
|
||||
checkedAt: 1_700_000_000_000,
|
||||
},
|
||||
});
|
||||
const ctx = makeInsertReleaseCtx(existingPackage, [latestRelease, pendingRelease], {
|
||||
"packageReleases:latest": latestRelease,
|
||||
"packageReleases:pending": pendingRelease,
|
||||
"packages:demo": existingPackage,
|
||||
});
|
||||
|
||||
await publishPendingReleaseInternalHandler(ctx, {
|
||||
releaseId: "packageReleases:pending",
|
||||
});
|
||||
|
||||
expect(ctx.patch).toHaveBeenCalledWith(
|
||||
"packageReleases:pending",
|
||||
expect.objectContaining({
|
||||
publicationStatus: "published",
|
||||
distTags: ["v1"],
|
||||
}),
|
||||
);
|
||||
expect(ctx.patch).toHaveBeenCalledWith(
|
||||
"packages:demo",
|
||||
expect.objectContaining({
|
||||
latestReleaseId: "packageReleases:latest",
|
||||
latestVersionSummary: { version: "2.0.0" },
|
||||
summary: "current summary",
|
||||
tags: {
|
||||
latest: "packageReleases:latest",
|
||||
v1: "packageReleases:pending",
|
||||
},
|
||||
stats: { downloads: 0, installs: 0, stars: 0, versions: 2 },
|
||||
}),
|
||||
);
|
||||
expect(ctx.scheduler.runAfter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("publishes a pending release without reading its file-heavy release history", async () => {
|
||||
const priorRelease = makeReleaseDoc({
|
||||
_id: "packageReleases:old",
|
||||
@@ -8333,6 +8402,63 @@ describe("packages public queries", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the latest tag when the version summary and release pointer are stale", async () => {
|
||||
const existingPackage = makePackageDoc({
|
||||
latestReleaseId: "packageReleases:stale",
|
||||
latestVersionSummary: undefined,
|
||||
tags: { latest: "packageReleases:latest" },
|
||||
summary: "current summary",
|
||||
stats: { downloads: 0, installs: 0, stars: 0, versions: 1 },
|
||||
});
|
||||
const staleRelease = makeReleaseDoc({
|
||||
_id: "packageReleases:stale",
|
||||
version: "1.0.0",
|
||||
distTags: [],
|
||||
});
|
||||
const latestRelease = makeReleaseDoc({
|
||||
_id: "packageReleases:latest",
|
||||
version: "2.0.0",
|
||||
distTags: ["latest"],
|
||||
});
|
||||
const ctx = makeInsertReleaseCtx(existingPackage, [staleRelease, latestRelease]);
|
||||
|
||||
await insertReleaseInternalHandler(ctx, {
|
||||
actorUserId: "users:owner",
|
||||
ownerUserId: "users:owner",
|
||||
name: "demo-plugin",
|
||||
displayName: "Demo Plugin v1",
|
||||
family: "code-plugin",
|
||||
version: "1.5.0",
|
||||
changelog: "backport",
|
||||
tags: ["latest", "v1"],
|
||||
summary: "backport summary",
|
||||
files: [],
|
||||
integritySha256: "backport-sha",
|
||||
});
|
||||
|
||||
expect(ctx.insert).toHaveBeenCalledWith(
|
||||
"packageReleases",
|
||||
expect.objectContaining({
|
||||
version: "1.5.0",
|
||||
distTags: ["v1"],
|
||||
}),
|
||||
);
|
||||
expect(ctx.patch).toHaveBeenCalledWith(
|
||||
"packages:demo",
|
||||
expect.objectContaining({
|
||||
latestReleaseId: "packageReleases:stale",
|
||||
latestVersionSummary: undefined,
|
||||
summary: "current summary",
|
||||
tags: {
|
||||
latest: "packageReleases:latest",
|
||||
v1: "packageReleases:new",
|
||||
},
|
||||
stats: { downloads: 0, installs: 0, stars: 0, versions: 2 },
|
||||
}),
|
||||
);
|
||||
expect(ctx.scheduler.runAfter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("publishes suspicious prepublication plugin results with a suspicious scan status", async () => {
|
||||
const ctx = makeInsertReleaseCtx(makePackageDoc());
|
||||
const llmAnalysis = {
|
||||
|
||||
+75
-6
@@ -10225,6 +10225,35 @@ type PackageReleaseTagCleanupAssignment = {
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
function resolvePackageReleaseTagsForPublish(params: {
|
||||
family: Doc<"packages">["family"];
|
||||
currentLatestExists: boolean;
|
||||
currentLatestVersion?: string;
|
||||
candidateVersion: string;
|
||||
requestedTags: string[];
|
||||
}) {
|
||||
const requestedLatest = params.requestedTags.some((tag) => tag.toLowerCase() === "latest");
|
||||
const currentLatestSemver = params.currentLatestVersion
|
||||
? semver.valid(params.currentLatestVersion)
|
||||
: null;
|
||||
const candidateSemver = semver.valid(params.candidateVersion);
|
||||
const latestUsesSemver = params.family === "code-plugin" || params.family === "claw";
|
||||
// `latest` is reserved for the highest semver on versioned package families.
|
||||
// Callers default to this tag, so accepting it blindly would let backports roll the catalog back.
|
||||
const shouldPromoteLatest =
|
||||
requestedLatest &&
|
||||
(!latestUsesSemver ||
|
||||
!params.currentLatestExists ||
|
||||
(currentLatestSemver !== null &&
|
||||
candidateSemver !== null &&
|
||||
semver.gt(candidateSemver, currentLatestSemver)));
|
||||
const effectiveTags = [
|
||||
...new Set(params.requestedTags.filter((tag) => tag.toLowerCase() !== "latest")),
|
||||
];
|
||||
if (shouldPromoteLatest) effectiveTags.push("latest");
|
||||
return { effectiveTags, shouldPromoteLatest };
|
||||
}
|
||||
|
||||
function getPackageTagReleaseId(
|
||||
pkg: Pick<Doc<"packages">, "_id" | "latestReleaseId" | "tags">,
|
||||
tag: string,
|
||||
@@ -10232,6 +10261,32 @@ function getPackageTagReleaseId(
|
||||
return tag === "latest" ? (pkg.tags.latest ?? pkg.latestReleaseId) : pkg.tags[tag];
|
||||
}
|
||||
|
||||
async function resolvePackageCurrentLatestForPublish(
|
||||
ctx: Pick<MutationCtx, "db">,
|
||||
pkg: Pick<
|
||||
Doc<"packages">,
|
||||
"_id" | "family" | "latestReleaseId" | "latestVersionSummary" | "tags"
|
||||
>,
|
||||
) {
|
||||
const latestReleaseId = getPackageTagReleaseId(pkg, "latest");
|
||||
if (!latestReleaseId) return { exists: false, version: undefined };
|
||||
if (pkg.family !== "code-plugin" && pkg.family !== "claw") {
|
||||
return { exists: true, version: pkg.latestVersionSummary?.version };
|
||||
}
|
||||
|
||||
const latestRelease = await ctx.db.get(latestReleaseId);
|
||||
if (
|
||||
latestRelease &&
|
||||
latestRelease.packageId === pkg._id &&
|
||||
isPublishedPackageRelease(latestRelease)
|
||||
) {
|
||||
return { exists: true, version: latestRelease.version };
|
||||
}
|
||||
// Keep the current pointer when its release row is missing or unusable.
|
||||
// Promotion without a comparable version could roll installs back to a backport.
|
||||
return { exists: true, version: pkg.latestVersionSummary?.version };
|
||||
}
|
||||
|
||||
function planPackageReleaseTagReassignment(
|
||||
pkg: Pick<Doc<"packages">, "_id" | "latestReleaseId" | "tags">,
|
||||
releaseId: Id<"packageReleases">,
|
||||
@@ -10399,8 +10454,14 @@ export const publishPendingReleaseInternal = internalMutation({
|
||||
|
||||
const now = Date.now();
|
||||
const metadata = pendingPackagePublicationMetadata(release);
|
||||
const effectiveTags = stringArrayPendingField(metadata, "tags") ?? release.distTags ?? [];
|
||||
const shouldPromoteLatest = effectiveTags.includes("latest");
|
||||
const currentLatest = await resolvePackageCurrentLatestForPublish(ctx, pkg);
|
||||
const { effectiveTags, shouldPromoteLatest } = resolvePackageReleaseTagsForPublish({
|
||||
family: pkg.family,
|
||||
currentLatestExists: currentLatest.exists,
|
||||
currentLatestVersion: currentLatest.version,
|
||||
candidateVersion: release.version,
|
||||
requestedTags: stringArrayPendingField(metadata, "tags") ?? release.distTags ?? [],
|
||||
});
|
||||
const scanStatus = resolvePackageReleaseScanStatus(release);
|
||||
const releaseVerification = release.verification
|
||||
? { ...release.verification, scanStatus }
|
||||
@@ -10409,6 +10470,7 @@ export const publishPendingReleaseInternal = internalMutation({
|
||||
...release,
|
||||
publicationStatus: "published" as const,
|
||||
pendingPublication: undefined,
|
||||
distTags: effectiveTags,
|
||||
verification: releaseVerification,
|
||||
} as Doc<"packageReleases">;
|
||||
|
||||
@@ -10421,6 +10483,7 @@ export const publishPendingReleaseInternal = internalMutation({
|
||||
await ctx.db.patch(release._id, {
|
||||
publicationStatus: "published",
|
||||
pendingPublication: undefined,
|
||||
distTags: effectiveTags,
|
||||
verification: releaseVerification,
|
||||
});
|
||||
|
||||
@@ -10738,10 +10801,16 @@ export const insertReleaseInternal = internalMutation({
|
||||
);
|
||||
}
|
||||
}
|
||||
const shouldPromoteLatest = args.tags.includes("latest");
|
||||
const effectiveTags = shouldPromoteLatest
|
||||
? Array.from(new Set([...args.tags, "latest"]))
|
||||
: args.tags;
|
||||
const currentLatest = existing
|
||||
? await resolvePackageCurrentLatestForPublish(ctx, existing)
|
||||
: { exists: false, version: undefined };
|
||||
const { effectiveTags, shouldPromoteLatest } = resolvePackageReleaseTagsForPublish({
|
||||
family: args.family,
|
||||
currentLatestExists: currentLatest.exists,
|
||||
currentLatestVersion: currentLatest.version,
|
||||
candidateVersion: args.version,
|
||||
requestedTags: args.tags,
|
||||
});
|
||||
|
||||
const releaseId = await ctx.db.insert("packageReleases", {
|
||||
packageId: pkgId,
|
||||
|
||||
Reference in New Issue
Block a user