mirror of
https://github.com/Azure/setup-helm.git
synced 2026-07-04 23:41:37 +00:00
Add version-file input to read the Helm version from a .tool-versions file (#281)
* feat: add version-file input to read helm version from .tool-versions * feat: validate semver shape of helm version from .tool-versions
This commit is contained in:
@@ -13,8 +13,25 @@ Acceptable values are latest or any semantic version string like v3.5.0 Use this
|
|||||||
id: install
|
id: install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Alternatively, the version can be read from a [`.tool-versions`](https://asdf-vm.com/manage/configuration.html) file (the format used by [asdf](https://asdf-vm.com/) and [mise](https://mise.jdx.dev/)) via the `version-file` input:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: azure/setup-helm@v5.0.0
|
||||||
|
with:
|
||||||
|
version-file: .tool-versions
|
||||||
|
id: install
|
||||||
|
```
|
||||||
|
|
||||||
|
The action reads the version declared for the `helm` tool, for example:
|
||||||
|
|
||||||
|
```
|
||||||
|
helm 3.18.4
|
||||||
|
```
|
||||||
|
|
||||||
|
If both `version` and `version-file` are set, an explicitly requested `version` takes precedence and `version-file` is ignored (a warning is emitted). Because `version` defaults to `latest`, `version-file` is only ignored when you set `version` to a specific value other than `latest`; if `version` is left at its default, the version from `version-file` is used.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> If something goes wrong with fetching the latest version the action will use the hardcoded default version (currently v3.18.3). If you rely on a certain version higher than the default, you should explicitly use that version instead of latest.
|
> If something goes wrong with fetching the latest version the action will use the hardcoded default version (currently v3.18.4). If you rely on a certain version higher than the default, you should explicitly use that version instead of latest.
|
||||||
|
|
||||||
The cached helm binary path is prepended to the PATH environment variable as well as stored in the helm-path output variable.
|
The cached helm binary path is prepended to the PATH environment variable as well as stored in the helm-path output variable.
|
||||||
Refer to the action metadata file for details about all the inputs https://github.com/Azure/setup-helm/blob/master/action.yml
|
Refer to the action metadata file for details about all the inputs https://github.com/Azure/setup-helm/blob/master/action.yml
|
||||||
|
|||||||
+4
-1
@@ -3,8 +3,11 @@ description: 'Install a specific version of helm binary. Acceptable values are l
|
|||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: 'Version of helm'
|
description: 'Version of helm'
|
||||||
required: true
|
required: false
|
||||||
default: 'latest'
|
default: 'latest'
|
||||||
|
version-file:
|
||||||
|
description: 'Path to a .tool-versions file to read the helm version from'
|
||||||
|
required: false
|
||||||
token:
|
token:
|
||||||
description: GitHub token. Used to be required to fetch the latest version
|
description: GitHub token. Used to be required to fetch the latest version
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
+136
-1
@@ -19,7 +19,8 @@ vi.mock('fs', async (importOriginal) => {
|
|||||||
readdirSync: vi.fn(),
|
readdirSync: vi.fn(),
|
||||||
statSync: vi.fn(),
|
statSync: vi.fn(),
|
||||||
chmodSync: vi.fn(),
|
chmodSync: vi.fn(),
|
||||||
readFileSync: vi.fn()
|
readFileSync: vi.fn(),
|
||||||
|
existsSync: vi.fn()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -161,6 +162,140 @@ describe('run.ts', () => {
|
|||||||
expect(run.getValidVersion('3.8.0')).toBe('v3.8.0')
|
expect(run.getValidVersion('3.8.0')).toBe('v3.8.0')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('parseToolVersions() - return the helm version from .tool-versions content', () => {
|
||||||
|
const content = ['nodejs 20.11.0', 'helm 3.14.0', 'terraform 1.7.0'].join(
|
||||||
|
'\n'
|
||||||
|
)
|
||||||
|
expect(run.parseToolVersions(content)).toBe('3.14.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parseToolVersions() - ignore comments and blank lines', () => {
|
||||||
|
const content = ['# tools', '', ' helm 3.15.2 ', ''].join('\n')
|
||||||
|
expect(run.parseToolVersions(content)).toBe('3.15.2')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parseToolVersions() - return the first version when several are listed', () => {
|
||||||
|
expect(run.parseToolVersions('helm 3.14.0 3.13.0')).toBe('3.14.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parseToolVersions() - return empty string when helm is not declared', () => {
|
||||||
|
expect(run.parseToolVersions('nodejs 20.11.0')).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getVersionFromToolVersionsFile() - read the helm version from a file', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('helm 3.14.0')
|
||||||
|
|
||||||
|
expect(run.getVersionFromToolVersionsFile('.tool-versions')).toBe(
|
||||||
|
'3.14.0'
|
||||||
|
)
|
||||||
|
expect(fs.readFileSync).toHaveBeenCalledWith('.tool-versions', 'utf8')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getVersionFromToolVersionsFile() - throw when the file does not exist', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(false)
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
run.getVersionFromToolVersionsFile('missing.tool-versions')
|
||||||
|
).toThrow("The version-file 'missing.tool-versions' does not exist")
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getVersionFromToolVersionsFile() - throw when no helm version is present', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('nodejs 20.11.0')
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
run.getVersionFromToolVersionsFile('.tool-versions')
|
||||||
|
).toThrow("No helm version found in '.tool-versions'")
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getVersionFromToolVersionsFile() - throw when the helm version is not semver-shaped', () => {
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('helm latest')
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
run.getVersionFromToolVersionsFile('.tool-versions')
|
||||||
|
).toThrow(
|
||||||
|
"The helm version 'latest' in '.tool-versions' is not a valid semantic version"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isSemVerShaped() - accept semver-shaped versions with or without a v prefix', () => {
|
||||||
|
expect(run.isSemVerShaped('3.14.0')).toBe(true)
|
||||||
|
expect(run.isSemVerShaped('v3.14.0')).toBe(true)
|
||||||
|
expect(run.isSemVerShaped('3.14.0-rc.1')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isSemVerShaped() - reject values that are not semver-shaped', () => {
|
||||||
|
expect(run.isSemVerShaped('latest')).toBe(false)
|
||||||
|
expect(run.isSemVerShaped('3.14')).toBe(false)
|
||||||
|
expect(run.isSemVerShaped('abc')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stubs the download chain so run() resolves to a cached helm binary,
|
||||||
|
// letting these tests focus on version-vs-version-file resolution.
|
||||||
|
const stubDownloadChain = () => {
|
||||||
|
vi.mocked(os.platform).mockReturnValue('linux')
|
||||||
|
vi.mocked(os.arch).mockReturnValue('x64')
|
||||||
|
vi.mocked(toolCache.find).mockReturnValue('pathToCachedDir')
|
||||||
|
vi.mocked(fs.chmodSync).mockImplementation(() => {})
|
||||||
|
vi.mocked(fs.readdirSync).mockReturnValue([
|
||||||
|
'helm' as unknown as fs.Dirent<NonSharedBuffer>
|
||||||
|
])
|
||||||
|
vi.mocked(fs.statSync).mockReturnValue({
|
||||||
|
isDirectory: () => false
|
||||||
|
} as fs.Stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = (version: string, versionFile: string) =>
|
||||||
|
vi.mocked(core.getInput).mockImplementation((name: string) => {
|
||||||
|
if (name === 'version') return version
|
||||||
|
if (name === 'version-file') return versionFile
|
||||||
|
if (name === 'downloadBaseURL') return downloadBaseURL
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
test('run() - resolve the version from version-file when version is not set', async () => {
|
||||||
|
stubDownloadChain()
|
||||||
|
inputs('', '.tool-versions')
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('helm 3.14.0')
|
||||||
|
|
||||||
|
await run.run()
|
||||||
|
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.14.0')
|
||||||
|
expect(core.setOutput).toHaveBeenCalledWith(
|
||||||
|
'helm-path',
|
||||||
|
path.join('pathToCachedDir', 'helm')
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('run() - resolve the version from version-file when version is left at the latest default', async () => {
|
||||||
|
stubDownloadChain()
|
||||||
|
inputs('latest', '.tool-versions')
|
||||||
|
vi.mocked(fs.existsSync).mockReturnValue(true)
|
||||||
|
vi.mocked(fs.readFileSync).mockReturnValue('helm 3.14.0')
|
||||||
|
|
||||||
|
await run.run()
|
||||||
|
|
||||||
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
|
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.14.0')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('run() - warn and prefer version over version-file when both are set', async () => {
|
||||||
|
stubDownloadChain()
|
||||||
|
inputs('3.5.0', '.tool-versions')
|
||||||
|
|
||||||
|
await run.run()
|
||||||
|
|
||||||
|
expect(core.warning).toHaveBeenCalledWith(
|
||||||
|
`Both 'version' and 'version-file' inputs are specified, only 'version' will be used.`
|
||||||
|
)
|
||||||
|
expect(fs.readFileSync).not.toHaveBeenCalled()
|
||||||
|
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.5.0')
|
||||||
|
})
|
||||||
|
|
||||||
test('walkSync() - return path to the all files matching fileToFind in dir', () => {
|
test('walkSync() - return path to the all files matching fileToFind in dir', () => {
|
||||||
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
|
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
|
||||||
if (file == 'mainFolder')
|
if (file == 'mainFolder')
|
||||||
|
|||||||
+63
-2
@@ -13,11 +13,27 @@ const helmToolName = 'helm'
|
|||||||
export const stableHelmVersion = 'v3.18.4'
|
export const stableHelmVersion = 'v3.18.4'
|
||||||
|
|
||||||
export async function run() {
|
export async function run() {
|
||||||
let version = core.getInput('version', {required: true})
|
let version = core.getInput('version')
|
||||||
|
const versionFile = core.getInput('version-file')
|
||||||
|
|
||||||
|
if (versionFile) {
|
||||||
|
if (version && version !== 'latest') {
|
||||||
|
core.warning(
|
||||||
|
`Both 'version' and 'version-file' inputs are specified, only 'version' will be used.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
version = getVersionFromToolVersionsFile(versionFile)
|
||||||
|
core.info(`Resolved Helm version '${version}' from '${versionFile}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!version) {
|
||||||
|
version = 'latest'
|
||||||
|
}
|
||||||
|
|
||||||
if (version !== 'latest' && version[0] !== 'v') {
|
if (version !== 'latest' && version[0] !== 'v') {
|
||||||
core.info('Getting latest Helm version')
|
|
||||||
version = getValidVersion(version)
|
version = getValidVersion(version)
|
||||||
|
core.info(`Normalized Helm version to '${version}'`)
|
||||||
}
|
}
|
||||||
if (version.toLocaleLowerCase() === 'latest') {
|
if (version.toLocaleLowerCase() === 'latest') {
|
||||||
version = await getLatestHelmVersion()
|
version = await getLatestHelmVersion()
|
||||||
@@ -46,6 +62,51 @@ export function getValidVersion(version: string): string {
|
|||||||
return 'v' + version
|
return 'v' + version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matches a semantic version (major.minor.patch) with an optional leading 'v'
|
||||||
|
// and optional pre-release / build-metadata suffixes, e.g. '3.14.0', 'v3.14.0',
|
||||||
|
// '3.14.0-rc.1'.
|
||||||
|
const semVerShape = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
||||||
|
|
||||||
|
// Returns true when version looks like a semantic version
|
||||||
|
export function isSemVerShaped(version: string): boolean {
|
||||||
|
return semVerShape.test(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reads a .tool-versions file and returns the helm version declared in it
|
||||||
|
export function getVersionFromToolVersionsFile(filePath: string): string {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error(`The version-file '${filePath}' does not exist`)
|
||||||
|
}
|
||||||
|
const content = fs.readFileSync(filePath, 'utf8')
|
||||||
|
const version = parseToolVersions(content)
|
||||||
|
if (!version) {
|
||||||
|
throw new Error(`No helm version found in '${filePath}'`)
|
||||||
|
}
|
||||||
|
if (!isSemVerShaped(version)) {
|
||||||
|
throw new Error(
|
||||||
|
`The helm version '${version}' in '${filePath}' is not a valid semantic version`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses .tool-versions content (asdf/mise format) and returns the first
|
||||||
|
// helm version, or an empty string when none is declared. Lines look like
|
||||||
|
// `helm 3.14.0`; comments (#) and blank lines are ignored.
|
||||||
|
export function parseToolVersions(content: string): string {
|
||||||
|
for (const line of content.split(/\r?\n/)) {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || trimmed.startsWith('#')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const [tool, version] = trimmed.split(/\s+/)
|
||||||
|
if (tool === helmToolName && version) {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
// Gets the latest helm version or returns a default stable if getting latest fails
|
// Gets the latest helm version or returns a default stable if getting latest fails
|
||||||
export async function getLatestHelmVersion(): Promise<string> {
|
export async function getLatestHelmVersion(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user