diff --git a/src/helpers.ts b/src/helpers.ts index a0838f6..0652265 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,6 +1,8 @@ import * as os from 'os' 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 { const arch = os.arch() if (arch === 'x64') { @@ -23,6 +25,29 @@ export function getkubectlDownloadURL(version: string, arch: string): string { } } +export async function getLatestPatchVersion( + major: string, + minor: string +): Promise { + const version = `${major}.${minor}` + const sourceURL = `https://cdn.dl.k8s.io/release/stable-${version}.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 ${version}`) + } + return latestPatch + } catch (error) { + core.debug(error) + core.warning('GetLatestPatchVersionFailed') + throw new Error(`Failed to get latest patch version for ${version}`) + } +} + export function getExecutableExtension(): string { if (os.type().match(/^Win/)) { return '.exe' diff --git a/src/run.test.ts b/src/run.test.ts index 0aa3579..410a07f 100644 --- a/src/run.test.ts +++ b/src/run.test.ts @@ -2,7 +2,8 @@ import * as run from './run' import { getkubectlDownloadURL, getKubectlArch, - getExecutableExtension + getExecutableExtension, + getLatestPatchVersion } from './helpers' import * as os from 'os' import * as toolCache from '@actions/tool-cache' @@ -12,6 +13,9 @@ import * as core from '@actions/core' import * as util from 'util' describe('Testing all functions in run file.', () => { + beforeEach(() => { + jest.clearAllMocks() + }) test('getExecutableExtension() - return .exe when os is Windows', () => { jest.spyOn(os, 'type').mockReturnValue('Windows_NT') expect(getExecutableExtension()).toBe('.exe') @@ -164,6 +168,59 @@ describe('Testing all functions in run file.', () => { ) expect(toolCache.downloadTool).not.toHaveBeenCalled() }) + test('getLatestPatchVersion() - download and return latest patch version', async () => { + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool') + jest.spyOn(fs, 'readFileSync').mockReturnValue('v1.27.15') + + const result = await getLatestPatchVersion('1', '27') + + expect(result).toBe('v1.27.15') + expect(toolCache.downloadTool).toHaveBeenCalledWith( + 'https://cdn.dl.k8s.io/release/stable-1.27.txt' + ) + }) + + test('getLatestPatchVersion() - throw error when patch version is empty', async () => { + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool') + jest.spyOn(fs, 'readFileSync').mockReturnValue('') + + await expect(getLatestPatchVersion('1', '27')).rejects.toThrow( + 'Failed to get latest patch version for 1.27' + ) + }) + + test('getLatestPatchVersion() - throw error when download fails', async () => { + jest + .spyOn(toolCache, 'downloadTool') + .mockRejectedValue(new Error('Network error')) + + await expect(getLatestPatchVersion('1', '27')).rejects.toThrow( + 'Failed to get latest patch version for 1.27' + ) + }) + test('resolveKubectlVersion() - expands major.minor to latest patch', async () => { + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool') + jest.spyOn(fs, 'readFileSync').mockReturnValue('v1.27.15') + + const result = await run.resolveKubectlVersion('1.27') + expect(result).toBe('v1.27.15') + }) + + test('resolveKubectlVersion() - returns full version unchanged', async () => { + const result = await run.resolveKubectlVersion('v1.27.15') + expect(result).toBe('v1.27.15') + }) + test('resolveKubectlVersion() - adds v prefix to full version', async () => { + const result = await run.resolveKubectlVersion('1.27.15') + expect(result).toBe('v1.27.15') + }) + test('resolveKubectlVersion() - expands v-prefixed major.minor to latest patch', async () => { + jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool') + jest.spyOn(fs, 'readFileSync').mockReturnValue('v1.27.15') + + const result = await run.resolveKubectlVersion('v1.27') + expect(result).toBe('v1.27.15') + }) test('run() - download specified version and set output', async () => { jest.spyOn(core, 'getInput').mockReturnValue('v1.15.5') jest.spyOn(toolCache, 'find').mockReturnValue('pathToCachedTool') diff --git a/src/run.ts b/src/run.ts index 53c1994..89de719 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,14 +1,13 @@ import * as path from 'path' import * as util from 'util' import * as fs from 'fs' - import * as toolCache from '@actions/tool-cache' import * as core from '@actions/core' - import { getkubectlDownloadURL, getKubectlArch, - getExecutableExtension + getExecutableExtension, + getLatestPatchVersion } from './helpers' const kubectlToolName = 'kubectl' @@ -20,6 +19,8 @@ export async function run() { let version = core.getInput('version', {required: true}) if (version.toLocaleLowerCase() === 'latest') { version = await getStableKubectlVersion() + } else { + version = await resolveKubectlVersion(version) } const cachedPath = await downloadKubectl(version) @@ -89,3 +90,28 @@ export async function downloadKubectl(version: string): Promise { fs.chmodSync(kubectlPath, '775') return kubectlPath } + +export async function resolveKubectlVersion(version: string): Promise { + const cleanedVersion = version.trim() + const versionMatch = cleanedVersion.match( + /^v?(?\d+)\.(?\d+)(?:\.(?\d+))?$/ + ) + + if (!versionMatch?.groups) { + 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").` + ) + } + + const {major, minor, patch} = versionMatch.groups + + if (patch) { + // Full version was provided, just ensure it has a 'v' prefix + return cleanedVersion.startsWith('v') + ? cleanedVersion + : `v${cleanedVersion}` + } + + // Patch version is missing, fetch the latest + return await getLatestPatchVersion(major, minor) +} diff --git a/tsconfig.json b/tsconfig.json index f9e4e8d..eddf09f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,8 @@ { "compilerOptions": { - "target": "ES6", - "module": "commonjs" + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"] }, "exclude": ["node_modules", "test"] }