Compare commits

..

1 Commits

Author SHA1 Message Date
GitHub Action 9bc31f4ebc build 2026-06-23 20:06:59 +00:00
9 changed files with 23485 additions and 1189 deletions
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 #v10.4.0
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 #v10.3.0
name: Setting issue as idle
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
@@ -24,7 +24,7 @@ jobs:
operations-per-run: 100
exempt-issue-labels: 'backlog'
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 #v10.4.0
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 #v10.3.0
name: Setting PR as idle
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
-6
View File
@@ -11,8 +11,6 @@ pids
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
@@ -64,7 +62,3 @@ node_modules
coverage
# Transpiled JS
lib/
# Planning docs
docs/
+1 -18
View File
@@ -13,25 +13,8 @@ Acceptable values are latest or any semantic version string like v3.5.0 Use this
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]
> 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.
> 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.
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
+1 -4
View File
@@ -3,11 +3,8 @@ description: 'Install a specific version of helm binary. Acceptable values are l
inputs:
version:
description: 'Version of helm'
required: false
required: true
default: 'latest'
version-file:
description: 'Path to a .tool-versions file to read the helm version from'
required: false
token:
description: GitHub token. Used to be required to fetch the latest version
required: false
+23304
View File
File diff suppressed because one or more lines are too long
+162 -553
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -25,11 +25,11 @@
"prepare": "husky"
},
"devDependencies": {
"@types/node": "^26.1.1",
"@types/node": "^26.0.0",
"esbuild": "^0.28",
"husky": "^9.1.7",
"prettier": "^3.9.5",
"typescript": "^7.0.2",
"prettier": "^3.8.4",
"typescript": "^6.0.3",
"vitest": "^4"
}
}
+5 -392
View File
@@ -19,8 +19,7 @@ vi.mock('fs', async (importOriginal) => {
readdirSync: vi.fn(),
statSync: vi.fn(),
chmodSync: vi.fn(),
readFileSync: vi.fn(),
existsSync: vi.fn()
readFileSync: vi.fn()
}
})
@@ -140,47 +139,20 @@ describe('run.ts', () => {
expect(os.platform).toHaveBeenCalled()
})
test('getHelmDownloadURL() - preserve a subpath base URL without a trailing slash', () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
const expected =
'https://mirror.example/kubernetes/helm/helm-v3.8.0-linux-amd64.tar.gz'
expect(
run.getHelmDownloadURL(
'https://mirror.example/kubernetes/helm',
'v3.8.0'
)
).toBe(expected)
})
test('getHelmDownloadURL() - preserve a subpath base URL with a trailing slash', () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
const expected =
'https://mirror.example/kubernetes/helm/helm-v3.8.0-linux-amd64.tar.gz'
expect(
run.getHelmDownloadURL(
'https://mirror.example/kubernetes/helm/',
'v3.8.0'
)
).toBe(expected)
})
test('getLatestHelmVersion() - return the latest version of HELM', async () => {
const res = {
status: 200,
text: async () => 'v9.99.999'
} as Response
vi.spyOn(globalThis, 'fetch').mockResolvedValue(res)
vi.stubGlobal('fetch', vi.fn().mockReturnValue(res))
expect(await run.getLatestHelmVersion()).toBe('v9.99.999')
})
test('getLatestHelmVersion() - return the stable version of HELM when simulating a network error', async () => {
const errorMessage: string = 'Network Error'
vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(
new Error(errorMessage)
vi.stubGlobal(
'fetch',
vi.fn().mockRejectedValueOnce(new Error(errorMessage))
)
expect(await run.getLatestHelmVersion()).toBe(run.stableHelmVersion)
})
@@ -189,365 +161,6 @@ describe('run.ts', () => {
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 valid', () => {
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 valid. Provide a full version (e.g. '3.14.0') or a major.minor version (e.g. '3.14' or '3.14.x')"
)
})
test('getVersionFromToolVersionsFile() - accept a major.minor version', () => {
vi.mocked(fs.existsSync).mockReturnValue(true)
vi.mocked(fs.readFileSync).mockReturnValue('helm 3.14')
expect(run.getVersionFromToolVersionsFile('.tool-versions')).toBe('3.14')
})
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)
})
test('isMajorMinorShaped() - accept major.minor with or without a v prefix', () => {
expect(run.isMajorMinorShaped('3.14')).toBe(true)
expect(run.isMajorMinorShaped('v3.14')).toBe(true)
})
test('isMajorMinorShaped() - accept a wildcard patch (.x / .*)', () => {
expect(run.isMajorMinorShaped('3.14.x')).toBe(true)
expect(run.isMajorMinorShaped('v3.14.x')).toBe(true)
expect(run.isMajorMinorShaped('3.14.*')).toBe(true)
expect(run.isMajorMinorShaped('v3.14.*')).toBe(true)
})
test('isMajorMinorShaped() - reject full versions and other values', () => {
expect(run.isMajorMinorShaped('3.14.0')).toBe(false)
expect(run.isMajorMinorShaped('latest')).toBe(false)
expect(run.isMajorMinorShaped('3')).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')
})
// Spies on global fetch so HEAD probes report the given set of versions as
// existing (200) and everything else as missing (404). Using vi.spyOn (rather
// than vi.stubGlobal) lets restoreAllMocks() in afterEach reliably restore the
// real fetch, so the mock never leaks into later tests.
const stubPatchProbes = (existing: string[]) => {
const present = new Set(existing)
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const version =
String(input).match(/helm-(v\d+\.\d+\.\d+)-/)?.[1] ?? ''
const ok = present.has(version)
return {ok, status: ok ? 200 : 404} as Response
})
}
test('helmPatchExists() - return true when the artifact responds 200', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
status: 200
} as Response)
expect(await run.helmPatchExists(downloadBaseURL, 'v3.14.4')).toBe(true)
})
test('helmPatchExists() - return false when the artifact responds 404', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: false,
status: 404
} as Response)
expect(await run.helmPatchExists(downloadBaseURL, 'v3.14.99')).toBe(false)
})
test('helmPatchExists() - throw on a non-404 error status', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: false,
status: 429
} as Response)
await expect(
run.helmPatchExists(downloadBaseURL, 'v3.14.4')
).rejects.toThrow('Unexpected HTTP 429')
})
test('resolveLatestPatchVersion() - return the newest existing patch', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.2', 'v3.14.3', 'v3.14.4'])
expect(await run.resolveLatestPatchVersion(downloadBaseURL, '3.14')).toBe(
'v3.14.4'
)
})
test('resolveLatestPatchVersion() - use the blob listing in a single request when supported', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
const listing =
'<EnumerationResults>' +
'<Blob><Name>helm-v3.14.0-linux-amd64.tar.gz</Name></Blob>' +
'<Blob><Name>helm-v3.14.0-rc.1-linux-amd64.tar.gz</Name></Blob>' +
'<Blob><Name>helm-v3.14.4-linux-amd64.tar.gz</Name></Blob>' +
'<Blob><Name>helm-v3.14.4-linux-amd64.tar.gz.sha256</Name></Blob>' +
'</EnumerationResults>'
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
text: async () => listing
} as Response)
expect(await run.resolveLatestPatchVersion(downloadBaseURL, '3.14')).toBe(
'v3.14.4'
)
// A single listing request; no per-patch HEAD probing.
expect(fetchSpy).toHaveBeenCalledTimes(1)
})
test('resolveLatestPatchVersion() - fall back to probing when listing is unavailable', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
const present = new Set(['v3.14.0', 'v3.14.1', 'v3.14.2'])
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
const url = String(input)
if (url.includes('comp=list')) {
return {ok: false, status: 404} as Response
}
const version = url.match(/helm-(v\d+\.\d+\.\d+)-/)?.[1] ?? ''
const ok = present.has(version)
return {ok, status: ok ? 200 : 404} as Response
})
expect(await run.resolveLatestPatchVersion(downloadBaseURL, '3.14')).toBe(
'v3.14.2'
)
})
test('resolveLatestPatchViaListing() - return null when the response is not a listing', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
ok: true,
text: async () => '<html>directory index, not a blob listing</html>'
} as Response)
expect(
await run.resolveLatestPatchViaListing(downloadBaseURL, '3', '14')
).toBeNull()
})
test('resolveLatestPatchVersion() - tolerate a skipped patch number', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.3'])
expect(
await run.resolveLatestPatchVersion(downloadBaseURL, 'v3.14')
).toBe('v3.14.3')
})
test('resolveLatestPatchVersion() - accept a wildcard patch (.x / .*)', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes(['v3.12.0', 'v3.12.1', 'v3.12.2', 'v3.12.3'])
expect(
await run.resolveLatestPatchVersion(downloadBaseURL, 'v3.12.x')
).toBe('v3.12.3')
expect(
await run.resolveLatestPatchVersion(downloadBaseURL, '3.12.*')
).toBe('v3.12.3')
})
test('resolveLatestPatchVersion() - throw when the minor has no releases', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
stubPatchProbes([])
await expect(
run.resolveLatestPatchVersion(downloadBaseURL, '9.99')
).rejects.toThrow('No Helm releases found for 9.99')
})
test('resolveLatestPatchVersion() - propagate network errors', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockRejectedValue(
new Error('Network Error')
)
await expect(
run.resolveLatestPatchVersion(downloadBaseURL, '3.14')
).rejects.toThrow('Network Error')
})
test('resolveLatestPatchVersion() - throw when the host reports every patch as existing', async () => {
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
vi.spyOn(globalThis, 'fetch').mockResolvedValue({ok: true} as Response)
await expect(
run.resolveLatestPatchVersion(downloadBaseURL, '3.14')
).rejects.toThrow('exceeded 100 probes')
})
test('run() - resolve the latest patch for a major.minor version input', async () => {
stubDownloadChain()
inputs('3.14', '')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.2', 'v3.14.3', 'v3.14.4'])
await run.run()
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.14.4')
})
test('run() - resolve the latest patch for a wildcard patch version input', async () => {
stubDownloadChain()
inputs('v3.12.x', '')
stubPatchProbes(['v3.12.0', 'v3.12.1', 'v3.12.2', 'v3.12.3'])
await run.run()
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.12.3')
})
test('run() - resolve the latest patch for a major.minor version-file entry', async () => {
stubDownloadChain()
inputs('', '.tool-versions')
vi.mocked(fs.existsSync).mockReturnValue(true)
vi.mocked(fs.readFileSync).mockReturnValue('helm 3.14')
stubPatchProbes(['v3.14.0', 'v3.14.1', 'v3.14.2', 'v3.14.3', 'v3.14.4'])
await run.run()
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.14.4')
})
test('walkSync() - return path to the all files matching fileToFind in dir', () => {
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
if (file == 'mainFolder')
+7 -211
View File
@@ -13,36 +13,18 @@ const helmToolName = 'helm'
export const stableHelmVersion = 'v3.18.4'
export async function run() {
let version = core.getInput('version')
const versionFile = core.getInput('version-file')
let version = core.getInput('version', {required: true})
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 !== 'latest' && version[0] !== 'v') {
core.info('Getting latest Helm version')
version = getValidVersion(version)
}
if (!version) {
version = 'latest'
if (version.toLocaleLowerCase() === 'latest') {
version = await getLatestHelmVersion()
}
const downloadBaseURL = core.getInput('downloadBaseURL', {required: false})
if (version.toLocaleLowerCase() === 'latest') {
version = await getLatestHelmVersion()
} else if (isMajorMinorShaped(version)) {
version = await resolveLatestPatchVersion(downloadBaseURL, version)
core.info(`Resolved latest patch Helm version to '${version}'`)
} else if (version[0] !== 'v') {
version = getValidVersion(version)
core.info(`Normalized Helm version to '${version}'`)
}
core.startGroup(`Installing ${version}`)
const cachedPath = await downloadHelm(downloadBaseURL, version)
core.endGroup()
@@ -64,64 +46,6 @@ export function getValidVersion(version: string): string {
return 'v' + version
}
// Matches a complete semantic version (major.minor.patch with optional
// pre-release / build metadata). This is the official regex suggested at
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
// with an added optional leading 'v' to accept Helm-style tags (e.g. 'v3.14.0').
const semVerShape =
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([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)
}
// Matches a major.minor version with an optional leading 'v' and either no
// patch component or a wildcard patch ('.x' / '.*'), e.g. '3.14', 'v3.14',
// '3.14.x', 'v3.14.*'.
const majorMinorShape = /^v?\d+\.\d+(?:\.[x*])?$/
// Returns true when version is a major.minor value (optionally with a wildcard
// patch such as '.x' or '.*')
export function isMajorMinorShaped(version: string): boolean {
return majorMinorShape.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) && !isMajorMinorShaped(version)) {
throw new Error(
`The helm version '${version}' in '${filePath}' is not valid. Provide a full version (e.g. '3.14.0') or a major.minor version (e.g. '3.14' or '3.14.x')`
)
}
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
export async function getLatestHelmVersion(): Promise<string> {
try {
@@ -136,130 +60,6 @@ export async function getLatestHelmVersion(): Promise<string> {
}
}
// Number of consecutive missing patches to probe before concluding the walk,
// and an upper bound to keep resolution from running unbounded.
const patchLookahead = 3
const maxPatch = 100
// Sends a HEAD request for the given version's download URL. Returns true when
// the artifact exists (2xx) and false only when it is definitively absent
// (404). Any other status (403/405/429/5xx, ...) and genuine network errors are
// thrown, so transient failures, rate-limiting, or a host that disallows HEAD
// are never mistaken for a missing patch.
export async function helmPatchExists(
baseURL: string,
version: string
): Promise<boolean> {
const url = getHelmDownloadURL(baseURL, version)
const response = await fetch(url, {method: 'HEAD'})
if (response.ok) {
return true
}
if (response.status === 404) {
return false
}
throw new Error(
`Unexpected HTTP ${response.status} while checking for Helm artifact at ${url}`
)
}
// Attempts to resolve the latest patch in a single request via the Azure Blob
// container-listing API that backs get.helm.sh. Returns the newest stable patch
// version, or null when the host does not support listing (any non-listing
// response, empty result, or error) so the caller can fall back to probing.
// Only 'major.minor.patch-<platform>' names are matched, so prereleases and
// sidecar files (.sha256, ...) are ignored.
export async function resolveLatestPatchViaListing(
baseURL: string,
major: string,
minor: string
): Promise<string | null> {
let body: string
try {
const listURL = new URL(baseURL)
listURL.searchParams.set('restype', 'container')
listURL.searchParams.set('comp', 'list')
listURL.searchParams.set('prefix', `helm-v${major}.${minor}.`)
const response = await fetch(listURL.toString())
if (!response.ok) {
return null
}
body = await response.text()
} catch {
return null
}
if (!body.includes('<EnumerationResults')) {
return null
}
const patchPattern = new RegExp(
`helm-v${major}\\.${minor}\\.(\\d+)-(?:darwin|linux|windows)`,
'g'
)
let latestPatch = -1
for (const match of body.matchAll(patchPattern)) {
const patch = Number(match[1])
if (patch > latestPatch) {
latestPatch = patch
}
}
if (latestPatch < 0) {
return null
}
return `v${major}.${minor}.${latestPatch}`
}
// Resolves a major.minor value (e.g. '3.14' or 'v3.14') to the newest available
// patch (e.g. 'v3.14.4'). Fast path: a single container-listing request (which
// get.helm.sh supports). When the host does not support listing, it falls back
// to probing the download host for sequential patches. Only 'major.minor.n'
// artifacts are considered, so prereleases are never selected.
export async function resolveLatestPatchVersion(
baseURL: string,
version: string
): Promise<string> {
const [major, minor] = (
version[0] === 'v' ? version.slice(1) : version
).split('.')
const listed = await resolveLatestPatchViaListing(baseURL, major, minor)
if (listed) {
return listed
}
if (!(await helmPatchExists(baseURL, `v${major}.${minor}.0`))) {
throw new Error(`No Helm releases found for ${major}.${minor}`)
}
let latestPatch = 0
let consecutiveMisses = 0
for (
let patch = 1;
patch <= maxPatch && consecutiveMisses < patchLookahead;
patch++
) {
if (await helmPatchExists(baseURL, `v${major}.${minor}.${patch}`)) {
latestPatch = patch
consecutiveMisses = 0
} else {
consecutiveMisses++
}
}
// The look-ahead is what should end the walk. Exhausting maxPatch without a
// trailing run of misses means the host answered 200 for every probe (e.g. a
// catch-all mirror), so the resolved version cannot be trusted.
if (consecutiveMisses < patchLookahead) {
throw new Error(
`Unable to resolve latest patch for ${major}.${minor} (exceeded ${maxPatch} probes)`
)
}
return `v${major}.${minor}.${latestPatch}`
}
export function getArch(): string {
return os.arch() === 'x64' ? 'amd64' : os.arch()
}
@@ -278,11 +78,7 @@ export function getExecutableExtension(): string {
export function getHelmDownloadURL(baseURL: string, version: string): string {
const urlPath = `helm-${version}-${getPlatform()}-${getArch()}.${getArchiveExtension()}`
// Ensure the base ends with '/' so a subpath mirror (e.g.
// 'https://example/kubernetes/helm') is preserved; otherwise URL resolution
// replaces the last path segment and points at the wrong location.
const base = baseURL.endsWith('/') ? baseURL : `${baseURL}/`
const url = new URL(urlPath, base)
const url = new URL(urlPath, baseURL)
return url.toString()
}