mirror of
https://github.com/Azure/setup-kubectl.git
synced 2025-12-12 21:21:19 +00:00
feat: validate semantic version and refactor patch logic
- Added validation to `resolveKubectlVersion` to ensure input follows "major.minor" or "major.minor.patch" format. - Moved `getLatestPatchVersion` from `run.ts` to `helpers.ts` to improve code organization and ensure a more robust testing process.
This commit is contained in:
parent
77e32d91cd
commit
e2483b9d19
3 changed files with 65 additions and 52 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as util from 'util'
|
import * as util from 'util'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as toolCache from '@actions/tool-cache'
|
||||||
export function getKubectlArch(): string {
|
export function getKubectlArch(): string {
|
||||||
const arch = os.arch()
|
const arch = os.arch()
|
||||||
if (arch === 'x64') {
|
if (arch === 'x64') {
|
||||||
|
|
@ -23,6 +25,29 @@ export function getkubectlDownloadURL(version: string, arch: string): string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getLatestPatchVersion(
|
||||||
|
major: string,
|
||||||
|
minor: string
|
||||||
|
): Promise<string> {
|
||||||
|
const sourceURL = `https://cdn.dl.k8s.io/release/stable-${major}.${minor}.txt`
|
||||||
|
try {
|
||||||
|
const downloadPath = await toolCache.downloadTool(sourceURL)
|
||||||
|
const latestPatch = fs
|
||||||
|
.readFileSync(downloadPath, 'utf8')
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
if (!latestPatch) {
|
||||||
|
throw new Error(`No patch version found for ${major}.${minor}`)
|
||||||
|
}
|
||||||
|
return latestPatch
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(error)
|
||||||
|
core.warning('GetLatestPatchVersionFailed')
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get latest patch version for ${major}.${minor}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
export function getExecutableExtension(): string {
|
export function getExecutableExtension(): string {
|
||||||
if (os.type().match(/^Win/)) {
|
if (os.type().match(/^Win/)) {
|
||||||
return '.exe'
|
return '.exe'
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import * as run from './run'
|
||||||
import {
|
import {
|
||||||
getkubectlDownloadURL,
|
getkubectlDownloadURL,
|
||||||
getKubectlArch,
|
getKubectlArch,
|
||||||
getExecutableExtension
|
getExecutableExtension,
|
||||||
|
getLatestPatchVersion
|
||||||
} from './helpers'
|
} from './helpers'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as toolCache from '@actions/tool-cache'
|
import * as toolCache from '@actions/tool-cache'
|
||||||
|
|
@ -11,7 +12,11 @@ import * as path from 'path'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as util from 'util'
|
import * as util from 'util'
|
||||||
|
|
||||||
|
|
||||||
describe('Testing all functions in run file.', () => {
|
describe('Testing all functions in run file.', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
test('getExecutableExtension() - return .exe when os is Windows', () => {
|
test('getExecutableExtension() - return .exe when os is Windows', () => {
|
||||||
jest.spyOn(os, 'type').mockReturnValue('Windows_NT')
|
jest.spyOn(os, 'type').mockReturnValue('Windows_NT')
|
||||||
expect(getExecutableExtension()).toBe('.exe')
|
expect(getExecutableExtension()).toBe('.exe')
|
||||||
|
|
@ -164,28 +169,27 @@ describe('Testing all functions in run file.', () => {
|
||||||
)
|
)
|
||||||
expect(toolCache.downloadTool).not.toHaveBeenCalled()
|
expect(toolCache.downloadTool).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('getLatestPatchVersion() - download and return latest patch version', async () => {
|
test('getLatestPatchVersion() - download and return latest patch version', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(toolCache, 'downloadTool')
|
.spyOn(toolCache, 'downloadTool')
|
||||||
.mockReturnValue(Promise.resolve('pathToTool'))
|
.mockResolvedValue('pathToTool')
|
||||||
jest.spyOn(fs, 'readFileSync').mockReturnValue('v1.27.15')
|
jest.spyOn(fs, 'readFileSync').mockReturnValue('v1.27.15')
|
||||||
|
|
||||||
const result = await run.getLatestPatchVersion('1', '27')
|
const result = await getLatestPatchVersion('1', '27')
|
||||||
|
|
||||||
expect(result).toBe('v1.27.15')
|
expect(result).toBe('v1.27.15')
|
||||||
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
expect(toolCache.downloadTool).toHaveBeenCalledWith(
|
||||||
'https://cdn.dl.k8s.io/release/stable-1.27.txt'
|
'https://cdn.dl.k8s.io/release/stable-1.27.txt'
|
||||||
)
|
)
|
||||||
expect(fs.readFileSync).toHaveBeenCalledWith('pathToTool', 'utf8')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('getLatestPatchVersion() - throw error when patch version is empty', async () => {
|
test('getLatestPatchVersion() - throw error when patch version is empty', async () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(toolCache, 'downloadTool')
|
.spyOn(toolCache, 'downloadTool')
|
||||||
.mockReturnValue(Promise.resolve('pathToTool'))
|
.mockResolvedValue('pathToTool')
|
||||||
jest.spyOn(fs, 'readFileSync').mockReturnValue('')
|
jest.spyOn(fs, 'readFileSync').mockReturnValue('')
|
||||||
|
|
||||||
await expect(run.getLatestPatchVersion('1', '27')).rejects.toThrow(
|
await expect(getLatestPatchVersion('1', '27')).rejects.toThrow(
|
||||||
'Failed to get latest patch version for 1.27'
|
'Failed to get latest patch version for 1.27'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
@ -195,35 +199,36 @@ describe('Testing all functions in run file.', () => {
|
||||||
.spyOn(toolCache, 'downloadTool')
|
.spyOn(toolCache, 'downloadTool')
|
||||||
.mockRejectedValue(new Error('Network error'))
|
.mockRejectedValue(new Error('Network error'))
|
||||||
|
|
||||||
await expect(run.getLatestPatchVersion('1', '27')).rejects.toThrow(
|
await expect(getLatestPatchVersion('1', '27')).rejects.toThrow(
|
||||||
'Failed to get latest patch version for 1.27'
|
'Failed to get latest patch version for 1.27'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
test('resolveKubectlVersion() - expands major.minor to latest patch', async () => {
|
test('resolveKubectlVersion() - expands major.minor to latest patch', async () => {
|
||||||
// Mock the getLatestPatchVersion call
|
jest
|
||||||
jest.spyOn(run, 'getLatestPatchVersion').mockResolvedValue('v1.27.15')
|
.spyOn(toolCache, 'downloadTool')
|
||||||
|
.mockResolvedValue('pathToTool')
|
||||||
|
jest.spyOn(fs, 'readFileSync').mockReturnValue('v1.27.15')
|
||||||
|
|
||||||
const result = await run.resolveKubectlVersion('1.27')
|
const result = await run.resolveKubectlVersion('1.27')
|
||||||
|
|
||||||
expect(result).toBe('v1.27.15')
|
expect(result).toBe('v1.27.15')
|
||||||
expect(run.getLatestPatchVersion).toHaveBeenCalledWith('1', '27')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolveKubectlVersion() - returns full version unchanged', async () => {
|
test('resolveKubectlVersion() - returns full version unchanged', async () => {
|
||||||
const result = await run.resolveKubectlVersion('v1.27.15')
|
const result = await run.resolveKubectlVersion('v1.27.15')
|
||||||
expect(result).toBe('v1.27.15')
|
expect(result).toBe('v1.27.15')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolveKubectlVersion() - adds v prefix to full version', async () => {
|
test('resolveKubectlVersion() - adds v prefix to full version', async () => {
|
||||||
const result = await run.resolveKubectlVersion('1.27.15')
|
const result = await run.resolveKubectlVersion('1.27.15')
|
||||||
expect(result).toBe('v1.27.15')
|
expect(result).toBe('v1.27.15')
|
||||||
})
|
})
|
||||||
test('resolveKubectlVersion() - expands v-prefixed major.minor to latest patch', async () => {
|
test('resolveKubectlVersion() - expands v-prefixed major.minor to latest patch', async () => {
|
||||||
jest.spyOn(run, 'getLatestPatchVersion').mockResolvedValue('v1.27.15')
|
jest
|
||||||
|
.spyOn(toolCache, 'downloadTool')
|
||||||
|
.mockResolvedValue('pathToTool')
|
||||||
|
jest.spyOn(fs, 'readFileSync').mockReturnValue('v1.27.15')
|
||||||
|
|
||||||
const result = await run.resolveKubectlVersion('v1.27')
|
const result = await run.resolveKubectlVersion('v1.27')
|
||||||
|
|
||||||
expect(result).toBe('v1.27.15')
|
expect(result).toBe('v1.27.15')
|
||||||
expect(run.getLatestPatchVersion).toHaveBeenCalledWith('1', '27')
|
|
||||||
})
|
})
|
||||||
test('run() - download specified version and set output', async () => {
|
test('run() - download specified version and set output', async () => {
|
||||||
jest.spyOn(core, 'getInput').mockReturnValue('v1.15.5')
|
jest.spyOn(core, 'getInput').mockReturnValue('v1.15.5')
|
||||||
|
|
|
||||||
53
src/run.ts
53
src/run.ts
|
|
@ -1,14 +1,13 @@
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as util from 'util'
|
import * as util from 'util'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as semver from 'semver'
|
|
||||||
import * as toolCache from '@actions/tool-cache'
|
import * as toolCache from '@actions/tool-cache'
|
||||||
import * as runModule from './run'
|
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {
|
import {
|
||||||
getkubectlDownloadURL,
|
getkubectlDownloadURL,
|
||||||
getKubectlArch,
|
getKubectlArch,
|
||||||
getExecutableExtension
|
getExecutableExtension,
|
||||||
|
getLatestPatchVersion
|
||||||
} from './helpers'
|
} from './helpers'
|
||||||
|
|
||||||
const kubectlToolName = 'kubectl'
|
const kubectlToolName = 'kubectl'
|
||||||
|
|
@ -92,43 +91,27 @@ export async function downloadKubectl(version: string): Promise<string> {
|
||||||
return kubectlPath
|
return kubectlPath
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLatestPatchVersion(
|
|
||||||
major: string,
|
|
||||||
minor: string
|
|
||||||
): Promise<string> {
|
|
||||||
const sourceURL = `https://cdn.dl.k8s.io/release/stable-${major}.${minor}.txt`
|
|
||||||
try {
|
|
||||||
const downloadPath = await toolCache.downloadTool(sourceURL)
|
|
||||||
const latestPatch = fs
|
|
||||||
.readFileSync(downloadPath, 'utf8')
|
|
||||||
.toString()
|
|
||||||
.trim()
|
|
||||||
if (!latestPatch) {
|
|
||||||
throw new Error(`No patch version found for ${major}.${minor}`)
|
|
||||||
}
|
|
||||||
return latestPatch
|
|
||||||
} catch (error) {
|
|
||||||
core.debug(error)
|
|
||||||
core.warning('GetLatestPatchVersionFailed')
|
|
||||||
throw new Error(
|
|
||||||
`Failed to get latest patch version for ${major}.${minor}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveKubectlVersion(version: string): Promise<string> {
|
export async function resolveKubectlVersion(version: string): Promise<string> {
|
||||||
const cleanedVersion = version.trim()
|
const cleanedVersion = version.trim()
|
||||||
|
|
||||||
/*------ detect "major.minor" only ----------------*/
|
/*------ detect "major.minor" only ----------------*/
|
||||||
const mmMatch = cleanedVersion.match(/^v?(?<major>\d+)\.(?<minor>\d+)$/)
|
const mmMatch = cleanedVersion.match(/^v?(?<major>\d+)\.(?<minor>\d+)$/)
|
||||||
if (!mmMatch || !mmMatch.groups) {
|
if (mmMatch?.groups) {
|
||||||
// User already provided a full version such as 1.27.15 – do nothing.
|
const {major, minor} = mmMatch.groups
|
||||||
return cleanedVersion.startsWith('v')
|
// Call the k8s CDN to get the latest patch version for the given major.minor
|
||||||
? cleanedVersion
|
return await getLatestPatchVersion(major, minor)
|
||||||
: `v${cleanedVersion}`
|
|
||||||
}
|
}
|
||||||
const {major, minor} = mmMatch.groups
|
|
||||||
|
|
||||||
// Call the k8s CDN to get the latest patch version for the given major.minor
|
/*------ detect "major.minor.patch" and validate ----------------*/
|
||||||
return await runModule.getLatestPatchVersion(major, minor)
|
const mmpMatch = cleanedVersion.match(/^v?(\d+\.\d+\.\d+)$/)
|
||||||
|
if (!mmpMatch) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid version format: "${version}". Version must be in "major.minor" or "major.minor.patch" format (e.g., "1.27" or "v1.27.15").`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// User provided a full version such as 1.27.15, ensure it has a 'v' prefix.
|
||||||
|
return cleanedVersion.startsWith('v')
|
||||||
|
? cleanedVersion
|
||||||
|
: `v${cleanedVersion}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue