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
This commit is contained in:
David Gamero
2026-02-24 14:57:56 -05:00
committed by GitHub
parent 84e2095bf0
commit 01cfe404ef
67 changed files with 1967 additions and 10133 deletions
+6 -6
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'
@@ -1,11 +1,15 @@
import {vi} from 'vitest'
import type {MockInstance} from 'vitest'
vi.mock('@actions/core')
import * as core from '@actions/core'
import {Kubectl} from '../../types/kubectl'
import {Kubectl} from '../../types/kubectl.js'
import {
deployPodCanary,
calculateReplicaCountForCanary
} from './podCanaryHelper'
} from './podCanaryHelper.js'
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
const kc = new Kubectl('')
@@ -35,18 +39,17 @@ const TIMEOUT_300S = '300s'
describe('Pod Canary Helper tests', () => {
let mockFilePaths: string[]
let kubectlApplySpy: jest.SpyInstance
let kubectlApplySpy: MockInstance
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
jest.restoreAllMocks()
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
mockFilePaths = testManifestFiles
kubectlApplySpy = jest.spyOn(kc, 'apply')
kubectlApplySpy = vi.spyOn(kc, 'apply')
// Mock core.getInput with default values
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
@@ -61,7 +64,7 @@ describe('Pod Canary Helper tests', () => {
})
afterEach(() => {
jest.restoreAllMocks()
vi.restoreAllMocks()
kubectlApplySpy.mockClear()
})
@@ -114,7 +117,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should throw error for invalid low percentage', async () => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return INVALID_LOW_PERCENTAGE.toString()
return ''
})
@@ -127,7 +130,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should throw error for invalid high percentage', async () => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return INVALID_HIGH_PERCENTAGE.toString()
return ''
})
@@ -143,7 +146,7 @@ describe('Pod Canary Helper tests', () => {
kubectlApplySpy.mockResolvedValue(mockSuccessResult)
// Test minimum valid percentage
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return MIN_PERCENTAGE.toString()
return ''
})
@@ -152,7 +155,7 @@ describe('Pod Canary Helper tests', () => {
expect(resultMin.execResult).toEqual(mockSuccessResult)
// Test maximum valid percentage
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
if (name === 'percentage') return MAX_PERCENTAGE.toString()
return ''
})
@@ -162,7 +165,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should handle force deployment option', async () => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
@@ -186,7 +189,7 @@ describe('Pod Canary Helper tests', () => {
})
test('should handle server-side apply option', async () => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return VALID_PERCENTAGE.toString()
@@ -1,15 +1,15 @@
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 {checkForErrors} from '../../utilities/kubectlUtils'
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[],
@@ -1,13 +1,34 @@
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'
import {Kubectl} from '../../types/kubectl.js'
import {
deploySMICanary,
redirectTrafficToCanaryDeployment,
redirectTrafficToStableDeployment
} from './smiCanaryHelper'
} from './smiCanaryHelper.js'
jest.mock('../../types/kubectl')
vi.mock('../../types/kubectl')
const kc = new Kubectl('')
@@ -40,22 +61,21 @@ const TIMEOUT_240S = '240s'
describe('SMI Canary Helper tests', () => {
let mockFilePaths: string[]
let kubectlApplySpy: jest.SpyInstance
let kubectlExecuteCommandSpy: jest.SpyInstance
let kubectlApplySpy: MockInstance
let kubectlExecuteCommandSpy: MockInstance
beforeEach(() => {
//@ts-ignore
Kubectl.mockClear()
jest.restoreAllMocks()
vi.mocked(Kubectl).mockClear()
vi.restoreAllMocks()
mockFilePaths = testManifestFiles
kubectlApplySpy = jest.spyOn(kc, 'apply')
kubectlExecuteCommandSpy = jest
kubectlApplySpy = vi.spyOn(kc, 'apply')
kubectlExecuteCommandSpy = vi
.spyOn(kc, 'executeCommand')
.mockResolvedValue(mockExecuteCommandResult)
// Mock core.getInput with default values
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'percentage':
return '50'
@@ -72,7 +92,7 @@ describe('SMI Canary Helper tests', () => {
})
afterEach(() => {
jest.restoreAllMocks()
vi.restoreAllMocks()
kubectlApplySpy.mockClear()
})
@@ -106,7 +126,7 @@ describe('SMI Canary Helper tests', () => {
})
test('should handle custom replica count from input', async () => {
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
switch (name) {
case 'baseline-and-canary-replicas':
return VALID_REPLICA_COUNT.toString()
+13 -10
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'