Compare commits

..

23 Commits

Author SHA1 Message Date
dependabot[bot] b258094592 chore(deps-dev): bump prettier from 3.9.5 to 3.9.6 in the actions group
Bumps the actions group with 1 update: [prettier](https://github.com/prettier/prettier).


Updates `prettier` from 3.9.5 to 3.9.6
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.5...3.9.6)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.9.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:42:59 +00:00
Ogheneobukome Ejaife 249bb27705 feat: resolve latest patch for a major.minor version (#293)
* feat: resolve latest patch for a major.minor version

Allow the version input and version-file entries to be a major.minor value like
3.14 / v3.14, resolving to the newest available patch (e.g. v3.14.4).

Resolution probes the download host directly: sequential HEAD requests for
helm-v{major}.{minor}.{n} against downloadBaseURL, taking the highest that
returns 200. Works with any file-serving mirror and needs no token or extra
dependency, and cannot resolve to a version that is not downloadable. Only
major.minor.n URLs are probed, so prereleases are never considered.

- Add isMajorMinorShaped, helmPatchExists, and resolveLatestPatchVersion.
- Walk stops after 3 consecutive 404s (look-ahead) to tolerate a skipped patch
  number, with a 100-probe safety cap; hitting the cap throws instead of
  returning a bogus version.
- Wire resolution into run() and relax getVersionFromToolVersionsFile to accept
  a major.minor value.

* feat: support .x/.* wildcards and harden patch probing

Extend the major.minor latest-patch resolution to accept wildcard patch syntax (`3.12.x`, `v3.12.x`, `3.12.*`), matching the syntax requested in Azure/setup-helm#109.

The following smaller fixes were made based on Copilot review feedback:

- helmPatchExists now treats only 404 as "patch absent"; any other non-2xx status (403/405/429/5xx) and network errors are thrown, so rate-limiting, outages, or a host that disallows HEAD can no longer be misread as a missing patch (which could yield a stale version or a false "No Helm releases found").
- Clarify the version-file validation error to reflect that both a full version and a major.minor value are accepted, with concrete examples.
- Tests: use vi.spyOn(globalThis, 'fetch') instead of vi.stubGlobal so restoreAllMocks() reliably restores fetch and the mock never leaks across tests; add coverage for wildcards and the non-404 throw path.

* ### feat: single-request listing fast-path + semver.org validation

**Listing fast-path (with fallback).** Following the discussion about enumeration, `resolveLatestPatchVersion` now first tries a single Azure Blob container-listing request against the download host (which `get.helm.sh` backs), and only falls back to the sequential HEAD-probe walk if the host doesn't return a valid listing. Enumeration and download stay on the **same host**, so there's no risk of resolving a version the host can't serve, and it still works on any mirror.

- Default host: resolution now takes **1 request** instead of ~8 probes (verified live: `3.9`→`v3.9.4`, `3.12`→`v3.12.3`, `3.14`→`v3.14.4`, `3.16`→`v3.16.4`, `2.17`→`v2.17.0`).
- Non-listing hosts fall back to probing; prereleases and `.sha256` sidecars are excluded from the listing parse.

**semver.org regex.** Per review feedback, `isSemVerShaped` now uses the official regex from semver.org (with an added optional leading `v` for Helm tags) instead of the hand-rolled one.

Added tests for the listing fast-path, the fallback, and a non-listing response; full suite is green (`npm test`, `typecheck`, `format-check`).
2026-07-20 15:27:29 -04:00
dependabot[bot] 78f94bcee2 Bump actions/stale in /.github/workflows in the actions group (#290)
Bumps the actions group in /.github/workflows with 1 update: [actions/stale](https://github.com/actions/stale).


Updates `actions/stale` from 10.3.0 to 10.4.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899...1e223db275d687790206a7acac4d1a11bd6fe629)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-16 12:55:00 -04:00
dependabot[bot] bb3997c8bc Bump the actions group across 1 directory with 4 updates (#291)
---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: prettier
  dependency-version: 3.9.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  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>
2026-07-16 12:54:23 -04:00
Ogheneobukome Ejaife eb399fcd4f fix: preserve subpath base URLs in getHelmDownloadURL (#292)
Ensure downloadBaseURL ends with '/' before URL resolution so a subpath
mirror (e.g. https://example/kubernetes/helm) is preserved instead of having
its last path segment replaced. Fixes downloads for all subpath mirrors.
2026-07-15 12:45:05 -07:00
somaz 017211e1b1 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
2026-06-23 13:07:39 -07:00
Ogheneobukome Ejaife 95ecf4967d Bump version to 5.0.1 and update CHANGELOG with recent changes (#287) 2026-06-23 13:05:33 -07:00
dependabot[bot] 4180b1c05c Bump the actions group with 2 updates (#285)
Bumps the actions group with 2 updates: [semver](https://github.com/npm/node-semver) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `semver` from 7.8.4 to 7.8.5
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.4...v7.8.5)

Updates `@types/node` from 25.9.3 to 26.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 26.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 11:53:47 -07:00
dependabot[bot] 6949b7ff3e Bump undici (#284)
Bumps  and [undici](https://github.com/nodejs/undici). These dependencies needed to be updated together.

Updates `undici` from 6.24.0 to 6.27.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.24.0...v6.27.0)

Updates `undici` from 7.24.2 to 7.28.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.24.0...v6.27.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.27.0
  dependency-type: indirect
- dependency-name: undici
  dependency-version: 7.28.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 13:55:16 -07:00
dependabot[bot] 51ce767c78 Bump actions/checkout in /.github/workflows in the actions group (#286)
Bumps the actions group in /.github/workflows with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.3 to 7.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 13:55:11 -07:00
dependabot[bot] d6da4f47af Bump the actions group with 5 updates (#283)
Bumps the actions group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [semver](https://github.com/npm/node-semver) | `7.8.2` | `7.8.4` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.2` | `25.9.3` |
| [esbuild](https://github.com/evanw/esbuild) | `0.28.0` | `0.28.1` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.8.4` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.8` | `4.1.9` |


Updates `semver` from 7.8.2 to 7.8.4
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.2...v7.8.4)

Updates `@types/node` from 25.9.2 to 25.9.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `esbuild` from 0.28.0 to 0.28.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

Updates `prettier` from 3.8.3 to 3.8.4
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.8.4)

Updates `vitest` from 4.1.8 to 4.1.9
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/HEAD/packages/vitest)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: prettier
  dependency-version: 3.8.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  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>
2026-06-15 17:17:19 -04:00
dependabot[bot] 7e2bc108ba Bump actions/checkout in /.github/workflows in the actions group (#280)
Bumps the actions group in /.github/workflows with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6.0.2 to 6.0.3
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/de0fac2e4500dabe0009e67214ff5f5447ce83dd...df4cb1c069e1874edd31b4311f1884172cec0e10)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.3
  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>
2026-06-13 13:27:32 +12:00
dependabot[bot] 64d6be9869 Bump the actions group with 2 updates (#279)
Bumps the actions group with 2 updates: [semver](https://github.com/npm/node-semver) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `semver` from 7.8.1 to 7.8.2
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.1...v7.8.2)

Updates `@types/node` from 25.9.1 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  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>
2026-06-13 13:27:10 +12:00
David Gamero 69214f9d74 fix: use chmod 755 instead of 777 for downloaded helm binary and folder (#278)
World-writable permissions allow other processes on shared runners to replace the helm binary or inject files between download and execution.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-05 11:25:36 +12:00
dependabot[bot] 9dad99fe3c Bump vitest from 4.1.7 to 4.1.8 in the actions group (#277)
Bumps the actions group with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `vitest` from 4.1.7 to 4.1.8
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.8/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.8
  dependency-type: direct:development
  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>
2026-06-05 07:23:12 +12:00
David Gamero 29c884e1c8 chore: remove deprecated OliverMKing release workflow (#276)
Remove tag-and-draft.yml which references OliverMKing/javascript-release-workflow
(unmaintained personal repo, last updated Sept 2022). This is superseded by
Azure/action-release-workflows in release-pr.yml.
2026-06-05 07:22:44 +12:00
dependabot[bot] 381aa48d5b Bump actions/stale in /.github/workflows in the actions group (#275)
Bumps the actions group in /.github/workflows with 1 update: [actions/stale](https://github.com/actions/stale).


Updates `actions/stale` from 10.2.0 to 10.3.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/b5d41d4e1d5dceea10e7104786b73624c18a190f...eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-26 18:19:45 +12:00
dependabot[bot] dc716b1277 Bump the actions group with 3 updates (#274)
Bumps the actions group with 3 updates: [semver](https://github.com/npm/node-semver), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `semver` from 7.8.0 to 7.8.1
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.0...v7.8.1)

Updates `@types/node` from 25.9.0 to 25.9.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vitest` from 4.1.6 to 4.1.7
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.7/packages/vitest)

---
updated-dependencies:
- dependency-name: semver
  dependency-version: 7.8.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 25.9.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: vitest
  dependency-version: 4.1.7
  dependency-type: direct:development
  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>
2026-05-26 18:18:50 +12:00
dependabot[bot] 407a08ffb6 Bump the actions group with 2 updates (#273)
Bumps the actions group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@types/node` from 25.6.2 to 25.9.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `vitest` from 4.1.5 to 4.1.6
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.6/packages/vitest)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: vitest
  dependency-version: 4.1.6
  dependency-type: direct:development
  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>
2026-05-18 10:19:21 -07:00
dependabot[bot] 7b39664db5 Bump the actions group across 1 directory with 7 updates (#269)
Bumps the actions group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core) | `3.0.0` | `3.0.1` |
| [semver](https://github.com/npm/node-semver) | `7.7.3` | `7.8.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.0.9` | `25.6.2` |
| [esbuild](https://github.com/evanw/esbuild) | `0.27.4` | `0.28.0` |
| [prettier](https://github.com/prettier/prettier) | `3.8.0` | `3.8.3` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.9.3` | `6.0.3` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.0` | `4.1.5` |



Updates `@actions/core` from 3.0.0 to 3.0.1
- [Changelog](https://github.com/actions/toolkit/blob/main/packages/core/RELEASES.md)
- [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/core)

Updates `semver` from 7.7.3 to 7.8.0
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.7.3...v7.8.0)

Updates `@types/node` from 25.0.9 to 25.6.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `esbuild` from 0.27.4 to 0.28.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.27.4...v0.28.0)

Updates `prettier` from 3.8.0 to 3.8.3
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.0...3.8.3)

Updates `typescript` from 5.9.3 to 6.0.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3)

Updates `vitest` from 4.1.0 to 4.1.5
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.5/packages/vitest)

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: esbuild
  dependency-version: 0.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: prettier
  dependency-version: 3.8.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: semver
  dependency-version: 7.7.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: vitest
  dependency-version: 4.1.5
  dependency-type: direct:development
  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>
2026-05-12 23:54:43 +12:00
David Gamero 3090233644 Fix TypeScript 6.0 compatibility errors in run.ts (#271) 2026-05-05 11:18:11 -07:00
dependabot[bot] 7202adf694 Bump vite from 8.0.0 to 8.0.5 (#268)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.0 to 8.0.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 21:15:59 +12:00
David Gamero 1e2e44e1bc Migrate to ESM with esbuild/vitest and upgrade to node24 (#260)
- Add "type": "module" to package.json for ESM support
- Replace ncc with esbuild for bundling (with createRequire banner for CJS compat)
- Replace jest/ts-jest with vitest for testing
- Upgrade @actions/core to ^3.0.0, @actions/exec to ^3.0.0, @actions/io to ^3.0.2, @actions/tool-cache to 4.0.0 (ESM-only versions)
- Update tsconfig.json to use NodeNext module resolution
- Convert all tests to use vi.mock() factory pattern for ESM-only module compatibility
- Update action.yml runtime from node20 to node24
- Bump version from 4.3.1 to 5.0.0 (major version bump for breaking node runtime change)
2026-03-24 10:46:19 -07:00
18 changed files with 2568 additions and 38637 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@b5d41d4e1d5dceea10e7104786b73624c18a190f #v10.2.0
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 #v10.4.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@b5d41d4e1d5dceea10e7104786b73624c18a190f #v10.2.0
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 #v10.4.0
name: Setting PR as idle
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: npm install and build
id: action-npm-build
run: |
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Enforce Prettier
run: npx prettier --check .
-10
View File
@@ -1,10 +0,0 @@
name: Tag and create release draft
on:
push:
branches:
- releases/*
jobs:
tag-and-release:
uses: OliverMKing/javascript-release-workflow/.github/workflows/tag-and-release.yml@c753e1545b144562237cd1177a95bab21a785cff # main
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Run L0 tests.
run: |
+6
View File
@@ -11,6 +11,8 @@ pids
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
@@ -62,3 +64,7 @@ node_modules
coverage
# Transpiled JS
lib/
# Planning docs
docs/
+24
View File
@@ -1,5 +1,29 @@
# Change Log
## [5.0.1] - 2026-06-23
### Fixed
- #271 [Fix TypeScript 6.0 compatibility errors in run.ts](https://github.com/Azure/setup-helm/pull/271)
- #278 [fix: use chmod 755 instead of 777 for downloaded helm binary and folder](https://github.com/Azure/setup-helm/pull/278)
### Changed
- #286 [Bump actions/checkout from 6.0.3 to 7.0.0 in /.github/workflows in the actions group](https://github.com/Azure/setup-helm/pull/286)
- #285 [Bump the actions group with 2 updates](https://github.com/Azure/setup-helm/pull/285)
- #284 [Bump undici](https://github.com/Azure/setup-helm/pull/284)
- #283 [Bump the actions group with 5 updates](https://github.com/Azure/setup-helm/pull/283)
- #280 [Bump actions/checkout from 6.0.2 to 6.0.3 in /.github/workflows in the actions group](https://github.com/Azure/setup-helm/pull/280)
- #279 [Bump the actions group with 2 updates](https://github.com/Azure/setup-helm/pull/279)
- #277 [Bump vitest from 4.1.7 to 4.1.8 in the actions group](https://github.com/Azure/setup-helm/pull/277)
- #276 [chore: remove deprecated OliverMKing release workflow, pin to SHA](https://github.com/Azure/setup-helm/pull/276)
- #275 [Bump actions/stale from 10.2.0 to 10.3.0 in /.github/workflows in the actions group](https://github.com/Azure/setup-helm/pull/275)
- #274 [Bump the actions group with 3 updates](https://github.com/Azure/setup-helm/pull/274)
- #273 [Bump the actions group with 2 updates](https://github.com/Azure/setup-helm/pull/273)
- #269 [Bump the actions group across 1 directory with 7 updates](https://github.com/Azure/setup-helm/pull/269)
- #268 [Bump vite from 8.0.0 to 8.0.5](https://github.com/Azure/setup-helm/pull/268)
- #260 [Migrate to ESM with esbuild/vitest and upgrade to node24 (v5.0.0)](https://github.com/Azure/setup-helm/pull/260)
## [5.0.0] - 2026-03-23
### Changed
+19 -2
View File
@@ -7,14 +7,31 @@ Install a specific version of helm binary on the runner.
Acceptable values are latest or any semantic version string like v3.5.0 Use this action in workflow to define which version of helm will be used. v2+ of this action only support Helm3.
```yaml
- uses: azure/setup-helm@v5.0.0
- uses: azure/setup-helm@v5
with:
version: '<version>' # default is latest (stable)
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.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.
Refer to the action metadata file for details about all the inputs https://github.com/Azure/setup-helm/blob/master/action.yml
+4 -1
View File
@@ -3,8 +3,11 @@ description: 'Install a specific version of helm binary. Acceptable values are l
inputs:
version:
description: 'Version of helm'
required: true
required: false
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
-18
View File
@@ -1,18 +0,0 @@
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true,
coverageThreshold: {
global: {
branches: 0,
functions: 14,
lines: 27,
statements: 27
}
}
}
-34325
View File
File diff suppressed because one or more lines are too long
+1738 -4108
View File
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -1,35 +1,35 @@
{
"name": "setuphelm",
"version": "5.0.0",
"version": "5.0.1",
"private": true,
"description": "Setup helm",
"author": "Anumita Shenoy",
"license": "MIT",
"type": "module",
"dependencies": {
"@actions/core": "^2.0.2",
"@actions/exec": "^2.0.0",
"@actions/io": "^2.0.0",
"@actions/tool-cache": "3.0.0",
"@actions/core": "^3.0.1",
"@actions/exec": "^3.0.0",
"@actions/io": "^3.0.2",
"@actions/tool-cache": "4.0.0",
"@octokit/action": "^8.0.4",
"semver": "^7.7.3"
"semver": "^7.8.5"
},
"main": "lib/index.js",
"scripts": {
"build": "ncc build src/index.ts -o lib",
"test": "jest",
"test-coverage": "jest --coverage",
"build": "tsc --noEmit && esbuild src/index.ts --bundle --platform=node --target=node20 --format=esm --outfile=lib/index.js --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);\"",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test-coverage": "vitest run --coverage",
"format": "prettier --write .",
"format-check": "prettier --check .",
"prepare": "husky"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^25.0.9",
"@vercel/ncc": "^0.38.4",
"@types/node": "^26.1.1",
"esbuild": "^0.28",
"husky": "^9.1.7",
"jest": "^30.2.0",
"prettier": "^3.8.0",
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
"prettier": "^3.9.6",
"typescript": "^7.0.2",
"vitest": "^4"
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import {run} from './run'
import {run} from './run.js'
import * as core from '@actions/core'
run().catch(core.setFailed)
+516 -76
View File
@@ -1,8 +1,63 @@
import * as run from './run'
import {vi, describe, test, expect, afterEach} from 'vitest'
import * as path from 'path'
// Mock os module
vi.mock('os', async (importOriginal) => {
const actual = await importOriginal<typeof import('os')>()
return {
...actual,
platform: vi.fn(),
arch: vi.fn()
}
})
// Mock fs module
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>()
return {
...actual,
readdirSync: vi.fn(),
statSync: vi.fn(),
chmodSync: vi.fn(),
readFileSync: vi.fn(),
existsSync: vi.fn()
}
})
// Mock @actions/core
vi.mock('@actions/core', async (importOriginal) => {
const actual = await importOriginal<typeof import('@actions/core')>()
return {
...actual,
getInput: vi.fn(),
info: vi.fn(),
debug: vi.fn(),
warning: vi.fn(),
startGroup: vi.fn(),
endGroup: vi.fn(),
addPath: vi.fn(),
setOutput: vi.fn(),
setFailed: vi.fn()
}
})
// Mock @actions/tool-cache
vi.mock('@actions/tool-cache', async (importOriginal) => {
const actual = await importOriginal<typeof import('@actions/tool-cache')>()
return {
...actual,
find: vi.fn(),
downloadTool: vi.fn(),
extractZip: vi.fn(),
extractTar: vi.fn(),
cacheDir: vi.fn()
}
})
import * as run from './run.js'
import * as os from 'os'
import * as toolCache from '@actions/tool-cache'
import * as fs from 'fs'
import * as path from 'path'
import * as core from '@actions/core'
describe('run.ts', () => {
@@ -10,26 +65,26 @@ describe('run.ts', () => {
// Cleanup mocks after each test to ensure that subsequent tests are not affected by the mocks.
afterEach(() => {
jest.restoreAllMocks()
vi.restoreAllMocks()
})
test('getExecutableExtension() - return .exe when os is Windows', () => {
jest.spyOn(os, 'platform').mockReturnValue('win32')
vi.mocked(os.platform).mockReturnValue('win32')
expect(run.getExecutableExtension()).toBe('.exe')
expect(os.platform).toHaveBeenCalled()
})
test('getExecutableExtension() - return empty string for non-windows OS', () => {
jest.spyOn(os, 'platform').mockReturnValue('darwin')
vi.mocked(os.platform).mockReturnValue('darwin')
expect(run.getExecutableExtension()).toBe('')
expect(os.platform).toHaveBeenCalled()
})
test('getHelmDownloadURL() - return the URL to download helm for Linux amd64', () => {
jest.spyOn(os, 'platform').mockReturnValue('linux')
jest.spyOn(os, 'arch').mockReturnValue('x64')
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('x64')
const expected = 'https://test.tld/helm-v3.8.0-linux-amd64.tar.gz'
expect(run.getHelmDownloadURL(downloadBaseURL, 'v3.8.0')).toBe(expected)
@@ -38,8 +93,8 @@ describe('run.ts', () => {
})
test('getHelmDownloadURL() - return the URL to download helm for Linux arm64', () => {
jest.spyOn(os, 'platform').mockReturnValue('linux')
jest.spyOn(os, 'arch').mockReturnValue('arm64')
vi.mocked(os.platform).mockReturnValue('linux')
vi.mocked(os.arch).mockReturnValue('arm64')
const expected = 'https://test.tld/helm-v3.8.0-linux-arm64.tar.gz'
expect(run.getHelmDownloadURL(downloadBaseURL, 'v3.8.0')).toBe(expected)
@@ -48,8 +103,8 @@ describe('run.ts', () => {
})
test('getHelmDownloadURL() - return the URL to download helm for Darwin x64', () => {
jest.spyOn(os, 'platform').mockReturnValue('darwin')
jest.spyOn(os, 'arch').mockReturnValue('x64')
vi.mocked(os.platform).mockReturnValue('darwin')
vi.mocked(os.arch).mockReturnValue('x64')
const expected = 'https://test.tld/helm-v3.8.0-darwin-amd64.tar.gz'
expect(run.getHelmDownloadURL(downloadBaseURL, 'v3.8.0')).toBe(expected)
@@ -58,8 +113,8 @@ describe('run.ts', () => {
})
test('getHelmDownloadURL() - return the URL to download helm for Darwin arm64', () => {
jest.spyOn(os, 'platform').mockReturnValue('darwin')
jest.spyOn(os, 'arch').mockReturnValue('arm64')
vi.mocked(os.platform).mockReturnValue('darwin')
vi.mocked(os.arch).mockReturnValue('arm64')
const expected = 'https://test.tld/helm-v3.8.0-darwin-arm64.tar.gz'
expect(run.getHelmDownloadURL(downloadBaseURL, 'v3.8.0')).toBe(expected)
@@ -68,8 +123,8 @@ describe('run.ts', () => {
})
test('getHelmDownloadURL() - return the URL to download helm for Windows x64', () => {
jest.spyOn(os, 'platform').mockReturnValue('win32')
jest.spyOn(os, 'arch').mockReturnValue('x64')
vi.mocked(os.platform).mockReturnValue('win32')
vi.mocked(os.arch).mockReturnValue('x64')
const expected = 'https://test.tld/helm-v3.8.0-windows-amd64.zip'
expect(run.getHelmDownloadURL(downloadBaseURL, 'v3.8.0')).toBe(expected)
@@ -77,26 +132,56 @@ describe('run.ts', () => {
})
test('getHelmDownloadURL() - return the URL to download helm for Windows arm64', () => {
jest.spyOn(os, 'platform').mockReturnValue('win32')
jest.spyOn(os, 'arch').mockReturnValue('arm64')
vi.mocked(os.platform).mockReturnValue('win32')
vi.mocked(os.arch).mockReturnValue('arm64')
const expected = 'https://test.tld/helm-v3.8.0-windows-arm64.zip'
expect(run.getHelmDownloadURL(downloadBaseURL, 'v3.8.0')).toBe(expected)
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
global.fetch = jest.fn().mockReturnValue(res)
vi.spyOn(globalThis, 'fetch').mockResolvedValue(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'
global.fetch = jest.fn().mockRejectedValueOnce(new Error(errorMessage))
vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(
new Error(errorMessage)
)
expect(await run.getLatestHelmVersion()).toBe(run.stableHelmVersion)
})
@@ -104,8 +189,367 @@ 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', () => {
jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => {
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
if (file == 'mainFolder')
return [
'file1' as unknown as fs.Dirent<NonSharedBuffer>,
@@ -125,8 +569,7 @@ describe('run.ts', () => {
]
return []
})
jest.spyOn(core, 'debug').mockImplementation()
jest.spyOn(fs, 'statSync').mockImplementation((file) => {
vi.mocked(fs.statSync).mockImplementation((file) => {
const isDirectory =
(file as string).toLowerCase().indexOf('file') == -1 ? true : false
return {isDirectory: () => isDirectory} as fs.Stats
@@ -140,7 +583,7 @@ describe('run.ts', () => {
})
test('walkSync() - return empty array if no file with name fileToFind exists', () => {
jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => {
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
if (file == 'mainFolder')
return [
'file1' as unknown as fs.Dirent<NonSharedBuffer>,
@@ -160,8 +603,7 @@ describe('run.ts', () => {
]
return []
})
jest.spyOn(core, 'debug').mockImplementation()
jest.spyOn(fs, 'statSync').mockImplementation((file) => {
vi.mocked(fs.statSync).mockImplementation((file) => {
const isDirectory =
(file as string).toLowerCase().indexOf('file') == -1 ? true : false
return {isDirectory: () => isDirectory} as fs.Stats
@@ -173,18 +615,18 @@ describe('run.ts', () => {
})
test('findHelm() - change access permissions and find the helm in given directory', () => {
jest.spyOn(fs, 'chmodSync').mockImplementation(() => {})
jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => {
vi.mocked(fs.chmodSync).mockImplementation(() => {})
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
if (file == 'mainFolder')
return ['helm.exe' as unknown as fs.Dirent<NonSharedBuffer>]
return []
})
jest.spyOn(fs, 'statSync').mockImplementation((file) => {
vi.mocked(fs.statSync).mockImplementation((file) => {
const isDirectory =
(file as string).indexOf('folder') == -1 ? false : true
return {isDirectory: () => isDirectory} as fs.Stats
})
jest.spyOn(os, 'platform').mockReturnValue('win32')
vi.mocked(os.platform).mockReturnValue('win32')
expect(run.findHelm('mainFolder')).toBe(
path.join('mainFolder', 'helm.exe')
@@ -192,15 +634,15 @@ describe('run.ts', () => {
})
test('findHelm() - throw error if executable not found', () => {
jest.spyOn(fs, 'chmodSync').mockImplementation(() => {})
jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => {
vi.mocked(fs.chmodSync).mockImplementation(() => {})
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => {
if (file == 'mainFolder') return []
return []
})
jest.spyOn(fs, 'statSync').mockImplementation((file) => {
vi.mocked(fs.statSync).mockImplementation((file) => {
return {isDirectory: () => true} as fs.Stats
})
jest.spyOn(os, 'platform').mockReturnValue('win32')
vi.mocked(os.platform).mockReturnValue('win32')
expect(() => run.findHelm('mainFolder')).toThrow(
'Helm executable not found in path mainFolder'
@@ -208,21 +650,19 @@ describe('run.ts', () => {
})
test('downloadHelm() - download helm and return path to it', async () => {
jest.spyOn(toolCache, 'find').mockReturnValue('')
jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool')
vi.mocked(toolCache.find).mockReturnValue('')
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
const response = JSON.stringify([{tag_name: 'v4.0.0'}])
jest.spyOn(fs, 'readFileSync').mockReturnValue(response)
jest.spyOn(os, 'platform').mockReturnValue('win32')
jest.spyOn(os, 'arch').mockReturnValue('x64')
jest.spyOn(fs, 'chmodSync').mockImplementation(() => {})
jest.spyOn(toolCache, 'extractZip').mockResolvedValue('extractedPath')
jest.spyOn(toolCache, 'cacheDir').mockResolvedValue('pathToCachedDir')
jest
.spyOn(fs, 'readdirSync')
.mockImplementation((file, _) => [
'helm.exe' as unknown as fs.Dirent<NonSharedBuffer>
])
jest.spyOn(fs, 'statSync').mockImplementation((file) => {
vi.mocked(fs.readFileSync).mockReturnValue(response)
vi.mocked(os.platform).mockReturnValue('win32')
vi.mocked(os.arch).mockReturnValue('x64')
vi.mocked(fs.chmodSync).mockImplementation(() => {})
vi.mocked(toolCache.extractZip).mockResolvedValue('extractedPath')
vi.mocked(toolCache.cacheDir).mockResolvedValue('pathToCachedDir')
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => [
'helm.exe' as unknown as fs.Dirent<NonSharedBuffer>
])
vi.mocked(fs.statSync).mockImplementation((file) => {
const isDirectory =
(file as string).indexOf('folder') == -1 ? false : true
return {isDirectory: () => isDirectory} as fs.Stats
@@ -235,21 +675,21 @@ describe('run.ts', () => {
expect(toolCache.downloadTool).toHaveBeenCalledWith(
'https://test.tld/helm-v4.0.0-windows-amd64.zip'
)
expect(fs.chmodSync).toHaveBeenCalledWith('pathToTool', '777')
expect(fs.chmodSync).toHaveBeenCalledWith('pathToTool', '755')
expect(toolCache.extractZip).toHaveBeenCalledWith('pathToTool')
expect(fs.chmodSync).toHaveBeenCalledWith(
path.join('pathToCachedDir', 'helm.exe'),
'777'
'755'
)
})
test('downloadHelm() - throw error if unable to download', async () => {
jest.spyOn(toolCache, 'find').mockReturnValue('')
jest.spyOn(toolCache, 'downloadTool').mockImplementation(async () => {
vi.mocked(toolCache.find).mockReturnValue('')
vi.mocked(toolCache.downloadTool).mockImplementation(async () => {
throw 'Unable to download'
})
jest.spyOn(os, 'platform').mockReturnValue('win32')
jest.spyOn(os, 'arch').mockReturnValue('x64')
vi.mocked(os.platform).mockReturnValue('win32')
vi.mocked(os.arch).mockReturnValue('x64')
const downloadUrl = 'https://test.tld/helm-v3.2.1-windows-amd64.zip'
await expect(run.downloadHelm(downloadBaseURL, 'v3.2.1')).rejects.toThrow(
@@ -260,17 +700,17 @@ describe('run.ts', () => {
})
test('downloadHelm() - return path to helm tool with same version from toolCache', async () => {
jest.spyOn(toolCache, 'find').mockReturnValue('pathToCachedDir')
jest.spyOn(toolCache, 'cacheDir').mockResolvedValue('pathToCachedDir')
jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool')
jest.spyOn(toolCache, 'extractZip').mockResolvedValue('extractedPath')
jest.spyOn(os, 'platform').mockReturnValue('win32')
jest.spyOn(os, 'arch').mockReturnValue('x64')
jest.spyOn(fs, 'chmodSync').mockImplementation(() => {})
jest
.spyOn(fs, 'readdirSync')
.mockReturnValue(['helm.exe' as unknown as fs.Dirent<NonSharedBuffer>])
jest.spyOn(fs, 'statSync').mockImplementation((file) => {
vi.mocked(toolCache.find).mockReturnValue('pathToCachedDir')
vi.mocked(toolCache.cacheDir).mockResolvedValue('pathToCachedDir')
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
vi.mocked(toolCache.extractZip).mockResolvedValue('extractedPath')
vi.mocked(os.platform).mockReturnValue('win32')
vi.mocked(os.arch).mockReturnValue('x64')
vi.mocked(fs.chmodSync).mockImplementation(() => {})
vi.mocked(fs.readdirSync).mockReturnValue([
'helm.exe' as unknown as fs.Dirent<NonSharedBuffer>
])
vi.mocked(fs.statSync).mockImplementation((file) => {
const isDirectory =
(file as string).indexOf('folder') == -1 ? false : true
return {isDirectory: () => isDirectory} as fs.Stats
@@ -282,21 +722,21 @@ describe('run.ts', () => {
expect(toolCache.find).toHaveBeenCalledWith('helm', 'v3.2.1')
expect(fs.chmodSync).toHaveBeenCalledWith(
path.join('pathToCachedDir', 'helm.exe'),
'777'
'755'
)
})
test('downloadHelm() - throw error is helm is not found in path', async () => {
jest.spyOn(toolCache, 'find').mockReturnValue('')
jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool')
jest.spyOn(toolCache, 'cacheDir').mockResolvedValue('pathToCachedDir')
jest.spyOn(toolCache, 'downloadTool').mockResolvedValue('pathToTool')
jest.spyOn(toolCache, 'extractZip').mockResolvedValue('extractedPath')
jest.spyOn(os, 'platform').mockReturnValue('win32')
jest.spyOn(os, 'arch').mockReturnValue('x64')
jest.spyOn(fs, 'chmodSync').mockImplementation()
jest.spyOn(fs, 'readdirSync').mockImplementation((file, _) => [])
jest.spyOn(fs, 'statSync').mockImplementation((file) => {
vi.mocked(toolCache.find).mockReturnValue('')
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
vi.mocked(toolCache.cacheDir).mockResolvedValue('pathToCachedDir')
vi.mocked(toolCache.downloadTool).mockResolvedValue('pathToTool')
vi.mocked(toolCache.extractZip).mockResolvedValue('extractedPath')
vi.mocked(os.platform).mockReturnValue('win32')
vi.mocked(os.arch).mockReturnValue('x64')
vi.mocked(fs.chmodSync).mockImplementation(() => {})
vi.mocked(fs.readdirSync).mockImplementation((file, _?) => [])
vi.mocked(fs.statSync).mockImplementation((file) => {
const isDirectory =
(file as string).indexOf('folder') == -1 ? false : true
return {isDirectory: () => isDirectory} as fs.Stats
@@ -309,7 +749,7 @@ describe('run.ts', () => {
expect(toolCache.downloadTool).toHaveBeenCalledWith(
'https://test.tld/helm-v3.2.1-windows-amd64.zip'
)
expect(fs.chmodSync).toHaveBeenCalledWith('pathToTool', '777')
expect(fs.chmodSync).toHaveBeenCalledWith('pathToTool', '755')
expect(toolCache.extractZip).toHaveBeenCalledWith('pathToTool')
})
})
+219 -15
View File
@@ -13,24 +13,42 @@ const helmToolName = 'helm'
export const stableHelmVersion = 'v3.18.4'
export async function run() {
let version = core.getInput('version', {required: true})
let version = core.getInput('version')
const versionFile = core.getInput('version-file')
if (version !== 'latest' && version[0] !== 'v') {
core.info('Getting latest Helm version')
version = getValidVersion(version)
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.toLocaleLowerCase() === 'latest') {
version = await getLatestHelmVersion()
if (!version) {
version = 'latest'
}
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()
try {
if (!process.env['PATH'].startsWith(path.dirname(cachedPath))) {
if (!process.env['PATH']?.startsWith(path.dirname(cachedPath))) {
core.addPath(path.dirname(cachedPath))
}
} catch {
@@ -46,6 +64,64 @@ 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 {
@@ -54,12 +130,136 @@ export async function getLatestHelmVersion(): Promise<string> {
return release
} catch (err) {
core.warning(
`Error while fetching latest Helm release: ${err.toString()}. Using default version ${stableHelmVersion}`
`Error while fetching latest Helm release: ${err instanceof Error ? err.message : String(err)}. Using default version ${stableHelmVersion}`
)
return stableHelmVersion
}
}
// 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()
}
@@ -78,7 +278,11 @@ export function getExecutableExtension(): string {
export function getHelmDownloadURL(baseURL: string, version: string): string {
const urlPath = `helm-${version}-${getPlatform()}-${getArch()}.${getArchiveExtension()}`
const url = new URL(urlPath, baseURL)
// 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)
return url.toString()
}
@@ -105,7 +309,7 @@ export async function downloadHelm(
)
}
fs.chmodSync(helmDownloadPath, '777')
fs.chmodSync(helmDownloadPath, '755')
const extractedPath =
getPlatform() === 'windows'
? await toolCache.extractZip(helmDownloadPath)
@@ -125,13 +329,13 @@ export async function downloadHelm(
)
}
fs.chmodSync(helmpath, '777')
fs.chmodSync(helmpath, '755')
return helmpath
}
export function findHelm(rootFolder: string): string {
fs.chmodSync(rootFolder, '777')
var filelist: string[] = []
fs.chmodSync(rootFolder, '755')
let filelist: string[] = []
walkSync(rootFolder, filelist, helmToolName + getExecutableExtension())
if (!filelist || filelist.length == 0) {
throw new Error(
@@ -142,8 +346,8 @@ export function findHelm(rootFolder: string): string {
}
}
export var walkSync = function (dir, filelist, fileToFind) {
var files = fs.readdirSync(dir)
export function walkSync(dir, filelist, fileToFind) {
const files = fs.readdirSync(dir)
filelist = filelist || []
files.forEach(function (file) {
if (fs.statSync(path.join(dir, file)).isDirectory()) {
+10 -60
View File
@@ -1,64 +1,14 @@
{
"compilerOptions": {
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "./lib" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
//"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"rootDir": "./src",
"outDir": "./lib",
"noImplicitAny": false,
"skipLibCheck": true,
"noEmit": true,
"types": ["node", "vitest/globals"]
},
"exclude": ["node_modules", "**/*.test.ts"]
"exclude": ["node_modules", "**/*.test.ts", "vitest.config.ts"]
}
+10
View File
@@ -0,0 +1,10 @@
import {defineConfig} from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
clearMocks: true
}
})