diff --git a/plugins/gs/src/components/deployments/DeploymentLayout/useEditDeploymentData.test.ts b/plugins/gs/src/components/deployments/DeploymentLayout/useEditDeploymentData.test.ts new file mode 100644 index 000000000..02f4cb80c --- /dev/null +++ b/plugins/gs/src/components/deployments/DeploymentLayout/useEditDeploymentData.test.ts @@ -0,0 +1,108 @@ +import { deriveChartTag } from './useEditDeploymentData'; + +describe('deriveChartTag', () => { + it('returns undefined when ref is undefined', () => { + expect(deriveChartTag(undefined)).toBeUndefined(); + }); + + it('returns tag when ref.tag is set', () => { + expect(deriveChartTag({ tag: '1.2.3' })).toBe('1.2.3'); + }); + + it('returns undefined when only digest is set', () => { + expect(deriveChartTag({ digest: 'sha256:abc' })).toBeUndefined(); + }); + + describe('concrete semver ranges', () => { + it('extracts version from >= range', () => { + expect(deriveChartTag({ semver: '>=1.2.3 <2.0.0' })).toBe('1.2.3'); + }); + + it('extracts version from tilde range', () => { + expect(deriveChartTag({ semver: '~1.2.3' })).toBe('1.2.3'); + }); + + it('extracts version from caret range', () => { + expect(deriveChartTag({ semver: '^1.2.3' })).toBe('1.2.3'); + }); + + it('extracts version with pre-release suffix', () => { + expect(deriveChartTag({ semver: '>=1.2.3-beta.1' })).toBe('1.2.3-beta.1'); + }); + + it('extracts bare version', () => { + expect(deriveChartTag({ semver: '1.2.3' })).toBe('1.2.3'); + }); + }); + + describe('patch wildcards', () => { + it('handles 1.2.x', () => { + expect(deriveChartTag({ semver: '1.2.x' })).toBe('1.2.0'); + }); + + it('handles 1.2.*', () => { + expect(deriveChartTag({ semver: '1.2.*' })).toBe('1.2.0'); + }); + + it('handles 1.2.X', () => { + expect(deriveChartTag({ semver: '1.2.X' })).toBe('1.2.0'); + }); + + it('handles ~1.2.x', () => { + expect(deriveChartTag({ semver: '~1.2.x' })).toBe('1.2.0'); + }); + }); + + describe('minor wildcards', () => { + it('handles 1.x', () => { + expect(deriveChartTag({ semver: '1.x' })).toBe('1.0.0'); + }); + + it('handles 1.*', () => { + expect(deriveChartTag({ semver: '1.*' })).toBe('1.0.0'); + }); + + it('handles 1.X', () => { + expect(deriveChartTag({ semver: '1.X' })).toBe('1.0.0'); + }); + + it('handles 1.x.x', () => { + expect(deriveChartTag({ semver: '1.x.x' })).toBe('1.0.0'); + }); + + it('handles 1.*.*', () => { + expect(deriveChartTag({ semver: '1.*.*' })).toBe('1.0.0'); + }); + + it('handles ^1.x', () => { + expect(deriveChartTag({ semver: '^1.x' })).toBe('1.0.0'); + }); + }); + + describe('full wildcards', () => { + it('handles *', () => { + expect(deriveChartTag({ semver: '*' })).toBe('0.0.0'); + }); + + it('handles x', () => { + expect(deriveChartTag({ semver: 'x' })).toBe('0.0.0'); + }); + + it('handles X', () => { + expect(deriveChartTag({ semver: 'X' })).toBe('0.0.0'); + }); + + it('handles x.x.x', () => { + expect(deriveChartTag({ semver: 'x.x.x' })).toBe('0.0.0'); + }); + + it('handles *.*.*', () => { + expect(deriveChartTag({ semver: '*.*.*' })).toBe('0.0.0'); + }); + }); + + it('handles leading/trailing whitespace', () => { + expect(deriveChartTag({ semver: ' 1.2.x ' })).toBe('1.2.0'); + expect(deriveChartTag({ semver: ' ~1.2.3 ' })).toBe('1.2.3'); + }); +}); diff --git a/plugins/gs/src/components/deployments/DeploymentLayout/useEditDeploymentData.ts b/plugins/gs/src/components/deployments/DeploymentLayout/useEditDeploymentData.ts index 6b408f0e9..498991009 100644 --- a/plugins/gs/src/components/deployments/DeploymentLayout/useEditDeploymentData.ts +++ b/plugins/gs/src/components/deployments/DeploymentLayout/useEditDeploymentData.ts @@ -19,21 +19,40 @@ function deriveChartRef(ociUrl: string | undefined): string | undefined { * Derives the chart tag from an OCIRepository reference. * For semver ranges like `>=1.2.3`, extracts the base version. * For exact tags like `1.2.3`, returns as-is. + * Supports Masterminds/semver wildcard placeholders (x, X, *), + * normalizing them to `0` (e.g. `1.2.x` → `1.2.0`). */ -function deriveChartTag( +export function deriveChartTag( ref: { semver?: string; tag?: string; digest?: string } | undefined, ): string | undefined { if (!ref) return undefined; - if (ref.tag) return ref.tag; + if (!ref.semver) return undefined; + + const s = ref.semver.trim(); - if (ref.semver) { - // Extract version from semver range, e.g. ">=1.2.3 <2.0.0" → "1.2.3" - const match = ref.semver.match(/(\d+\.\d+\.\d+(?:-[^\s<>]+)?)/); - return match ? match[1] : undefined; + // Strip leading constraint operator (>=, <=, !=, >, <, =, ~, ^) + const stripped = s.replace(/^(?:>=|<=|!=|[><=~^])\s*/, ''); + + // Match a version where components may be digits or wildcards (x, X, *) + const wcMatch = stripped.match( + /^(\d+|[xX*])(?:\.(\d+|[xX*])(?:\.(\d+|[xX*])(?:-([^\s<>]+))?)?)?$/, + ); + if (wcMatch) { + const toNum = (v: string | undefined) => + !v || /^[xX*]$/.test(v) ? '0' : v; + const major = toNum(wcMatch[1]); + const minor = toNum(wcMatch[2]); + const patch = toNum(wcMatch[3]); + const pre = wcMatch[4]; + return pre + ? `${major}.${minor}.${patch}-${pre}` + : `${major}.${minor}.${patch}`; } - return undefined; + // Fallback: extract first concrete version from compound range (e.g. ">=1.2.3 <2.0.0") + const match = s.match(/(\d+\.\d+\.\d+(?:-[^\s<>]+)?)/); + return match ? match[1] : undefined; } export function useEditDeploymentData(