diff --git a/plugins/cad/src/components/AddPackagePage/AddPackagePage.tsx b/plugins/cad/src/components/AddPackagePage/AddPackagePage.tsx index eed0e2dd..d6606d6a 100644 --- a/plugins/cad/src/components/AddPackagePage/AddPackagePage.tsx +++ b/plugins/cad/src/components/AddPackagePage/AddPackagePage.tsx @@ -206,7 +206,9 @@ export const AddPackagePage = ({ action }: AddPackagePageProps) => { ]); allRepositories.current = thisAllRepositories; - allClonablePackageRevisions.current = allPackages.filter(canCloneRevision); + allClonablePackageRevisions.current = allPackages.filter(packageRevision => + canCloneRevision(packageRevision, allPackages), + ); const thisRepository = repositoryName ? getRepository(thisAllRepositories, repositoryName) : undefined; diff --git a/plugins/cad/src/types/PackageRevision.ts b/plugins/cad/src/types/PackageRevision.ts index 7aaa4c0b..43936316 100644 --- a/plugins/cad/src/types/PackageRevision.ts +++ b/plugins/cad/src/types/PackageRevision.ts @@ -35,7 +35,7 @@ export type PackageRevisionSpec = { packageName: string; repository: string; workspaceName?: string; - revision?: string; + revision?: string | number; lifecycle: PackageRevisionLifecycle; tasks: PackageRevisionTask[]; readinessGates?: ReadinessGate[]; @@ -53,6 +53,7 @@ export type PackageRevisionTask = { clone?: PackageRevisionTaskClone; update?: PackageRevisionTaskUpdate; eval?: PackageRevisionTaskEval; + edit?: PackageRevisionTaskEdit; }; export type PackageRevisionTaskInit = { @@ -61,6 +62,12 @@ export type PackageRevisionTaskInit = { site?: string; }; +export type PackageRevisionTaskEdit = { + sourceRef: { + name: string; + }; +}; + export type PackageRevisionTaskClone = { upstreamRef: PackageRevisionTaskUpstreamRef; }; diff --git a/plugins/cad/src/utils/packageRevision.ts b/plugins/cad/src/utils/packageRevision.ts index 2d05897a..bf0aa3fe 100644 --- a/plugins/cad/src/utils/packageRevision.ts +++ b/plugins/cad/src/utils/packageRevision.ts @@ -25,19 +25,22 @@ import { } from '../types/PackageRevision'; import { toLowerCase } from './string'; -const getRevisionNumber = (revision: string, defaultNumber: number = NaN): number => { - if (revision && revision.startsWith('v')) { - const revisionNumber = parseInt(revision.substring(1), 10); - +const getRevisionNumber = (revision: string | number, defaultNumber: number = Number.NaN): number => { + // Handle integer revision (new Porch API returns int instead of string) + if (typeof revision === 'number' && Number.isInteger(revision)) { + return revision; + } + if (revision && String(revision).startsWith('v')) { + const revisionNumber = Number.parseInt(String(revision).substring(1), 10); if (Number.isInteger(revisionNumber)) { return revisionNumber; } } - return defaultNumber; }; -const getNextRevision = (revision: string): string => { +const getNextRevision = (revision: string | number): string => { + if (revision === -1 || revision === '-1') return 'v1'; const revisionNumber = getRevisionNumber(revision, 0); return `v${revisionNumber + 1}`; @@ -71,17 +74,60 @@ export const getUpstreamPackageRevisionDetails = ( return undefined; }; -export const isLatestPublishedRevision = (packageRevision: PackageRevision): boolean => { - return ( - packageRevision.spec.lifecycle === PackageRevisionLifecycle.PUBLISHED && - !!packageRevision.metadata.labels?.['kpt.dev/latest-revision'] - ); +const getWorkspaceNameVersion = (workspaceName: string): number[] => { + // 'main' gets version [0] (lowest) + if (!workspaceName || workspaceName === 'main') return [0]; + // 'v3.0.0' → [3, 0, 0] + const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(workspaceName); + if (match) return [Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)]; + // fallback + return [0]; }; -export const findLatestPublishedRevision = (packageRevisions: PackageRevision[]): PackageRevision | undefined => { - const latestPublishedRevision = packageRevisions.find(isLatestPublishedRevision); +const compareWorkspaceVersions = (a: string, b: string): number => { + const aVer = getWorkspaceNameVersion(a); + const bVer = getWorkspaceNameVersion(b); + for (let i = 0; i < Math.max(aVer.length, bVer.length); i++) { + const diff = (bVer[i] ?? 0) - (aVer[i] ?? 0); + if (diff !== 0) return diff; + } + return 0; +}; + +export const isLatestPublishedRevision = ( + packageRevision: PackageRevision, + allRevisions?: PackageRevision[], +): boolean => { + if (packageRevision.spec.lifecycle !== PackageRevisionLifecycle.PUBLISHED) { + return false; + } + + // External catalog repos use revision -1 with no labels + if (packageRevision.spec.revision === -1 || packageRevision.spec.revision === '-1') { + if (allRevisions) { + // Find all revisions for the same package in the same repo + const siblings = allRevisions.filter( + r => + r.spec.packageName === packageRevision.spec.packageName && + r.spec.repository === packageRevision.spec.repository && + r.spec.lifecycle === PackageRevisionLifecycle.PUBLISHED && + (r.spec.revision === -1 || r.spec.revision === '-1'), + ); + // Sort by workspaceName version descending + const sorted = [...siblings].sort((a, b) => + compareWorkspaceVersions(a.spec.workspaceName ?? '', b.spec.workspaceName ?? ''), + ); + // Only the highest versioned one is "latest" + return sorted[0]?.metadata.name === packageRevision.metadata.name; + } + return true; + } + + return !!packageRevision.metadata.labels?.['kpt.dev/latest-revision']; +}; - return latestPublishedRevision; +export const findLatestPublishedRevision = (packageRevisions: PackageRevision[]): PackageRevision | undefined => { + return packageRevisions.find(r => isLatestPublishedRevision(r, packageRevisions)); }; export const findPackageRevision = ( @@ -99,7 +145,12 @@ export const findPackageRevision = ( }; export const getPackageRevisionRevision = (packageRevision: PackageRevision): string => { - return packageRevision.spec.revision || ''; + const revision = packageRevision.spec.revision; + if (revision === undefined || revision === null) return ''; + if (typeof revision === 'number') { + return revision === -1 ? '-1' : `v${revision}`; + } + return String(revision); }; export const isPublishedRevision = (packageRevision: PackageRevision): boolean => { @@ -107,14 +158,14 @@ export const isPublishedRevision = (packageRevision: PackageRevision): boolean = }; export const getPackageRevisionTitle = (packageRevision: PackageRevision, packageNameOnly: boolean = false): string => { - const { packageName, lifecycle, revision } = packageRevision.spec; + const { packageName, lifecycle } = packageRevision.spec; if (packageNameOnly) { return packageName; } if (isPublishedRevision(packageRevision)) { - return `${packageName} ${revision}`; + return `${packageName} ${getPackageRevisionRevision(packageRevision)}`; } return `${packageName} ${toLowerCase(lifecycle)} revision`; @@ -130,7 +181,7 @@ export const filterPackageRevisions = ( packageRevision.spec.packageName === packageName && packageRevision.spec.repository === repositoryName && (!isPublishedRevision(packageRevision) || - Number.isFinite(getRevisionNumber(packageRevision.spec.revision || ''))), + Number.isFinite(getRevisionNumber(packageRevision.spec.revision ?? ''))), ); }; @@ -146,8 +197,8 @@ export const getPackageRevision = (packageRevisions: PackageRevision[], fullPack return packageRevision; }; -export const canCloneRevision = (packageRevision: PackageRevision): boolean => { - return isLatestPublishedRevision(packageRevision); +export const canCloneRevision = (packageRevision: PackageRevision, allRevisions?: PackageRevision[]): boolean => { + return isLatestPublishedRevision(packageRevision, allRevisions); }; export const isNotAPublishedRevision = (packageRevision: PackageRevision): boolean => { @@ -182,6 +233,17 @@ export const getCloneTask = (fullPackageName: string): PackageRevisionTask => { return cloneTask; }; +export const getEditTask = (fullPackageName: string): PackageRevisionTask => { + return { + type: 'edit', + edit: { + sourceRef: { + name: fullPackageName, + }, + }, + }; +}; + export const getUpdateTask = (fullUpstreamPackageName: string): PackageRevisionTask => { const updateTask: PackageRevisionTask = { type: 'update', @@ -223,16 +285,12 @@ export const getPackageRevisionResource = ( }; export const getNextPackageRevisionResource = (currentRevision: PackageRevision): PackageRevision => { - const { repository, packageName, tasks } = currentRevision.spec; + const { repository, packageName } = currentRevision.spec; const nextRevision = getNextRevision(getPackageRevisionRevision(currentRevision)); - const resource = getPackageRevisionResource( - repository, - packageName, - nextRevision, - PackageRevisionLifecycle.DRAFT, - cloneDeep(tasks), - ); + const resource = getPackageRevisionResource(repository, packageName, nextRevision, PackageRevisionLifecycle.DRAFT, [ + getEditTask(currentRevision.metadata.name), + ]); return resource; }; @@ -286,9 +344,8 @@ export const getPackageConditions = (packageRevision: PackageRevision): Conditio const allConditions = cloneDeep(conditions); const readinessConditions = readinessGates.map(gate => gate.conditionType); - const existingConditions = conditions.map(condition => condition.type); - - const missingReadinessConditions = readinessConditions.filter(type => !existingConditions.includes(type)); + const existingConditions = new Set(conditions.map(condition => condition.type)); + const missingReadinessConditions = readinessConditions.filter(type => !existingConditions.has(type)); missingReadinessConditions.forEach(type => allConditions.push({ type: type, diff --git a/plugins/cad/src/utils/packageSummary.ts b/plugins/cad/src/utils/packageSummary.ts index e30d53ac..5f4786b8 100644 --- a/plugins/cad/src/utils/packageSummary.ts +++ b/plugins/cad/src/utils/packageSummary.ts @@ -53,7 +53,8 @@ export const getPackageSummariesForRepository = ( allRepositories: Repository[], ): PackageSummary[] => { const latestPackageRevisions = packageRevisions.filter( - packageRevision => isNotAPublishedRevision(packageRevision) || isLatestPublishedRevision(packageRevision), + packageRevision => + isNotAPublishedRevision(packageRevision) || isLatestPublishedRevision(packageRevision, packageRevisions), ); latestPackageRevisions.sort(sortByPackageNameAndRevisionComparison); diff --git a/yarn.lock b/yarn.lock index c5b51946..1822455d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7580,13 +7580,13 @@ before-after-hook@^2.2.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.3.tgz#c51e809c81a4e354084422b9b26bad88249c517c" integrity sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ== -better-sqlite3@^7.5.0: - version "7.6.2" - resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-7.6.2.tgz#47cd8cad5b9573cace535f950ac321166bc31384" - integrity sha512-S5zIU1Hink2AH4xPsN0W43T1/AJ5jrPh7Oy07ocuW/AKYYY02GWzz9NH0nbSMn/gw6fDZ5jZ1QsHt1BXAwJ6Lg== +better-sqlite3@^11.8.0: + version "11.10.0" + resolved "https://registry.yarnpkg.com/better-sqlite3/-/better-sqlite3-11.10.0.tgz#2b1b14c5acd75a43fd84d12cc291ea98cef57d98" + integrity sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ== dependencies: bindings "^1.5.0" - prebuild-install "^7.1.0" + prebuild-install "^7.1.1" bfj@^8.0.0: version "8.0.0" @@ -15082,10 +15082,10 @@ nanoid@^5.0.7: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.7.tgz#6452e8c5a816861fd9d2b898399f7e5fd6944cc6" integrity sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ== -napi-build-utils@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" - integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== +napi-build-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz#13c22c0187fcfccce1461844136372a47ddc027e" + integrity sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA== natural-compare@^1.4.0: version "1.4.0" @@ -16637,17 +16637,17 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" -prebuild-install@^7.1.0: - version "7.1.2" - resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.2.tgz#a5fd9986f5a6251fbc47e1e5c65de71e68c0a056" - integrity sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== +prebuild-install@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.3.tgz#d630abad2b147443f20a212917beae68b8092eec" + integrity sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug== dependencies: detect-libc "^2.0.0" expand-template "^2.0.3" github-from-package "0.0.0" minimist "^1.2.3" mkdirp-classic "^0.5.3" - napi-build-utils "^1.0.1" + napi-build-utils "^2.0.0" node-abi "^3.3.0" pump "^3.0.0" rc "^1.2.7"