Compare commits

...

88 Commits
v5.0.2 ... main

Author SHA1 Message Date
dependabot[bot]
4ce14be7f2
Bump the actions group with 2 updates (#514)
Bumps the actions group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [esbuild](https://github.com/evanw/esbuild).


Updates `@types/node` from 25.5.0 to 25.5.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)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: esbuild
  dependency-version: 0.28.0
  dependency-type: direct:development
  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-04-07 21:15:20 +12:00
dependabot[bot]
d1db10bbe0
Bump vite from 8.0.3 to 8.0.5 (#513) 2026-04-07 06:44:09 +12:00
dependabot[bot]
9b1aca534a
Bump vitest from 4.1.1 to 4.1.2 in the actions group (#511)
Bumps the actions group with 1 update: [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


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

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.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>
Co-authored-by: Tatsat (Tats) Mishra 🐉 <tamishra@microsoft.com>
2026-04-06 07:56:39 +00:00
dependabot[bot]
566b1027eb
Bump the actions group in /.github/workflows with 2 updates (#512)
Bumps the actions group in /.github/workflows with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [Azure/setup-kubectl](https://github.com/azure/setup-kubectl).


Updates `github/codeql-action` from 4.34.1 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](3869755554...c10b8064de)

Updates `Azure/setup-kubectl` from 4.0.1 to 5.0.0
- [Release notes](https://github.com/azure/setup-kubectl/releases)
- [Changelog](https://github.com/Azure/setup-kubectl/blob/main/CHANGELOG.md)
- [Commits](776406bce9...15650b3ad7)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: Azure/setup-kubectl
  dependency-version: 5.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-04-05 10:06:34 +12:00
dependabot[bot]
4bab0cb90d
Bump the actions group across 1 directory with 2 updates (#510)
Bumps the actions group with 2 updates in the /.github/workflows directory: [github/codeql-action](https://github.com/github/codeql-action) and [azure/login](https://github.com/azure/login).


Updates `github/codeql-action` from 4.32.6 to 4.34.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](0d579ffd05...3869755554)

Updates `azure/login` from 2.3.0 to 3.0.0
- [Release notes](https://github.com/azure/login/releases)
- [Commits](https://github.com/azure/login/compare/v2.3.0...v3.0.0)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: azure/login
  dependency-version: 3.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-03-25 23:39:46 +00:00
dependabot[bot]
2c9e6e7762
Bump the actions group across 1 directory with 4 updates (#509)
Bumps the actions group with 4 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [esbuild](https://github.com/evanw/esbuild), [typescript](https://github.com/microsoft/TypeScript) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


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

Updates `esbuild` from 0.27.3 to 0.27.4
- [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.3...v0.27.4)

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

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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: esbuild
  dependency-version: 0.27.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: typescript
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: vitest
  dependency-version: 4.1.1
  dependency-type: direct:development
  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-03-24 17:36:16 +13:00
dependabot[bot]
104a7063f6
Bump @types/node from 25.3.3 to 25.4.0 in the actions group (#501)
Bumps the actions group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.4.0
  dependency-type: direct:development
  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>
Co-authored-by: David Gamero <david340804@gmail.com>
Co-authored-by: Tatsat (Tats) Mishra 🐉 <tamishra@microsoft.com>
2026-03-14 10:25:33 +00:00
David Gamero
e7aa4f9d0c
Update Node.js runtime from node20 to node24 (#504)
Co-authored-by: Suneha Bose <123775811+bosesuneha@users.noreply.github.com>
Co-authored-by: Tatsat (Tats) Mishra 🐉 <tamishra@microsoft.com>
2026-03-14 23:22:26 +13:00
dependabot[bot]
30dbc03366
Bump undici from 6.23.0 to 6.24.1 (#506)
Bumps [undici](https://github.com/nodejs/undici) from 6.23.0 to 6.24.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.23.0...v6.24.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.24.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-14 22:48:42 +13:00
dependabot[bot]
f4bacf1216
Bump github/codeql-action in /.github/workflows in the actions group (#502)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.32.5 to 4.32.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](c793b717bc...0d579ffd05)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.6
  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-03-12 16:31:23 -07:00
David Gamero
49d0f2a6fd
docs: update action version references in README to latest majors (#500) 2026-03-05 12:02:12 -08:00
David Gamero
a4d35bd653
releases/v5.1.0 (#499)
* chore(release): bump version to 5.1.0

* chore(release): update package-lock.json to 5.1.0
2026-03-03 13:16:48 -08:00
David Gamero
e202ed4d60
Add typecheck to build script (#498) 2026-03-03 12:34:01 -08:00
dependabot[bot]
80628307ac
Bump the actions group in /.github/workflows with 2 updates (#494)
Bumps the actions group in /.github/workflows with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [actions/stale](https://github.com/actions/stale).


Updates `github/codeql-action` from 4.32.3 to 4.32.4
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](9e907b5e64...89a39a4e59)

Updates `actions/stale` from 10.1.1 to 10.2.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](997185467f...b5d41d4e1d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/stale
  dependency-version: 10.2.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>
Co-authored-by: David Gamero <david340804@gmail.com>
2026-03-03 17:28:00 +00:00
dependabot[bot]
332b91818e
Bump @types/node from 25.3.0 to 25.3.3 in the actions group (#497)
Bumps the actions group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  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-03-03 12:07:07 -05:00
dependabot[bot]
8bb8e3e420
Bump the actions group across 1 directory with 2 updates (#495)
Bumps the actions group with 2 updates in the / directory: [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@octokit/plugin-retry` from 8.0.3 to 8.1.0
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v8.0.3...v8.1.0)

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

---
updated-dependencies:
- dependency-name: "@octokit/plugin-retry"
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 25.3.0
  dependency-type: direct:development
  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>
Co-authored-by: David Gamero <david340804@gmail.com>
2026-03-02 18:06:52 +00:00
dependabot[bot]
f3086d990b
Bump rollup from 4.57.1 to 4.59.0 (#496)
Bumps [rollup](https://github.com/rollup/rollup) from 4.57.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.57.1...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.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-03-02 13:04:32 -05:00
David Gamero
01cfe404ef
Migrate to esbuild/Vitest and upgrade @actions/* to ESM-only versions (#492)
* Migrate build toolchain from ncc/Jest to esbuild/Vitest

Replace the legacy ncc/Jest/Babel build stack with a modern ESM toolchain:

Build:
- Replace @vercel/ncc with esbuild (--platform=node --target=node20 --format=esm)
- Add createRequire banner for CJS interop in ESM bundle
- Add "type": "module" to package.json
- Add tsc --noEmit typecheck script (esbuild strips types without checking)
- Add typecheck to husky pre-commit hook

Dependencies:
- Bump @actions/core@3, exec@3, io@3, tool-cache@4 (ESM-only)
- Replace jest/ts-jest/@babel/* with vitest@4

Tests:
- Convert 29 test files: jest.fn()→vi.fn(), jest.mock()→vi.mock(), jest.spyOn()→vi.spyOn()
- Fix vitest 4 compat: mockImplementation requires args, mock call tracking, await .rejects

CI:
- Update build step from ncc build → npm run build
- Update composite action to use npm run build

* Switch tsconfig to NodeNext module resolution

Change module/moduleResolution from ES2022/bundler to NodeNext/NodeNext
and target from ES2022 to ES2020.

- Add .js extensions to all relative imports across 59 source/test files
  (required by NodeNext module resolution)
- Add vitest/globals to tsconfig types array for global test API declarations
2026-02-24 11:57:56 -08:00
dependabot[bot]
84e2095bf0
Bump github/codeql-action in /.github/workflows in the actions group (#491)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.32.0 to 4.32.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](b20883b0cd...9e907b5e64)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.32.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>
Co-authored-by: Tatsat (Tats) Mishra 🐉 <tamishra@microsoft.com>
2026-02-17 15:18:18 -05:00
dependabot[bot]
1ad0b3bc5b
Bump undici and @actions/http-client (#490)
Bumps [undici](https://github.com/nodejs/undici) and [@actions/http-client](https://github.com/actions/toolkit/tree/HEAD/packages/http-client). These dependencies needed to be updated together.

Updates `undici` from 5.29.0 to 6.23.0
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.29.0...v6.23.0)

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

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 6.23.0
  dependency-type: indirect
- dependency-name: "@actions/http-client"
  dependency-version: 3.0.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 08:51:55 +13:00
dependabot[bot]
8f82d97be7
Bump the actions group in /.github/workflows with 3 updates (#488)
Bumps the actions group in /.github/workflows with 3 updates: [actions/checkout](https://github.com/actions/checkout), [github/codeql-action](https://github.com/github/codeql-action) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `actions/checkout` from 6.0.1 to 6.0.2
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](8e8c483db8...de0fac2e45)

Updates `github/codeql-action` from 4.31.10 to 4.32.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cdefb33c0f...b20883b0cd)

Updates `actions/setup-python` from 6.1.0 to 6.2.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](83679a892e...a309ff8b42)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: 4.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 6.2.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-02-10 12:51:17 -08:00
dependabot[bot]
d9732d2f20
Bump the actions group with 2 updates (#486)
Bumps the actions group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [prettier](https://github.com/prettier/prettier).


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

Updates `prettier` from 3.7.4 to 3.8.0
- [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.7.4...3.8.0)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: prettier
  dependency-version: 3.8.0
  dependency-type: direct:development
  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-01-20 10:34:31 -05:00
dependabot[bot]
fc94f1c6e0
Bump github/codeql-action in /.github/workflows in the actions group (#485)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.31.9 to 4.31.10
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](5d4e8d1aca...cdefb33c0f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.10
  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>
Co-authored-by: David Gamero <david340804@gmail.com>
2026-01-13 16:34:53 -08:00
dependabot[bot]
ac0cc3d225
Bump the actions group with 4 updates (#484)
Bumps the actions group with 4 updates: [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core), [@actions/tool-cache](https://github.com/actions/toolkit/tree/HEAD/packages/tool-cache), [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

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

Updates `@babel/preset-env` from 7.28.5 to 7.28.6
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.6/packages/babel-preset-env)

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

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@actions/tool-cache"
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 25.0.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-01-13 16:26:29 -05:00
dependabot[bot]
cf2c9c0edd
Bump @types/node from 25.0.2 to 25.0.3 in the actions group (#480)
Bumps the actions group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.0.3
  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>
Co-authored-by: David Gamero <david340804@gmail.com>
2026-01-06 21:28:06 +00:00
dependabot[bot]
d206adcc7f
Bump github/codeql-action in /.github/workflows in the actions group (#481)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.31.8 to 4.31.9
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](1b168cd394...5d4e8d1aca)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.9
  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-01-06 16:25:29 -05:00
David Gamero
68aff7a5a7
docker driver (#482)
* docker driver

* centralize action setup

* distribute timeout to call sites

* bump timeout
2026-01-05 18:23:31 -08:00
dependabot[bot]
1748cb02b8
Bump the actions group with 3 updates (#478)
Bumps the actions group with 3 updates: [@actions/core](https://github.com/actions/toolkit/tree/HEAD/packages/core), [@actions/exec](https://github.com/actions/toolkit/tree/HEAD/packages/exec) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

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

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

---
updated-dependencies:
- dependency-name: "@actions/core"
  dependency-version: 2.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: "@actions/exec"
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 25.0.2
  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>
2025-12-16 17:21:37 +00:00
dependabot[bot]
6aac2fd790
Bump the actions group in /.github/workflows with 2 updates (#479)
Bumps the actions group in /.github/workflows with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [medyagh/setup-minikube](https://github.com/medyagh/setup-minikube).


Updates `github/codeql-action` from 4.31.7 to 4.31.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cf1bb45a27...1b168cd394)

Updates `medyagh/setup-minikube` from 0.0.20 to 0.0.21
- [Release notes](https://github.com/medyagh/setup-minikube/releases)
- [Commits](e3c7f79eb1...e9e035a86b)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: medyagh/setup-minikube
  dependency-version: 0.0.21
  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>
2025-12-16 09:17:07 -08:00
dependabot[bot]
511707c4a0
Bump prettier from 3.7.3 to 3.7.4 in the actions group (#476)
Bumps the actions group with 1 update: [prettier](https://github.com/prettier/prettier).


Updates `prettier` from 3.7.3 to 3.7.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.7.3...3.7.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.7.4
  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>
2025-12-15 16:34:46 +00:00
dependabot[bot]
b35cf6be4f
Bump the actions group in /.github/workflows with 4 updates (#477)
Bumps the actions group in /.github/workflows with 4 updates: [actions/checkout](https://github.com/actions/checkout), [github/codeql-action](https://github.com/github/codeql-action), [actions/stale](https://github.com/actions/stale) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `actions/checkout` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](1af3b93b68...8e8c483db8)

Updates `github/codeql-action` from 4.31.5 to 4.31.7
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](fdbfb4d275...cf1bb45a27)

Updates `actions/stale` from 10.1.0 to 10.1.1
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](5f858e3efb...997185467f)

Updates `actions/setup-python` from 6.0.0 to 6.1.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](e797f83bcb...83679a892e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: 4.31.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/stale
  dependency-version: 10.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 6.1.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>
2025-12-15 11:26:46 -05:00
dependabot[bot]
07c26e70d3
Bump the actions group with 2 updates (#475)
Bumps the actions group with 2 updates: [prettier](https://github.com/prettier/prettier) and [ts-jest](https://github.com/kulshekhar/ts-jest).


Updates `prettier` from 3.6.2 to 3.7.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.6.2...3.7.3)

Updates `ts-jest` from 29.4.5 to 29.4.6
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.4.5...v29.4.6)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.7.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: ts-jest
  dependency-version: 29.4.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>
2025-12-08 12:22:29 -08:00
dependabot[bot]
0090ff3ba3
Bump the actions group in /.github/workflows with 2 updates (#474)
Bumps the actions group in /.github/workflows with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `actions/checkout` from 5.0.1 to 6.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](93cb6efe18...1af3b93b68)

Updates `github/codeql-action` from 4.31.3 to 4.31.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](014f16e7ab...fdbfb4d275)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: 4.31.5
  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>
2025-12-01 14:19:36 -05:00
dependabot[bot]
f67ee4df16
Bump @types/node in the actions group across 1 directory (#473)
Bumps the actions group with 1 update in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.10.1
  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>
2025-11-18 22:29:40 +00:00
dependabot[bot]
1cd48cb18d
Bump the actions group across 1 directory with 2 updates (#470)
Bumps the actions group with 2 updates in the /.github/workflows directory: [actions/checkout](https://github.com/actions/checkout) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `actions/checkout` from 5.0.0 to 5.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](08c6903cd8...93cb6efe18)

Updates `github/codeql-action` from 4.30.9 to 4.31.3
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](16140ae1a1...014f16e7ab)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: 4.31.3
  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>
2025-11-18 22:18:58 +00:00
dependabot[bot]
f754598e02
Bump glob from 10.4.5 to 10.5.0 (#472)
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.5 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.5...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 11:13:58 +13:00
dependabot[bot]
2842c5ae3c
Bump js-yaml from 4.1.0 to 4.1.1 (#468)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-18 10:46:51 -08:00
dependabot[bot]
c384f1639a
Bump the actions group with 4 updates (#467)
Bumps the actions group with 4 updates: [@actions/io](https://github.com/actions/toolkit/tree/HEAD/packages/io), [@octokit/core](https://github.com/octokit/core.js), [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

Updates `@octokit/core` from 7.0.5 to 7.0.6
- [Release notes](https://github.com/octokit/core.js/releases)
- [Commits](https://github.com/octokit/core.js/compare/v7.0.5...v7.0.6)

Updates `@octokit/plugin-retry` from 8.0.2 to 8.0.3
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v8.0.2...v8.0.3)

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

---
updated-dependencies:
- dependency-name: "@actions/io"
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: "@octokit/core"
  dependency-version: 7.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@octokit/plugin-retry"
  dependency-version: 8.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 24.10.0
  dependency-type: direct:development
  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>
2025-11-11 18:49:13 -05:00
dependabot[bot]
b497d9edf8
Bump the actions group with 3 updates (#466)
Bumps the actions group with 3 updates: [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env), [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@babel/preset-env` from 7.28.3 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-env)

Updates `@babel/preset-typescript` from 7.27.1 to 7.28.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.5/packages/babel-preset-typescript)

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

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@babel/preset-typescript"
  dependency-version: 7.28.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 24.9.1
  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>
2025-10-30 19:00:09 -07:00
dependabot[bot]
4f9a03ce42
Bump @types/node from 24.7.2 to 24.9.0 in the actions group (#464)
Bumps the actions group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.9.0
  dependency-type: direct:development
  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>
2025-10-22 00:37:08 +00:00
dependabot[bot]
0ca601cacf
Bump the actions group across 1 directory with 2 updates (#463)
Bumps the actions group with 2 updates in the /.github/workflows directory: [github/codeql-action](https://github.com/github/codeql-action) and [actions/stale](https://github.com/actions/stale).


Updates `github/codeql-action` from 3.30.1 to 3.30.6
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](f1f6e5f6af...64d10c1313)

Updates `actions/stale` from 10.0.0 to 10.1.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](3a9db7e6a4...5f858e3efb)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/stale
  dependency-version: 10.1.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>
2025-10-21 14:17:18 -04:00
dependabot[bot]
e237e65ac2
Bump the actions group across 1 directory with 7 updates (#462)
Bumps the actions group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@octokit/core](https://github.com/octokit/core.js) | `7.0.3` | `7.0.5` |
| [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js) | `8.0.1` | `8.0.2` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.3.1` | `24.7.0` |
| [@vercel/ncc](https://github.com/vercel/ncc) | `0.38.3` | `0.38.4` |
| [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) | `30.1.3` | `30.2.0` |
| [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.4.1` | `29.4.4` |
| [typescript](https://github.com/microsoft/TypeScript) | `5.9.2` | `5.9.3` |



Updates `@octokit/core` from 7.0.3 to 7.0.5
- [Release notes](https://github.com/octokit/core.js/releases)
- [Commits](https://github.com/octokit/core.js/compare/v7.0.3...v7.0.5)

Updates `@octokit/plugin-retry` from 8.0.1 to 8.0.2
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v8.0.1...v8.0.2)

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

Updates `@vercel/ncc` from 0.38.3 to 0.38.4
- [Release notes](https://github.com/vercel/ncc/releases)
- [Commits](https://github.com/vercel/ncc/compare/0.38.3...0.38.4)

Updates `jest` from 30.1.3 to 30.2.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.2.0/packages/jest)

Updates `ts-jest` from 29.4.1 to 29.4.4
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.4.1...v29.4.4)

Updates `typescript` from 5.9.2 to 5.9.3
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.2...v5.9.3)

---
updated-dependencies:
- dependency-name: "@octokit/core"
  dependency-version: 7.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@octokit/plugin-retry"
  dependency-version: 8.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 24.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: "@vercel/ncc"
  dependency-version: 0.38.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: jest
  dependency-version: 30.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: ts-jest
  dependency-version: 29.4.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: typescript
  dependency-version: 5.9.3
  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>
2025-10-15 15:12:08 -04:00
Craig Duncan
862964743d
Ensure error messages display the correct namespace (#458)
* Ensure the kubectl is configured with a default namespace, and override it on the deployment resource

* Provide a helper method to get the namespace of a resource but fallback to the default if unavailable

* Ensure error messages display the correct namespace
2025-10-08 14:40:50 -04:00
dependabot[bot]
f647f23716
Bump the actions group across 1 directory with 2 updates (#456)
Bumps the actions group with 2 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest).


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

Updates `jest` from 30.0.5 to 30.1.3
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.1.3/packages/jest)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.3.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: jest
  dependency-version: 30.1.3
  dependency-type: direct:development
  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>
Co-authored-by: David Gamero <david340804@gmail.com>
2025-09-17 16:58:00 -04:00
dependabot[bot]
d703e5aec9
Bump the actions group in /.github/workflows with 3 updates (#457)
Bumps the actions group in /.github/workflows with 3 updates: [github/codeql-action](https://github.com/github/codeql-action), [actions/stale](https://github.com/actions/stale) and [actions/setup-python](https://github.com/actions/setup-python).


Updates `github/codeql-action` from 3.30.0 to 3.30.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](2d92b76c45...f1f6e5f6af)

Updates `actions/stale` from 9.1.0 to 10.0.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](5bef64f19d...3a9db7e6a4)

Updates `actions/setup-python` from 5.6.0 to 6.0.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](a26af69be9...e797f83bcb)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/stale
  dependency-version: 10.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-python
  dependency-version: 6.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>
2025-09-17 16:49:35 -04:00
dependabot[bot]
65b4b3bcfe
Bump github/codeql-action in /.github/workflows in the actions group (#454)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.11 to 3.30.0
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](3c3833e0f8...2d92b76c45)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.30.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>
2025-09-02 15:08:29 -04:00
dependabot[bot]
790189df18
Bump github/codeql-action in /.github/workflows in the actions group (#453)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.10 to 3.29.11
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](96f518a34f...3c3833e0f8)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.11
  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>
2025-08-26 15:05:44 -04:00
dependabot[bot]
8e0af0340e
Bump the actions group with 2 updates (#451)
Bumps the actions group with 2 updates: [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@babel/preset-env` from 7.28.0 to 7.28.3
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.3/packages/babel-preset-env)

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

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 24.3.0
  dependency-type: direct:development
  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>
Co-authored-by: David Gamero <david340804@gmail.com>
2025-08-19 19:08:05 +00:00
dependabot[bot]
16365ba04f
Bump github/codeql-action in /.github/workflows in the actions group (#452)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.8 to 3.29.10
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](76621b61de...96f518a34f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.10
  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>
2025-08-19 15:02:49 -04:00
dependabot[bot]
233b2737fd
Bump @types/node from 24.2.0 to 24.2.1 in the actions group (#449)
Bumps the actions group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.2.1
  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>
Co-authored-by: David Gamero <david340804@gmail.com>
2025-08-12 19:09:58 +00:00
dependabot[bot]
a4659bf19e
Bump the actions group in /.github/workflows with 2 updates (#450)
Bumps the actions group in /.github/workflows with 2 updates: [actions/checkout](https://github.com/actions/checkout) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `actions/checkout` from 4.2.2 to 5.0.0
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](11bd71901b...08c6903cd8)

Updates `github/codeql-action` from 3.29.7 to 3.29.8
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](51f77329af...76621b61de)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: github/codeql-action
  dependency-version: 3.29.8
  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>
2025-08-12 15:05:54 -04:00
Suneha Bose
e73e618811
chore(release): bump version to 5.0.4 (#448) 2025-08-06 17:03:53 -07:00
Suneha Bose
1bc669b02c
docs(changelog): update changelog for version 5.0.4 (#446) 2025-08-06 10:19:42 -07:00
dependabot[bot]
a6356b08f6
Bump the actions group with 4 updates (#444)
Bumps the actions group with 4 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest), [ts-jest](https://github.com/kulshekhar/ts-jest) and [typescript](https://github.com/microsoft/TypeScript).


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

Updates `jest` from 30.0.4 to 30.0.5
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest)

Updates `ts-jest` from 29.4.0 to 29.4.1
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.4.0...v29.4.1)

Updates `typescript` from 5.8.3 to 5.9.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release-publish.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.8.3...v5.9.2)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: jest
  dependency-version: 30.0.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: ts-jest
  dependency-version: 29.4.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: typescript
  dependency-version: 5.9.2
  dependency-type: direct:development
  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>
Co-authored-by: Suneha Bose <123775811+bosesuneha@users.noreply.github.com>
2025-08-05 19:18:07 +00:00
dependabot[bot]
c0773c9877
Bump github/codeql-action in /.github/workflows in the actions group (#445)
Bumps the actions group in /.github/workflows with 1 update: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 3.29.2 to 3.29.5
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](181d5eefc2...51f77329af)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.29.5
  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>
2025-08-05 12:14:04 -07:00
Ogheneobukome Ejaife
bf3422cff9
Feature Request: Add Enhanced Deployment Error Reporting and Logging (#440)
* Enhance manifest stability with detailed error reporting and logging

* Enhance manifest stability with detailed error reporting and logging

* Refactored the getContainerErrors Function to enhance readability

* Added an early return for the getContainerErrors function

* Eliminated redundant conditionals

---------

Co-authored-by: benjamin <145829787+benjaminbob21@users.noreply.github.com>
Co-authored-by: Suneha Bose <123775811+bosesuneha@users.noreply.github.com>
Co-authored-by: David Gamero <david340804@gmail.com>
2025-07-29 15:07:04 -04:00
dependabot[bot]
c93bf5dafe
Bump @types/node from 24.0.13 to 24.0.15 in the actions group (#443)
---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.0.15
  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>
2025-07-22 15:05:37 -04:00
benjamin
4755eabeba
Add support for ScaledJob (#436)
* Added ScaledJob support

* Fixed getReplicaCount error

* Fixed file length error in fileUtils.test.ts

* Adjust scaledJob spec path

* Made updateImagesInK8sObj more concise
2025-07-17 13:21:46 -04:00
benjamin
7a954ab84c
Added timeout input description to README (#441)
* Added timeout input description to README

* Changed position of timeout in action.yml and README
2025-07-16 15:04:02 -04:00
benjamin
7395c391d9
Added error check for canary promote actions (#432)
* Added checkForErrors so canary promote action fails when there is an error

* Added tests for checkForErrors

* Probable integration error fix

* Probable integration error fix

* Revert changes back

* Added checkForErrors unit tests

* Fixed multiple tests issue

---------

Co-authored-by: Suneha Bose <123775811+bosesuneha@users.noreply.github.com>
2025-07-15 15:00:09 -06:00
dependabot[bot]
f17d8559ed
Bump the actions group with 2 updates (#439)
Bumps the actions group with 2 updates: [@octokit/core](https://github.com/octokit/core.js) and [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


Updates `@octokit/core` from 7.0.2 to 7.0.3
- [Release notes](https://github.com/octokit/core.js/releases)
- [Commits](https://github.com/octokit/core.js/compare/v7.0.2...v7.0.3)

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

---
updated-dependencies:
- dependency-name: "@octokit/core"
  dependency-version: 7.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 24.0.13
  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>
Co-authored-by: benjamin <145829787+benjaminbob21@users.noreply.github.com>
2025-07-15 15:25:40 -04:00
dependabot[bot]
b832d899e2
Bump medyagh/setup-minikube in /.github/workflows in the actions group (#438)
Bumps the actions group in /.github/workflows with 1 update: [medyagh/setup-minikube](https://github.com/medyagh/setup-minikube).


Updates `medyagh/setup-minikube` from 0.0.19 to 0.0.20
- [Release notes](https://github.com/medyagh/setup-minikube/releases)
- [Commits](cea3367532...e3c7f79eb1)

---
updated-dependencies:
- dependency-name: medyagh/setup-minikube
  dependency-version: 0.0.20
  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>
Co-authored-by: benjamin <145829787+benjaminbob21@users.noreply.github.com>
2025-07-15 15:10:42 -04:00
David Gamero
6b432c15b6
update owners (#437) 2025-07-15 12:04:23 -04:00
benjamin
ac0b58c9a5
Add timeout to the rollout status (#425)
* Added timeout to the rollout status and tests for it

* Fixed integration test errors

* Fix for blue green integration test

* Probable fix for integration errors

* No jobs run error fixed

* Changed timeout to file level constant

* Added parsing logic for timeout

* Made tests more concise

* implemented timeout validation check in an extracted utils mod

* Changed function name to parseDuration

* Removed timeout parameter from getResource

---------

Co-authored-by: David Gamero <david340804@gmail.com>
Co-authored-by: Suneha Bose <123775811+bosesuneha@users.noreply.github.com>
2025-07-09 10:22:21 -07:00
benjamin
e207ec429b
Added additional check in getTempdirectory function (#428)
* added additional check in tempdirectory func

* Removed runner.tempDirectory

* Fixed prettier error
2025-07-08 18:38:26 -04:00
dependabot[bot]
901a2fa489
Bump the actions group across 1 directory with 4 updates (#430)
* Bump the actions group across 1 directory with 4 updates

Bumps the actions group with 4 updates in the / directory: [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) and [prettier](https://github.com/prettier/prettier).


Updates `@babel/preset-env` from 7.27.2 to 7.28.0
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.0/packages/babel-preset-env)

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

Updates `jest` from 30.0.0 to 30.0.4
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.4/packages/jest)

Updates `prettier` from 3.5.3 to 3.6.2
- [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.5.3...3.6.2)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-version: 7.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 24.0.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: jest
  dependency-version: 30.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: prettier
  dependency-version: 3.6.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fixed prettier error

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: David Gamero <david340804@gmail.com>
Co-authored-by: Benjamin Bamisile <bambobtim@gmail.com>
2025-07-08 15:37:00 -04:00
dependabot[bot]
e3266b84c0
Bump Azure/setup-kubectl in /.github/workflows in the actions group (#429)
Bumps the actions group in /.github/workflows with 1 update: [Azure/setup-kubectl](https://github.com/azure/setup-kubectl).


Updates `Azure/setup-kubectl` from 4.0.0 to 4.0.1
- [Release notes](https://github.com/azure/setup-kubectl/releases)
- [Changelog](https://github.com/Azure/setup-kubectl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/azure/setup-kubectl/compare/v4...776406bce94f63e41d621b960d78ee25c8b76ede)

---
updated-dependencies:
- dependency-name: Azure/setup-kubectl
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 15:08:41 -04:00
benjamin
cc1e193d23
add server-side option for kubectl apply commands (#424) 2025-07-03 11:15:06 -04:00
benjamin
b9529f8427
Make namespace input optional (#420)
Signed-off-by: Tatsat Mishra <tamishra@microsoft.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tatsinnit <tamishra@microsoft.com>
2025-07-02 09:28:12 +12:00
Tatsinnit
5b189c0bf7
Add husky pre-commit hook. (#418)
Signed-off-by: Tatsat Mishra <tamishra@microsoft.com>
2025-07-01 19:35:46 +00:00
dependabot[bot]
fd0d4accb4
Bump the actions group across 1 directory with 2 updates (#421) 2025-07-02 07:14:49 +12:00
Tatsinnit
6fd713ca6a
Fix the major update packages including Jest. (#414)
Signed-off-by: Tatsat Mishra <tamishra@microsoft.com>
2025-06-18 10:12:52 -07:00
dependabot[bot]
1feba4ce5c
Bump github/codeql-action in /.github/workflows in the actions group (#413) 2025-06-17 15:11:59 -04:00
dependabot[bot]
1baea844ac
Bump github/codeql-action in /.github/workflows in the actions group (#410) 2025-06-10 12:13:21 -07:00
dependabot[bot]
76a7e4f5b5
Bump @types/node from 22.15.29 to 22.15.30 in the actions group (#411) 2025-06-10 12:07:48 -07:00
dependabot[bot]
ba7d4d1daf
Bump @types/node from 22.15.21 to 22.15.29 in the actions group (#409) 2025-06-03 10:24:51 -04:00
benjamin
e1c4475ce4
Add missing README.md and action.yml parameters (#408)
Co-authored-by: Benjamin Bamisile <t-bbamisile@microsoft.com>
2025-05-27 16:56:40 -04:00
dependabot[bot]
1a3dd6ebf8
Bump the actions group with 3 updates (#407) 2025-05-27 15:05:43 -04:00
dependabot[bot]
bba7c16f36
Bump the actions group with 2 updates (#405) 2025-05-20 19:17:46 +00:00
dependabot[bot]
fbde009ab5
Bump github/codeql-action in /.github/workflows in the actions group (#406) 2025-05-20 12:12:26 -07:00
dependabot[bot]
f09b591a1a
Bump the actions group with 2 updates (#404) 2025-05-13 15:16:01 -04:00
dependabot[bot]
33d7498881
Bump the actions group with 3 updates (#402) 2025-05-06 19:12:45 +00:00
dependabot[bot]
7004a1f114
Bump github/codeql-action in /.github/workflows in the actions group (#403) 2025-05-06 15:08:30 -04:00
dependabot[bot]
648274edaf
Bump the actions group in /.github/workflows with 2 updates (#400) 2025-04-29 19:11:49 +00:00
dependabot[bot]
b2568065ec
Bump @types/node from 22.14.1 to 22.15.3 in the actions group (#401) 2025-04-29 15:06:32 -04:00
dependabot[bot]
ac1831102a
Bump the actions group with 4 updates (#395)
Bumps the actions group with 4 updates: [@octokit/core](https://github.com/octokit/core.js), [@octokit/plugin-retry](https://github.com/octokit/plugin-retry.js), [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [ts-jest](https://github.com/kulshekhar/ts-jest).


Updates `@octokit/core` from 6.1.4 to 6.1.5
- [Release notes](https://github.com/octokit/core.js/releases)
- [Commits](https://github.com/octokit/core.js/compare/v6.1.4...v6.1.5)

Updates `@octokit/plugin-retry` from 7.2.0 to 7.2.1
- [Release notes](https://github.com/octokit/plugin-retry.js/releases)
- [Commits](https://github.com/octokit/plugin-retry.js/compare/v7.2.0...v7.2.1)

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

Updates `ts-jest` from 29.3.1 to 29.3.2
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.3.1...v29.3.2)

---
updated-dependencies:
- dependency-name: "@octokit/core"
  dependency-version: 6.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@octokit/plugin-retry"
  dependency-version: 7.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: "@types/node"
  dependency-version: 22.14.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: ts-jest
  dependency-version: 29.3.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>
Co-authored-by: Reinier Cruz <30391816+ReinierCC@users.noreply.github.com>
2025-04-16 18:25:25 +00:00
David Gamero
3b11c64ce0
release v5.0.3 (#399)
* release v5.0.3

* format
2025-04-16 13:09:51 -04:00
David Gamero
99510dff95
case-insensitive resource type (#398)
* case-insensitive resource type

* inline error and throw outside switch

* consistent input naming

* catch failed clustertype parse

* protect raw input

* naming

* format
2025-04-16 11:47:29 -04:00
90 changed files with 6152 additions and 9562 deletions

2
.github/CODEOWNERS vendored
View File

@ -1 +1 @@
* @Azure/aks-atlanta
* @Azure/cloud-native-github-action-owners

View File

@ -0,0 +1,53 @@
name: 'Setup Minikube Test Environment'
description: 'Common setup steps for minikube integration tests'
inputs:
install-smi:
description: 'Install Linkerd SMI for service mesh tests'
required: false
default: 'false'
runs:
using: 'composite'
steps:
- name: Install dependencies
shell: bash
run: |
rm -rf node_modules/
npm install
- name: Build
shell: bash
run: npm run build
- name: Install conntrack
shell: bash
run: sudo apt-get install -y conntrack
- uses: Azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4.0.1
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@e9e035a86bbc3caea26a450bd4dbf9d0c453682e # v0.0.21
with:
minikube-version: 1.37.0
kubernetes-version: 1.31.0
driver: 'docker'
- name: Install Linkerd and SMI
if: inputs.install-smi == 'true'
shell: bash
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
export PATH=$PATH:/home/runner/.linkerd2/bin
curl -sL https://linkerd.github.io/linkerd-smi/install | sh
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
linkerd install --crds | kubectl apply -f -
linkerd install --set proxyInit.runAsRoot=true | kubectl apply -f -
linkerd smi install | kubectl apply -f -
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # 6.1.0
name: Install Python
with:
python-version: '3.x'

View File

@ -16,7 +16,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
@ -24,7 +24,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 #v3.28.15
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 #v3.29.5
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
@ -32,7 +32,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 #v3.28.15
uses: github/codeql-action/autobuild@c10b8064de6f491fea524254123dbe5e09572f13 #v3.29.5
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -46,4 +46,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 #v3.28.15
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 #v3.29.5

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@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.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@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
name: Setting PR as idle
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: install deps
run: npm install

View File

@ -18,39 +18,15 @@ jobs:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # v0.0.19
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}

View File

@ -12,45 +12,21 @@ on:
jobs:
run-integration-test:
name: Run Minikube Integration Tests
name: Blue-Green Ingress Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # v0.0.19
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'nginx-service' ${{ env.NAMESPACE }}

View File

@ -12,45 +12,21 @@ on:
jobs:
run-integration-test:
name: Run Minikube Integration Tests
name: Blue-Green Service Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # v0.0.19
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}

View File

@ -12,56 +12,23 @@ on:
jobs:
run-integration-test:
name: Run Minikube Integration Tests
name: Blue-Green SMI Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # v0.0.19
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- name: Install linkerd and add controlplane to cluster
run: |
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
export PATH=$PATH:/home/runner/.linkerd2/bin
curl -sL https://linkerd.github.io/linkerd-smi/install | sh
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
linkerd install --crds | kubectl apply -f -
linkerd install --set proxyInit.runAsRoot=true | kubectl apply -f -
linkerd smi install | kubectl apply -f -
install-smi: 'true'
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}

View File

@ -12,45 +12,21 @@ on:
jobs:
run-integration-test:
name: Run Minikube Integration Tests
name: Canary Pod Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # v0.0.19
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}

View File

@ -12,53 +12,23 @@ on:
jobs:
run-integration-test:
name: Run Minikube Integration Tests
name: Canary SMI Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # v0.0.19
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
export PATH=$PATH:/home/runner/.linkerd2/bin
curl -sL https://linkerd.github.io/linkerd-smi/install | sh
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml
linkerd install --crds | kubectl apply -f -
linkerd install --set proxyInit.runAsRoot=true | kubectl apply -f -
linkerd smi install | kubectl apply -f -
install-smi: 'true'
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}

View File

@ -0,0 +1,113 @@
# This workflow is inspired by `run-integration-tests-bluegreen-ingress.yml` and introduces namespace-specific testing for manifests.
# It ensures deployments respect manifest-defined namespaces and tests deployments to multiple namespaces.
name: Minikube Integration Tests - Namespace Optional
on:
pull_request:
branches:
- main
- 'releases/*'
push:
branches:
- main
- 'releases/*'
workflow_dispatch:
jobs:
run-integration-test:
name: Namespace Optional Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE1: integration-test-namespace1-${{ github.run_id }}
NAMESPACE2: integration-test-namespace2-${{ github.run_id }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Create namespaces for tests
run: |
kubectl create ns ${{ env.NAMESPACE1 }}
kubectl create ns ${{ env.NAMESPACE2 }}
kubectl create ns test-namespace
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Deployment' 'all' ${{ env.NAMESPACE1 }}
python test/integration/k8s-deploy-delete.py 'Deployment' 'all' ${{ env.NAMESPACE2 }}
# This tests whether the deployment respects the namespace defined in the manifest instead of defaulting to "default" when namespace is not provided.
- name: Test - Handles namespace correctly based on manifest
uses: ./
with:
images: nginx
manifests: |
test/integration/manifests/test_with_ns.yaml
test/integration/manifests/test_no_ns.yaml
action: deploy # Deploys manifests to specified namespaces or uses the namespace defined in the manifest
- name: Verify Deployment - test_with_ns.yaml (test-namespace)
run: |
python test/integration/k8s-deploy-test.py \
namespace=test-namespace \
kind=Deployment \
name=test-deployment \
containerName=nginx \
labels=app:test-app,workflow:actions.github.com-k8s-deploy,workflowFriendlyName:Minikube_Integration_Tests_-_Namespace_Optional \
selectorLabels=app:test-app
- name: Verify Deployment - test_no_ns.yaml (default namespace)
run: |
python test/integration/k8s-deploy-test.py \
namespace=default \
kind=Deployment \
name=test-deployment-no-ns \
containerName=nginx \
labels=app:test-app,workflow:actions.github.com-k8s-deploy,workflowFriendlyName:Minikube_Integration_Tests_-_Namespace_Optional \
selectorLabels=app:test-app
# This tests whether the deployment works when a file is deployed to two different provided namespaces.
- name: Test - Deploys the resource to namespace1
uses: ./
with:
namespace: ${{ env.NAMESPACE1 }}
images: nginx
manifests: |
test/integration/manifests/test_no_ns.yaml
action: deploy
- name: Test - Deploys the resource to namespace2
uses: ./
with:
namespace: ${{ env.NAMESPACE2 }}
images: nginx
manifests: |
test/integration/manifests/test_no_ns.yaml
action: deploy
- name: Verify Deployments in NAMESPACE1 & NAMESPACE2
run: |
for ns in ${{ env.NAMESPACE1 }} ${{ env.NAMESPACE2 }}; do
python test/integration/k8s-deploy-test.py \
namespace=$ns \
kind=Deployment \
name=test-deployment-no-ns \
containerName=nginx \
labels=app:test-app,workflow:actions.github.com-k8s-deploy,workflowFriendlyName:Minikube_Integration_Tests_-_Namespace_Optional \
selectorLabels=app:test-app
done
- name: Cleanup
run: |
echo "Cleaning up resources..."
python test/integration/k8s-deploy-delete.py 'Deployment' 'test-deployment' test-namespace
python test/integration/k8s-deploy-delete.py 'Deployment' 'test-deployment-no-ns' ${{ env.NAMESPACE1 }}
python test/integration/k8s-deploy-delete.py 'Deployment' 'test-deployment-no-ns' ${{ env.NAMESPACE2 }}
python test/integration/k8s-deploy-delete.py 'Deployment' 'test-deployment-no-ns' default
kubectl delete ns ${{ env.NAMESPACE1 }}
kubectl delete ns ${{ env.NAMESPACE2 }}
kubectl delete ns test-namespace
rm -rf test_with_ns.yaml test_no_ns.yaml

View File

@ -19,24 +19,24 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Build
run: ncc build src/run.ts -o lib
run: npm run build
- name: Azure login
uses: azure/login@v2.3.0
uses: azure/login@v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
- uses: Azure/setup-kubectl@15650b3ad78fff148532a140b8a4c821796b2d7b # v5.0.0
name: Install Kubectl
- name: Create private AKS cluster and set context
@ -51,7 +51,7 @@ jobs:
run: |
az aks command invoke --resource-group ${{ env.NAMESPACE }} --name ${{ env.NAMESPACE }} --command "kubectl create ns ${{ env.NAMESPACE }}"
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # 6.2.0
name: Install Python
with:
python-version: '3.x'

View File

@ -12,45 +12,21 @@ on:
jobs:
run-integration-test:
name: Run Minikube Integration Tests
name: Resource Annotation Tests
runs-on: ubuntu-22.04
env:
KUBECONFIG: /home/runner/.kube/config
NAMESPACE: test-${{ github.run_id }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install dependencies
run: |
rm -rf node_modules/
npm install
- name: Install ncc
run: npm i -g @vercel/ncc
- name: Install conntrack
run: sudo apt-get install -y conntrack
- name: Build
run: ncc build src/run.ts -o lib
- uses: Azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
name: Install Kubectl
- id: setup-minikube
name: Setup Minikube
uses: medyagh/setup-minikube@cea33675329b799adccc9526aa5daccc26cd5052 # v0.0.19
with:
minikube-version: 1.34.0
kubernetes-version: 1.31.0
driver: 'none'
timeout-minutes: 3
- uses: ./.github/actions/minikube-setup
name: Setup Minikube Environment
timeout-minutes: 5
- name: Create namespace to run tests
run: kubectl create ns ${{ env.NAMESPACE }}
- uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # 5.5.0
name: Install Python
with:
python-version: '3.x'
- name: Cleaning any previously created items
run: |
python test/integration/k8s-deploy-delete.py 'Service' 'all' ${{ env.NAMESPACE }}

View File

@ -14,7 +14,7 @@ jobs:
name: Run Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: |
npm install
npm test

13
.husky/pre-commit Normal file
View File

@ -0,0 +1,13 @@
npm run typecheck || {
echo ""
echo "❌ Type check failed."
echo "💡 Run 'npm run typecheck' to see errors."
exit 1
}
npm test
npm run format-check || {
echo ""
echo "❌ Formatting check failed."
echo "💡 Run 'npm run format' or 'prettier --write .' to fix formatting issues."
exit 1
}

View File

@ -1,5 +1,36 @@
# Changelog
## [5.1.0] - 2026-03-03
### Added
- #458 [Ensure error messages display the correct namespace](https://github.com/Azure/k8s-deploy/pull/458)
- #482 [docker driver](https://github.com/Azure/k8s-deploy/pull/482)
- #492 [Migrate to esbuild/Vitest and upgrade @actions/\* to ESM-only versions](https://github.com/Azure/k8s-deploy/pull/492)
- #498 [Add typecheck to build script](https://github.com/Azure/k8s-deploy/pull/498)
## [5.0.4] - 2025-08-05
### Added
- #408 [Add missing README.md and action.yml parameters](https://github.com/Azure/k8s-deploy/pull/408)
- #414 [Fix the major update packages including Jest](https://github.com/Azure/k8s-deploy/pull/414)
- #418 [Add husky pre-commit hook.](https://github.com/Azure/k8s-deploy/pull/418)
- #420 [Make namespace input optional](https://github.com/Azure/k8s-deploy/pull/420)
- #424 [add server-side option for kubectl apply commands](https://github.com/Azure/k8s-deploy/pull/424)
- #425 [Add timeout to the rollout status](https://github.com/Azure/k8s-deploy/pull/425)
- #428 [Added additional check in getTempdirectory function](https://github.com/Azure/k8s-deploy/pull/428)
- #432 [Added error check for canary promote actions](https://github.com/Azure/k8s-deploy/pull/432)
- #436 [Add support for ScaledJob](https://github.com/Azure/k8s-deploy/pull/436)
- #440 [Add Enhanced Deployment Error Reporting and Logging](https://github.com/Azure/k8s-deploy/pull/440)
- #441 [Added timeout input description to README](https://github.com/Azure/k8s-deploy/pull/441)
## [5.0.3] - 2025-04-16
### Added
- #398 case-insensitive resource type
## [5.0.2] - 2025-04-15
### Added

View File

@ -1,6 +1,6 @@
# Deploy manifests action for Kubernetes
This action is used to deploy manifests to Kubernetes clusters. It requires that the cluster context be set earlier in the workflow by using either the [Azure/aks-set-context](https://github.com/Azure/aks-set-context/tree/releases/v1) action or the [Azure/k8s-set-context](https://github.com/Azure/k8s-set-context/tree/releases/v1) action. It also requires Kubectl to be installed (you can use the [Azure/setup-kubectl](https://github.com/Azure/setup-kubectl) action).
This action is used to deploy manifests to Kubernetes clusters. It requires that the cluster context be set earlier in the workflow by using either the [Azure/aks-set-context](https://github.com/Azure/aks-set-context/tree/releases/v4) action or the [Azure/k8s-set-context](https://github.com/Azure/k8s-set-context/tree/releases/v4) action. It also requires Kubectl to be installed (you can use the [Azure/setup-kubectl](https://github.com/Azure/setup-kubectl) action).
If you are looking to automate your workflows to deploy to [Azure Web Apps](https://azure.microsoft.com/en-us/services/app-service/web/) and [Azure Web App for Containers](https://azure.microsoft.com/en-us/services/app-service/containers/), consider using [`Azure/webapps-deploy`](https://github.com/Azure/webapps-deploy) action.
@ -24,12 +24,10 @@ Following are the key capabilities of this action:
- **Secret handling**: The secret names specified as inputs in the action are used to augment the input manifest files with imagePullSecrets values before deploying to the cluster. Also, checkout the [Azure/k8s-create-secret](https://github.com/Azure/k8s-create-secret) action for creation of generic or docker-registry secrets in the cluster.
- **Deployment strategy** Supports both canary and blue-green deployment strategies
- **Canary strategy**: Workloads suffixed with '-baseline' and '-canary' are created. There are two methods of traffic splitting supported:
- **Service Mesh Interface**: Service Mesh Interface abstraction allows for plug-and-play configuration with service mesh providers such as [Linkerd](https://linkerd.io/) and [Istio](https://istio.io/). Meanwhile, this action takes away the hard work of mapping SMI's TrafficSplit objects to the stable, baseline and canary services during the lifecycle of the deployment strategy. Service mesh based canary deployments using this action are more accurate as service mesh providers enable granular percentage traffic split (via service registry and sidecar containers injected into pods alongside application containers).
- **Only Kubernetes (no service mesh)**: In the absence of service mesh, while it may not be possible to achieve exact percentage split at the request level, it is still possible to perform canary deployments by deploying -baseline and -canary workload variants next to the stable variant. The service routes requests to pods of all three workload variants as the selector-label constraints are met (KubernetesManifest will honor these when creating -baseline and -canary variants). This achieves the intended effect of routing only a portion of total requests to the canary.
- **Blue-Green strategy**: Choosing blue-green strategy with this action leads to creation of workloads suffixed with '-green'. An identified service is one that is supplied as part of the input manifest(s) and targets a workload in the supplied manifest(s). There are three route-methods supported in the action:
- **Service route-method**: Identified services are configured to target the green deployments.
- **Ingress route-method**: Along with deployments, new services are created with '-green' suffix (for identified services), and the ingresses are in turn updated to target the new services.
- **SMI route-method**: A new [TrafficSplit](https://github.com/servicemeshinterface/smi-spec/blob/master/apis/traffic-split/v1alpha3/traffic-split.md) object is created for each identified service. The TrafficSplit object is updated to target the new deployments. This works only if SMI is set up in the cluster.
@ -110,9 +108,25 @@ Following are the key capabilities of this action:
<td>Acceptable values: true, false</br>Default value: false.</td>
</tr>
<tr>
<td>force </br></br>(Optional)</td>
<td>resource-group</br></br>(Optional)</td>
<td>Name of resource group - Only required if using private cluster</td>
</tr>
<tr>
<td>name</br></br>(Optional)</td>
<td>Name of the private cluster - Only required if using private cluster</td>
</tr>
<tr>
<td>force</br></br>(Optional)</td>
<td>Deploy when a previous deployment already exists. If true then '--force' argument is added to the apply command. Using '--force' argument is not recommended in production.</td>
</tr>
<tr>
<td>server-side </br></br>(Optional)</td>
<td>The apply command runs in the server instead of the client. If true then '--server-side' argument is added to the apply command.</td>
</tr>
<tr>
<td>timeout</br></br>(Optional)</td>
<td>Default value: 10m</br>Timeout for the rollout status. Accepts time units like '10m', '1h', '30s'. If only a number is provided (e.g., '30'), it is assumed to be minutes.</td>
</tr>
<tr>
<td>annotate-resources</br></br>(Optional)</td>
<td>Acceptable values: true/false</br>Default value: true</br>Switch whether to annotate the resources or not. If set to false all annotations are skipped completely.</td>
@ -296,9 +310,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: Azure/docker-login@v1
- uses: Azure/docker-login@v2
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@ -317,7 +331,7 @@ jobs:
cluster-name: contoso
resource-group: contoso-rg
- uses: Azure/k8s-create-secret@v4
- uses: Azure/k8s-create-secret@v5
with:
container-registry-url: contoso.azurecr.io
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
@ -345,9 +359,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: Azure/docker-login@v1
- uses: Azure/docker-login@v2
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@ -363,14 +377,14 @@ jobs:
with:
kubeconfig: ${{ secrets.KUBE_CONFIG }}
- uses: Azure/k8s-create-secret@v4
- uses: Azure/k8s-create-secret@v5
with:
container-registry-url: contoso.azurecr.io
container-registry-username: ${{ secrets.REGISTRY_USERNAME }}
container-registry-password: ${{ secrets.REGISTRY_PASSWORD }}
secret-name: demo-k8s-secret
- uses: Azure/k8s-deploy@v4
- uses: Azure/k8s-deploy@v5
with:
action: deploy
manifests: |
@ -395,9 +409,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: Azure/docker-login@v1
- uses: Azure/docker-login@v2
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@ -419,9 +433,9 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- uses: actions/checkout@v6
- uses: Azure/docker-login@v1
- uses: Azure/docker-login@v2
with:
login-server: contoso.azurecr.io
username: ${{ secrets.REGISTRY_USERNAME }}
@ -436,7 +450,7 @@ jobs:
cluster-name: contoso
resource-group: contoso-rg
- uses: Azure/k8s-create-secret@v4
- uses: Azure/k8s-create-secret@v5
with:
namespace: ${{ env.NAMESPACE }}
container-registry-url: contoso.azurecr.io

View File

@ -4,9 +4,9 @@ inputs:
# Please ensure you have used either azure/k8s-actions/aks-set-context or azure/k8s-actions/k8s-set-context in the workflow before this action
# You also need to have kubectl installed (azure/setup-kubectl)
namespace:
description: 'Choose the target Kubernetes namespace. If the namespace is not provided, the commands will run in the default namespace.'
description: 'Choose the target Kubernetes namespace. If the namespace is not provided, the commands will automatically use the namespace defined in the manifest files first or otherwise run in the default namespace.'
required: false
default: default
default: ''
manifests:
description: 'Path to the manifest files which will be used for deployment.'
required: true
@ -55,6 +55,14 @@ inputs:
description: 'Deploy when a previous deployment already exists. If true then --force argument is added to the apply command'
required: false
default: false
server-side:
description: 'The apply command runs in the server instead of the client. If true then --server-side argument is added to the apply command.'
required: false
default: false
timeout:
description: 'Timeout for the rollout status'
required: false
default: 10m
token:
description: 'Github token'
default: ${{ github.token }}
@ -64,7 +72,7 @@ inputs:
required: false
default: true
annotate-namespace:
description: 'Annotate the target namespace. Ignored when annotate-resources is set to false.'
description: 'Annotate the target namespace. Ignored when annotate-resources is set to false or no namespace is provided.'
required: false
default: true
private-cluster:
@ -75,7 +83,7 @@ inputs:
description: 'Name of resource group - Only required if using private cluster'
required: false
name:
description: 'Resource group name - Only required if using private cluster'
description: 'Name of the private cluster - Only required if using private cluster'
required: false
skip-tls-verify:
description: True if the insecure-skip-tls-verify option should be used. Input should be 'true' or 'false'.
@ -88,5 +96,5 @@ inputs:
branding:
color: 'green'
runs:
using: 'node20'
using: 'node24'
main: 'lib/index.js'

View File

@ -1,20 +0,0 @@
module.exports = {
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
transform: {
'\\.[jt]sx?$': 'babel-jest'
},
transformIgnorePatterns: [
'node_modules/(?!' +
[
'@octokit',
'universal-user-agent',
'before-after-hook',
'minimist'
].join('|') +
')'
],
verbose: true,
testTimeout: 9000
}

10037
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,36 @@
{
"name": "k8s-deploy-action",
"version": "5.0.0",
"version": "5.1.0",
"author": "Deepak Sattiraju",
"license": "MIT",
"type": "module",
"scripts": {
"prebuild": "npm i @vercel/ncc",
"build": "ncc build src/run.ts -o lib",
"test": "jest",
"coverage": "jest --coverage=true",
"build": "tsc --noEmit && esbuild src/run.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",
"coverage": "vitest run --coverage",
"format": "prettier --write .",
"format-check": "prettier --check ."
"format-check": "prettier --check .",
"prepare": "husky"
},
"dependencies": {
"@actions/core": "^1.11.1",
"@actions/exec": "^1.0.0",
"@actions/io": "^1.1.3",
"@actions/tool-cache": "2.0.2",
"@babel/preset-env": "^7.26.9",
"@babel/preset-typescript": "^7.27.0",
"@octokit/core": "^6.1.4",
"@octokit/plugin-retry": "^7.2.0",
"@types/minipass": "^3.3.5",
"js-yaml": "4.1.0",
"@actions/core": "^3.0.0",
"@actions/exec": "^3.0.0",
"@actions/io": "^3.0.2",
"@actions/tool-cache": "4.0.0",
"@octokit/core": "^7.0.6",
"@octokit/plugin-retry": "^8.1.0",
"js-yaml": "4.1.1",
"minimist": "^1.2.8"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/js-yaml": "^4.0.9",
"@types/minimist": "^1.2.5",
"@types/node": "^22.14.0",
"@vercel/ncc": "^0.38.3",
"jest": "^29.7.0",
"prettier": "^3.5.3",
"ts-jest": "^29.3.1",
"typescript": "5.8.3"
"@types/node": "^25.5.2",
"esbuild": "^0.28",
"husky": "^9.1.7",
"prettier": "^3.8.1",
"typescript": "6.0.2",
"vitest": "^4"
}
}

View File

@ -1,29 +1,28 @@
import * as core from '@actions/core'
import * as models from '../types/kubernetesTypes'
import * as KubernetesConstants from '../types/kubernetesTypes'
import {Kubectl, Resource} from '../types/kubectl'
import * as models from '../types/kubernetesTypes.js'
import * as KubernetesConstants from '../types/kubernetesTypes.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import {
getResources,
updateManifestFiles
} from '../utilities/manifestUpdateUtils'
} from '../utilities/manifestUpdateUtils.js'
import {
annotateAndLabelResources,
checkManifestStability,
deployManifests
} from '../strategyHelpers/deploymentHelper'
import {DeploymentStrategy} from '../types/deploymentStrategy'
import {parseTrafficSplitMethod} from '../types/trafficSplitMethod'
} from '../strategyHelpers/deploymentHelper.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
import {parseTrafficSplitMethod} from '../types/trafficSplitMethod.js'
import {ClusterType} from '../inputUtils.js'
export const ResourceTypeManagedCluster =
'Microsoft.ContainerService/managedClusters'
export const ResourceTypeFleet = 'Microsoft.ContainerService/fleets'
export type ClusterType =
| typeof ResourceTypeManagedCluster
| typeof ResourceTypeFleet
export async function deploy(
kubectl: Kubectl,
manifestFilePaths: string[],
deploymentStrategy: DeploymentStrategy
deploymentStrategy: DeploymentStrategy,
resourceType: ClusterType,
timeout?: string
) {
// update manifests
const inputManifestFiles: string[] = updateManifestFiles(manifestFilePaths)
@ -38,15 +37,14 @@ export async function deploy(
inputManifestFiles,
deploymentStrategy,
kubectl,
trafficSplitMethod
trafficSplitMethod,
timeout
)
core.debug(`Deployed manifest files: ${deployedManifestFiles}`)
core.endGroup()
// check manifest stability
core.startGroup('Checking manifest stability')
const resourceTypeInput =
core.getInput('resource-type') || ResourceTypeManagedCluster
const resourceTypes: Resource[] = getResources(
deployedManifestFiles,
models.DEPLOYMENT_TYPES.concat([
@ -54,16 +52,7 @@ export async function deploy(
])
)
if (
resourceTypeInput !== ResourceTypeManagedCluster &&
resourceTypeInput !== ResourceTypeFleet
) {
let errMsg = `Invalid resource type: ${resourceTypeInput}. Supported resource types are: ${ResourceTypeManagedCluster} (default), ${ResourceTypeFleet}`
core.setFailed(errMsg)
throw new Error(errMsg)
}
await checkManifestStability(kubectl, resourceTypes, resourceTypeInput)
await checkManifestStability(kubectl, resourceTypes, resourceType, timeout)
core.endGroup()
// print ingresses

View File

@ -1,63 +1,69 @@
import * as core from '@actions/core'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
import * as PodCanaryHelper from '../strategyHelpers/canary/podCanaryHelper'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper.js'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper.js'
import * as PodCanaryHelper from '../strategyHelpers/canary/podCanaryHelper.js'
import {
getResources,
updateManifestFiles
} from '../utilities/manifestUpdateUtils'
import {annotateAndLabelResources} from '../strategyHelpers/deploymentHelper'
import * as models from '../types/kubernetesTypes'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
} from '../utilities/manifestUpdateUtils.js'
import {annotateAndLabelResources} from '../strategyHelpers/deploymentHelper.js'
import * as models from '../types/kubernetesTypes.js'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils.js'
import {
deleteGreenObjects,
getManifestObjects,
NONE_LABEL_VALUE
} from '../strategyHelpers/blueGreen/blueGreenHelper'
} from '../strategyHelpers/blueGreen/blueGreenHelper.js'
import {BlueGreenManifests} from '../types/blueGreenTypes'
import {DeployResult} from '../types/deployResult'
import {BlueGreenManifests} from '../types/blueGreenTypes.js'
import {DeployResult} from '../types/deployResult.js'
import {
promoteBlueGreenIngress,
promoteBlueGreenService,
promoteBlueGreenSMI
} from '../strategyHelpers/blueGreen/promote'
} from '../strategyHelpers/blueGreen/promote.js'
import {
routeBlueGreenService,
routeBlueGreenIngressUnchanged,
routeBlueGreenSMI
} from '../strategyHelpers/blueGreen/route'
} from '../strategyHelpers/blueGreen/route.js'
import {cleanupSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper'
import {Kubectl, Resource} from '../types/kubectl'
import {DeploymentStrategy} from '../types/deploymentStrategy'
import {cleanupSMI} from '../strategyHelpers/blueGreen/smiBlueGreenHelper.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from '../types/trafficSplitMethod'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
import {ResourceTypeFleet, ResourceTypeManagedCluster} from './deploy'
} from '../types/trafficSplitMethod.js'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy.js'
import {ClusterType} from '../inputUtils.js'
export async function promote(
kubectl: Kubectl,
manifests: string[],
deploymentStrategy: DeploymentStrategy
deploymentStrategy: DeploymentStrategy,
resourceType: ClusterType,
timeout?: string
) {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY:
await promoteCanary(kubectl, manifests)
await promoteCanary(kubectl, manifests, timeout)
break
case DeploymentStrategy.BLUE_GREEN:
await promoteBlueGreen(kubectl, manifests)
await promoteBlueGreen(kubectl, manifests, resourceType, timeout)
break
default:
throw Error('Invalid promote deployment strategy')
}
}
async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
async function promoteCanary(
kubectl: Kubectl,
manifests: string[],
timeout?: string
) {
let includeServices = false
const manifestFilesForDeployment: string[] = updateManifestFiles(manifests)
@ -75,7 +81,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
core.startGroup('Redirecting traffic to canary deployment')
await SMICanaryDeploymentHelper.redirectTrafficToCanaryDeployment(
kubectl,
manifests
manifests,
timeout
)
core.endGroup()
@ -86,7 +93,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
promoteResult = await SMICanaryDeploymentHelper.deploySMICanary(
manifestFilesForDeployment,
kubectl,
true
true,
timeout
)
core.endGroup()
@ -95,7 +103,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
const stableRedirectManifests =
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl,
manifests
manifests,
timeout
)
filesToAnnotate = promoteResult.manifestFiles.concat(
@ -108,7 +117,8 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
promoteResult = await PodCanaryHelper.deployPodCanary(
manifestFilesForDeployment,
kubectl,
true
true,
timeout
)
filesToAnnotate = promoteResult.manifestFiles
core.endGroup()
@ -140,7 +150,12 @@ async function promoteCanary(kubectl: Kubectl, manifests: string[]) {
core.endGroup()
}
async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
async function promoteBlueGreen(
kubectl: Kubectl,
manifests: string[],
resourceType: ClusterType,
timeout?: string
) {
// update container images and pull secrets
const inputManifestFiles: string[] = updateManifestFiles(manifests)
const manifestObjects: BlueGreenManifests =
@ -155,11 +170,19 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
const {deployResult} = await (async () => {
switch (routeStrategy) {
case RouteStrategy.INGRESS:
return await promoteBlueGreenIngress(kubectl, manifestObjects)
return await promoteBlueGreenIngress(
kubectl,
manifestObjects,
timeout
)
case RouteStrategy.SMI:
return await promoteBlueGreenSMI(kubectl, manifestObjects)
return await promoteBlueGreenSMI(kubectl, manifestObjects, timeout)
default:
return await promoteBlueGreenService(kubectl, manifestObjects)
return await promoteBlueGreenService(
kubectl,
manifestObjects,
timeout
)
}
})()
@ -167,8 +190,6 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
// checking stability of newly created deployments
core.startGroup('Checking manifest stability')
const resourceType =
core.getInput('resource-type') || ResourceTypeManagedCluster
const deployedManifestFiles = deployResult.manifestFiles
const resources: Resource[] = getResources(
deployedManifestFiles,
@ -176,18 +197,11 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
models.DiscoveryAndLoadBalancerResource.SERVICE
])
)
if (
resourceType !== ResourceTypeManagedCluster &&
resourceType !== ResourceTypeFleet
) {
const errMsg = `Invalid resource type: ${resourceType}. Supported resource types are: ${ResourceTypeManagedCluster} (default), fleet`
core.setFailed(errMsg)
throw new Error(errMsg)
}
await KubernetesManifestUtility.checkManifestStability(
kubectl,
resources,
resourceType
resourceType,
timeout
)
core.endGroup()
@ -206,7 +220,8 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
[].concat(
manifestObjects.deploymentEntityList,
manifestObjects.serviceEntityList
)
),
timeout
)
} else if (routeStrategy == RouteStrategy.SMI) {
await routeBlueGreenSMI(
@ -214,15 +229,23 @@ async function promoteBlueGreen(kubectl: Kubectl, manifests: string[]) {
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
)
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
await cleanupSMI(kubectl, manifestObjects.serviceEntityList)
await deleteGreenObjects(
kubectl,
manifestObjects.deploymentEntityList,
timeout
)
await cleanupSMI(kubectl, manifestObjects.serviceEntityList, timeout)
} else {
await routeBlueGreenService(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
)
await deleteGreenObjects(kubectl, manifestObjects.deploymentEntityList)
await deleteGreenObjects(
kubectl,
manifestObjects.deploymentEntityList,
timeout
)
}
core.endGroup()

View File

@ -1,39 +1,44 @@
import * as core from '@actions/core'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper'
import {Kubectl} from '../types/kubectl'
import {BlueGreenManifests} from '../types/blueGreenTypes'
import * as canaryDeploymentHelper from '../strategyHelpers/canary/canaryHelper.js'
import * as SMICanaryDeploymentHelper from '../strategyHelpers/canary/smiCanaryHelper.js'
import {Kubectl} from '../types/kubectl.js'
import {BlueGreenManifests} from '../types/blueGreenTypes.js'
import {
rejectBlueGreenIngress,
rejectBlueGreenService,
rejectBlueGreenSMI
} from '../strategyHelpers/blueGreen/reject'
import {getManifestObjects} from '../strategyHelpers/blueGreen/blueGreenHelper'
import {DeploymentStrategy} from '../types/deploymentStrategy'
} from '../strategyHelpers/blueGreen/reject.js'
import {getManifestObjects} from '../strategyHelpers/blueGreen/blueGreenHelper.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from '../types/trafficSplitMethod'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy'
} from '../types/trafficSplitMethod.js'
import {parseRouteStrategy, RouteStrategy} from '../types/routeStrategy.js'
export async function reject(
kubectl: Kubectl,
manifests: string[],
deploymentStrategy: DeploymentStrategy
deploymentStrategy: DeploymentStrategy,
timeout?: string
) {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY:
await rejectCanary(kubectl, manifests)
await rejectCanary(kubectl, manifests, timeout)
break
case DeploymentStrategy.BLUE_GREEN:
await rejectBlueGreen(kubectl, manifests)
await rejectBlueGreen(kubectl, manifests, timeout)
break
default:
throw 'Invalid delete deployment strategy'
}
}
async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
async function rejectCanary(
kubectl: Kubectl,
manifests: string[],
timeout?: string
) {
let includeServices = false
const trafficSplitMethod = parseTrafficSplitMethod(
@ -44,7 +49,8 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
includeServices = true
await SMICanaryDeploymentHelper.redirectTrafficToStableDeployment(
kubectl,
manifests
manifests,
timeout
)
core.endGroup()
}
@ -53,12 +59,17 @@ async function rejectCanary(kubectl: Kubectl, manifests: string[]) {
await canaryDeploymentHelper.deleteCanaryDeployment(
kubectl,
manifests,
includeServices
includeServices,
timeout
)
core.endGroup()
}
async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
async function rejectBlueGreen(
kubectl: Kubectl,
manifests: string[],
timeout?: string
) {
const routeStrategy = parseRouteStrategy(
core.getInput('route-method', {required: true})
)
@ -67,11 +78,11 @@ async function rejectBlueGreen(kubectl: Kubectl, manifests: string[]) {
const manifestObjects: BlueGreenManifests = getManifestObjects(manifests)
if (routeStrategy == RouteStrategy.INGRESS) {
await rejectBlueGreenIngress(kubectl, manifestObjects)
await rejectBlueGreenIngress(kubectl, manifestObjects, timeout)
} else if (routeStrategy == RouteStrategy.SMI) {
await rejectBlueGreenSMI(kubectl, manifestObjects)
await rejectBlueGreenSMI(kubectl, manifestObjects, timeout)
} else {
await rejectBlueGreenService(kubectl, manifestObjects)
await rejectBlueGreenService(kubectl, manifestObjects, timeout)
}
core.endGroup()
}

33
src/inputUtils.test.ts Normal file
View File

@ -0,0 +1,33 @@
import {parseResourceTypeInput} from './inputUtils.js'
import {
ResourceTypeFleet,
ResourceTypeManagedCluster
} from './actions/deploy.js'
describe('InputUtils', () => {
describe('parseResourceTypeInput', () => {
it('should extract fleet exact match resource type', () => {
expect(
parseResourceTypeInput('Microsoft.ContainerService/fleets')
).toEqual(ResourceTypeFleet)
})
it('should match fleet case-insensitively', () => {
expect(
parseResourceTypeInput('Microsoft.containerservice/fleets')
).toEqual(ResourceTypeFleet)
})
it('should match managed cluster case-insensitively', () => {
expect(
parseResourceTypeInput('Microsoft.containerservice/MAnaGedClusterS')
).toEqual(ResourceTypeManagedCluster)
})
it('should error on unexpected values', () => {
expect(() => {
parseResourceTypeInput('icrosoft.ContainerService/ManagedCluster')
}).toThrow()
expect(() => {
parseResourceTypeInput('wrong-value')
}).toThrow()
})
})
})

View File

@ -1,5 +1,9 @@
import * as core from '@actions/core'
import {parseAnnotations} from './types/annotations'
import {parseAnnotations} from './types/annotations.js'
import {
ResourceTypeFleet,
ResourceTypeManagedCluster
} from './actions/deploy.js'
export const inputAnnotations = parseAnnotations(
core.getInput('annotations', {required: false})
@ -14,3 +18,18 @@ export function getBufferTime(): number {
return inputBufferTime
}
export function parseResourceTypeInput(rawInput: string): ClusterType {
switch (rawInput.toLowerCase()) {
case ResourceTypeFleet.toLowerCase():
return ResourceTypeFleet
case ResourceTypeManagedCluster.toLowerCase():
return ResourceTypeManagedCluster
}
throw new Error(
`Invalid resource type: ${rawInput}. Supported resource types are: ${ResourceTypeManagedCluster} (default), ${ResourceTypeFleet}`
)
}
export type ClusterType =
| typeof ResourceTypeManagedCluster
| typeof ResourceTypeFleet

View File

@ -1,12 +1,19 @@
import * as core from '@actions/core'
import {getKubectlPath, Kubectl} from './types/kubectl'
import {deploy} from './actions/deploy'
import {promote} from './actions/promote'
import {reject} from './actions/reject'
import {Action, parseAction} from './types/action'
import {parseDeploymentStrategy} from './types/deploymentStrategy'
import {getFilesFromDirectoriesAndURLs} from './utilities/fileUtils'
import {PrivateKubectl} from './types/privatekubectl'
import {getKubectlPath, Kubectl} from './types/kubectl.js'
import {
deploy,
ResourceTypeFleet,
ResourceTypeManagedCluster
} from './actions/deploy.js'
import {ClusterType} from './inputUtils.js'
import {promote} from './actions/promote.js'
import {reject} from './actions/reject.js'
import {Action, parseAction} from './types/action.js'
import {parseDeploymentStrategy} from './types/deploymentStrategy.js'
import {getFilesFromDirectoriesAndURLs} from './utilities/fileUtils.js'
import {PrivateKubectl} from './types/privatekubectl.js'
import {parseResourceTypeInput} from './inputUtils.js'
import {parseDuration} from './utilities/durationUtils.js'
export async function run() {
// verify kubeconfig is set
@ -29,13 +36,34 @@ export async function run() {
const fullManifestFilePaths =
await getFilesFromDirectoriesAndURLs(manifestFilePaths)
const kubectlPath = await getKubectlPath()
const namespace = core.getInput('namespace') || 'default'
const namespace = core.getInput('namespace') || '' // Sets namespace to an empty string if not provided, allowing the manifest-defined namespace to take precedence instead of "default".
const isPrivateCluster =
core.getInput('private-cluster').toLowerCase() === 'true'
const resourceGroup = core.getInput('resource-group') || ''
const resourceName = core.getInput('name') || ''
const skipTlsVerify = core.getBooleanInput('skip-tls-verify')
let resourceType: ClusterType
try {
// included in the trycatch to allow raw input to go out of scope after parsing
const resourceTypeInput = core.getInput('resource-type')
resourceType = parseResourceTypeInput(resourceTypeInput)
} catch (e) {
core.setFailed(e)
return
}
// Parse and validate timeout using extracted utility
let timeout: string
try {
const timeoutInput = core.getInput('timeout') || '10m'
timeout = parseDuration(timeoutInput)
core.debug(`Using timeout: ${timeout}`)
} catch (e) {
core.setFailed(`Invalid timeout parameter: ${e.message}`)
return
}
const kubectl = isPrivateCluster
? new PrivateKubectl(
kubectlPath,
@ -49,15 +77,27 @@ export async function run() {
// run action
switch (action) {
case Action.DEPLOY: {
await deploy(kubectl, fullManifestFilePaths, strategy)
await deploy(
kubectl,
fullManifestFilePaths,
strategy,
resourceType,
timeout
)
break
}
case Action.PROMOTE: {
await promote(kubectl, fullManifestFilePaths, strategy)
await promote(
kubectl,
fullManifestFilePaths,
strategy,
resourceType,
timeout
)
break
}
case Action.REJECT: {
await reject(kubectl, fullManifestFilePaths, strategy)
await reject(kubectl, fullManifestFilePaths, strategy, timeout)
break
}
default: {

View File

@ -1,67 +1,106 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {
deployWithLabel,
deleteGreenObjects,
deployObjects,
fetchResource,
getDeploymentMatchLabels,
getManifestObjects,
getNewBlueGreenObject,
GREEN_LABEL_VALUE,
isServiceRouted
} from './blueGreenHelper'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import * as bgHelper from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
import {K8sObject} from '../../types/k8sObject'
import * as manifestUpdateUtils from '../../utilities/manifestUpdateUtils'
} from './blueGreenHelper.js'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
import * as bgHelper from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import {K8sObject} from '../../types/k8sObject.js'
import * as manifestUpdateUtils from '../../utilities/manifestUpdateUtils.js'
import {ExecOutput} from '@actions/exec'
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
const kubectl = new Kubectl('')
const TEST_TIMEOUT = '60s'
// Test constants to follow DRY principle
const EXPECTED_GREEN_OBJECTS = [
{name: 'nginx-service-green', kind: 'Service'},
{name: 'nginx-deployment-green', kind: 'Deployment'}
]
const MOCK_EXEC_OUTPUT = {
exitCode: 0,
stderr: '',
stdout: ''
} as ExecOutput
describe('bluegreenhelper functions', () => {
let testObjects
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
vi.restoreAllMocks()
vi.mocked(Kubectl).mockClear()
testObjects = getManifestObjects(['test/unit/manifests/test-ingress.yml'])
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
})
test('correctly deletes services and workloads according to label', async () => {
jest.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
vi.spyOn(bgHelper, 'deleteObjects').mockReturnValue({} as Promise<void>)
const value = await deleteGreenObjects(
kubectl,
[].concat(
testObjects.deploymentEntityList,
testObjects.serviceEntityList
)
),
TEST_TIMEOUT
)
expect(value).toHaveLength(2)
expect(value).toContainEqual({
name: 'nginx-service-green',
kind: 'Service'
expect(value).toHaveLength(EXPECTED_GREEN_OBJECTS.length)
EXPECTED_GREEN_OBJECTS.forEach((expectedObject) => {
expect(value).toContainEqual(expectedObject)
})
expect(value).toContainEqual({
name: 'nginx-deployment-green',
kind: 'Deployment'
})
test('handles timeout when deleting objects', async () => {
const deleteMock = vi.fn().mockResolvedValue(MOCK_EXEC_OUTPUT)
kubectl.delete = deleteMock
const deleteList = EXPECTED_GREEN_OBJECTS
await bgHelper.deleteObjects(kubectl, deleteList, TEST_TIMEOUT)
expect(deleteMock).toHaveBeenCalledTimes(deleteList.length)
deleteList.forEach(({name, kind}) => {
expect(deleteMock).toHaveBeenCalledWith(
[kind, name],
undefined,
TEST_TIMEOUT
)
})
})
test('parses objects correctly from one file (getManifestObjects)', () => {
expect(testObjects.deploymentEntityList[0].kind).toBe('Deployment')
expect(testObjects.serviceEntityList[0].kind).toBe('Service')
expect(testObjects.ingressEntityList[0].kind).toBe('Ingress')
const expectedTypes = [
{
list: testObjects.deploymentEntityList,
kind: 'Deployment',
selectorApp: 'nginx'
},
{list: testObjects.serviceEntityList, kind: 'Service'},
{list: testObjects.ingressEntityList, kind: 'Ingress'}
]
expect(
testObjects.deploymentEntityList[0].spec.selector.matchLabels.app
).toBe('nginx')
expectedTypes.forEach(({list, kind, selectorApp}) => {
expect(list[0].kind).toBe(kind)
if (selectorApp) {
expect(list[0].spec.selector.matchLabels.app).toBe(selectorApp)
}
})
})
test('parses other kinds of objects (getManifestObjects)', () => {
@ -93,32 +132,41 @@ describe('bluegreenhelper functions', () => {
})
test('correctly makes labeled workloads', async () => {
const kubectlApplySpy = vi.spyOn(kubectl, 'apply').mockResolvedValue({
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
})
const cwlResult: BlueGreenDeployment = await deployWithLabel(
kubectl,
testObjects.deploymentEntityList,
GREEN_LABEL_VALUE
)
expect(cwlResult.deployResult.manifestFiles[0]).toBe('')
kubectlApplySpy.mockRestore()
})
test('correctly makes new blue green object (getNewBlueGreenObject and addBlueGreenLabelsAndAnnotations)', () => {
const modifiedDeployment = getNewBlueGreenObject(
testObjects.deploymentEntityList[0],
GREEN_LABEL_VALUE
)
const testCases = [
{
object: testObjects.deploymentEntityList[0],
expectedName: 'nginx-deployment-green'
},
{
object: testObjects.serviceEntityList[0],
expectedName: 'nginx-service-green'
}
]
expect(modifiedDeployment.metadata.name).toBe('nginx-deployment-green')
expect(modifiedDeployment.metadata.labels['k8s.deploy.color']).toBe(
'green'
)
const modifiedSvc = getNewBlueGreenObject(
testObjects.serviceEntityList[0],
GREEN_LABEL_VALUE
)
expect(modifiedSvc.metadata.name).toBe('nginx-service-green')
expect(modifiedSvc.metadata.labels['k8s.deploy.color']).toBe('green')
testCases.forEach(({object, expectedName}) => {
const modifiedObject = getNewBlueGreenObject(object, GREEN_LABEL_VALUE)
expect(modifiedObject.metadata.name).toBe(expectedName)
expect(modifiedObject.metadata.labels['k8s.deploy.color']).toBe(
'green'
)
})
})
test('correctly fetches k8s objects', async () => {
@ -128,9 +176,9 @@ describe('bluegreenhelper functions', () => {
exitCode: 0
}
jest
.spyOn(kubectl, 'getResource')
.mockImplementation(() => Promise.resolve(mockExecOutput))
vi.spyOn(kubectl, 'getResource').mockImplementation(() =>
Promise.resolve(mockExecOutput)
)
const fetched = await fetchResource(
kubectl,
'nginx-deployment',
@ -140,40 +188,61 @@ describe('bluegreenhelper functions', () => {
})
test('exits when fails to fetch k8s objects', async () => {
const mockExecOutput = {
stdout: 'this should not matter',
exitCode: 0,
stderr: 'this is a fake error'
} as ExecOutput
jest
.spyOn(kubectl, 'getResource')
.mockImplementation(() => Promise.resolve(mockExecOutput))
let fetched = await fetchResource(
kubectl,
'nginx-deployment',
'Deployment'
)
expect(fetched).toBe(null)
const errorTestCases = [
{
description: 'with stderr error',
mockOutput: {
stdout: 'this should not matter',
exitCode: 0,
stderr: 'this is a fake error'
} as ExecOutput,
mockImplementation: () => Promise.resolve
},
{
description: 'with undefined implementation',
mockOutput: null,
mockImplementation: () => undefined
}
]
jest.spyOn(kubectl, 'getResource').mockImplementation()
fetched = await fetchResource(kubectl, 'nginx-deployment', 'Deployment')
expect(fetched).toBe(null)
for (const testCase of errorTestCases) {
const spy = vi.spyOn(kubectl, 'getResource')
if (testCase.mockOutput) {
spy.mockImplementation(() => Promise.resolve(testCase.mockOutput))
} else {
spy.mockResolvedValue(null)
}
const fetched = await fetchResource(
kubectl,
'nginx-deployment',
'Deployment'
)
expect(fetched).toBe(null)
spy.mockRestore()
}
})
test('returns null when fetch fails to unset k8s objects', async () => {
test('returns undefined when fetch fails to unset k8s objects', async () => {
const mockExecOutput = {
stdout: 'this should not matter',
stdout: JSON.stringify(testObjects.deploymentEntityList[0]),
exitCode: 0,
stderr: 'this is a fake error'
stderr: ''
} as ExecOutput
jest
.spyOn(manifestUpdateUtils, 'UnsetClusterSpecificDetails')
.mockImplementation(() => {
throw new Error('test error')
})
vi.spyOn(kubectl, 'getResource').mockResolvedValue(mockExecOutput)
vi.spyOn(
manifestUpdateUtils,
'UnsetClusterSpecificDetails'
).mockImplementation(() => {
throw new Error('test error')
})
expect(
await fetchResource(kubectl, 'nginx-deployment', 'Deployment')
).toBe(null)
).toBeUndefined()
})
test('gets deployment labels', () => {
@ -193,4 +262,72 @@ describe('bluegreenhelper functions', () => {
getDeploymentMatchLabels(testObjects.deploymentEntityList[0])['app']
).toBe('nginx')
})
describe('deployObjects', () => {
let mockObjects: any[]
let kubectlApplySpy: MockInstance
const mockSuccessResult: ExecOutput = {
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
}
const mockFailureResult: ExecOutput = {
stdout: '',
stderr: 'error: deployment failed',
exitCode: 1
}
beforeEach(() => {
// //@ts-ignore
// Kubectl.mockClear()
mockObjects = [testObjects.deploymentEntityList[0]]
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
})
afterEach(() => {
vi.clearAllMocks()
})
it('should return execution result and manifest files when kubectl apply succeeds', async () => {
kubectlApplySpy.mockClear()
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deployObjects(kubectl, mockObjects)
expect(result.execResult).toEqual(mockSuccessResult)
const timeoutArg = kubectlApplySpy.mock.calls[0][3]
expect(
typeof timeoutArg === 'string' || timeoutArg === undefined
).toBe(true)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Boolean),
expect.any(Boolean),
timeoutArg
)
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
it('should throw an error when kubectl apply fails with non-zero exit code', async () => {
kubectlApplySpy.mockClear()
kubectlApplySpy.mockResolvedValue(mockFailureResult)
await expect(deployObjects(kubectl, mockObjects)).rejects.toThrow()
const timeoutArg = kubectlApplySpy.mock.calls[0][3]
expect(
typeof timeoutArg === 'string' || timeoutArg === undefined
).toBe(true)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Boolean),
expect.any(Boolean),
timeoutArg
)
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -2,27 +2,27 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import {DeployResult} from '../../types/deployResult'
import {K8sObject, K8sDeleteObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import {DeployResult} from '../../types/deployResult.js'
import {K8sObject, K8sDeleteObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import {
isDeploymentEntity,
isIngressEntity,
isServiceEntity,
KubernetesWorkload
} from '../../types/kubernetesTypes'
} from '../../types/kubernetesTypes.js'
import {
BlueGreenDeployment,
BlueGreenManifests
} from '../../types/blueGreenTypes'
import * as fileHelper from '../../utilities/fileUtils'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
import {checkForErrors} from '../../utilities/kubectlUtils'
} from '../../types/blueGreenTypes.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
import {
UnsetClusterSpecificDetails,
updateObjectLabels,
updateSelectorLabels
} from '../../utilities/manifestUpdateUtils'
} from '../../utilities/manifestUpdateUtils.js'
export const GREEN_LABEL_VALUE = 'green'
export const NONE_LABEL_VALUE = 'None'
@ -32,7 +32,8 @@ export const STABLE_SUFFIX = '-stable'
export async function deleteGreenObjects(
kubectl: Kubectl,
toDelete: K8sObject[]
toDelete: K8sObject[],
timeout?: string
): Promise<K8sDeleteObject[]> {
// const resourcesToDelete: K8sDeleteObject[] = []
const resourcesToDelete: K8sDeleteObject[] = toDelete.map((obj) => {
@ -45,18 +46,23 @@ export async function deleteGreenObjects(
core.debug(`deleting green objects: ${JSON.stringify(resourcesToDelete)}`)
await deleteObjects(kubectl, resourcesToDelete)
await deleteObjects(kubectl, resourcesToDelete, timeout)
return resourcesToDelete
}
export async function deleteObjects(
kubectl: Kubectl,
deleteList: K8sDeleteObject[]
deleteList: K8sDeleteObject[],
timeout?: string
) {
// delete services and deployments
for (const delObject of deleteList) {
try {
const result = await kubectl.delete([delObject.kind, delObject.name])
const result = await kubectl.delete(
[delObject.kind, delObject.name],
delObject.namespace,
timeout
)
checkForErrors([result])
} catch (ex) {
core.debug(`failed to delete object ${delObject.name}: ${ex}`)
@ -141,7 +147,8 @@ export function isServiceRouted(
export async function deployWithLabel(
kubectl: Kubectl,
deploymentObjectList: any[],
nextLabel: string
nextLabel: string,
timeout?: string
): Promise<BlueGreenDeployment> {
const newObjectsList = deploymentObjectList.map((inputObject) =>
getNewBlueGreenObject(inputObject, nextLabel)
@ -150,7 +157,7 @@ export async function deployWithLabel(
core.debug(
`objects deployed with label are ${JSON.stringify(newObjectsList)}`
)
const deployResult = await deployObjects(kubectl, newObjectsList)
const deployResult = await deployObjects(kubectl, newObjectsList, timeout)
return {deployResult, objects: newObjectsList}
}
@ -267,10 +274,28 @@ export async function fetchResource(
export async function deployObjects(
kubectl: Kubectl,
objectsList: any[]
objectsList: any[],
timeout?: string
): Promise<DeployResult> {
const manifestFiles = fileHelper.writeObjectsToFile(objectsList)
const execResult = await kubectl.apply(manifestFiles)
// Handle empty objects list gracefully to prevent "Configuration paths must exist" error
if (!objectsList || objectsList.length === 0) {
core.debug('No objects to deploy, skipping kubectl apply')
return {
execResult: {exitCode: 0, stdout: '', stderr: ''},
manifestFiles: []
}
}
const manifestFiles = fileHelper.writeObjectsToFile(objectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const serverSideApply = core.getInput('server-side').toLowerCase() === 'true'
const execResult = await kubectl.apply(
manifestFiles,
forceDeployment,
serverSideApply,
timeout
)
checkForErrors([execResult])
return {execResult, manifestFiles}
}

View File

@ -1,39 +1,66 @@
import {getManifestObjects} from './blueGreenHelper'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import {deployBlueGreen, deployBlueGreenIngress} from './deploy'
import * as routeTester from './route'
import {Kubectl} from '../../types/kubectl'
import {RouteStrategy} from '../../types/routeStrategy'
import * as TSutils from '../../utilities/trafficSplitUtils'
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
import {
deployBlueGreen,
deployBlueGreenIngress,
deployBlueGreenService,
deployBlueGreenSMI
} from './deploy.js'
import * as routeTester from './route.js'
import {Kubectl} from '../../types/kubectl.js'
import {RouteStrategy} from '../../types/routeStrategy.js'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import * as bgHelper from './blueGreenHelper.js'
import * as smiHelper from './smiBlueGreenHelper.js'
import {ExecOutput} from '@actions/exec'
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
// Shared variables and mock objects used across all test suites
const mockDeployResult = {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
}
const mockBgDeployment: BlueGreenDeployment = {
deployResult: mockDeployResult,
objects: []
}
describe('deploy tests', () => {
let testObjects
let kubectl: Kubectl
let kubectlApplySpy: MockInstance
const mockSuccessResult: ExecOutput = {
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
}
const mockFailureResult: ExecOutput = {
stdout: '',
stderr: 'error: deployment failed',
exitCode: 1
}
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
testObjects = getManifestObjects(ingressFilepath)
vi.mocked(Kubectl).mockClear()
kubectl = new Kubectl('')
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
})
test('correctly determines deploy type and acts accordingly', async () => {
const kubectl = new Kubectl('')
const mockBgDeployment: BlueGreenDeployment = {
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
}
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
jest
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockImplementation(() => Promise.resolve(mockBgDeployment))
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(() =>
Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
const ingressResult = await deployBlueGreen(
kubectl,
@ -61,8 +88,9 @@ describe('deploy tests', () => {
})
test('correctly deploys blue/green ingress', async () => {
const kc = new Kubectl('')
const value = await deployBlueGreenIngress(kc, ingressFilepath)
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const value = await deployBlueGreenIngress(kubectl, ingressFilepath)
const nol = value.objects.map((obj) => {
if (obj.kind === 'Service') {
expect(obj.metadata.name).toBe('nginx-service-green')
@ -72,4 +100,278 @@ describe('deploy tests', () => {
}
})
})
// Consolidated error tests
test.each([
{
name: 'should throw error when kubectl apply fails during blue/green ingress deployment',
fn: () => deployBlueGreenIngress(kubectl, ingressFilepath),
setup: () => {}
},
{
name: 'should throw error when kubectl apply fails during blue/green deployment with INGRESS strategy',
fn: () =>
deployBlueGreen(kubectl, ingressFilepath, RouteStrategy.INGRESS),
setup: () => {
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(
() => Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green deployment with SERVICE strategy',
fn: () =>
deployBlueGreen(kubectl, ingressFilepath, RouteStrategy.SERVICE),
setup: () => {
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(
() => Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green deployment with SMI strategy',
fn: () => deployBlueGreen(kubectl, ingressFilepath, RouteStrategy.SMI),
setup: () => {
vi.spyOn(routeTester, 'routeBlueGreenForDeploy').mockImplementation(
() => Promise.resolve(mockBgDeployment)
)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
}
},
{
name: 'should throw error when deployBlueGreenService fails',
fn: () => deployBlueGreenService(kubectl, ingressFilepath),
setup: () => {}
},
{
name: 'should throw error when deployBlueGreenSMI fails',
fn: () => deployBlueGreenSMI(kubectl, ingressFilepath),
setup: () => {}
}
])('$name', async ({fn, setup}) => {
kubectlApplySpy.mockResolvedValue(mockFailureResult)
setup()
await expect(fn()).rejects.toThrow()
const timeoutArg = kubectlApplySpy.mock.calls[0][3]
expect(typeof timeoutArg === 'string' || timeoutArg === undefined).toBe(
true
)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Boolean),
expect.any(Boolean),
timeoutArg
)
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
})
// Timeout tests
describe('deploy timeout tests', () => {
let kubectl: Kubectl
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
kubectl = new Kubectl('')
})
test('deployBlueGreen with timeout passes to strategy functions', async () => {
const timeout = '300s'
// Mock the helper functions that are actually called
const deployWithLabelSpy = vi
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
const setupSMISpy = vi
.spyOn(smiHelper, 'setupSMI')
.mockResolvedValue(mockBgDeployment)
const routeSpy = vi
.spyOn(routeTester, 'routeBlueGreenForDeploy')
.mockResolvedValue(mockBgDeployment)
// Test INGRESS strategy
await deployBlueGreen(
kubectl,
ingressFilepath,
RouteStrategy.INGRESS,
timeout
)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Test SERVICE strategy
deployWithLabelSpy.mockClear()
deployObjectsSpy.mockClear()
await deployBlueGreen(
kubectl,
ingressFilepath,
RouteStrategy.SERVICE,
timeout
)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Test SMI strategy
deployWithLabelSpy.mockClear()
setupSMISpy.mockClear()
await deployBlueGreen(
kubectl,
ingressFilepath,
RouteStrategy.SMI,
timeout
)
expect(setupSMISpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
setupSMISpy.mockRestore()
routeSpy.mockRestore()
})
test('deployBlueGreenIngress with timeout', async () => {
const timeout = '240s'
// Mock the dependencies
const deployWithLabelSpy = vi
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
await deployBlueGreenIngress(kubectl, ingressFilepath, timeout)
// Verify deployWithLabel was called with timeout
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
})
test('deployBlueGreenService with timeout', async () => {
const timeout = '180s'
// Mock the dependencies
const deployWithLabelSpy = vi
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
await deployBlueGreenService(kubectl, ingressFilepath, timeout)
// Verify deployWithLabel was called with timeout
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
})
test('deployBlueGreenSMI with timeout', async () => {
const timeout = '360s'
// Mock the dependencies
const setupSMISpy = vi
.spyOn(smiHelper, 'setupSMI')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
const deployWithLabelSpy = vi
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
await deployBlueGreenSMI(kubectl, ingressFilepath, timeout)
// Verify setupSMI was called with timeout
expect(setupSMISpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
timeout
)
setupSMISpy.mockRestore()
deployObjectsSpy.mockRestore()
deployWithLabelSpy.mockRestore()
})
test('deploy functions without timeout should pass undefined', async () => {
const deployWithLabelSpy = vi
.spyOn(bgHelper, 'deployWithLabel')
.mockResolvedValue(mockBgDeployment)
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue(mockDeployResult)
await deployBlueGreenIngress(kubectl, ingressFilepath)
// Verify deployWithLabel was called with undefined timeout
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
expect.any(String),
undefined
)
deployWithLabelSpy.mockRestore()
deployObjectsSpy.mockRestore()
})
})

View File

@ -1,37 +1,38 @@
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl'
import {Kubectl} from '../../types/kubectl.js'
import {
BlueGreenDeployment,
BlueGreenManifests
} from '../../types/blueGreenTypes'
} from '../../types/blueGreenTypes.js'
import {RouteStrategy} from '../../types/routeStrategy'
import {RouteStrategy} from '../../types/routeStrategy.js'
import {
deployWithLabel,
getManifestObjects,
GREEN_LABEL_VALUE,
deployObjects
} from './blueGreenHelper'
import {setupSMI} from './smiBlueGreenHelper'
} from './blueGreenHelper.js'
import {setupSMI} from './smiBlueGreenHelper.js'
import {routeBlueGreenForDeploy} from './route'
import {DeployResult} from '../../types/deployResult'
import {routeBlueGreenForDeploy} from './route.js'
import {DeployResult} from '../../types/deployResult.js'
export async function deployBlueGreen(
kubectl: Kubectl,
files: string[],
routeStrategy: RouteStrategy
routeStrategy: RouteStrategy,
timeout?: string
): Promise<BlueGreenDeployment> {
const blueGreenDeployment = await (async () => {
switch (routeStrategy) {
case RouteStrategy.INGRESS:
return await deployBlueGreenIngress(kubectl, files)
return await deployBlueGreenIngress(kubectl, files, timeout)
case RouteStrategy.SMI:
return await deployBlueGreenSMI(kubectl, files)
return await deployBlueGreenSMI(kubectl, files, timeout)
default:
return await deployBlueGreenService(kubectl, files)
return await deployBlueGreenService(kubectl, files, timeout)
}
})()
@ -39,7 +40,8 @@ export async function deployBlueGreen(
const routeDeployment = await routeBlueGreenForDeploy(
kubectl,
files,
routeStrategy
routeStrategy,
timeout
)
core.endGroup()
@ -52,7 +54,8 @@ export async function deployBlueGreen(
export async function deployBlueGreenSMI(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
timeout?: string
): Promise<BlueGreenDeployment> {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@ -67,20 +70,23 @@ export async function deployBlueGreenSMI(
const otherObjDeployment: DeployResult = await deployObjects(
kubectl,
newObjectsList
newObjectsList,
timeout
)
// make extraservices and trafficsplit
const smiAndSvcDeployment = await setupSMI(
kubectl,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
// create new deloyments
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE
GREEN_LABEL_VALUE,
timeout
)
blueGreenDeployment.objects.push(...newObjectsList)
@ -98,7 +104,8 @@ export async function deployBlueGreenSMI(
export async function deployBlueGreenIngress(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
timeout?: string
): Promise<BlueGreenDeployment> {
// get all kubernetes objects defined in manifest files
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@ -111,14 +118,15 @@ export async function deployBlueGreenIngress(
const workloadDeployment: BlueGreenDeployment = await deployWithLabel(
kubectl,
servicesAndDeployments,
GREEN_LABEL_VALUE
GREEN_LABEL_VALUE,
timeout
)
const otherObjects = [].concat(
manifestObjects.otherObjects,
manifestObjects.unroutedServiceEntityList
)
await deployObjects(kubectl, otherObjects)
await deployObjects(kubectl, otherObjects, timeout)
core.debug(
`new objects after processing services and other objects: \n
${JSON.stringify(servicesAndDeployments)}`
@ -132,7 +140,8 @@ export async function deployBlueGreenIngress(
export async function deployBlueGreenService(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
timeout?: string
): Promise<BlueGreenDeployment> {
const manifestObjects: BlueGreenManifests = getManifestObjects(filePaths)
@ -140,7 +149,8 @@ export async function deployBlueGreenService(
const blueGreenDeployment: BlueGreenDeployment = await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
GREEN_LABEL_VALUE
GREEN_LABEL_VALUE,
timeout
)
// create other non deployment and non service entities
@ -150,7 +160,7 @@ export async function deployBlueGreenService(
manifestObjects.unroutedServiceEntityList
)
await deployObjects(kubectl, newObjectsList)
await deployObjects(kubectl, newObjectsList, timeout)
// returning deployment details to check for rollout stability
return {
deployResult: blueGreenDeployment.deployResult,

View File

@ -1,27 +1,27 @@
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper'
import * as bgHelper from './blueGreenHelper'
import {vi} from 'vitest'
import {getManifestObjects, GREEN_LABEL_VALUE} from './blueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
import {
getUpdatedBlueGreenIngress,
isIngressRouted,
validateIngresses
} from './ingressBlueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
} from './ingressBlueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
const betaFilepath = ['test/unit/manifests/test-ingress.yml']
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
const kubectl = new Kubectl('')
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
describe('ingress blue green helpers', () => {
let testObjects
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
vi.mocked(Kubectl).mockClear()
testObjects = getManifestObjects(ingressFilepath)
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
})
test('it should correctly classify ingresses', () => {
@ -72,7 +72,7 @@ describe('ingress blue green helpers', () => {
test('it should validate ingresses', async () => {
// what if nothing gets returned from fetchResource?
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(null)
let validResponse = await validateIngresses(
kubectl,
testObjects.ingressEntityList,
@ -89,9 +89,9 @@ describe('ingress blue green helpers', () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
mockIngress.metadata.labels = mockLabels
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockIngress))
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockIngress)
)
validResponse = await validateIngresses(
kubectl,
testObjects.ingressEntityList,

View File

@ -1,12 +1,12 @@
import * as core from '@actions/core'
import {K8sIngress} from '../../types/k8sObject'
import {K8sIngress} from '../../types/k8sObject.js'
import {
addBlueGreenLabelsAndAnnotations,
BLUE_GREEN_VERSION_LABEL,
GREEN_LABEL_VALUE,
fetchResource
} from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
} from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
const BACKEND = 'backend'

View File

@ -1,34 +1,64 @@
import * as core from '@actions/core'
import {getManifestObjects} from './blueGreenHelper'
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {getManifestObjects} from './blueGreenHelper.js'
import {
promoteBlueGreenIngress,
promoteBlueGreenService,
promoteBlueGreenSMI
} from './promote'
import {TrafficSplitObject} from '../../types/k8sObject'
import * as servicesTester from './serviceBlueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import {MAX_VAL, MIN_VAL, TRAFFIC_SPLIT_OBJECT} from './smiBlueGreenHelper'
import * as smiTester from './smiBlueGreenHelper'
import * as bgHelper from './blueGreenHelper'
} from './promote.js'
import {TrafficSplitObject} from '../../types/k8sObject.js'
import * as servicesTester from './serviceBlueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import {MAX_VAL, MIN_VAL, TRAFFIC_SPLIT_OBJECT} from './smiBlueGreenHelper.js'
import * as smiTester from './smiBlueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
import {ExecOutput} from '@actions/exec'
let testObjects
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
// Shared variables used across all test suites
let testObjects: any
const kubectl = new Kubectl('')
// Shared mock objects following DRY principle
const mockSuccessResult: ExecOutput = {
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
}
const mockFailureResult: ExecOutput = {
stdout: '',
stderr: 'error: deployment failed',
exitCode: 1
}
const mockBgDeployment = {
deployResult: {
execResult: {exitCode: 0, stderr: '', stdout: ''},
manifestFiles: []
},
objects: []
}
describe('promote tests', () => {
let kubectlApplySpy: MockInstance
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
vi.mocked(Kubectl).mockClear()
testObjects = getManifestObjects(ingressFilepath)
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
})
test('promote blue/green ingress', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Ingress',
spec: {},
@ -53,7 +83,7 @@ describe('promote tests', () => {
test('fail to promote invalid blue/green ingress', async () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Ingress',
spec: {},
@ -63,13 +93,15 @@ describe('promote tests', () => {
await expect(
promoteBlueGreenIngress(kubectl, testObjects)
).rejects.toThrowError()
).rejects.toThrow()
})
test('promote blue/green service', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {selector: mockLabels},
@ -89,23 +121,25 @@ describe('promote tests', () => {
test('fail to promote invalid blue/green service', async () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {},
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
})
)
jest
.spyOn(servicesTester, 'validateServicesState')
.mockImplementationOnce(() => Promise.resolve(false))
vi.spyOn(servicesTester, 'validateServicesState').mockImplementationOnce(
() => Promise.resolve(false)
)
await expect(
promoteBlueGreenService(kubectl, testObjects)
).rejects.toThrowError()
).rejects.toThrow()
})
test('promote blue/green SMI', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
@ -131,9 +165,9 @@ describe('promote tests', () => {
]
}
}
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockTsObject))
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockTsObject)
)
const deployResult = await promoteBlueGreenSMI(kubectl, testObjects)
@ -149,10 +183,217 @@ describe('promote tests', () => {
test('promote blue/green SMI with bad trafficsplit', async () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = bgHelper.NONE_LABEL_VALUE
jest
.spyOn(smiTester, 'validateTrafficSplitsState')
.mockImplementation(() => Promise.resolve(false))
vi.spyOn(smiTester, 'validateTrafficSplitsState').mockImplementation(() =>
Promise.resolve(false)
)
expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrowError()
await expect(promoteBlueGreenSMI(kubectl, testObjects)).rejects.toThrow()
})
// Consolidated error tests
test.each([
{
name: 'should throw error when kubectl apply fails during blue/green ingress promotion',
fn: () => promoteBlueGreenIngress(kubectl, testObjects),
setup: () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] =
bgHelper.GREEN_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Ingress',
spec: {},
metadata: {labels: mockLabels, name: 'nginx-ingress-green'}
})
)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green service promotion',
fn: () => promoteBlueGreenService(kubectl, testObjects),
setup: () => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] =
bgHelper.GREEN_LABEL_VALUE
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {selector: mockLabels},
metadata: {labels: mockLabels, name: 'nginx-service-green'}
})
)
vi.spyOn(servicesTester, 'validateServicesState').mockResolvedValue(
true
)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green SMI promotion',
fn: () => promoteBlueGreenSMI(kubectl, testObjects),
setup: () => {
const mockTsObject: TrafficSplitObject = {
apiVersion: 'v1alpha3',
kind: TRAFFIC_SPLIT_OBJECT,
metadata: {
name: 'nginx-service-trafficsplit',
labels: new Map<string, string>(),
annotations: new Map<string, string>()
},
spec: {
service: 'nginx-service',
backends: [
{service: 'nginx-service-stable', weight: MIN_VAL},
{service: 'nginx-service-green', weight: MAX_VAL}
]
}
}
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(mockTsObject)
vi.spyOn(smiTester, 'validateTrafficSplitsState').mockResolvedValue(
true
)
}
}
])('$name', async ({fn, setup}) => {
kubectlApplySpy.mockClear()
kubectlApplySpy.mockResolvedValue(mockFailureResult)
setup()
await expect(fn()).rejects.toThrow()
const timeoutArg = kubectlApplySpy.mock.calls[0][3]
expect(typeof timeoutArg === 'string' || timeoutArg === undefined).toBe(
true
)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Boolean),
expect.any(Boolean),
timeoutArg
)
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
})
// Timeout tests
describe('promote timeout tests', () => {
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
testObjects = getManifestObjects(ingressFilepath)
})
const mockDeployWithLabel = () =>
vi.spyOn(bgHelper, 'deployWithLabel').mockResolvedValue(mockBgDeployment)
const setupFetchResource = (
kind: string,
name: string,
labelValue: string
) => {
const mockLabels = new Map<string, string>()
mockLabels[bgHelper.BLUE_GREEN_VERSION_LABEL] = labelValue
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue({
kind,
spec: {},
metadata: {labels: mockLabels, name}
})
}
test.each([
{
name: 'promoteBlueGreenIngress with timeout',
fn: promoteBlueGreenIngress,
kind: 'Ingress',
resourceName: 'nginx-ingress-green',
timeout: '300s',
setup: () =>
setupFetchResource(
'Ingress',
'nginx-ingress-green',
bgHelper.GREEN_LABEL_VALUE
)
},
{
name: 'promoteBlueGreenService with timeout',
fn: promoteBlueGreenService,
kind: 'Service',
resourceName: 'nginx-service-green',
timeout: '240s',
setup: () => {
setupFetchResource(
'Service',
'nginx-service-green',
bgHelper.GREEN_LABEL_VALUE
)
vi.spyOn(servicesTester, 'validateServicesState').mockResolvedValue(
true
)
}
},
{
name: 'promoteBlueGreenSMI with timeout',
fn: promoteBlueGreenSMI,
kind: 'TrafficSplit',
resourceName: 'nginx-service-trafficsplit',
timeout: '180s',
setup: () => {
const mockTsObject: TrafficSplitObject = {
apiVersion: 'v1alpha3',
kind: TRAFFIC_SPLIT_OBJECT,
metadata: {
name: 'nginx-service-trafficsplit',
labels: new Map<string, string>(),
annotations: new Map<string, string>()
},
spec: {
service: 'nginx-service',
backends: [
{service: 'nginx-service-stable', weight: MIN_VAL},
{service: 'nginx-service-green', weight: MAX_VAL}
]
}
}
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(mockTsObject)
vi.spyOn(smiTester, 'validateTrafficSplitsState').mockResolvedValue(
true
)
}
}
])('$name', async ({fn, timeout, setup}) => {
setup()
const deployWithLabelSpy = mockDeployWithLabel()
await fn(kubectl, testObjects, timeout)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
bgHelper.NONE_LABEL_VALUE,
timeout
)
deployWithLabelSpy.mockRestore()
})
test('promote functions without timeout should pass undefined', async () => {
setupFetchResource(
'Ingress',
'nginx-ingress-green',
bgHelper.GREEN_LABEL_VALUE
)
const deployWithLabelSpy = mockDeployWithLabel()
await promoteBlueGreenIngress(kubectl, testObjects)
expect(deployWithLabelSpy).toHaveBeenCalledWith(
kubectl,
expect.any(Array),
bgHelper.NONE_LABEL_VALUE,
undefined
)
deployWithLabelSpy.mockRestore()
})
})

View File

@ -1,17 +1,18 @@
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl'
import {Kubectl} from '../../types/kubectl.js'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
import {deployWithLabel, NONE_LABEL_VALUE} from './blueGreenHelper'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
import {deployWithLabel, NONE_LABEL_VALUE} from './blueGreenHelper.js'
import {validateIngresses} from './ingressBlueGreenHelper'
import {validateServicesState} from './serviceBlueGreenHelper'
import {validateTrafficSplitsState} from './smiBlueGreenHelper'
import {validateIngresses} from './ingressBlueGreenHelper.js'
import {validateServicesState} from './serviceBlueGreenHelper.js'
import {validateTrafficSplitsState} from './smiBlueGreenHelper.js'
export async function promoteBlueGreenIngress(
kubectl: Kubectl,
manifestObjects
manifestObjects,
timeout?: string
): Promise<BlueGreenDeployment> {
//checking if anything to promote
const {areValid, invalidIngresses} = await validateIngresses(
@ -32,7 +33,8 @@ export async function promoteBlueGreenIngress(
manifestObjects.deploymentEntityList,
manifestObjects.serviceEntityList
),
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
// create stable services with new configuration
@ -41,7 +43,8 @@ export async function promoteBlueGreenIngress(
export async function promoteBlueGreenService(
kubectl: Kubectl,
manifestObjects
manifestObjects,
timeout?: string
): Promise<BlueGreenDeployment> {
// checking if services are in the right state ie. targeting green deployments
if (
@ -54,13 +57,15 @@ export async function promoteBlueGreenService(
return await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
}
export async function promoteBlueGreenSMI(
kubectl: Kubectl,
manifestObjects
manifestObjects,
timeout?: string
): Promise<BlueGreenDeployment> {
// checking if there is something to promote
if (
@ -76,6 +81,7 @@ export async function promoteBlueGreenSMI(
return await deployWithLabel(
kubectl,
manifestObjects.deploymentEntityList,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
}

View File

@ -1,29 +1,74 @@
import {getManifestObjects} from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
import {BlueGreenRejectResult} from '../../types/blueGreenTypes'
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {getManifestObjects} from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import * as TSutils from '../../utilities/trafficSplitUtils'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import {
rejectBlueGreenIngress,
rejectBlueGreenService,
rejectBlueGreenSMI
} from './reject'
} from './reject.js'
import * as bgHelper from './blueGreenHelper.js'
import * as routeHelper from './route.js'
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
const kubectl = new Kubectl('')
const TEST_TIMEOUT_SHORT = '60s'
const TEST_TIMEOUT_LONG = '120s'
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
// Shared mock objects following DRY principle
const mockSuccessResult = {
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
}
const mockFailureResult = {
stdout: '',
stderr: 'error: deployment failed',
exitCode: 1
}
const mockBgDeployment = {
deployResult: {
execResult: {stdout: '', stderr: '', exitCode: 0},
manifestFiles: []
},
objects: [
{
kind: 'Ingress',
metadata: {
name: 'nginx-ingress',
labels: new Map<string, string>()
},
spec: {}
}
]
}
const mockDeleteResult = [
{name: 'nginx-service-green', kind: 'Service'},
{name: 'nginx-deployment-green', kind: 'Deployment'}
]
describe('reject tests', () => {
let testObjects
let testObjects: any
let kubectlApplySpy: MockInstance
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
testObjects = getManifestObjects(ingressFilepath)
kubectlApplySpy = vi.spyOn(kubectl, 'apply')
})
test('reject blue/green ingress', async () => {
// Mock kubectl.apply to return successful result
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const value = await rejectBlueGreenIngress(kubectl, testObjects)
const bgDeployment = value.routeResult
@ -43,24 +88,183 @@ describe('reject tests', () => {
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-ingress')
})
test('reject blue/green service', async () => {
const value = await rejectBlueGreenService(kubectl, testObjects)
test('reject blue/green ingress with timeout', async () => {
// Mock routeBlueGreenIngressUnchanged and deleteGreenObjects
vi.spyOn(routeHelper, 'routeBlueGreenIngressUnchanged').mockResolvedValue(
mockBgDeployment
)
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
const value = await rejectBlueGreenIngress(
kubectl,
testObjects,
TEST_TIMEOUT_LONG
)
const bgDeployment = value.routeResult
const deleteResult = value.deleteResult
expect(deleteResult).toHaveLength(2)
for (const obj of deleteResult) {
if (obj.kind === 'Service') {
expect(obj.name).toBe('nginx-service-green')
}
if (obj.kind === 'Deployment') {
expect(obj.name).toBe('nginx-deployment-green')
}
}
expect(bgDeployment.objects).toHaveLength(1)
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-ingress')
// Verify deleteGreenObjects is called with timeout
expect(bgHelper.deleteGreenObjects).toHaveBeenCalledWith(
kubectl,
[].concat(
testObjects.deploymentEntityList,
testObjects.serviceEntityList
),
TEST_TIMEOUT_LONG
)
expect(routeHelper.routeBlueGreenIngressUnchanged).toHaveBeenCalledWith(
kubectl,
testObjects.serviceNameMap,
testObjects.ingressEntityList,
TEST_TIMEOUT_LONG
)
})
test('reject blue/green service', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
const value = await rejectBlueGreenService(
kubectl,
testObjects,
TEST_TIMEOUT_SHORT
)
const deleteResult = value.deleteResult
expect(deleteResult).toHaveLength(2)
expect(deleteResult).toContainEqual({
name: 'nginx-service-green',
kind: 'Service'
})
expect(deleteResult).toContainEqual({
name: 'nginx-deployment-green',
kind: 'Deployment'
})
})
test('reject blue/green service with timeout', async () => {
// Mock routeBlueGreenService and deleteGreenObjects
vi.spyOn(routeHelper, 'routeBlueGreenService').mockResolvedValue({
deployResult: {
execResult: {stdout: '', stderr: '', exitCode: 0},
manifestFiles: []
},
objects: [
{
kind: 'Service',
metadata: {
name: 'nginx-service',
labels: new Map<string, string>()
},
spec: {}
}
]
})
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue([
{name: 'nginx-deployment-green', kind: 'Deployment'}
])
const value = await rejectBlueGreenService(
kubectl,
testObjects,
TEST_TIMEOUT_LONG
)
const bgDeployment = value.routeResult
const deleteResult = value.deleteResult
// Verify deleteGreenObjects is called with timeout
expect(bgHelper.deleteGreenObjects).toHaveBeenCalledWith(
kubectl,
testObjects.deploymentEntityList,
TEST_TIMEOUT_LONG
)
// Assertions for routeResult and deleteResult
expect(deleteResult).toHaveLength(1)
expect(deleteResult[0].name).toBe('nginx-deployment-green')
expect(bgDeployment.objects).toHaveLength(1)
expect(bgDeployment.objects[0].metadata.name).toBe('nginx-service')
})
test('reject blue/green SMI', async () => {
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
// Mock kubectl.apply to return successful result
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
const rejectResult = await rejectBlueGreenSMI(kubectl, testObjects)
expect(rejectResult.deleteResult).toHaveLength(2)
})
// Consolidated error tests
test.each([
{
name: 'should throw error when kubectl apply fails during blue/green ingress rejection',
fn: () => rejectBlueGreenIngress(kubectl, testObjects),
setup: () => {
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green service rejection',
fn: () => rejectBlueGreenService(kubectl, testObjects),
setup: () => {
vi.spyOn(bgHelper, 'deleteGreenObjects').mockResolvedValue(
mockDeleteResult
)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green SMI rejection',
fn: () => rejectBlueGreenSMI(kubectl, testObjects),
setup: () => {
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
}
}
])('$name', async ({fn, setup}) => {
kubectlApplySpy.mockClear()
kubectlApplySpy.mockResolvedValue(mockFailureResult)
setup()
await expect(fn()).rejects.toThrow()
const timeoutArg = kubectlApplySpy.mock.calls[0][3]
expect(typeof timeoutArg === 'string' || timeoutArg === undefined).toBe(
true
)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Boolean),
expect.any(Boolean),
timeoutArg
)
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
})

View File

@ -1,25 +1,27 @@
import {K8sDeleteObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import {K8sDeleteObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import {
BlueGreenDeployment,
BlueGreenManifests,
BlueGreenRejectResult
} from '../../types/blueGreenTypes'
import {deleteGreenObjects, NONE_LABEL_VALUE} from './blueGreenHelper'
import {routeBlueGreenSMI} from './route'
import {cleanupSMI} from './smiBlueGreenHelper'
import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route'
} from '../../types/blueGreenTypes.js'
import {deleteGreenObjects, NONE_LABEL_VALUE} from './blueGreenHelper.js'
import {routeBlueGreenSMI} from './route.js'
import {cleanupSMI} from './smiBlueGreenHelper.js'
import {routeBlueGreenIngressUnchanged, routeBlueGreenService} from './route.js'
export async function rejectBlueGreenIngress(
kubectl: Kubectl,
manifestObjects: BlueGreenManifests
manifestObjects: BlueGreenManifests,
timeout?: string
): Promise<BlueGreenRejectResult> {
// get all kubernetes objects defined in manifest files
// route ingress to stables services
const routeResult = await routeBlueGreenIngressUnchanged(
kubectl,
manifestObjects.serviceNameMap,
manifestObjects.ingressEntityList
manifestObjects.ingressEntityList,
timeout
)
// delete green services and deployments
@ -28,7 +30,8 @@ export async function rejectBlueGreenIngress(
[].concat(
manifestObjects.deploymentEntityList,
manifestObjects.serviceEntityList
)
),
timeout
)
return {routeResult, deleteResult}
@ -36,19 +39,22 @@ export async function rejectBlueGreenIngress(
export async function rejectBlueGreenService(
kubectl: Kubectl,
manifestObjects: BlueGreenManifests
manifestObjects: BlueGreenManifests,
timeout?: string
): Promise<BlueGreenRejectResult> {
// route to stable objects
const routeResult = await routeBlueGreenService(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
// delete new deployments with green suffix
const deleteResult = await deleteGreenObjects(
kubectl,
manifestObjects.deploymentEntityList
manifestObjects.deploymentEntityList,
timeout
)
return {routeResult, deleteResult}
@ -56,25 +62,29 @@ export async function rejectBlueGreenService(
export async function rejectBlueGreenSMI(
kubectl: Kubectl,
manifestObjects: BlueGreenManifests
manifestObjects: BlueGreenManifests,
timeout?: string
): Promise<BlueGreenRejectResult> {
// route trafficsplit to stable deployments
const routeResult = await routeBlueGreenSMI(
kubectl,
NONE_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
// delete rejected new bluegreen deployments
const deletedObjects = await deleteGreenObjects(
kubectl,
manifestObjects.deploymentEntityList
manifestObjects.deploymentEntityList,
timeout
)
// delete trafficsplit and extra services
const cleanupResult = await cleanupSMI(
kubectl,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
return {routeResult, deleteResult: [].concat(deletedObjects, cleanupResult)}

View File

@ -1,41 +1,60 @@
import * as core from '@actions/core'
import {K8sIngress, TrafficSplitObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
import * as TSutils from '../../utilities/trafficSplitUtils'
import {RouteStrategy} from '../../types/routeStrategy'
import {getBufferTime} from '../../inputUtils'
import * as inputUtils from '../../inputUtils'
import {BlueGreenManifests} from '../../types/blueGreenTypes'
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
import {K8sIngress, TrafficSplitObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import {RouteStrategy} from '../../types/routeStrategy.js'
import {BlueGreenManifests} from '../../types/blueGreenTypes.js'
import {
BLUE_GREEN_VERSION_LABEL,
getManifestObjects,
GREEN_LABEL_VALUE
} from './blueGreenHelper'
} from './blueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
import * as smiHelper from './smiBlueGreenHelper.js'
import {
routeBlueGreenIngress,
routeBlueGreenService,
routeBlueGreenForDeploy
} from './route'
routeBlueGreenForDeploy,
routeBlueGreenSMI,
routeBlueGreenIngressUnchanged
} from './route.js'
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
const kc = new Kubectl('')
// Shared mock objects following DRY principle
const mockSuccessResult = {
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
}
const mockFailureResult = {
stdout: '',
stderr: 'error: deployment failed',
exitCode: 1
}
describe('route function tests', () => {
let testObjects: BlueGreenManifests
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
let kubectlApplySpy: MockInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
testObjects = getManifestObjects(ingressFilepath)
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
kubectlApplySpy = vi.spyOn(kc, 'apply')
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
})
test('correctly prepares blue/green ingresses for deployment', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const unroutedIngCopy: K8sIngress = JSON.parse(
JSON.stringify(testObjects.ingressEntityList[0])
)
@ -80,9 +99,9 @@ describe('route function tests', () => {
})
test('correctly identifies route pattern and acts accordingly', async () => {
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve('v1alpha3'))
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
const ingressResult = await routeBlueGreenForDeploy(
kc,
@ -116,4 +135,208 @@ describe('route function tests', () => {
(smiResult.objects as TrafficSplitObject[])[0].spec.backends
).toHaveLength(2)
})
// Consolidated error tests
test.each([
{
name: 'should throw error when kubectl apply fails during blue/green ingress routing',
fn: () =>
routeBlueGreenIngress(
kc,
testObjects.serviceNameMap,
testObjects.ingressEntityList
),
setup: () => {}
},
{
name: 'should throw error when kubectl apply fails during blue/green service routing',
fn: () =>
routeBlueGreenService(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList
),
setup: () => {}
},
{
name: 'should throw error when kubectl apply fails during blue/green SMI routing',
fn: () =>
routeBlueGreenSMI(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList
),
setup: () => {
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(
() => Promise.resolve('v1alpha3')
)
}
},
{
name: 'should throw error when kubectl apply fails during blue/green ingress unchanged routing',
fn: () =>
routeBlueGreenIngressUnchanged(
kc,
testObjects.serviceNameMap,
testObjects.ingressEntityList
),
setup: () => {}
}
])('$name', async ({fn, setup}) => {
kubectlApplySpy.mockClear()
kubectlApplySpy.mockResolvedValue(mockFailureResult)
setup()
await expect(fn()).rejects.toThrow()
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
})
// Timeout tests
describe('route timeout tests', () => {
let testObjects: BlueGreenManifests
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
testObjects = getManifestObjects(ingressFilepath)
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
})
afterEach(() => {
vi.restoreAllMocks()
})
test('routeBlueGreenService with timeout', async () => {
const timeout = '240s'
// Mock deployObjects to capture timeout parameter
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
})
const value = await routeBlueGreenService(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList,
timeout
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
expect(value.objects).toHaveLength(1)
deployObjectsSpy.mockRestore()
})
test('routeBlueGreenSMI with timeout', async () => {
const timeout = '300s'
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('v1alpha3')
)
// Mock deployObjects and createTrafficSplitObject to capture timeout parameter
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
})
const createTrafficSplitSpy = vi
.spyOn(smiHelper, 'createTrafficSplitObject')
.mockResolvedValue({
apiVersion: 'split.smi-spec.io/v1alpha3',
kind: 'TrafficSplit',
metadata: {
name: 'nginx-service-trafficsplit',
labels: new Map(),
annotations: new Map()
},
spec: {service: 'nginx-service', backends: []}
})
const value = await routeBlueGreenSMI(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList,
timeout
)
expect(createTrafficSplitSpy).toHaveBeenCalledWith(
kc,
'nginx-service',
GREEN_LABEL_VALUE,
timeout
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
expect(value.objects).toHaveLength(1)
deployObjectsSpy.mockRestore()
createTrafficSplitSpy.mockRestore()
})
test('routeBlueGreenIngressUnchanged with timeout', async () => {
const timeout = '180s'
// Mock deployObjects to capture timeout parameter
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
})
const value = await routeBlueGreenIngressUnchanged(
kc,
testObjects.serviceNameMap,
testObjects.ingressEntityList,
timeout
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
expect(value.objects).toHaveLength(1)
deployObjectsSpy.mockRestore()
})
test('route functions without timeout should pass undefined', async () => {
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
})
// Test routeBlueGreenService without timeout
await routeBlueGreenService(
kc,
GREEN_LABEL_VALUE,
testObjects.serviceEntityList
)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
deployObjectsSpy.mockRestore()
})
})

View File

@ -1,31 +1,32 @@
import {sleep} from '../../utilities/timeUtils'
import {RouteStrategy} from '../../types/routeStrategy'
import {Kubectl} from '../../types/kubectl'
import {sleep} from '../../utilities/timeUtils.js'
import {RouteStrategy} from '../../types/routeStrategy.js'
import {Kubectl} from '../../types/kubectl.js'
import {
BlueGreenDeployment,
BlueGreenManifests
} from '../../types/blueGreenTypes'
} from '../../types/blueGreenTypes.js'
import {
getManifestObjects,
GREEN_LABEL_VALUE,
deployObjects
} from './blueGreenHelper'
} from './blueGreenHelper.js'
import {
getUpdatedBlueGreenIngress,
isIngressRouted
} from './ingressBlueGreenHelper'
import {getUpdatedBlueGreenService} from './serviceBlueGreenHelper'
import {createTrafficSplitObject} from './smiBlueGreenHelper'
} from './ingressBlueGreenHelper.js'
import {getUpdatedBlueGreenService} from './serviceBlueGreenHelper.js'
import {createTrafficSplitObject} from './smiBlueGreenHelper.js'
import * as core from '@actions/core'
import {K8sObject, TrafficSplitObject} from '../../types/k8sObject'
import {getBufferTime} from '../../inputUtils'
import {K8sObject, TrafficSplitObject} from '../../types/k8sObject.js'
import {getBufferTime} from '../../inputUtils.js'
export async function routeBlueGreenForDeploy(
kubectl: Kubectl,
inputManifestFiles: string[],
routeStrategy: RouteStrategy
routeStrategy: RouteStrategy,
timeout?: string
): Promise<BlueGreenDeployment> {
// sleep for buffer time
const bufferTime: number = getBufferTime()
@ -47,19 +48,22 @@ export async function routeBlueGreenForDeploy(
return await routeBlueGreenIngress(
kubectl,
manifestObjects.serviceNameMap,
manifestObjects.ingressEntityList
manifestObjects.ingressEntityList,
timeout
)
} else if (routeStrategy == RouteStrategy.SMI) {
return await routeBlueGreenSMI(
kubectl,
GREEN_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
} else {
return await routeBlueGreenService(
kubectl,
GREEN_LABEL_VALUE,
manifestObjects.serviceEntityList
manifestObjects.serviceEntityList,
timeout
)
}
}
@ -67,7 +71,8 @@ export async function routeBlueGreenForDeploy(
export async function routeBlueGreenIngress(
kubectl: Kubectl,
serviceNameMap: Map<string, string>,
ingressEntityList: any[]
ingressEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
// const newObjectsList = []
const newObjectsList: K8sObject[] = ingressEntityList.map((obj) => {
@ -84,7 +89,7 @@ export async function routeBlueGreenIngress(
}
})
const deployResult = await deployObjects(kubectl, newObjectsList)
const deployResult = await deployObjects(kubectl, newObjectsList, timeout)
return {deployResult, objects: newObjectsList}
}
@ -92,26 +97,28 @@ export async function routeBlueGreenIngress(
export async function routeBlueGreenIngressUnchanged(
kubectl: Kubectl,
serviceNameMap: Map<string, string>,
ingressEntityList: any[]
ingressEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
const objects = ingressEntityList.filter((ingress) =>
isIngressRouted(ingress, serviceNameMap)
)
const deployResult = await deployObjects(kubectl, objects)
const deployResult = await deployObjects(kubectl, objects, timeout)
return {deployResult, objects}
}
export async function routeBlueGreenService(
kubectl: Kubectl,
nextLabel: string,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
const objects = serviceEntityList.map((serviceObject) =>
getUpdatedBlueGreenService(serviceObject, nextLabel)
)
const deployResult = await deployObjects(kubectl, objects)
const deployResult = await deployObjects(kubectl, objects, timeout)
return {deployResult, objects}
}
@ -119,7 +126,8 @@ export async function routeBlueGreenService(
export async function routeBlueGreenSMI(
kubectl: Kubectl,
nextLabel: string,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
// let tsObjects: TrafficSplitObject[] = []
@ -128,14 +136,15 @@ export async function routeBlueGreenSMI(
const tsObject: TrafficSplitObject = await createTrafficSplitObject(
kubectl,
serviceObject.metadata.name,
nextLabel
nextLabel,
timeout
)
return tsObject
})
)
const deployResult = await deployObjects(kubectl, tsObjects)
const deployResult = await deployObjects(kubectl, tsObjects, timeout)
return {deployResult, objects: tsObjects}
}

View File

@ -1,26 +1,26 @@
import {vi} from 'vitest'
import * as core from '@actions/core'
import {
BLUE_GREEN_VERSION_LABEL,
getManifestObjects,
GREEN_LABEL_VALUE
} from './blueGreenHelper'
import * as bgHelper from './blueGreenHelper'
import {Kubectl} from '../../types/kubectl'
} from './blueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
import {Kubectl} from '../../types/kubectl.js'
import {
getServiceSpecLabel,
getUpdatedBlueGreenService,
validateServicesState
} from './serviceBlueGreenHelper'
} from './serviceBlueGreenHelper.js'
let testObjects
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
const kubectl = new Kubectl('')
describe('blue/green service helper tests', () => {
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
vi.mocked(Kubectl).mockClear()
testObjects = getManifestObjects(ingressFilepath)
})
@ -42,7 +42,7 @@ describe('blue/green service helper tests', () => {
mockLabels[BLUE_GREEN_VERSION_LABEL] = bgHelper.GREEN_LABEL_VALUE
const mockSelectors = new Map<string, string>()
mockSelectors[BLUE_GREEN_VERSION_LABEL] = GREEN_LABEL_VALUE
jest.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve({
kind: 'Service',
spec: {selector: mockSelectors},

View File

@ -1,12 +1,12 @@
import * as core from '@actions/core'
import {K8sServiceObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import {K8sServiceObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import {
addBlueGreenLabelsAndAnnotations,
BLUE_GREEN_VERSION_LABEL,
fetchResource,
GREEN_LABEL_VALUE
} from './blueGreenHelper'
} from './blueGreenHelper.js'
// add green labels to configure existing service
export function getUpdatedBlueGreenService(

View File

@ -1,16 +1,16 @@
import * as core from '@actions/core'
import {TrafficSplitObject} from '../../types/k8sObject'
import {Kubectl} from '../../types/kubectl'
import * as fileHelper from '../../utilities/fileUtils'
import * as TSutils from '../../utilities/trafficSplitUtils'
import {vi} from 'vitest'
import {TrafficSplitObject} from '../../types/k8sObject.js'
import {Kubectl} from '../../types/kubectl.js'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as TSutils from '../../utilities/trafficSplitUtils.js'
import {BlueGreenManifests} from '../../types/blueGreenTypes'
import {BlueGreenManifests} from '../../types/blueGreenTypes.js'
import {
BLUE_GREEN_VERSION_LABEL,
getManifestObjects,
GREEN_LABEL_VALUE,
NONE_LABEL_VALUE
} from './blueGreenHelper'
} from './blueGreenHelper.js'
import {
cleanupSMI,
@ -21,15 +21,28 @@ import {
MIN_VAL,
setupSMI,
TRAFFIC_SPLIT_OBJECT,
TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX,
validateTrafficSplitsState
} from './smiBlueGreenHelper'
import * as bgHelper from './blueGreenHelper'
} from './smiBlueGreenHelper.js'
import * as bgHelper from './blueGreenHelper.js'
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
const kc = new Kubectl('')
const ingressFilepath = ['test/unit/manifests/test-ingress-new.yml']
// Shared mock objects following DRY principle
const mockSuccessResult = {
stdout: 'service/nginx-service-stable created',
stderr: '',
exitCode: 0
}
const mockFailureResult = {
stdout: '',
stderr: 'error: service creation failed',
exitCode: 1
}
const mockTsObject: TrafficSplitObject = {
apiVersion: 'v1alpha3',
kind: TRAFFIC_SPLIT_OBJECT,
@ -56,20 +69,21 @@ const mockTsObject: TrafficSplitObject = {
describe('SMI Helper tests', () => {
let testObjects: BlueGreenManifests
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
vi.mocked(Kubectl).mockClear()
jest
.spyOn(TSutils, 'getTrafficSplitAPIVersion')
.mockImplementation(() => Promise.resolve(''))
vi.spyOn(TSutils, 'getTrafficSplitAPIVersion').mockImplementation(() =>
Promise.resolve('')
)
testObjects = getManifestObjects(ingressFilepath)
jest
.spyOn(fileHelper, 'writeObjectsToFile')
.mockImplementationOnce(() => [''])
vi.spyOn(fileHelper, 'writeObjectsToFile').mockImplementationOnce(() => [
''
])
})
test('setupSMI tests', async () => {
vi.spyOn(kc, 'apply').mockResolvedValue(mockSuccessResult)
const smiResults = await setupSMI(kc, testObjects.serviceEntityList)
let found = 0
@ -160,9 +174,9 @@ describe('SMI Helper tests', () => {
})
test('validateTrafficSplitsState', async () => {
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockTsObject))
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockTsObject)
)
let valResult = await validateTrafficSplitsState(
kc,
@ -173,9 +187,9 @@ describe('SMI Helper tests', () => {
const mockTsCopy = JSON.parse(JSON.stringify(mockTsObject))
mockTsCopy.spec.backends[0].weight = MAX_VAL
jest
.spyOn(bgHelper, 'fetchResource')
.mockImplementation(() => Promise.resolve(mockTsCopy))
vi.spyOn(bgHelper, 'fetchResource').mockImplementation(() =>
Promise.resolve(mockTsCopy)
)
valResult = await validateTrafficSplitsState(
kc,
@ -183,7 +197,7 @@ describe('SMI Helper tests', () => {
)
expect(valResult).toBe(false)
jest.spyOn(bgHelper, 'fetchResource').mockImplementation()
vi.spyOn(bgHelper, 'fetchResource').mockResolvedValue(null)
valResult = await validateTrafficSplitsState(
kc,
testObjects.serviceEntityList
@ -197,4 +211,208 @@ describe('SMI Helper tests', () => {
expect(deleteObjects[0].name).toBe('nginx-service-green')
expect(deleteObjects[0].kind).toBe('Service')
})
// Consolidated error tests using test.each for DRY principle
test.each([
{
name: 'should throw error when kubectl apply fails during SMI setup',
fn: () => setupSMI(kc, testObjects.serviceEntityList),
setup: () => {
vi.spyOn(kc, 'apply').mockResolvedValue(mockFailureResult)
}
}
])('$name', async ({fn, setup}) => {
setup()
await expect(fn()).rejects.toThrow()
})
// Timeout-specific tests
test('setupSMI with timeout test', async () => {
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
})
const timeout = '300s'
const smiResults = await setupSMI(
kc,
testObjects.serviceEntityList,
timeout
)
// Verify deployObjects was called with timeout
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
expect(smiResults.objects).toBeDefined()
expect(smiResults.deployResult).toBeDefined()
deployObjectsSpy.mockRestore()
})
test('createTrafficSplitObject with timeout test', async () => {
const deleteObjectsSpy = vi
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const timeout = '180s'
const tsObject = await createTrafficSplitObject(
kc,
testObjects.serviceEntityList[0].metadata.name,
NONE_LABEL_VALUE,
timeout
)
// Verify deleteObjects was called with timeout
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.arrayContaining([
expect.objectContaining({
name: 'nginx-service-trafficsplit',
kind: TRAFFIC_SPLIT_OBJECT
})
]),
timeout
)
expect(tsObject.metadata.name).toBe('nginx-service-trafficsplit')
expect(tsObject.spec.backends).toHaveLength(2)
deleteObjectsSpy.mockRestore()
})
test('createTrafficSplitObject with GREEN_LABEL_VALUE and timeout test', async () => {
const deleteObjectsSpy = vi
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const timeout = '240s'
const tsObject = await createTrafficSplitObject(
kc,
testObjects.serviceEntityList[0].metadata.name,
GREEN_LABEL_VALUE,
timeout
)
// Verify deleteObjects was called with timeout
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
timeout
)
// Verify weights are correct for green deployment
for (const be of tsObject.spec.backends) {
if (be.service === 'nginx-service-stable') {
expect(be.weight).toBe(MIN_VAL)
}
if (be.service === 'nginx-service-green') {
expect(be.weight).toBe(MAX_VAL)
}
}
deleteObjectsSpy.mockRestore()
})
test('cleanupSMI with timeout test', async () => {
const deleteObjectsSpy = vi
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const timeout = '120s'
const deleteObjects = await cleanupSMI(
kc,
testObjects.serviceEntityList,
timeout
)
// Verify deleteObjects was called with timeout
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.arrayContaining([
expect.objectContaining({
name: 'nginx-service-green',
kind: 'Service'
})
]),
timeout
)
expect(deleteObjects).toHaveLength(1)
expect(deleteObjects[0].name).toBe('nginx-service-green')
expect(deleteObjects[0].kind).toBe('Service')
deleteObjectsSpy.mockRestore()
})
test('setupSMI without timeout test', async () => {
const deployObjectsSpy = vi
.spyOn(bgHelper, 'deployObjects')
.mockResolvedValue({
execResult: mockSuccessResult,
manifestFiles: []
})
const smiResults = await setupSMI(kc, testObjects.serviceEntityList)
// Verify deployObjects was called without timeout (undefined)
expect(deployObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
expect(smiResults.objects).toBeDefined()
expect(smiResults.deployResult).toBeDefined()
deployObjectsSpy.mockRestore()
})
test('createTrafficSplitObject without timeout test', async () => {
const deleteObjectsSpy = vi
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const tsObject = await createTrafficSplitObject(
kc,
testObjects.serviceEntityList[0].metadata.name,
NONE_LABEL_VALUE
)
// Verify deleteObjects was called without timeout (undefined)
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
expect(tsObject.metadata.name).toBe('nginx-service-trafficsplit')
deleteObjectsSpy.mockRestore()
})
test('cleanupSMI without timeout test', async () => {
const deleteObjectsSpy = vi
.spyOn(bgHelper, 'deleteObjects')
.mockResolvedValue()
const deleteObjects = await cleanupSMI(kc, testObjects.serviceEntityList)
// Verify deleteObjects was called without timeout (undefined)
expect(deleteObjectsSpy).toHaveBeenCalledWith(
kc,
expect.any(Array),
undefined
)
expect(deleteObjects).toHaveLength(1)
deleteObjectsSpy.mockRestore()
})
})

View File

@ -1,6 +1,6 @@
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl'
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
import {Kubectl} from '../../types/kubectl.js'
import * as kubectlUtils from '../../utilities/trafficSplitUtils.js'
import {
deleteObjects,
deployObjects,
@ -11,15 +11,15 @@ import {
GREEN_SUFFIX,
NONE_LABEL_VALUE,
STABLE_SUFFIX
} from './blueGreenHelper'
import {BlueGreenDeployment} from '../../types/blueGreenTypes'
} from './blueGreenHelper.js'
import {BlueGreenDeployment} from '../../types/blueGreenTypes.js'
import {
K8sDeleteObject,
K8sObject,
TrafficSplitObject
} from '../../types/k8sObject'
import {DeployResult} from '../../types/deployResult'
import {inputAnnotations} from '../../inputUtils'
} from '../../types/k8sObject.js'
import {DeployResult} from '../../types/deployResult.js'
import {inputAnnotations} from '../../inputUtils.js'
export const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-trafficsplit'
export const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
@ -28,7 +28,8 @@ export const MAX_VAL = 100
export async function setupSMI(
kubectl: Kubectl,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<BlueGreenDeployment> {
const newObjectsList = []
const trafficObjectList = []
@ -49,7 +50,8 @@ export async function setupSMI(
const tsObject = await createTrafficSplitObject(
kubectl,
svc.metadata.name,
NONE_LABEL_VALUE
NONE_LABEL_VALUE,
timeout
)
tsObjects.push(tsObject as TrafficSplitObject)
}
@ -59,7 +61,8 @@ export async function setupSMI(
// create services
const smiDeploymentResult: DeployResult = await deployObjects(
kubectl,
objectsToDeploy
objectsToDeploy,
timeout
)
return {
@ -73,7 +76,8 @@ let trafficSplitAPIVersion = ''
export async function createTrafficSplitObject(
kubectl: Kubectl,
name: string,
nextLabel: string
nextLabel: string,
timeout?: string
): Promise<TrafficSplitObject> {
// cache traffic split api version
if (!trafficSplitAPIVersion)
@ -112,6 +116,13 @@ export async function createTrafficSplitObject(
}
}
const deleteList: K8sDeleteObject[] = [
{
name: trafficSplitObject.metadata.name,
kind: trafficSplitObject.kind
}
]
await deleteObjects(kubectl, deleteList, timeout)
return trafficSplitObject
}
@ -173,7 +184,8 @@ export async function validateTrafficSplitsState(
export async function cleanupSMI(
kubectl: Kubectl,
serviceEntityList: any[]
serviceEntityList: any[],
timeout?: string
): Promise<K8sDeleteObject[]> {
const deleteList: K8sDeleteObject[] = []
@ -189,7 +201,7 @@ export async function cleanupSMI(
})
// delete all objects
await deleteObjects(kubectl, deleteList)
await deleteObjects(kubectl, deleteList, timeout)
return deleteList
}

View File

@ -1,4 +1,4 @@
import {Kubectl} from '../../types/kubectl'
import {Kubectl} from '../../types/kubectl.js'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as core from '@actions/core'
@ -7,15 +7,15 @@ import {
isDeploymentEntity,
isServiceEntity,
KubernetesWorkload
} from '../../types/kubernetesTypes'
import * as utils from '../../utilities/manifestUpdateUtils'
} from '../../types/kubernetesTypes.js'
import * as utils from '../../utilities/manifestUpdateUtils.js'
import {
updateObjectAnnotations,
updateObjectLabels,
updateSelectorLabels
} from '../../utilities/manifestUpdateUtils'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils'
import {checkForErrors} from '../../utilities/kubectlUtils'
} from '../../utilities/manifestUpdateUtils.js'
import {updateSpecLabels} from '../../utilities/manifestSpecLabelUtils.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
export const CANARY_VERSION_LABEL = 'workflow/version'
const BASELINE_SUFFIX = '-baseline'
@ -28,7 +28,8 @@ export const STABLE_LABEL_VALUE = 'stable'
export async function deleteCanaryDeployment(
kubectl: Kubectl,
manifestFilePaths: string[],
includeServices: boolean
includeServices: boolean,
timeout?: string
): Promise<string[]> {
if (manifestFilePaths == null || manifestFilePaths.length == 0) {
throw new Error('Manifest files for deleting canary deployment not found')
@ -37,7 +38,8 @@ export async function deleteCanaryDeployment(
const deletedFiles = await cleanUpCanary(
kubectl,
manifestFilePaths,
includeServices
includeServices,
timeout
)
return deletedFiles
}
@ -193,7 +195,8 @@ function addCanaryLabelsAndAnnotations(inputObject: any, type: string) {
async function cleanUpCanary(
kubectl: Kubectl,
files: string[],
includeServices: boolean
includeServices: boolean,
timeout?: string
): Promise<string[]> {
const deleteObject = async function (
kind: string,
@ -201,7 +204,7 @@ async function cleanUpCanary(
namespace: string | undefined
) {
try {
const result = await kubectl.delete([kind, name], namespace)
const result = await kubectl.delete([kind, name], namespace, timeout)
checkForErrors([result])
} catch (ex) {
// Ignore failures of delete if it doesn't exist

View File

@ -0,0 +1,288 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
vi.mock('@actions/core')
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl.js'
import {
deployPodCanary,
calculateReplicaCountForCanary
} from './podCanaryHelper.js'
vi.mock('../../types/kubectl')
const kc = new Kubectl('')
// Shared mock objects following DRY principle
const mockSuccessResult = {
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
}
const mockFailureResult = {
stdout: '',
stderr: 'error: deployment failed',
exitCode: 1
}
// Use existing test manifest files
const testManifestFiles = ['test/unit/manifests/basic-test.yml']
// Test constants
const VALID_PERCENTAGE = 50
const INVALID_LOW_PERCENTAGE = -10
const INVALID_HIGH_PERCENTAGE = 150
const MIN_PERCENTAGE = 0
const MAX_PERCENTAGE = 100
const TIMEOUT_300S = '300s'
describe('Pod Canary Helper tests', () => {
let mockFilePaths: string[]
let kubectlApplySpy: MockInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
mockFilePaths = testManifestFiles
kubectlApplySpy = vi.spyOn(kc, 'apply')
// Mock core.getInput with default values
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
case 'force':
return 'false'
case 'server-side':
return 'false'
default:
return ''
}
})
})
afterEach(() => {
vi.restoreAllMocks()
kubectlApplySpy.mockClear()
})
describe('deployPodCanary', () => {
test('should deploy canary successfully when kubectl apply succeeds', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deployPodCanary(mockFilePaths, kc, false)
expect(result.execResult).toEqual(mockSuccessResult)
expect(result.manifestFiles).toBeDefined()
expect(kubectlApplySpy).toHaveBeenCalled()
})
test('should throw error when kubectl apply fails', async () => {
kubectlApplySpy.mockResolvedValue(mockFailureResult)
await expect(
deployPodCanary(mockFilePaths, kc, false)
).rejects.toThrow()
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
test('should deploy stable only when onlyDeployStable is true', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deployPodCanary(mockFilePaths, kc, true)
expect(result.execResult).toEqual(mockSuccessResult)
expect(kubectlApplySpy).toHaveBeenCalled()
})
test('should handle timeout parameter', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deployPodCanary(
mockFilePaths,
kc,
false,
TIMEOUT_300S
)
expect(result.execResult).toEqual(mockSuccessResult)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
false,
false,
TIMEOUT_300S
)
})
test('should throw error for invalid low percentage', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return INVALID_LOW_PERCENTAGE.toString()
return ''
})
await expect(
deployPodCanary(mockFilePaths, kc, false)
).rejects.toThrow(
`Percentage must be between ${MIN_PERCENTAGE} and ${MAX_PERCENTAGE}`
)
})
test('should throw error for invalid high percentage', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return INVALID_HIGH_PERCENTAGE.toString()
return ''
})
await expect(
deployPodCanary(mockFilePaths, kc, false)
).rejects.toThrow(
`Percentage must be between ${MIN_PERCENTAGE} and ${MAX_PERCENTAGE}`
)
})
test('should handle valid edge case percentages', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
// Test minimum valid percentage
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return MIN_PERCENTAGE.toString()
return ''
})
const resultMin = await deployPodCanary(mockFilePaths, kc, false)
expect(resultMin.execResult).toEqual(mockSuccessResult)
// Test maximum valid percentage
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return MAX_PERCENTAGE.toString()
return ''
})
const resultMax = await deployPodCanary(mockFilePaths, kc, false)
expect(resultMax.execResult).toEqual(mockSuccessResult)
})
test('should handle force deployment option', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
case 'force':
return 'true'
default:
return ''
}
})
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deployPodCanary(mockFilePaths, kc, false)
expect(result.execResult).toEqual(mockSuccessResult)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
true, // force should be true
false,
undefined
)
})
test('should handle server-side apply option', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
case 'server-side':
return 'true'
default:
return ''
}
})
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deployPodCanary(mockFilePaths, kc, false)
expect(result.execResult).toEqual(mockSuccessResult)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
false,
true, // server-side should be true
undefined
)
})
})
describe('calculateReplicaCountForCanary', () => {
test('should calculate correct replica count for given percentage', () => {
const mockObject = {
kind: 'Deployment',
metadata: {
name: 'test-deployment'
},
spec: {
replicas: 10
}
}
// 50% of 10 replicas = 5
const result50 = calculateReplicaCountForCanary(mockObject, 50)
expect(result50).toBe(5)
// 25% of 10 replicas = 2.5, rounded to 3
const result25 = calculateReplicaCountForCanary(mockObject, 25)
expect(result25).toBe(3)
// 10% of 10 replicas = 1
const result10 = calculateReplicaCountForCanary(mockObject, 10)
expect(result10).toBe(1)
})
test('should return minimum 1 replica even for very low percentages', () => {
const mockObject = {
kind: 'Deployment',
metadata: {
name: 'test-deployment'
},
spec: {
replicas: 2
}
}
// 1% of 2 replicas = 0.02, but should return minimum 1
const result = calculateReplicaCountForCanary(mockObject, 1)
expect(result).toBe(1)
})
test('should handle 100% percentage correctly', () => {
const mockObject = {
kind: 'Deployment',
metadata: {
name: 'test-deployment'
},
spec: {
replicas: 5
}
}
const result = calculateReplicaCountForCanary(mockObject, 100)
expect(result).toBe(5)
})
test('should handle 0% percentage correctly', () => {
const mockObject = {
kind: 'Deployment',
metadata: {
name: 'test-deployment'
},
spec: {
replicas: 10
}
}
// 0% should still return minimum 1 replica
const result = calculateReplicaCountForCanary(mockObject, 0)
expect(result).toBe(1)
})
})
})

View File

@ -1,19 +1,21 @@
import {Kubectl} from '../../types/kubectl'
import {Kubectl} from '../../types/kubectl.js'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as fileHelper from '../../utilities/fileUtils'
import * as canaryDeploymentHelper from './canaryHelper'
import {isDeploymentEntity} from '../../types/kubernetesTypes'
import {getReplicaCount} from '../../utilities/manifestUpdateUtils'
import {DeployResult} from '../../types/deployResult'
import {K8sObject} from '../../types/k8sObject'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as canaryDeploymentHelper from './canaryHelper.js'
import {isDeploymentEntity} from '../../types/kubernetesTypes.js'
import {getReplicaCount} from '../../utilities/manifestUpdateUtils.js'
import {DeployResult} from '../../types/deployResult.js'
import {K8sObject} from '../../types/k8sObject.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
export async function deployPodCanary(
filePaths: string[],
kubectl: Kubectl,
onlyDeployStable: boolean = false
onlyDeployStable: boolean = false,
timeout?: string
): Promise<DeployResult> {
const newObjectsList = []
const percentage = parseInt(core.getInput('percentage', {required: true}))
@ -95,8 +97,15 @@ export async function deployPodCanary(
core.debug('New objects list: ' + JSON.stringify(newObjectsList))
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const serverSideApply = core.getInput('server-side').toLowerCase() === 'true'
const execResult = await kubectl.apply(manifestFiles, forceDeployment)
const execResult = await kubectl.apply(
manifestFiles,
forceDeployment,
serverSideApply,
timeout
)
checkForErrors([execResult])
return {execResult, manifestFiles}
}

View File

@ -0,0 +1,223 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
vi.mock('@actions/core', async (importOriginal) => {
const actual: any = await importOriginal()
return {
...actual,
getInput: vi.fn().mockReturnValue(''),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn(),
group: vi
.fn()
.mockImplementation(
async (_name: string, fn: () => Promise<void>) => await fn()
)
}
})
import * as core from '@actions/core'
import * as fs from 'fs'
import {Kubectl} from '../../types/kubectl.js'
import {
deploySMICanary,
redirectTrafficToCanaryDeployment,
redirectTrafficToStableDeployment
} from './smiCanaryHelper.js'
vi.mock('../../types/kubectl')
const kc = new Kubectl('')
// Shared mock objects following DRY principle
const mockSuccessResult = {
stdout: 'deployment.apps/nginx-deployment created',
stderr: '',
exitCode: 0
}
const mockFailureResult = {
stdout: '',
stderr: 'error: deployment failed',
exitCode: 1
}
const mockExecuteCommandResult = {
stdout: 'split.smi-spec.io/v1alpha1\nsplit.smi-spec.io/v1alpha2',
stderr: '',
exitCode: 0
}
// Use existing test manifest files
const testManifestFiles = ['test/unit/manifests/basic-test.yml']
// Test constants
const VALID_REPLICA_COUNT = 5
const TIMEOUT_300S = '300s'
const TIMEOUT_240S = '240s'
describe('SMI Canary Helper tests', () => {
let mockFilePaths: string[]
let kubectlApplySpy: MockInstance
let kubectlExecuteCommandSpy: MockInstance
beforeEach(() => {
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
mockFilePaths = testManifestFiles
kubectlApplySpy = vi.spyOn(kc, 'apply')
kubectlExecuteCommandSpy = vi
.spyOn(kc, 'executeCommand')
.mockResolvedValue(mockExecuteCommandResult)
// Mock core.getInput with default values
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return '50'
case 'baseline-and-canary-replicas':
return ''
case 'force':
return 'false'
case 'server-side':
return 'false'
default:
return ''
}
})
})
afterEach(() => {
vi.restoreAllMocks()
kubectlApplySpy.mockClear()
})
describe('deploySMICanary', () => {
test('should deploy canary successfully when kubectl apply succeeds', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deploySMICanary(mockFilePaths, kc, false)
expect(result.execResult).toEqual(mockSuccessResult)
expect(result.manifestFiles).toBeDefined()
expect(kubectlApplySpy).toHaveBeenCalled()
})
test('should throw error when kubectl apply fails', async () => {
kubectlApplySpy.mockResolvedValue(mockFailureResult)
await expect(
deploySMICanary(mockFilePaths, kc, false)
).rejects.toThrow()
expect(kubectlApplySpy).toHaveBeenCalledTimes(2)
})
test('should deploy stable only when onlyDeployStable is true', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deploySMICanary(mockFilePaths, kc, true)
expect(result.execResult).toEqual(mockSuccessResult)
expect(kubectlApplySpy).toHaveBeenCalled()
})
test('should handle custom replica count from input', async () => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'baseline-and-canary-replicas':
return VALID_REPLICA_COUNT.toString()
case 'percentage':
return '50'
default:
return ''
}
})
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await deploySMICanary(mockFilePaths, kc, false)
expect(result.execResult).toEqual(mockSuccessResult)
})
})
describe('redirectTrafficToCanaryDeployment', () => {
test('should redirect traffic to canary deployment successfully', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
await redirectTrafficToCanaryDeployment(kc, mockFilePaths)
expect(kubectlApplySpy).toHaveBeenCalled()
})
test('should handle timeout parameter', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
await redirectTrafficToCanaryDeployment(
kc,
mockFilePaths,
TIMEOUT_300S
)
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
false,
false,
TIMEOUT_300S
)
})
test('should throw error when kubectl apply fails', async () => {
kubectlApplySpy.mockResolvedValue(mockFailureResult)
await expect(
redirectTrafficToCanaryDeployment(kc, mockFilePaths)
).rejects.toThrow()
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
})
describe('redirectTrafficToStableDeployment', () => {
test('should redirect traffic to stable deployment successfully', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await redirectTrafficToStableDeployment(
kc,
mockFilePaths
)
expect(result).toBeDefined()
expect(kubectlApplySpy).toHaveBeenCalled()
})
test('should handle timeout parameter', async () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
const result = await redirectTrafficToStableDeployment(
kc,
mockFilePaths,
TIMEOUT_240S
)
expect(result).toBeDefined()
expect(kubectlApplySpy).toHaveBeenCalledWith(
expect.any(Array),
false,
false,
TIMEOUT_240S
)
})
test('should throw error when kubectl apply fails during traffic redirect to stable', async () => {
kubectlApplySpy.mockResolvedValue(mockFailureResult)
await expect(
redirectTrafficToStableDeployment(kc, mockFilePaths)
).rejects.toThrow()
expect(kubectlApplySpy).toHaveBeenCalledTimes(1)
})
})
})

View File

@ -1,17 +1,20 @@
import {Kubectl} from '../../types/kubectl'
import {Kubectl} from '../../types/kubectl.js'
import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as fileHelper from '../../utilities/fileUtils'
import * as kubectlUtils from '../../utilities/trafficSplitUtils'
import * as canaryDeploymentHelper from './canaryHelper'
import * as podCanaryHelper from './podCanaryHelper'
import {isDeploymentEntity, isServiceEntity} from '../../types/kubernetesTypes'
import {checkForErrors} from '../../utilities/kubectlUtils'
import {inputAnnotations} from '../../inputUtils'
import {DeployResult} from '../../types/deployResult'
import {K8sObject} from '../../types/k8sObject'
import * as fileHelper from '../../utilities/fileUtils.js'
import * as kubectlUtils from '../../utilities/trafficSplitUtils.js'
import * as canaryDeploymentHelper from './canaryHelper.js'
import * as podCanaryHelper from './podCanaryHelper.js'
import {
isDeploymentEntity,
isServiceEntity
} from '../../types/kubernetesTypes.js'
import {checkForErrors} from '../../utilities/kubectlUtils.js'
import {inputAnnotations} from '../../inputUtils.js'
import {DeployResult} from '../../types/deployResult.js'
import {K8sObject} from '../../types/k8sObject.js'
const TRAFFIC_SPLIT_OBJECT_NAME_SUFFIX = '-workflow-rollout'
const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
@ -19,7 +22,8 @@ const TRAFFIC_SPLIT_OBJECT = 'TrafficSplit'
export async function deploySMICanary(
filePaths: string[],
kubectl: Kubectl,
onlyDeployStable: boolean = false
onlyDeployStable: boolean = false,
timeout?: string
): Promise<DeployResult> {
const canaryReplicasInput = core.getInput('baseline-and-canary-replicas')
let canaryReplicaCount
@ -106,15 +110,28 @@ export async function deploySMICanary(
)
const newFilePaths = fileHelper.writeObjectsToFile(newObjectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const result = await kubectl.apply(newFilePaths, forceDeployment)
const svcDeploymentFiles = await createCanaryService(kubectl, filePaths)
const serverSideApply = core.getInput('server-side').toLowerCase() === 'true'
const result = await kubectl.apply(
newFilePaths,
forceDeployment,
serverSideApply,
timeout
)
const svcDeploymentFiles = await createCanaryService(
kubectl,
filePaths,
timeout
)
checkForErrors([result])
newFilePaths.push(...svcDeploymentFiles)
return {execResult: result, manifestFiles: newFilePaths}
}
async function createCanaryService(
kubectl: Kubectl,
filePaths: string[]
filePaths: string[],
timeout?: string
): Promise<string[]> {
const newObjectsList = []
const trafficObjectsList: string[] = []
@ -156,7 +173,8 @@ async function createCanaryService(
name,
0,
0,
1000
1000,
timeout
)
trafficObjectsList.push(trafficObject)
@ -210,31 +228,40 @@ async function createCanaryService(
const manifestFiles = fileHelper.writeObjectsToFile(newObjectsList)
manifestFiles.push(...trafficObjectsList)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const serverSideApply = core.getInput('server-side').toLowerCase() === 'true'
const result = await kubectl.apply(manifestFiles, forceDeployment)
const result = await kubectl.apply(
manifestFiles,
forceDeployment,
serverSideApply,
timeout
)
checkForErrors([result])
return manifestFiles
}
export async function redirectTrafficToCanaryDeployment(
kubectl: Kubectl,
manifestFilePaths: string[]
manifestFilePaths: string[],
timeout?: string
) {
await adjustTraffic(kubectl, manifestFilePaths, 0, 1000)
await adjustTraffic(kubectl, manifestFilePaths, 0, 1000, timeout)
}
export async function redirectTrafficToStableDeployment(
kubectl: Kubectl,
manifestFilePaths: string[]
manifestFilePaths: string[],
timeout?: string
): Promise<string[]> {
return await adjustTraffic(kubectl, manifestFilePaths, 1000, 0)
return await adjustTraffic(kubectl, manifestFilePaths, 1000, 0, timeout)
}
async function adjustTraffic(
kubectl: Kubectl,
manifestFilePaths: string[],
stableWeight: number,
canaryWeight: number
canaryWeight: number,
timeout?: string
) {
if (!manifestFilePaths || manifestFilePaths?.length == 0) {
return
@ -259,7 +286,8 @@ async function adjustTraffic(
name,
stableWeight,
0,
canaryWeight
canaryWeight,
timeout
)
)
}
@ -275,7 +303,13 @@ async function adjustTraffic(
}
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const result = await kubectl.apply(trafficSplitManifests, forceDeployment)
const serverSideApply = core.getInput('server-side').toLowerCase() === 'true'
const result = await kubectl.apply(
trafficSplitManifests,
forceDeployment,
serverSideApply,
timeout
)
checkForErrors([result])
return trafficSplitManifests
}
@ -314,14 +348,16 @@ async function createTrafficSplitManifestFile(
serviceName: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number
canaryWeight: number,
timeout?: string
): Promise<string> {
const smiObjectString = await getTrafficSplitObject(
kubectl,
serviceName,
stableWeight,
baselineWeight,
canaryWeight
canaryWeight,
timeout
)
const manifestFile = fileHelper.writeManifestToFile(
smiObjectString,
@ -343,7 +379,8 @@ async function getTrafficSplitObject(
name: string,
stableWeight: number,
baselineWeight: number,
canaryWeight: number
canaryWeight: number,
timeout?: string
): Promise<string> {
// cached version
if (!trafficSplitAPIVersion) {

View File

@ -1,54 +1,55 @@
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as canaryDeploymentHelper from './canary/canaryHelper'
import * as models from '../types/kubernetesTypes'
import {isDeploymentEntity} from '../types/kubernetesTypes'
import * as fileHelper from '../utilities/fileUtils'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils'
import {Kubectl, Resource} from '../types/kubectl'
import * as canaryDeploymentHelper from './canary/canaryHelper.js'
import * as models from '../types/kubernetesTypes.js'
import {isDeploymentEntity} from '../types/kubernetesTypes.js'
import * as fileHelper from '../utilities/fileUtils.js'
import * as KubernetesManifestUtility from '../utilities/manifestStabilityUtils.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import {deployPodCanary} from './canary/podCanaryHelper'
import {deploySMICanary} from './canary/smiCanaryHelper'
import {DeploymentConfig} from '../types/deploymentConfig'
import {deployBlueGreen} from './blueGreen/deploy'
import {DeploymentStrategy} from '../types/deploymentStrategy'
import {deployPodCanary} from './canary/podCanaryHelper.js'
import {deploySMICanary} from './canary/smiCanaryHelper.js'
import {DeploymentConfig} from '../types/deploymentConfig.js'
import {deployBlueGreen} from './blueGreen/deploy.js'
import {DeploymentStrategy} from '../types/deploymentStrategy.js'
import * as core from '@actions/core'
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from '../types/trafficSplitMethod'
import {parseRouteStrategy} from '../types/routeStrategy'
} from '../types/trafficSplitMethod.js'
import {parseRouteStrategy} from '../types/routeStrategy.js'
import {ExecOutput} from '@actions/exec'
import {
getWorkflowAnnotationKeyLabel,
getWorkflowAnnotations,
cleanLabel
} from '../utilities/workflowAnnotationUtils'
} from '../utilities/workflowAnnotationUtils.js'
import {
annotateChildPods,
checkForErrors,
getLastSuccessfulRunSha
} from '../utilities/kubectlUtils'
} from '../utilities/kubectlUtils.js'
import {
getWorkflowFilePath,
normalizeWorkflowStrLabel
} from '../utilities/githubUtils'
import {getDeploymentConfig} from '../utilities/dockerUtils'
import {DeployResult} from '../types/deployResult'
import {ClusterType} from '../actions/deploy'
} from '../utilities/githubUtils.js'
import {getDeploymentConfig} from '../utilities/dockerUtils.js'
import {DeployResult} from '../types/deployResult.js'
import {ClusterType} from '../inputUtils.js'
export async function deployManifests(
files: string[],
deploymentStrategy: DeploymentStrategy,
kubectl: Kubectl,
trafficSplitMethod: TrafficSplitMethod
trafficSplitMethod: TrafficSplitMethod,
timeout?: string
): Promise<string[]> {
switch (deploymentStrategy) {
case DeploymentStrategy.CANARY: {
const canaryDeployResult: DeployResult =
trafficSplitMethod == TrafficSplitMethod.SMI
? await deploySMICanary(files, kubectl)
: await deployPodCanary(files, kubectl)
? await deploySMICanary(files, kubectl, false, timeout)
: await deployPodCanary(files, kubectl, false, timeout)
checkForErrors([canaryDeployResult.execResult])
return canaryDeployResult.manifestFiles
@ -61,7 +62,8 @@ export async function deployManifests(
const blueGreenDeployment = await deployBlueGreen(
kubectl,
files,
routeStrategy
routeStrategy,
timeout
)
core.debug(
`objects deployed for ${routeStrategy}: ${JSON.stringify(
@ -84,16 +86,25 @@ export async function deployManifests(
)
const forceDeployment = core.getInput('force').toLowerCase() === 'true'
const serverSideApply =
core.getInput('server-side').toLowerCase() === 'true'
if (trafficSplitMethod === TrafficSplitMethod.SMI) {
const updatedManifests = appendStableVersionLabelToResource(files)
const result = await kubectl.apply(
updatedManifests,
forceDeployment
forceDeployment,
serverSideApply,
timeout
)
checkForErrors([result])
} else {
const result = await kubectl.apply(files, forceDeployment)
const result = await kubectl.apply(
files,
forceDeployment,
serverSideApply,
timeout
)
checkForErrors([result])
}
@ -140,12 +151,14 @@ function appendStableVersionLabelToResource(files: string[]): string[] {
export async function checkManifestStability(
kubectl: Kubectl,
resources: Resource[],
resourceType: ClusterType
resourceType: ClusterType,
timeout?: string
): Promise<void> {
await KubernetesManifestUtility.checkManifestStability(
kubectl,
resources,
resourceType
resourceType,
timeout
)
}
@ -196,7 +209,7 @@ async function annotateResources(
deploymentConfig: DeploymentConfig
) {
const annotateResults: ExecOutput[] = []
const namespace = core.getInput('namespace') || 'default'
const namespace = core.getInput('namespace') || '' // Sets namespace to an empty string if not provided, allowing the manifest-defined namespace to take precedence instead of "default".
const lastSuccessSha = await getLastSuccessfulRunSha(
kubectl,
namespace,
@ -227,8 +240,10 @@ async function annotateResources(
)}`
const annotateNamespace = !(
namespace === '' ||
core.getInput('annotate-namespace').toLowerCase() === 'false'
)
) // If namespace is empty, we don't annotate it. If the input is false, we also don't annotate it.
if (annotateNamespace) {
annotateResults.push(
await kubectl.annotate(

View File

@ -1,4 +1,4 @@
import {Action, parseAction} from './action'
import {Action, parseAction} from './action.js'
describe('Action type', () => {
test('it has required values', () => {

View File

@ -1,5 +1,5 @@
import {DeployResult} from './deployResult'
import {K8sObject, K8sDeleteObject} from './k8sObject'
import {DeployResult} from './deployResult.js'
import {K8sObject, K8sDeleteObject} from './k8sObject.js'
export interface BlueGreenDeployment {
deployResult: DeployResult

View File

@ -1,4 +1,7 @@
import {DeploymentStrategy, parseDeploymentStrategy} from './deploymentStrategy'
import {
DeploymentStrategy,
parseDeploymentStrategy
} from './deploymentStrategy.js'
describe('Deployment strategy type', () => {
test('it has required values', () => {

View File

@ -1,4 +1,7 @@
import {DockerExec} from './docker'
import {vi} from 'vitest'
vi.mock('@actions/exec')
import {DockerExec} from './docker.js'
import * as actions from '@actions/exec'
const dockerPath = 'dockerPath'
@ -12,14 +15,14 @@ describe('Docker class', () => {
const execReturn = {exitCode: 0, stdout: 'Output', stderr: ''}
beforeEach(() => {
jest.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
vi.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
return execReturn
})
})
test('pulls an image', async () => {
await docker.pull(image, args)
expect(actions.getExecOutput).toBeCalledWith(
expect(actions.getExecOutput).toHaveBeenCalledWith(
dockerPath,
['pull', image, ...args],
{silent: false}
@ -28,7 +31,7 @@ describe('Docker class', () => {
test('pulls an image silently', async () => {
await docker.pull(image, args, true)
expect(actions.getExecOutput).toBeCalledWith(
expect(actions.getExecOutput).toHaveBeenCalledWith(
dockerPath,
['pull', image, ...args],
{silent: true}
@ -38,7 +41,7 @@ describe('Docker class', () => {
test('inspects a docker image', async () => {
const result = await docker.inspect(image, args)
expect(result).toBe(execReturn.stdout)
expect(actions.getExecOutput).toBeCalledWith(
expect(actions.getExecOutput).toHaveBeenCalledWith(
dockerPath,
['inspect', image, ...args],
{silent: false}
@ -48,7 +51,7 @@ describe('Docker class', () => {
test('inspects a docker image silently', async () => {
const result = await docker.inspect(image, args, true)
expect(result).toBe(execReturn.stdout)
expect(actions.getExecOutput).toBeCalledWith(
expect(actions.getExecOutput).toHaveBeenCalledWith(
dockerPath,
['inspect', image, ...args],
{silent: true}
@ -60,7 +63,7 @@ describe('Docker class', () => {
const execReturn = {exitCode: 3, stdout: '', stderr: ''}
beforeEach(() => {
jest.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
vi.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
return execReturn
})
})
@ -80,7 +83,7 @@ describe('Docker class', () => {
const execReturn = {exitCode: 0, stdout: '', stderr: 'Output'}
beforeEach(() => {
jest.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
vi.spyOn(actions, 'getExecOutput').mockImplementation(async () => {
return execReturn
})
})

View File

@ -1,4 +1,10 @@
import {getKubectlPath, Kubectl} from './kubectl'
import {vi} from 'vitest'
vi.mock('@actions/exec')
vi.mock('@actions/io')
vi.mock('@actions/core')
vi.mock('@actions/tool-cache')
import {getKubectlPath, Kubectl} from './kubectl.js'
import * as exec from '@actions/exec'
import * as io from '@actions/io'
import * as core from '@actions/core'
@ -9,27 +15,27 @@ describe('Kubectl path', () => {
const path = 'path'
it('gets the kubectl path', async () => {
jest.spyOn(core, 'getInput').mockImplementationOnce(() => undefined)
jest.spyOn(io, 'which').mockImplementationOnce(async () => path)
vi.spyOn(core, 'getInput').mockImplementationOnce(() => '')
vi.spyOn(io, 'which').mockImplementationOnce(async () => path)
expect(await getKubectlPath()).toBe(path)
})
it('gets the kubectl path with version', async () => {
jest.spyOn(core, 'getInput').mockImplementationOnce(() => version)
jest.spyOn(toolCache, 'find').mockImplementationOnce(() => path)
vi.spyOn(core, 'getInput').mockImplementationOnce(() => version)
vi.spyOn(toolCache, 'find').mockImplementationOnce(() => path)
expect(await getKubectlPath()).toBe(path)
})
it('throws if kubectl not found', async () => {
// without version
jest.spyOn(io, 'which').mockImplementationOnce(async () => undefined)
vi.spyOn(io, 'which').mockImplementationOnce(async () => '')
await expect(() => getKubectlPath()).rejects.toThrow()
// with verision
jest.spyOn(core, 'getInput').mockImplementationOnce(() => undefined)
jest.spyOn(io, 'which').mockImplementationOnce(async () => undefined)
vi.spyOn(core, 'getInput').mockImplementationOnce(() => '')
vi.spyOn(io, 'which').mockImplementationOnce(async () => '')
await expect(() => getKubectlPath()).rejects.toThrow()
})
})
@ -38,22 +44,24 @@ const kubectlPath = 'kubectlPath'
const testNamespace = 'testNamespace'
const defaultNamespace = 'default'
const otherNamespace = 'otherns'
const TEST_TIMEOUT = '120s'
describe('Kubectl class', () => {
describe('with a success exec return in testNamespace', () => {
const kubectl = new Kubectl(kubectlPath, testNamespace)
const execReturn = {exitCode: 0, stdout: 'Output', stderr: ''}
const mockExecReturn = {exitCode: 0, stdout: 'Output', stderr: ''}
beforeEach(() => {
jest.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
return execReturn
vi.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
return mockExecReturn
})
})
it('applies a configuration with a single config path', async () => {
const configPaths = 'configPaths'
const result = await kubectl.apply(configPaths)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
['apply', '-f', configPaths, '--namespace', testNamespace],
{silent: false}
@ -63,8 +71,8 @@ describe('Kubectl class', () => {
it('applies a configuration with multiple config paths', async () => {
const configPaths = ['configPath1', 'configPath2', 'configPath3']
const result = await kubectl.apply(configPaths)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'apply',
@ -80,8 +88,8 @@ describe('Kubectl class', () => {
it('applies a configuration with force when specified', async () => {
const configPaths = ['configPath1', 'configPath2', 'configPath3']
const result = await kubectl.apply(configPaths, true)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'apply',
@ -95,12 +103,120 @@ describe('Kubectl class', () => {
)
})
it('applies a configuration with server-side when specified', async () => {
const configPaths = ['configPath1', 'configPath2', 'configPath3']
const result = await kubectl.apply(configPaths, false, true)
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'apply',
'-f',
configPaths[0] + ',' + configPaths[1] + ',' + configPaths[2],
'--server-side',
'--namespace',
testNamespace
],
{silent: false}
)
})
it('applies a configuration with both force and server-side when specified', async () => {
const configPaths = ['configPath1', 'configPath2', 'configPath3']
const result = await kubectl.apply(configPaths, true, true)
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'apply',
'-f',
configPaths[0] + ',' + configPaths[1] + ',' + configPaths[2],
'--force',
'--server-side',
'--namespace',
testNamespace
],
{silent: false}
)
})
it('applies a configuration with timeout when specified', async () => {
const configPaths = ['configPath1', 'configPath2', 'configPath3']
const result = await kubectl.apply(
configPaths,
false,
false,
TEST_TIMEOUT
)
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'apply',
'-f',
configPaths[0] + ',' + configPaths[1] + ',' + configPaths[2],
`--timeout=${TEST_TIMEOUT}`,
'--namespace',
testNamespace
],
{silent: false}
)
})
it('applies a configuration with force and timeout when specified', async () => {
const configPaths = ['configPath1', 'configPath2', 'configPath3']
const result = await kubectl.apply(
configPaths,
true,
false,
TEST_TIMEOUT
)
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'apply',
'-f',
configPaths[0] + ',' + configPaths[1] + ',' + configPaths[2],
'--force',
`--timeout=${TEST_TIMEOUT}`,
'--namespace',
testNamespace
],
{silent: false}
)
})
it('applies a configuration with server-side and timeout when specified', async () => {
const configPaths = ['configPath1', 'configPath2', 'configPath3']
const result = await kubectl.apply(
configPaths,
false,
true,
TEST_TIMEOUT
)
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'apply',
'-f',
configPaths[0] + ',' + configPaths[1] + ',' + configPaths[2],
'--server-side',
`--timeout=${TEST_TIMEOUT}`,
'--namespace',
testNamespace
],
{silent: false}
)
})
it('describes a resource', async () => {
const resourceType = 'type'
const resourceName = 'name'
const result = await kubectl.describe(resourceType, resourceName)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'describe',
@ -120,7 +236,7 @@ describe('Kubectl class', () => {
silent,
otherNamespace
)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'describe',
@ -137,8 +253,8 @@ describe('Kubectl class', () => {
const resourceType = 'type'
const resourceName = 'name'
const result = await kubectl.describe(resourceType, resourceName, true)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'describe',
@ -158,7 +274,7 @@ describe('Kubectl class', () => {
silent,
otherNamespace
)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'describe',
@ -180,8 +296,8 @@ describe('Kubectl class', () => {
resourceName,
annotation
)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'annotate',
@ -202,7 +318,7 @@ describe('Kubectl class', () => {
annotation,
otherNamespace
)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'annotate',
@ -221,8 +337,8 @@ describe('Kubectl class', () => {
const file = 'file'
const annotation = 'annotation'
const result = await kubectl.annotateFiles(file, annotation)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'annotate',
@ -238,7 +354,7 @@ describe('Kubectl class', () => {
// override ns
await kubectl.annotateFiles(file, annotation, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'annotate',
@ -257,8 +373,8 @@ describe('Kubectl class', () => {
const files = ['file1', 'file2', 'file3']
const annotation = 'annotation'
const result = await kubectl.annotateFiles(files, annotation)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'annotate',
@ -274,7 +390,7 @@ describe('Kubectl class', () => {
// override ns
await kubectl.annotateFiles(files, annotation, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'annotate',
@ -293,8 +409,8 @@ describe('Kubectl class', () => {
const file = 'file'
const labels = ['label1', 'label2']
const result = await kubectl.labelFiles(file, labels)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'label',
@ -309,7 +425,7 @@ describe('Kubectl class', () => {
)
await kubectl.labelFiles(file, labels, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'label',
@ -328,8 +444,8 @@ describe('Kubectl class', () => {
const files = ['file1', 'file2', 'file3']
const labels = ['label1', 'label2']
const result = await kubectl.labelFiles(files, labels)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(result).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'label',
@ -344,7 +460,7 @@ describe('Kubectl class', () => {
)
await kubectl.labelFiles(files, labels, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'label',
@ -360,8 +476,8 @@ describe('Kubectl class', () => {
})
it('gets all pods', async () => {
expect(await kubectl.getAllPods()).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(await kubectl.getAllPods()).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
['get', 'pods', '-o', 'json', '--namespace', testNamespace],
{silent: true}
@ -372,9 +488,9 @@ describe('Kubectl class', () => {
const resourceType = 'type'
const name = 'name'
expect(await kubectl.checkRolloutStatus(resourceType, name)).toBe(
execReturn
mockExecReturn
)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'rollout',
@ -388,7 +504,7 @@ describe('Kubectl class', () => {
// override ns
await kubectl.checkRolloutStatus(resourceType, name, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'rollout',
@ -399,13 +515,35 @@ describe('Kubectl class', () => {
],
{silent: false}
)
// with timeout
await kubectl.checkRolloutStatus(
resourceType,
name,
testNamespace,
'5m'
)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'rollout',
'status',
`${resourceType}/${name}`,
'--namespace',
testNamespace,
'--timeout=5m'
],
{silent: false}
)
})
it('gets resource', async () => {
const resourceType = 'type'
const name = 'name'
expect(await kubectl.getResource(resourceType, name)).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(await kubectl.getResource(resourceType, name)).toBe(
mockExecReturn
)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'get',
@ -421,7 +559,7 @@ describe('Kubectl class', () => {
// override ns
const silent = true
await kubectl.getResource(resourceType, name, silent, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[
'get',
@ -438,8 +576,8 @@ describe('Kubectl class', () => {
it('executes a command', async () => {
// no args
const command = 'command'
expect(await kubectl.executeCommand(command)).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(await kubectl.executeCommand(command)).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[command, '--namespace', testNamespace],
{silent: false}
@ -447,8 +585,10 @@ describe('Kubectl class', () => {
// with args
const args = 'args'
expect(await kubectl.executeCommand(command, args)).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(await kubectl.executeCommand(command, args)).toBe(
mockExecReturn
)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[command, args, '--namespace', testNamespace],
{silent: false}
@ -457,8 +597,8 @@ describe('Kubectl class', () => {
it('deletes with single argument', async () => {
const arg = 'argument'
expect(await kubectl.delete(arg)).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(await kubectl.delete(arg)).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
['delete', arg, '--namespace', testNamespace],
{silent: false}
@ -466,7 +606,7 @@ describe('Kubectl class', () => {
// override ns
await kubectl.delete(arg, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
['delete', arg, '--namespace', otherNamespace],
{silent: false}
@ -475,8 +615,8 @@ describe('Kubectl class', () => {
it('deletes with multiple arguments', async () => {
const args = ['argument1', 'argument2', 'argument3']
expect(await kubectl.delete(args)).toBe(execReturn)
expect(exec.getExecOutput).toBeCalledWith(
expect(await kubectl.delete(args)).toBe(mockExecReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
['delete', ...args, '--namespace', testNamespace],
{silent: false}
@ -484,7 +624,7 @@ describe('Kubectl class', () => {
// override ns
await kubectl.delete(args, otherNamespace)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
['delete', ...args, '--namespace', otherNamespace],
{silent: false}
@ -503,7 +643,7 @@ describe('Kubectl class', () => {
stderr: ''
}
jest.spyOn(exec, 'getExecOutput').mockImplementationOnce(async () => {
vi.spyOn(exec, 'getExecOutput').mockImplementationOnce(async () => {
return describeReturn
})
@ -516,13 +656,13 @@ describe('Kubectl class', () => {
const skipTls = true
const kubectl = new Kubectl(kubectlPath, testNamespace, skipTls)
jest.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
vi.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
return {exitCode: 0, stderr: '', stdout: ''}
})
const command = 'command'
kubectl.executeCommand(command)
expect(exec.getExecOutput).toBeCalledWith(
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
[command, '--insecure-skip-tls-verify', '--namespace', testNamespace],
{silent: false}
@ -530,8 +670,59 @@ describe('Kubectl class', () => {
const kubectlNoFlags = new Kubectl(kubectlPath)
kubectlNoFlags.executeCommand(command)
expect(exec.getExecOutput).toBeCalledWith(kubectlPath, [command], {
expect(exec.getExecOutput).toHaveBeenCalledWith(kubectlPath, [command], {
silent: false
})
})
})
describe('Kubectl namespace handling', () => {
const kubectlPath = 'kubectlPath'
const testNamespace = 'testNamespace'
const configPaths = 'configPaths'
const execReturn = {exitCode: 0, stdout: 'Output', stderr: ''}
beforeEach(() => {
vi.spyOn(exec, 'getExecOutput').mockResolvedValue(execReturn)
})
const runApply = async (namespace?: string) => {
const kubectl = new Kubectl(kubectlPath, namespace)
return kubectl.apply(configPaths)
}
it.each([
{
namespace: undefined,
expectedArgs: ['apply', '-f', configPaths],
description: 'namespace omitted'
},
{
namespace: '',
expectedArgs: ['apply', '-f', configPaths],
description: 'namespace is an empty string (default namespace)'
},
{
namespace: testNamespace,
expectedArgs: [
'apply',
'-f',
configPaths,
'--namespace',
testNamespace
],
description: 'namespace provided'
}
])(
'handles namespace when $description',
async ({namespace, expectedArgs}) => {
const result = await runApply(namespace)
expect(result).toBe(execReturn)
expect(exec.getExecOutput).toHaveBeenCalledWith(
kubectlPath,
expectedArgs,
{silent: false}
)
}
)
})

View File

@ -1,5 +1,5 @@
import {ExecOutput, getExecOutput} from '@actions/exec'
import {createInlineArray} from '../utilities/arrayUtils'
import {createInlineArray} from '../utilities/arrayUtils.js'
import * as core from '@actions/core'
import * as toolCache from '@actions/tool-cache'
import * as io from '@actions/io'
@ -34,7 +34,9 @@ export class Kubectl {
public async apply(
configurationPaths: string | string[],
force: boolean = false
force: boolean = false,
serverSide: boolean = false,
timeout?: string
): Promise<ExecOutput> {
try {
if (!configurationPaths || configurationPaths?.length === 0)
@ -46,10 +48,13 @@ export class Kubectl {
createInlineArray(configurationPaths)
]
if (force) applyArgs.push('--force')
if (serverSide) applyArgs.push('--server-side')
if (timeout) applyArgs.push(`--timeout=${timeout}`)
return await this.execute(applyArgs.concat(this.getFlags()))
} catch (err) {
core.debug('Kubectl apply failed:' + err)
throw err
}
}
@ -157,13 +162,16 @@ export class Kubectl {
public async checkRolloutStatus(
resourceType: string,
name: string,
namespace?: string
namespace?: string,
timeout?: string
): Promise<ExecOutput> {
return await this.execute(
['rollout', 'status', `${resourceType}/${name}`].concat(
this.getFlags(namespace)
)
const command = ['rollout', 'status', `${resourceType}/${name}`].concat(
this.getFlags(namespace)
)
if (timeout) {
command.push(`--timeout=${timeout}`)
}
return await this.execute(command)
}
public async getResource(
@ -183,33 +191,64 @@ export class Kubectl {
)
}
public executeCommand(command: string, args?: string) {
public executeCommand(command: string, args?: string, timeout?: string) {
if (!command) throw new Error('Command must be defined')
const a = args ? [args] : []
return this.execute([command, ...a.concat(this.getFlags())])
return this.execute(
[command, ...a.concat(this.getFlags())],
false,
timeout
)
}
public delete(args: string | string[], namespace?: string) {
public delete(
args: string | string[],
namespace?: string,
timeout?: string
) {
if (typeof args === 'string')
return this.execute(['delete', args].concat(this.getFlags(namespace)))
return this.execute(['delete', ...args.concat(this.getFlags(namespace))])
return this.execute(
['delete', args].concat(this.getFlags(namespace)),
false,
timeout
)
return this.execute(
['delete', ...args.concat(this.getFlags(namespace))],
false,
timeout
)
}
protected async execute(args: string[], silent: boolean = false) {
core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`)
protected async execute(
args: string[],
silent: boolean = false,
timeout?: string
) {
if (timeout) {
args.push(`--timeout=${timeout}`)
}
// core.debug(`Kubectl run with command: ${this.kubectlPath} ${args}`)
core.debug(
`Kubectl run with command: ${this.kubectlPath} ${args.join(' ')}`
)
return await getExecOutput(this.kubectlPath, args, {
silent
})
}
public getNamespace(namespaceOverride?: string): string {
return namespaceOverride || this.namespace
}
protected getFlags(namespaceOverride?: string): string[] {
const flags = []
if (this.ignoreSSLErrors) {
flags.push('--insecure-skip-tls-verify')
}
const ns = namespaceOverride || this.namespace
const ns = this.getNamespace(namespaceOverride)
if (ns) {
flags.push('--namespace', ns)
}

View File

@ -10,7 +10,7 @@ import {
ServiceTypes,
WORKLOAD_TYPES,
WORKLOAD_TYPES_WITH_ROLLOUT_STATUS
} from './kubernetesTypes'
} from './kubernetesTypes.js'
describe('Kubernetes types', () => {
it('contains kubernetes workloads', () => {
@ -21,6 +21,7 @@ describe('Kubernetes types', () => {
expect(KubernetesWorkload.DAEMON_SET).toBe('DaemonSet')
expect(KubernetesWorkload.JOB).toBe('job')
expect(KubernetesWorkload.CRON_JOB).toBe('cronjob')
expect(KubernetesWorkload.SCALED_JOB).toBe('scaledjob')
})
it('contains discovery and load balancer resources', () => {
@ -53,7 +54,8 @@ describe('Kubernetes types', () => {
'pod',
'statefulset',
'job',
'cronjob'
'cronjob',
'scaledjob'
]
expect(expected.every((val) => WORKLOAD_TYPES.includes(val))).toBe(true)
})

View File

@ -6,6 +6,7 @@ export class KubernetesWorkload {
public static DAEMON_SET: string = 'DaemonSet'
public static JOB: string = 'job'
public static CRON_JOB: string = 'cronjob'
public static SCALED_JOB: string = 'scaledjob'
}
export class DiscoveryAndLoadBalancerResource {
@ -34,7 +35,8 @@ export const WORKLOAD_TYPES: string[] = [
'pod',
'statefulset',
'job',
'cronjob'
'cronjob',
'scaledjob'
]
export const WORKLOAD_TYPES_WITH_ROLLOUT_STATUS: string[] = [

View File

@ -1,10 +1,13 @@
import * as fileUtils from '../utilities/fileUtils'
import {vi} from 'vitest'
vi.mock('@actions/exec')
import * as fileUtils from '../utilities/fileUtils.js'
import fs from 'node:fs'
import {
PrivateKubectl,
extractFileNames,
replaceFileNamesWithShallowNamesRelativeToTemp
} from './privatekubectl'
} from './privatekubectl.js'
import * as exec from '@actions/exec'
describe('Private kubectl', () => {
@ -17,14 +20,14 @@ describe('Private kubectl', () => {
'resourceName'
)
const spy = jest
const spy = vi
.spyOn(fileUtils, 'getTempDirectory')
.mockImplementation(() => {
return '/tmp'
})
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
jest.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
vi.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
return 'test contents'
})
@ -48,7 +51,7 @@ describe('Private kubectl', () => {
test('Should throw well defined Error on error from Azure', async () => {
const errorMsg = 'An error message'
jest.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
vi.spyOn(exec, 'getExecOutput').mockImplementation(async () => {
return {exitCode: 1, stdout: '', stderr: errorMsg}
})

View File

@ -1,10 +1,10 @@
import {Kubectl} from './kubectl'
import {Kubectl} from './kubectl.js'
import minimist from 'minimist'
import {ExecOptions, ExecOutput, getExecOutput} from '@actions/exec'
import * as core from '@actions/core'
import fs from 'node:fs'
import * as path from 'path'
import {getTempDirectory} from '../utilities/fileUtils'
import {getTempDirectory} from '../utilities/fileUtils.js'
export class PrivateKubectl extends Kubectl {
protected async execute(args: string[], silent: boolean = false) {

View File

@ -1,4 +1,4 @@
import {parseRouteStrategy, RouteStrategy} from './routeStrategy'
import {parseRouteStrategy, RouteStrategy} from './routeStrategy.js'
describe('Route strategy type', () => {
test('it has required values', () => {

View File

@ -1,4 +1,7 @@
import {parseTrafficSplitMethod, TrafficSplitMethod} from './trafficSplitMethod'
import {
parseTrafficSplitMethod,
TrafficSplitMethod
} from './trafficSplitMethod.js'
describe('Traffic split method type', () => {
test('it has required values', () => {

View File

@ -1,4 +1,4 @@
import {createInlineArray} from './arrayUtils'
import {createInlineArray} from './arrayUtils.js'
describe('array utilities', () => {
it('creates an inline array', () => {

View File

@ -1,15 +1,20 @@
import {vi} from 'vitest'
vi.mock('@actions/io')
import * as io from '@actions/io'
import {checkDockerPath} from './dockerUtils'
import {checkDockerPath} from './dockerUtils.js'
describe('docker utilities', () => {
it('checks if docker is installed', async () => {
// docker installed
const path = 'path'
jest.spyOn(io, 'which').mockImplementationOnce(async () => path)
vi.spyOn(io, 'which').mockImplementationOnce(async () => path)
expect(() => checkDockerPath()).not.toThrow()
// docker not installed
jest.spyOn(io, 'which').mockImplementationOnce(async () => undefined)
vi.spyOn(io, 'which').mockImplementationOnce(async () => {
throw new Error('not found')
})
await expect(() => checkDockerPath()).rejects.toThrow()
})
})

View File

@ -1,8 +1,8 @@
import * as io from '@actions/io'
import {DeploymentConfig} from '../types/deploymentConfig'
import {DeploymentConfig} from '../types/deploymentConfig.js'
import * as core from '@actions/core'
import {DockerExec} from '../types/docker'
import {getNormalizedPath} from './githubUtils'
import {DockerExec} from '../types/docker.js'
import {getNormalizedPath} from './githubUtils.js'
export async function getDeploymentConfig(): Promise<DeploymentConfig> {
let helmChartPaths: string[] =

View File

@ -0,0 +1,167 @@
import {vi, type Mocked} from 'vitest'
import {parseDuration} from './durationUtils.js'
import * as core from '@actions/core'
// Mock core.debug
vi.mock('@actions/core')
const mockCore = core as Mocked<typeof core>
// Test data constants
const VALID_TIMEOUTS = {
withUnits: ['5s', '10m', '1h', '500ms'],
decimals: ['0.5s', '1.25m', '2.5h'],
caseInsensitive: ['5S', '10M', '1H'],
expectedLowercase: ['5s', '10m', '1h'],
bareNumbers: ['5', '15', '120'],
expectedWithMinutes: ['5m', '15m', '120m'],
whitespace: [' 10s', '1m ', '\t2h\n'],
expectedTrimmed: ['10s', '1m', '2h'],
rangeValid: ['1ms', '999ms', '0.5s', '1439m', '23.999h'],
edgeCases: ['0.001s', '0.0167m', '24h']
}
const INVALID_TIMEOUTS = {
badFormats: ['', 'abc', '30x', '30 s', '30sm'],
negative: ['-5m', '-1s', '-0.5h'],
zero: ['0s', '0m', '0h', '0ms'],
belowMin: ['0.0001s', '0.00001ms'],
aboveMax: ['25h', '1441m', '86401s']
}
const ERROR_MESSAGES = {
invalidFormat: (input: string) =>
`Invalid duration format: "${input}". Use: number + unit (30s, 5m, 1h) or just number (assumes minutes)`,
notPositive: (input: string) => `Duration must be positive: "${input}"`,
outOfRange: (input: string) =>
`Duration out of range (1ms to 24h): "${input}"`
}
// Helper functions
const expectValidTimeout = (input: string, expected: string) => {
expect(parseDuration(input)).toBe(expected)
}
const expectInvalidTimeout = (input: string, expectedError: string) => {
expect(() => parseDuration(input)).toThrow(expectedError)
}
describe('validateTimeoutDuration', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('valid timeout formats', () => {
const validCases: Array<[string, string, string]> = [
...VALID_TIMEOUTS.withUnits.map((v): [string, string, string] => [
v,
v,
'accepts number with valid units'
]),
...VALID_TIMEOUTS.decimals.map((v): [string, string, string] => [
v,
v,
'accepts decimal number with units'
]),
...VALID_TIMEOUTS.caseInsensitive.map(
(v, i): [string, string, string] => [
v,
VALID_TIMEOUTS.expectedLowercase[i],
'handles case-insensitive units'
]
),
...VALID_TIMEOUTS.bareNumbers.map((v, i): [string, string, string] => [
v,
VALID_TIMEOUTS.expectedWithMinutes[i],
'assumes minutes for bare numbers'
]),
...VALID_TIMEOUTS.whitespace.map((v, i): [string, string, string] => [
v,
VALID_TIMEOUTS.expectedTrimmed[i],
'trims whitespace'
])
]
test.each(validCases)('%s → %s (%s)', (input, expected, description) => {
expectValidTimeout(input, expected)
})
test('logs assumption for bare numbers only', () => {
parseDuration('5')
expect(mockCore.debug).toHaveBeenCalledWith(
'No unit specified for timeout "5", assuming minutes'
)
vi.clearAllMocks()
parseDuration('30s')
expect(mockCore.debug).not.toHaveBeenCalled()
})
})
describe('invalid timeout formats', () => {
const invalidCases: Array<[string, string]> = [
...INVALID_TIMEOUTS.badFormats.map((t): [string, string] => [
t,
ERROR_MESSAGES.invalidFormat(t)
]),
...INVALID_TIMEOUTS.negative.map((t): [string, string] => [
t,
ERROR_MESSAGES.invalidFormat(t)
]),
...INVALID_TIMEOUTS.zero.map((t): [string, string] => [
t,
ERROR_MESSAGES.notPositive(t)
])
]
test.each(invalidCases)('rejects %s', (input, expectedError) => {
expectInvalidTimeout(input, expectedError)
})
})
describe('range validation', () => {
const rangeCases: Array<[string, string, boolean]> = [
...VALID_TIMEOUTS.rangeValid.map((v): [string, string, boolean] => [
v,
v,
true
]),
...INVALID_TIMEOUTS.belowMin.map((v): [string, string, boolean] => [
v,
ERROR_MESSAGES.outOfRange(v),
false
]),
...INVALID_TIMEOUTS.aboveMax.map((v): [string, string, boolean] => [
v,
ERROR_MESSAGES.outOfRange(v),
false
]),
...VALID_TIMEOUTS.edgeCases.map((v): [string, string, boolean] => [
v,
v,
true
])
]
test.each(rangeCases)('%s is %s', (input, expected, isValid) => {
if (isValid) {
expectValidTimeout(input, expected)
} else {
expectInvalidTimeout(input, expected)
}
})
})
describe('edge cases', () => {
test.each([
['0.001s', '0.001s'],
['0.0167m', '0.0167m'],
['23.999h', '23.999h'],
['1439m', '1439m'],
['5.0m', '5m'],
['005s', '5s']
])('parses and normalizes: %s → %s', (input, expected) => {
expectValidTimeout(input, expected)
})
})
})

View File

@ -0,0 +1,38 @@
import * as core from '@actions/core'
export function parseDuration(duration: string): string {
const trimmed = duration.trim()
// Parse number and optional unit using regex
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/i.exec(trimmed)
if (!match) {
throw new Error(
`Invalid duration format: "${duration}". Use: number + unit (30s, 5m, 1h) or just number (assumes minutes)`
)
}
const value = parseFloat(match[1])
const unit = match[2]?.toLowerCase() || 'm'
if (value <= 0) {
throw new Error(`Duration must be positive: "${duration}"`)
}
// Calculate total seconds for validation
const multipliers = {ms: 0.001, s: 1, m: 60, h: 3600}
const totalSeconds = value * multipliers[unit as keyof typeof multipliers]
// Validate bounds (1ms to 24h)
if (totalSeconds < 0.001 || totalSeconds > 86400) {
throw new Error(`Duration out of range (1ms to 24h): "${duration}"`)
}
// Log assumption for bare numbers (when no unit was provided)
if (!duration.trim().match(/\d+(ms|s|m|h)$/i)) {
core.debug(
`No unit specified for timeout "${duration}", assuming minutes`
)
}
return `${value}${unit}`
}

View File

@ -1,9 +1,10 @@
import * as fileUtils from './fileUtils'
import {vi} from 'vitest'
import * as fileUtils from './fileUtils.js'
import * as yaml from 'js-yaml'
import fs from 'node:fs'
import * as path from 'path'
import {K8sObject} from '../types/k8sObject'
import {K8sObject} from '../types/k8sObject.js'
const sampleYamlUrl =
'https://raw.githubusercontent.com/kubernetes/website/main/content/en/examples/controllers/nginx-deployment.yaml'
@ -45,10 +46,11 @@ describe('File utils', () => {
'test/unit/manifests/manifest_test_dir/nested-test-service.yaml',
'test/unit/manifests/test-ingress.yml',
'test/unit/manifests/test-ingress-new.yml',
'test/unit/manifests/test-service.yml'
'test/unit/manifests/test-service.yml',
'test/unit/manifests/basic-test.yml'
]
expect(testSearch).toHaveLength(8)
expect(testSearch).toHaveLength(10)
expectedManifests.forEach((fileName) => {
if (fileName.startsWith('test/unit')) {
expect(testSearch).toContain(fileName)
@ -68,9 +70,9 @@ describe('File utils', () => {
'manifest_test_dir'
)
expect(
await expect(
fileUtils.getFilesFromDirectoriesAndURLs([badPath, goodPath])
).rejects.toThrowError()
).rejects.toThrow()
})
it("doesn't duplicate files when nested dir included", async () => {
@ -94,7 +96,7 @@ describe('File utils', () => {
fileAtOuter,
innerPath
])
).toHaveLength(7)
).toHaveLength(9)
})
it('throws an error for an invalid URL', async () => {
@ -107,8 +109,8 @@ describe('File utils', () => {
describe('moving files to temp', () => {
it('correctly moves the contents of a file to the temporary directory', () => {
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
jest.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
vi.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
return 'test contents'
})
const originalFilePath = path.join('path', 'in', 'repo')

View File

@ -4,15 +4,15 @@ import * as path from 'path'
import * as core from '@actions/core'
import * as os from 'os'
import * as yaml from 'js-yaml'
import {Errorable, succeeded, failed, Failed} from '../types/errorable'
import {getCurrentTime} from './timeUtils'
import {isHttpUrl} from './githubUtils'
import {K8sObject} from '../types/k8sObject'
import {Errorable, succeeded, failed, Failed} from '../types/errorable.js'
import {getCurrentTime} from './timeUtils.js'
import {isHttpUrl} from './githubUtils.js'
import {K8sObject} from '../types/k8sObject.js'
export const urlFileKind = 'urlfile'
export function getTempDirectory(): string {
return process.env['runner.tempDirectory'] || os.tmpdir()
return process.env['RUNNER_TEMP'] || os.tmpdir()
}
export function writeObjectsToFile(inputObjects: any[]): string[] {

View File

@ -2,7 +2,7 @@ import {
getNormalizedPath,
isHttpUrl,
normalizeWorkflowStrLabel
} from './githubUtils'
} from './githubUtils.js'
describe('Github utils', () => {
it('normalizes workflow string labels', () => {

View File

@ -1,4 +1,4 @@
import {GitHubClient, OkStatusCode} from '../types/githubClient'
import {GitHubClient, OkStatusCode} from '../types/githubClient.js'
import * as core from '@actions/core'
export async function getWorkflowFilePath(

View File

@ -1,6 +1,9 @@
import {vi} from 'vitest'
vi.mock('@actions/core')
import * as core from '@actions/core'
import {ExecOutput} from '@actions/exec'
import {checkForErrors} from './kubectlUtils'
import {checkForErrors} from './kubectlUtils.js'
describe('Kubectl utils', () => {
it('checks for errors', () => {
@ -39,23 +42,24 @@ describe('Kubectl utils', () => {
).toThrow()
// with warn behavior
jest.spyOn(core, 'warning').mockImplementation(() => {})
const warnSpy = vi.spyOn(core, 'warning').mockImplementation(() => {})
warnSpy.mockClear()
let warningCalls = 0
expect(() => checkForErrors([success], true)).not.toThrow()
expect(core.warning).toBeCalledTimes(warningCalls)
expect(core.warning).toHaveBeenCalledTimes(warningCalls)
expect(() => checkForErrors([successWithStderr], true)).not.toThrow()
expect(core.warning).toBeCalledTimes(++warningCalls)
expect(core.warning).toHaveBeenCalledTimes(++warningCalls)
expect(() =>
checkForErrors([success, successWithStderr], true)
).not.toThrow()
expect(core.warning).toBeCalledTimes(++warningCalls)
expect(core.warning).toHaveBeenCalledTimes(++warningCalls)
expect(() => checkForErrors([failWithExitCode], true)).not.toThrow()
expect(core.warning).toBeCalledTimes(++warningCalls)
expect(core.warning).toHaveBeenCalledTimes(++warningCalls)
expect(() => checkForErrors([failWithExitWithStderr], true)).not.toThrow()
expect(core.warning).toBeCalledTimes(++warningCalls)
expect(core.warning).toHaveBeenCalledTimes(++warningCalls)
})
})

View File

@ -1,6 +1,6 @@
import * as core from '@actions/core'
import {ExecOutput} from '@actions/exec'
import {Kubectl} from '../types/kubectl'
import {Kubectl} from '../types/kubectl.js'
const NAMESPACE = 'namespace'

View File

@ -1,20 +1,23 @@
import {KubernetesWorkload} from '../types/kubernetesTypes'
import {KubernetesWorkload} from '../types/kubernetesTypes.js'
export function getImagePullSecrets(inputObject: any) {
if (!inputObject?.spec) return null
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec
if (
inputObject.kind.toLowerCase() ===
KubernetesWorkload.CRON_JOB.toLowerCase()
)
return inputObject?.spec?.jobTemplate?.spec?.template?.spec
?.imagePullSecrets
if (!spec || !kind) return null
if (inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase())
return inputObject.spec.imagePullSecrets
switch (kind) {
case KubernetesWorkload.CRON_JOB.toLowerCase():
return spec.jobTemplate?.spec?.template?.spec?.imagePullSecrets
if (inputObject?.spec?.template?.spec) {
return inputObject.spec.template.spec.imagePullSecrets
case KubernetesWorkload.SCALED_JOB.toLowerCase():
return spec.jobTargetRef?.template?.spec?.imagePullSecrets
case KubernetesWorkload.POD.toLowerCase():
return spec.imagePullSecrets
default:
return spec.template?.spec?.imagePullSecrets || null
}
}
@ -22,27 +25,34 @@ export function setImagePullSecrets(
inputObject: any,
newImagePullSecrets: any
) {
if (!inputObject || !inputObject.spec || !newImagePullSecrets) return
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec
if (
inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase()
) {
inputObject.spec.imagePullSecrets = newImagePullSecrets
return
}
if (!inputObject || !spec || !newImagePullSecrets || !kind) return
if (
inputObject.kind.toLowerCase() ===
KubernetesWorkload.CRON_JOB.toLowerCase()
) {
if (inputObject?.spec?.jobTemplate?.spec?.template?.spec)
inputObject.spec.jobTemplate.spec.template.spec.imagePullSecrets =
newImagePullSecrets
return
}
switch (kind) {
case KubernetesWorkload.POD.toLowerCase():
spec.imagePullSecrets = newImagePullSecrets
break
if (inputObject?.spec?.template?.spec) {
inputObject.spec.template.spec.imagePullSecrets = newImagePullSecrets
return
case KubernetesWorkload.CRON_JOB.toLowerCase():
if (spec.jobTemplate?.spec?.template?.spec) {
spec.jobTemplate.spec.template.spec.imagePullSecrets =
newImagePullSecrets
}
break
case KubernetesWorkload.SCALED_JOB.toLowerCase():
if (spec.jobTargetRef?.template?.spec) {
spec.jobTargetRef.template.spec.imagePullSecrets =
newImagePullSecrets
}
break
default:
if (spec.template?.spec) {
spec.template.spec.imagePullSecrets = newImagePullSecrets
}
break
}
}

View File

@ -3,7 +3,7 @@ import {
isServiceEntity,
KubernetesWorkload,
NullInputObjectError
} from '../types/kubernetesTypes'
} from '../types/kubernetesTypes.js'
export function updateSpecLabels(
inputObject: any,
@ -30,30 +30,54 @@ export function updateSpecLabels(
}
function getSpecLabels(inputObject: any) {
if (!inputObject) return null
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec
if (inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase())
return inputObject.metadata.labels
if (!inputObject || !kind) return null
if (inputObject?.spec?.template?.metadata)
return inputObject.spec.template.metadata.labels
switch (kind) {
case KubernetesWorkload.POD.toLowerCase():
return inputObject.metadata.labels
return null
case KubernetesWorkload.CRON_JOB.toLowerCase():
return spec?.jobTemplate?.spec?.template?.metadata?.labels
case KubernetesWorkload.SCALED_JOB.toLowerCase():
return spec?.jobTargetRef?.template?.metadata?.labels
default:
return spec?.template?.metadata?.labels || null
}
}
function setSpecLabels(inputObject: any, newLabels: any) {
if (!inputObject || !newLabels) return null
const kind = inputObject?.kind?.toLowerCase()
const spec = inputObject?.spec
if (
inputObject.kind.toLowerCase() === KubernetesWorkload.POD.toLowerCase()
) {
inputObject.metadata.labels = newLabels
return
}
if (!inputObject || !newLabels || !kind) return null
if (inputObject?.spec?.template?.metatada) {
inputObject.spec.template.metatada.labels = newLabels
return
switch (kind) {
case KubernetesWorkload.POD.toLowerCase():
inputObject.metadata.labels = newLabels
break
case KubernetesWorkload.CRON_JOB.toLowerCase():
if (spec?.jobTemplate?.spec?.template?.metadata) {
spec.jobTemplate.spec.template.metadata.labels = newLabels
}
break
case KubernetesWorkload.SCALED_JOB.toLowerCase():
if (spec?.jobTargetRef?.template?.metadata) {
spec.jobTargetRef.template.metadata.labels = newLabels
}
break
default:
if (spec?.template?.metadata) {
spec.template.metadata.labels = newLabels
}
break
}
}

View File

@ -1,8 +1,29 @@
import * as manifestStabilityUtils from './manifestStabilityUtils'
import {Kubectl} from '../types/kubectl'
import {ResourceTypeFleet, ResourceTypeManagedCluster} from '../actions/deploy'
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
vi.mock('@actions/core', async (importOriginal) => {
const actual: any = await importOriginal()
return {
...actual,
getInput: vi.fn().mockReturnValue(''),
debug: vi.fn(),
info: vi.fn(),
warning: vi.fn(),
error: vi.fn(),
setFailed: vi.fn(),
setOutput: vi.fn()
}
})
import * as manifestStabilityUtils from './manifestStabilityUtils.js'
import {Kubectl} from '../types/kubectl.js'
import {
ResourceTypeFleet,
ResourceTypeManagedCluster
} from '../actions/deploy.js'
import {ExecOutput} from '@actions/exec'
import {exitCode, stdout} from 'process'
import * as core from '@actions/core'
import * as timeUtils from './timeUtils.js'
describe('manifestStabilityUtils', () => {
const kc = new Kubectl('')
@ -15,8 +36,8 @@ describe('manifestStabilityUtils', () => {
]
it('should return immediately if the resource type is fleet', async () => {
const spy = jest.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = jest.spyOn(kc, 'checkRolloutStatus')
const spy = vi.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = vi.spyOn(kc, 'checkRolloutStatus')
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
@ -28,8 +49,8 @@ describe('manifestStabilityUtils', () => {
})
it('should run fully if the resource type is managedCluster', async () => {
const spy = jest.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = jest
const spy = vi.spyOn(manifestStabilityUtils, 'checkManifestStability')
const checkRolloutStatusSpy = vi
.spyOn(kc, 'checkRolloutStatus')
.mockImplementation(() => {
return new Promise<ExecOutput>((resolve, reject) => {
@ -49,4 +70,553 @@ describe('manifestStabilityUtils', () => {
expect(checkRolloutStatusSpy).toHaveBeenCalled()
expect(spy).toHaveReturned()
})
it('should pass timeout to checkRolloutStatus when provided', async () => {
const timeout = '300s'
const checkRolloutStatusSpy = vi
.spyOn(kc, 'checkRolloutStatus')
.mockImplementation(() => {
return new Promise<ExecOutput>((resolve, reject) => {
resolve({
exitCode: 0,
stderr: '',
stdout: ''
})
})
})
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster,
timeout
)
expect(checkRolloutStatusSpy).toHaveBeenCalledWith(
'deployment',
'test',
'default',
timeout
)
})
it('should call checkRolloutStatus without timeout when not provided', async () => {
const checkRolloutStatusSpy = vi
.spyOn(kc, 'checkRolloutStatus')
.mockImplementation(() => {
return new Promise<ExecOutput>((resolve, reject) => {
resolve({
exitCode: 0,
stderr: '',
stdout: ''
})
})
})
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
expect(checkRolloutStatusSpy).toHaveBeenCalledWith(
'deployment',
'test',
'default',
undefined
)
})
})
describe('checkManifestStability failure and resource-specific scenarios', () => {
let kc: Kubectl
let coreErrorSpy: MockInstance
let coreInfoSpy: MockInstance
let coreWarningSpy: MockInstance
beforeEach(() => {
kc = new Kubectl('', 'default')
coreErrorSpy = vi.spyOn(core, 'error').mockImplementation(() => {})
coreInfoSpy = vi.spyOn(core, 'info').mockImplementation(() => {})
coreWarningSpy = vi.spyOn(core, 'warning').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('should call describe and collect errors when a rollout fails', async () => {
const resources = [
{type: 'deployment', name: 'failing-app', namespace: 'app-ns-123'}
]
const rolloutError = new Error('Progress deadline exceeded')
const describeOutput =
'Events:\n Type\tReason\tMessage\n Normal\tScalingReplicaSet\tScaled up replica set failing-app-123 to 1'
// Arrange: Mock rollout to fail and describe to succeed
const checkRolloutStatusSpy = vi
.spyOn(kc, 'checkRolloutStatus')
.mockRejectedValue(rolloutError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
stdout: describeOutput,
stderr: '',
exitCode: 0
})
// Act & Assert: Expect the function to throw the final aggregated error
const expectedErrorMessage = `Rollout failed for deployment/failing-app in namespace app-ns-123: ${rolloutError.message}`
await expect(
manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
).rejects.toThrow(
`Rollout status failed for the following resources:\n${expectedErrorMessage}`
)
// Assert that the correct functions were called
expect(checkRolloutStatusSpy).toHaveBeenCalledTimes(1)
expect(coreErrorSpy).toHaveBeenCalledWith(expectedErrorMessage)
expect(describeSpy).toHaveBeenCalledWith(
'deployment',
'failing-app',
false,
'app-ns-123'
)
expect(coreInfoSpy).toHaveBeenCalledWith(
`Describe output for deployment/failing-app:\n${describeOutput}`
)
})
it('should use the default kubectl namespace when none is provided', async () => {
const resources = [{type: 'deployment', name: 'failing-app'}]
const rolloutError = new Error('Progress deadline exceeded')
const describeOutput =
'Events:\n Type\tReason\tMessage\n Normal\tScalingReplicaSet\tScaled up replica set failing-app-123 to 1'
// Arrange: Mock rollout to fail and describe to succeed
const checkRolloutStatusSpy = vi
.spyOn(kc, 'checkRolloutStatus')
.mockRejectedValue(rolloutError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
stdout: describeOutput,
stderr: '',
exitCode: 0
})
// Act & Assert: Expect the function to throw the final aggregated error
const expectedErrorMessage = `Rollout failed for deployment/failing-app in namespace default: ${rolloutError.message}`
await expect(
manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
).rejects.toThrow(
`Rollout status failed for the following resources:\n${expectedErrorMessage}`
)
// Assert that the correct functions were called
expect(checkRolloutStatusSpy).toHaveBeenCalledTimes(1)
expect(coreErrorSpy).toHaveBeenCalledWith(expectedErrorMessage)
expect(describeSpy).toHaveBeenCalledWith(
'deployment',
'failing-app',
false,
undefined
)
expect(coreInfoSpy).toHaveBeenCalledWith(
`Describe output for deployment/failing-app:\n${describeOutput}`
)
})
it('should call checkPodStatus for pod resources', async () => {
const resources = [{type: 'Pod', name: 'test-pod', namespace: 'default'}]
// Arrange: Spy on checkPodStatus and checkRolloutStatus
const checkPodStatusSpy = vi
.spyOn(manifestStabilityUtils, 'checkPodStatus')
.mockResolvedValue() // Assume pod becomes ready
const checkRolloutStatusSpy = vi.spyOn(kc, 'checkRolloutStatus')
// Act
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
// Assert
expect(checkPodStatusSpy).toHaveBeenCalledWith(kc, resources[0])
expect(checkRolloutStatusSpy).not.toHaveBeenCalled()
})
it('should warn and describe when a pod check fails', async () => {
const resources = [
{type: 'Pod', name: 'failing-pod', namespace: 'default'}
]
const podError = new Error('Pod rollout failed')
// Arrange: Mock checkPodStatus to fail
const checkPodStatusSpy = vi
.spyOn(manifestStabilityUtils, 'checkPodStatus')
.mockRejectedValue(podError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe output',
stderr: '',
exitCode: 0
})
// Act: This should not throw, only warn.
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
// Assert
expect(checkPodStatusSpy).toHaveBeenCalled()
expect(coreWarningSpy).toHaveBeenCalledWith(
expect.stringContaining(`Could not determine pod status`)
)
expect(describeSpy).toHaveBeenCalledWith(
'Pod',
'failing-pod',
false,
'default'
)
})
it('should wait for external IP for a LoadBalancer service', async () => {
//Spying on sleep to avoid actual delays in tests
vi.spyOn(timeUtils, 'sleep').mockResolvedValue(undefined)
const resources = [
{type: 'service', name: 'test-svc', namespace: 'default'}
]
const serviceWithoutIp = {
spec: {type: 'LoadBalancer'},
status: {loadBalancer: {}}
}
const serviceWithIp = {
spec: {type: 'LoadBalancer'},
status: {loadBalancer: {ingress: [{ip: '8.8.8.8'}]}}
}
// Arrange: Mock getResource to simulate the IP being assigned on the second poll
const getResourceSpy = vi
.spyOn(kc, 'getResource')
// First call: Initial service check
.mockResolvedValueOnce({
stdout: JSON.stringify(serviceWithoutIp),
stderr: '',
exitCode: 0
})
// Second call: First polling iteration (no IP yet)
.mockResolvedValueOnce({
stdout: JSON.stringify(serviceWithoutIp),
stderr: '',
exitCode: 0
})
// Third call: Second polling iteration (IP assigned)
.mockResolvedValueOnce({
stdout: JSON.stringify(serviceWithIp),
stderr: '',
exitCode: 0
})
// Act
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
// Assert
expect(getResourceSpy).toHaveBeenCalledTimes(3)
expect(coreInfoSpy).toHaveBeenCalledWith(
'ServiceExternalIP test-svc 8.8.8.8'
)
})
it('should warn and describe when a service check fails', async () => {
const resources = [
{type: 'service', name: 'broken-svc', namespace: 'default'}
]
const getServiceError = new Error('Service not found')
// Arrange: Mock getService to fail, and describe to succeed
// Note: We mock getResource because getService is a private helper
const getResourceSpy = vi
.spyOn(kc, 'getResource')
.mockRejectedValue(getServiceError)
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe output',
stderr: '',
exitCode: 0
})
// Act: Run the stability check. It should NOT throw an error, only warn.
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
// Assert
expect(getResourceSpy).toHaveBeenCalled()
expect(coreWarningSpy).toHaveBeenCalledWith(
expect.stringContaining(
`Could not determine service status of: broken-svc`
)
)
expect(describeSpy).toHaveBeenCalledWith(
'service',
'broken-svc',
false,
'default'
)
})
it('should not wait for an IP for a ClusterIP service', async () => {
const resources = [
{type: 'service', name: 'cluster-ip-svc', namespace: 'default'}
]
const clusterIpService = {
spec: {type: 'ClusterIP'}, // Not a LoadBalancer
status: {}
}
// Arrange
const getResourceSpy = vi.spyOn(kc, 'getResource').mockResolvedValue({
stdout: JSON.stringify(clusterIpService),
stderr: '',
exitCode: 0
})
// Act
await manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
// Assert: getResource is called once to get the spec, but not again for polling.
expect(getResourceSpy).toHaveBeenCalledTimes(1)
expect(coreInfoSpy).not.toHaveBeenCalledWith(
expect.stringContaining('ServiceExternalIP')
)
})
})
describe('checkManifestStability additional scenarios', () => {
let kc: Kubectl
let coreErrorSpy: MockInstance
let coreInfoSpy: MockInstance
let coreWarningSpy: MockInstance
beforeEach(() => {
kc = new Kubectl('')
coreErrorSpy = vi.spyOn(core, 'error').mockImplementation(() => {})
coreInfoSpy = vi.spyOn(core, 'info').mockImplementation(() => {})
coreWarningSpy = vi.spyOn(core, 'warning').mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
it('should aggregate errors from deployment and pod failures', async () => {
const resources = [
{type: 'deployment', name: 'deploy-failure', namespace: 'default'},
{type: 'pod', name: 'pod-failure', namespace: 'default'}
]
const deploymentError = new Error('Deployment rollout failed')
const podError = new Error('Pod not ready in time')
// Arrange: Mock failures
const checkRolloutStatusSpy = vi
.spyOn(kc, 'checkRolloutStatus')
.mockRejectedValue(deploymentError)
// For pod: simulate a pod check failure
const checkPodStatusSpy = vi
.spyOn(manifestStabilityUtils, 'checkPodStatus')
.mockRejectedValue(podError)
// For both, simulate a successful describe call to provide additional details
const describeSpy = vi.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe aggregated output',
stderr: '',
exitCode: 0
})
// Act & Assert:
const expectedDeploymentError = `Rollout failed for deployment/deploy-failure in namespace default: ${deploymentError.message}`
const expectedFullError = `Rollout status failed for the following resources:\n${expectedDeploymentError}`
await expect(
manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
).rejects.toThrow(expectedFullError)
// Assert that each failure was caught and processed
expect(checkRolloutStatusSpy).toHaveBeenCalledWith(
'deployment',
'deploy-failure',
'default',
undefined
)
expect(checkPodStatusSpy).toHaveBeenCalledWith(kc, resources[1])
expect(describeSpy).toHaveBeenCalled()
expect(coreErrorSpy).toHaveBeenCalledWith(expectedDeploymentError)
})
it('should complete without errors when all resources are stable', async () => {
const resources = [
{type: 'deployment', name: 'stable-deploy', namespace: 'default'},
{type: 'pod', name: 'stable-pod', namespace: 'default'},
{type: 'service', name: 'stable-svc', namespace: 'default'}
]
// Arrange:
// Deployment rollout succeeds
vi.spyOn(kc, 'checkRolloutStatus').mockResolvedValue({
exitCode: 0,
stderr: '',
stdout: ''
})
// Pod becomes ready
vi.spyOn(manifestStabilityUtils, 'checkPodStatus').mockResolvedValue()
// Simulate a LoadBalancer service that already has an external IP
const stableService = {
spec: {type: 'LoadBalancer'},
status: {loadBalancer: {ingress: [{ip: '1.2.3.4'}]}}
}
vi.spyOn(kc, 'getResource').mockResolvedValue({
stdout: JSON.stringify(stableService),
stderr: '',
exitCode: 0
})
// Provide a describe result to avoid warnings
vi.spyOn(kc, 'describe').mockResolvedValue({
stdout: 'describe output stable',
stderr: '',
exitCode: 0
})
// Act & Assert:
await expect(
manifestStabilityUtils.checkManifestStability(
kc,
resources,
ResourceTypeManagedCluster
)
).resolves.not.toThrow()
})
})
describe('getContainerErrors', () => {
it('should return an empty string if all containers are ready', () => {
const podStatus = {
containerStatuses: [
{
name: 'app',
ready: true,
state: {running: {startedAt: '2025-07-18T10:00:00Z'}}
}
]
}
expect(manifestStabilityUtils.getContainerErrors(podStatus)).toBe('')
})
it('should report an error for a waiting container', () => {
const podStatus = {
containerStatuses: [
{
name: 'app',
ready: false,
state: {
waiting: {
reason: 'ImagePullBackOff',
message: 'Back-off pulling image "my-image:latest"'
}
}
}
]
}
const expectedError =
'Container issues: Container \'app\' is waiting: ImagePullBackOff - Back-off pulling image "my-image:latest"'
expect(manifestStabilityUtils.getContainerErrors(podStatus)).toBe(
expectedError
)
})
it('should report an error for a terminated container', () => {
const podStatus = {
containerStatuses: [
{
name: 'job-runner',
ready: false,
state: {
terminated: {
reason: 'Error',
message: 'The job failed with exit code 1'
}
}
}
]
}
const expectedError =
"Container issues: Container 'job-runner' terminated: Error - The job failed with exit code 1"
expect(manifestStabilityUtils.getContainerErrors(podStatus)).toBe(
expectedError
)
})
it('should report an error for a waiting init container', () => {
const podStatus = {
initContainerStatuses: [
{
name: 'init-db',
ready: false,
state: {
waiting: {
reason: 'PodInitializing'
}
}
}
]
}
const expectedError =
"Container issues: Init container 'init-db' is waiting: PodInitializing - No message"
expect(manifestStabilityUtils.getContainerErrors(podStatus)).toBe(
expectedError
)
})
it('should combine errors from multiple containers', () => {
const podStatus = {
containerStatuses: [
{
name: 'main-app',
ready: false,
state: {waiting: {reason: 'CrashLoopBackOff'}}
}
],
initContainerStatuses: [
{
name: 'init-migrations',
ready: false,
state: {terminated: {reason: 'Error'}}
}
]
}
const expectedError =
"Container issues: Container 'main-app' is waiting: CrashLoopBackOff - No message; Init container 'init-migrations' terminated: Error - No message"
expect(manifestStabilityUtils.getContainerErrors(podStatus)).toBe(
expectedError
)
})
})

View File

@ -1,9 +1,10 @@
import * as core from '@actions/core'
import * as KubernetesConstants from '../types/kubernetesTypes'
import {Kubectl, Resource} from '../types/kubectl'
import {checkForErrors} from './kubectlUtils'
import {sleep} from './timeUtils'
import {ClusterType, ResourceTypeFleet} from '../actions/deploy'
import * as KubernetesConstants from '../types/kubernetesTypes.js'
import {Kubectl, Resource} from '../types/kubectl.js'
import {checkForErrors} from './kubectlUtils.js'
import {sleep} from './timeUtils.js'
import {ResourceTypeFleet} from '../actions/deploy.js'
import {ClusterType} from '../inputUtils.js'
const IS_SILENT = false
const POD = 'pod'
@ -11,14 +12,23 @@ const POD = 'pod'
export async function checkManifestStability(
kubectl: Kubectl,
resources: Resource[],
clusterTyper: ClusterType
resourceType: ClusterType,
timeout?: string
): Promise<void> {
// Skip if resource type is microsoft.containerservice/fleets
if (clusterTyper === ResourceTypeFleet) {
if (resourceType === ResourceTypeFleet) {
core.info(`Skipping checkManifestStability for ${ResourceTypeFleet}`)
return
}
let rolloutStatusHasErrors = false
// Collect errors for reporting
// This will be used to throw a detailed error at the end if any rollout fails
// This is useful for debugging and understanding which resources failed
// their rollout status check
// It will also include the describe output for the resource that failed
// to provide more context on the failure
const rolloutErrors: string[] = []
for (let i = 0; i < resources.length; i++) {
const resource = resources[i]
@ -31,24 +41,42 @@ export async function checkManifestStability(
const result = await kubectl.checkRolloutStatus(
resource.type,
resource.name,
resource.namespace
resource.namespace,
timeout
)
checkForErrors([result])
} catch (ex) {
core.error(ex)
await kubectl.describe(
resource.type,
resource.name,
IS_SILENT,
resource.namespace
)
const errorMessage = `Rollout failed for ${resource.type}/${resource.name} in namespace ${kubectl.getNamespace(resource.namespace)}: ${ex.message || ex}`
core.error(errorMessage)
rolloutErrors.push(errorMessage)
// Get more detailed information
try {
const describeResult = await kubectl.describe(
resource.type,
resource.name,
IS_SILENT,
resource.namespace
)
core.info(
`Describe output for ${resource.type}/${resource.name}:\n${describeResult.stdout}`
)
} catch (describeEx) {
core.warning(
`Could not describe ${resource.type}/${resource.name}: ${describeEx}`
)
}
rolloutStatusHasErrors = true
}
}
if (resource.type == KubernetesConstants.KubernetesWorkload.POD) {
if (
resource.type.toLowerCase() ===
KubernetesConstants.KubernetesWorkload.POD.toLowerCase()
) {
try {
await checkPodStatus(kubectl, resource)
await exports.checkPodStatus(kubectl, resource)
} catch (ex) {
core.warning(
`Could not determine pod status: ${JSON.stringify(ex)}`
@ -78,21 +106,31 @@ export async function checkManifestStability(
}
}
} catch (ex) {
core.warning(
`Could not determine service status of: ${resource.name} Error: ${ex}`
)
await kubectl.describe(
resource.type,
resource.name,
IS_SILENT,
resource.namespace
)
const errorMessage = `Could not determine service status of: ${resource.name} in namespace ${kubectl.getNamespace(resource.namespace)}. Error: ${ex.message || ex}`
core.warning(errorMessage)
try {
const describeResult = await kubectl.describe(
resource.type,
resource.name,
IS_SILENT,
resource.namespace
)
core.info(
`Describe output for service/${resource.name}:\n${describeResult.stdout}`
)
} catch (describeEx) {
core.warning(
`Could not describe service/${resource.name}: ${describeEx}`
)
}
}
}
}
if (rolloutStatusHasErrors) {
throw new Error('Rollout status error')
const detailedError = `Rollout status failed for the following resources:\n${rolloutErrors.join('\n')}`
throw new Error(detailedError)
}
}
@ -105,6 +143,8 @@ export async function checkPodStatus(
let podStatus
let kubectlDescribeNeeded = false
let errorDetails = ''
for (let i = 0; i < iterations; i++) {
await sleep(sleepTimeout)
@ -121,31 +161,56 @@ export async function checkPodStatus(
}
podStatus = await getPodStatus(kubectl, pod)
// Get container statuses for detailed error information
const containerErrors = getContainerErrors(podStatus)
switch (podStatus.phase) {
case 'Succeeded':
case 'Running':
if (isPodReady(podStatus)) {
console.log(`pod/${pod.name} is successfully rolled out`)
} else {
errorDetails = `Pod ${pod.name} is ${podStatus.phase} but not ready. ${containerErrors}`
core.error(errorDetails)
kubectlDescribeNeeded = true
}
break
case 'Pending':
if (!isPodReady(podStatus)) {
core.warning(`pod/${pod.name} rollout status check timed out`)
errorDetails = `Pod ${pod.name} rollout status check timed out (still Pending after ${(iterations * sleepTimeout) / 1000} seconds). ${containerErrors}`
core.warning(errorDetails)
kubectlDescribeNeeded = true
}
break
case 'Failed':
core.error(`pod/${pod.name} rollout failed`)
errorDetails = `Pod ${pod.name} rollout failed. ${containerErrors}`
core.error(errorDetails)
kubectlDescribeNeeded = true
break
default:
core.warning(`pod/${pod.name} rollout status: ${podStatus.phase}`)
errorDetails = `Pod ${pod.name} has unexpected status: ${podStatus.phase}. ${containerErrors}`
core.warning(errorDetails)
kubectlDescribeNeeded = true
}
if (kubectlDescribeNeeded) {
await kubectl.describe(POD, pod.name, IS_SILENT, pod.namespace)
try {
const describeResult = await kubectl.describe(
POD,
pod.name,
IS_SILENT,
pod.namespace
)
core.info(
`Describe output for pod/${pod.name}:\n${describeResult.stdout}`
)
} catch (describeEx) {
core.warning(`Could not describe pod/${pod.name}: ${describeEx}`)
}
// Throw error with detailed information
if (errorDetails) {
throw new Error(errorDetails)
}
}
}
@ -179,6 +244,32 @@ function isPodReady(podStatus: any): boolean {
return allContainersAreReady
}
export function getContainerErrors(podStatus: any): string {
const errors: string[] = []
const collectErrors = (containers: any[], label: string) => {
containers?.forEach(({name, ready, state}) => {
if (ready) return
if (state?.waiting) {
errors.push(
`${label} '${name}' is waiting: ${state.waiting.reason} - ${state.waiting.message || 'No message'}`
)
} else if (state?.terminated) {
errors.push(
`${label} '${name}' terminated: ${state.terminated.reason} - ${state.terminated.message || 'No message'}`
)
} else {
errors.push(
`${label} '${name}' is not ready: ${JSON.stringify(state)}`
)
}
})
}
collectErrors(podStatus.containerStatuses, 'Container')
collectErrors(podStatus.initContainerStatuses, 'Init container')
return errors.length ? `Container issues: ${errors.join('; ')}` : ''
}
async function getService(kubectl: Kubectl, service: Resource) {
const serviceResult = await kubectl.getResource(
KubernetesConstants.DiscoveryAndLoadBalancerResource.SERVICE,

View File

@ -1,14 +1,17 @@
import * as fileUtils from './fileUtils'
import * as manifestUpdateUtils from './manifestUpdateUtils'
import {vi} from 'vitest'
vi.mock('fs')
import * as fileUtils from './fileUtils.js'
import * as manifestUpdateUtils from './manifestUpdateUtils.js'
import * as path from 'path'
import * as fs from 'fs'
describe('manifestUpdateUtils', () => {
jest.spyOn(fileUtils, 'moveFileToTmpDir').mockImplementation((filename) => {
vi.spyOn(fileUtils, 'moveFileToTmpDir').mockImplementation((filename) => {
return path.join('/tmp', filename)
})
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
jest.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
vi.spyOn(fs, 'readFileSync').mockImplementation((filename) => {
return 'test contents'
})

View File

@ -2,25 +2,25 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as yaml from 'js-yaml'
import * as path from 'path'
import * as fileHelper from './fileUtils'
import {moveFileToTmpDir} from './fileUtils'
import * as fileHelper from './fileUtils.js'
import {moveFileToTmpDir} from './fileUtils.js'
import {
InputObjectKindNotDefinedError,
InputObjectMetadataNotDefinedError,
isWorkloadEntity,
KubernetesWorkload,
NullInputObjectError
} from '../types/kubernetesTypes'
} from '../types/kubernetesTypes.js'
import {
getSpecSelectorLabels,
setSpecSelectorLabels
} from './manifestSpecLabelUtils'
} from './manifestSpecLabelUtils.js'
import {
getImagePullSecrets,
setImagePullSecrets
} from './manifestPullSecretUtils'
import {Resource} from '../types/kubectl'
import {K8sObject} from '../types/k8sObject'
} from './manifestPullSecretUtils.js'
import {Resource} from '../types/kubectl.js'
import {K8sObject} from '../types/k8sObject.js'
export function updateManifestFiles(manifestFilePaths: string[]) {
if (manifestFilePaths?.length === 0) {
@ -79,70 +79,81 @@ function updateContainerImagesInManifestFiles(
filePaths: string[],
containers: string[]
): string[] {
if (filePaths?.length <= 0) return filePaths
if (!filePaths?.length) return filePaths
// update container images
filePaths.forEach((filePath: string) => {
let contents = fs.readFileSync(filePath).toString()
containers.forEach((container: string) => {
let [imageName] = container.split(':')
if (imageName.indexOf('@') > 0) {
imageName = imageName.split('@')[0]
}
const fileContents = fs.readFileSync(filePath, 'utf8')
const inputObjects = yaml.loadAll(fileContents) as K8sObject[]
if (contents.indexOf(imageName) > 0)
contents = substituteImageNameInSpecFile(
contents,
imageName,
container
)
const updatedObjects = inputObjects.map((obj) => {
if (!isWorkloadEntity(obj.kind)) return obj
containers.forEach((container: string) => {
let [imageName] = container.split(':')
if (imageName.includes('@')) {
imageName = imageName.split('@')[0]
}
updateImagesInK8sObject(obj, imageName, container)
})
return obj
})
// write updated files
fs.writeFileSync(path.join(filePath), contents)
const newYaml = updatedObjects.map((o) => yaml.dump(o)).join('---\n')
fs.writeFileSync(path.join(filePath), newYaml)
})
return filePaths
}
/*
Example:
const SPECIAL_CONTAINER_SPEC_PATHS: Record<string, string> = {
[KubernetesWorkload.POD.toLowerCase()]: 'spec',
[KubernetesWorkload.CRON_JOB.toLowerCase()]:
'spec.jobTemplate.spec.template.spec',
[KubernetesWorkload.SCALED_JOB.toLowerCase()]:
'spec.jobTargetRef.template.spec'
}
Input of
currentString: `image: "example/example-image"`
imageName: `example/example-image`
imageNameWithNewTag: `example/example-image:identifiertag`
const DEFAULT_CONTAINER_SPEC_PATH = 'spec.template.spec'
would return
`image: "example/example-image:identifiertag"`
*/
export function substituteImageNameInSpecFile(
spec: string,
export function updateImagesInK8sObject(
obj: any,
imageName: string,
imageNameWithNewTag: string
newImage: string
) {
if (spec.indexOf(imageName) < 0) return spec
const kind = obj?.kind?.toLowerCase()
const specPath =
SPECIAL_CONTAINER_SPEC_PATHS[kind] || DEFAULT_CONTAINER_SPEC_PATH
return spec.split('\n').reduce((acc, line) => {
const imageKeyword = line.match(/^ *-? *image:/)
if (imageKeyword) {
let [currentImageName] = line
.substring(imageKeyword[0].length) // consume the line from keyword onwards
.trim()
.replace(/[',"]/g, '') // replace allowed quotes with nothing
.split(':')
// Convert dot-separated path string into nested object traversal with full optional chaining
// Example: 'spec.jobTargetRef.template.spec' becomes obj?.spec?.jobTargetRef?.template?.spec
// The reduce function walks through each key safely with optional chaining at every step
const path = specPath
.split('.')
.reduce((current, key) => current?.[key], obj)
if (currentImageName?.indexOf(' ') > 0) {
currentImageName = currentImageName.split(' ')[0] // remove comments
}
if (path?.containers) {
updateImageInContainerArray(path.containers, imageName, newImage)
}
if (path?.initContainers) {
updateImageInContainerArray(path.initContainers, imageName, newImage)
}
}
if (currentImageName === imageName) {
return acc + `${imageKeyword[0]} ${imageNameWithNewTag}\n`
}
function updateImageInContainerArray(
containers: any[],
imageName: string,
newImage: string
) {
if (!Array.isArray(containers)) return
containers.forEach((container) => {
if (
container.image &&
(container.image === imageName ||
container.image.startsWith(imageName + ':') ||
container.image.startsWith(imageName + '@'))
) {
container.image = newImage
}
return acc + line + '\n'
}, '')
})
}
export function getReplicaCount(inputObject: any): any {
@ -152,12 +163,17 @@ export function getReplicaCount(inputObject: any): any {
throw InputObjectKindNotDefinedError
}
const {kind} = inputObject
if (
kind.toLowerCase() !== KubernetesWorkload.POD.toLowerCase() &&
kind.toLowerCase() !== KubernetesWorkload.DAEMON_SET.toLowerCase()
)
return inputObject.spec.replicas
const kind = inputObject.kind.toLowerCase()
const workloadsWithReplicas = new Set([
KubernetesWorkload.DEPLOYMENT.toLowerCase(),
KubernetesWorkload.REPLICASET.toLowerCase(),
KubernetesWorkload.STATEFUL_SET.toLowerCase()
])
if (workloadsWithReplicas.has(kind)) {
return inputObject.spec?.replicas
}
return 0
}

View File

@ -0,0 +1,70 @@
import {
getImagePullSecrets,
setImagePullSecrets
} from './manifestPullSecretUtils.js'
import {updateSpecLabels} from './manifestSpecLabelUtils.js'
import {getReplicaCount} from './manifestUpdateUtils.js'
import * as yaml from 'js-yaml'
import * as fs from 'fs'
import {isWorkloadEntity, isDeploymentEntity} from '../types/kubernetesTypes.js'
describe('ScaledJob Support', () => {
let scaledJobObject: any
beforeEach(() => {
const fileContents = fs.readFileSync(
'test/unit/manifests/test-scaledjob.yml'
)
scaledJobObject = yaml.load(fileContents.toString()) as any
})
describe('Image Pull Secrets', () => {
it('should get image pull secrets from ScaledJob', () => {
const secrets = getImagePullSecrets(scaledJobObject)
expect(secrets).toEqual([{name: 'test-secret'}])
})
it('should set image pull secrets in ScaledJob', () => {
const newSecrets = [{name: 'new-secret'}, {name: 'another-secret'}]
setImagePullSecrets(scaledJobObject, newSecrets)
const updatedSecrets = getImagePullSecrets(scaledJobObject)
expect(updatedSecrets).toEqual(newSecrets)
})
})
describe('Spec Labels', () => {
it('should update spec labels in ScaledJob', () => {
const newLabels = new Map<string, string>()
newLabels['environment'] = 'test'
newLabels['version'] = '1.0.0'
updateSpecLabels(scaledJobObject, newLabels, false)
const updatedLabels =
scaledJobObject.spec.jobTargetRef.template.metadata.labels
expect(updatedLabels['app']).toBe('test-scaledjob') // original label
expect(updatedLabels['environment']).toBe('test') // new label
expect(updatedLabels['version']).toBe('1.0.0') // new label
})
})
describe('Replica Count', () => {
it('should return 0 for ScaledJob replica count', () => {
const replicaCount = getReplicaCount(scaledJobObject)
expect(replicaCount).toBe(0)
})
})
describe('Workload Classification', () => {
it('should classify ScaledJob as workload entity', () => {
expect(isWorkloadEntity('ScaledJob')).toBe(true)
expect(isWorkloadEntity('scaledjob')).toBe(true)
})
it('should not classify ScaledJob as deployment entity', () => {
expect(isDeploymentEntity('scaledjob')).toBe(false)
expect(isDeploymentEntity('ScaledJob')).toBe(false)
})
})
})

View File

@ -1,4 +1,4 @@
import {Kubectl} from '../types/kubectl'
import {Kubectl} from '../types/kubectl.js'
const trafficSplitAPIVersionPrefix = 'split.smi-spec.io'

View File

@ -2,7 +2,7 @@ import {
cleanLabel,
removeInvalidLabelCharacters,
VALID_LABEL_REGEX
} from '../utilities/workflowAnnotationUtils'
} from '../utilities/workflowAnnotationUtils.js'
describe('WorkflowAnnotationUtils', () => {
describe('cleanLabel', () => {

View File

@ -1,4 +1,4 @@
import {DeploymentConfig} from '../types/deploymentConfig'
import {DeploymentConfig} from '../types/deploymentConfig.js'
const ANNOTATION_PREFIX = 'actions.github.com'

View File

@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment-no-ns
labels:
app: test-app
spec:
replicas: 1
selector:
matchLabels:
app: test-app
template:
metadata:
labels:
app: test-app
spec:
containers:
- name: test-container
image: nginx
ports:
- containerPort: 80

View File

@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: test-deployment
namespace: test-namespace
labels:
app: test-app
spec:
replicas: 1
selector:
matchLabels:
app: test-app
template:
metadata:
labels:
app: test-app
spec:
containers:
- name: test-container
image: nginx
ports:
- containerPort: 80

View File

@ -19,12 +19,10 @@ serviceAccount:
# If not set and create is true, a name is generated using the fullname template
name:
podSecurityContext:
{}
podSecurityContext: {}
# fsGroup: 2000
securityContext:
{}
securityContext: {}
# capabilities:
# drop:
# - ALL
@ -38,8 +36,7 @@ service:
ingress:
enabled: false
annotations:
{}
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
@ -50,8 +47,7 @@ ingress:
# hosts:
# - chart-example.local
resources:
{}
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following

View File

@ -0,0 +1,33 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80

View File

@ -0,0 +1,27 @@
apiVersion: keda.sh/v1alpha1
kind: ScaledJob
metadata:
name: test-scaledjob
labels:
app: test-scaledjob
spec:
jobTargetRef:
template:
metadata:
labels:
app: test-scaledjob
spec:
containers:
- name: processor
image: busybox:latest
imagePullPolicy: IfNotPresent
command: ['echo', 'hello world']
imagePullSecrets:
- name: test-secret
restartPolicy: Never
triggers:
- type: cron
metadata:
timezone: Etc/UTC
start: 0 */5 * * * *
end: 1 */5 * * * *

View File

@ -1,8 +1,15 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"esModuleInterop": true
"target": "ES2020",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["vitest/globals"],
"esModuleInterop": true,
"strict": false,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": false,
"sourceMap": true
},
"exclude": ["node_modules", "test", "src/**/*.test.ts"]
"exclude": ["node_modules", "test", "lib"]
}

11
vitest.config.ts Normal file
View File

@ -0,0 +1,11 @@
import {defineConfig} from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['**/*.test.ts'],
testTimeout: 9000,
clearMocks: true
}
})