* feat: Implement resolveKubectlVersion function with comprehensive test coverage
Introduce resolveKubectlVersion function that enables automatic selection of the latest patch version when provided with major.minor version input (e.g., '1.27' resolves to 'v1.27.15')
Test Coverage:
- Major.minor version expansion to latest available patch
- Full version passthrough behavior (returns unchanged)
- Single matching version selection logic
- Comprehensive unit tests for kubectl version resolution scenarios
* chore: fix Prettier formatting
* refactor(resolveKubectlVersion): switch to k8s CDN for security patch retrieval
Replaced GitHub API Octo client with k8s CDN to fetch the latest security patch for improved reliability. Separated the API call logic from resolveKubectlVersion to enhance testability and readability.
* 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.
* Bump github/codeql-action in /.github/workflows in the actions group (#173)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).
Updates `github/codeql-action` from 3.29.0 to 3.29.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](ce28f5bb42...39edc492db)
---
updated-dependencies:
- dependency-name: github/codeql-action
dependency-version: 3.29.1
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: actions
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore: fix code style issues with Prettier
* revised parsing logic
* Improved readability and maintainability
* regenerated package-lock.json
* Regenerated Package-lock.json
* removed unnecessary files
* regenerated package-lock.json
* Regenerate package-lock.json to match package.json version ranges
* Restore package-lock.json to previous version
* uninstall ncc and regenerate package-lock.json using npm ci
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
41a1936057
commit
7500adf963
@@ -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 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 {
|
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'
|
||||||
@@ -12,6 +13,9 @@ 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,6 +168,59 @@ 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 () => {
|
||||||
|
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 () => {
|
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')
|
||||||
jest.spyOn(toolCache, 'find').mockReturnValue('pathToCachedTool')
|
jest.spyOn(toolCache, 'find').mockReturnValue('pathToCachedTool')
|
||||||
|
|||||||
32
src/run.ts
32
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 toolCache from '@actions/tool-cache'
|
import * as toolCache from '@actions/tool-cache'
|
||||||
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'
|
||||||
@@ -20,6 +19,8 @@ export async function run() {
|
|||||||
let version = core.getInput('version', {required: true})
|
let version = core.getInput('version', {required: true})
|
||||||
if (version.toLocaleLowerCase() === 'latest') {
|
if (version.toLocaleLowerCase() === 'latest') {
|
||||||
version = await getStableKubectlVersion()
|
version = await getStableKubectlVersion()
|
||||||
|
} else {
|
||||||
|
version = await resolveKubectlVersion(version)
|
||||||
}
|
}
|
||||||
const cachedPath = await downloadKubectl(version)
|
const cachedPath = await downloadKubectl(version)
|
||||||
|
|
||||||
@@ -89,3 +90,28 @@ export async function downloadKubectl(version: string): Promise<string> {
|
|||||||
fs.chmodSync(kubectlPath, '775')
|
fs.chmodSync(kubectlPath, '775')
|
||||||
return kubectlPath
|
return kubectlPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function resolveKubectlVersion(version: string): Promise<string> {
|
||||||
|
const cleanedVersion = version.trim()
|
||||||
|
const versionMatch = cleanedVersion.match(
|
||||||
|
/^v?(?<major>\d+)\.(?<minor>\d+)(?:\.(?<patch>\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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES6",
|
"target": "ES2020",
|
||||||
"module": "commonjs"
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020", "DOM"]
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "test"]
|
"exclude": ["node_modules", "test"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user