mirror of
https://github.com/Azure/k8s-set-context.git
synced 2026-04-17 22:42:16 +08:00
migrate to esbuild and vitest
This commit is contained in:
parent
fd396fb546
commit
6ad4fe0a14
@ -107,3 +107,7 @@ provided by the bot. You will only need to do this once across all repos using o
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
### Local development
|
||||
|
||||
Use `npm run build` to bundle the action with esbuild (output is `lib/index.cjs`, referenced by `action.yml`) and `npm run test` to execute the Vitest suite, including an integration test that runs the built action.
|
||||
|
||||
@ -40,4 +40,4 @@ branding:
|
||||
color: 'blue'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'lib/index.js'
|
||||
main: 'lib/index.cjs'
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
// babel.config.js
|
||||
export default {
|
||||
presets: [
|
||||
'@babel/preset-env', // For handling ES modules
|
||||
'@babel/preset-typescript' // For handling TypeScript
|
||||
]
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
export default {
|
||||
restoreMocks: true,
|
||||
clearMocks: true,
|
||||
resetMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest', // Use ts-jest for TypeScript files
|
||||
'^.+\\.js$': 'babel-jest' // Transform TypeScript files
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!@kubernetes/client-node)/' // Make sure to transform the Kubernetes client module
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^.+\\.css$': 'jest-transform-stub' // Handle CSS imports (if any)
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'], // Treat TypeScript files as ESM
|
||||
verbose: true,
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 0,
|
||||
functions: 40,
|
||||
lines: 22,
|
||||
statements: 22
|
||||
}
|
||||
}
|
||||
}
|
||||
11706
package-lock.json
generated
11706
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@ -2,13 +2,12 @@
|
||||
"name": "k8s-set-context-action",
|
||||
"version": "4.0.2",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"main": "lib/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prebuild": "npm i @vercel/ncc",
|
||||
"build": "ncc build src/run.ts -o lib",
|
||||
"test": "jest",
|
||||
"test-coverage": "jest --coverage",
|
||||
"build": "tsc --noEmit && node ./scripts/build.mjs",
|
||||
"test": "vitest run",
|
||||
"test-coverage": "vitest run --coverage",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"prepare": "husky"
|
||||
@ -29,16 +28,12 @@
|
||||
"js-yaml": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.28.6",
|
||||
"@babel/preset-typescript": "^7.28.5",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^25.0.9",
|
||||
"@vercel/ncc": "^0.38.4",
|
||||
"babel-jest": "^30.2.0",
|
||||
"jest": "^30.2.0",
|
||||
"@vitest/coverage-v8": "^2.0.4",
|
||||
"esbuild": "^0.24.0",
|
||||
"prettier": "^3.8.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^5.9.3",
|
||||
"vitest": "^2.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
26
scripts/build.mjs
Normal file
26
scripts/build.mjs
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {build} from 'esbuild'
|
||||
import {mkdir} from 'fs/promises'
|
||||
import {dirname} from 'path'
|
||||
|
||||
const outFile = 'lib/index.cjs'
|
||||
|
||||
const main = async () => {
|
||||
await mkdir(dirname(outFile), {recursive: true})
|
||||
await build({
|
||||
entryPoints: ['src/run.ts'],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node20',
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
outfile: outFile,
|
||||
legalComments: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@ -1,30 +1,56 @@
|
||||
import {getRequiredInputError} from '../tests/util'
|
||||
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
||||
import {run} from './action'
|
||||
import fs from 'fs'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import os from 'os'
|
||||
import * as utils from './utils'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
describe('Run', () => {
|
||||
const initialEnv = {...process.env}
|
||||
const tempDirs: string[] = []
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
process.env = {...initialEnv}
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
vi.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
tempDirs.forEach((dir) => {
|
||||
try {
|
||||
fs.rmSync(dir, {recursive: true, force: true})
|
||||
} catch {}
|
||||
})
|
||||
tempDirs.length = 0
|
||||
})
|
||||
|
||||
it('throws error without cluster type', async () => {
|
||||
await expect(run()).rejects.toThrow(getRequiredInputError('cluster-type'))
|
||||
})
|
||||
|
||||
it('writes kubeconfig and sets context', async () => {
|
||||
const kubeconfig = 'kubeconfig'
|
||||
const fixedTimestamp = 42
|
||||
|
||||
process.env['INPUT_CLUSTER-TYPE'] = 'default'
|
||||
process.env['RUNNER_TEMP'] = '/sample/path'
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kubeconfig-test-'))
|
||||
tempDirs.push(tmpDir)
|
||||
process.env['RUNNER_TEMP'] = tmpDir
|
||||
|
||||
jest
|
||||
.spyOn(utils, 'getKubeconfig')
|
||||
.mockImplementation(async () => kubeconfig)
|
||||
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {})
|
||||
jest.spyOn(fs, 'chmodSync').mockImplementation(() => {})
|
||||
jest.spyOn(utils, 'setContext').mockImplementation(() => kubeconfig)
|
||||
vi.spyOn(Date, 'now').mockReturnValue(fixedTimestamp)
|
||||
vi.spyOn(utils, 'getKubeconfig').mockResolvedValue(kubeconfig)
|
||||
vi.spyOn(utils, 'setContext').mockImplementation(() => kubeconfig)
|
||||
|
||||
expect(await run())
|
||||
await expect(run()).resolves.toBeUndefined()
|
||||
|
||||
const outputPath = path.join(tmpDir, `kubeconfig_${fixedTimestamp}`)
|
||||
expect(fs.existsSync(outputPath)).toBe(true)
|
||||
expect(fs.readFileSync(outputPath, 'utf-8')).toBe(kubeconfig)
|
||||
expect(process.env['KUBECONFIG']).toBe(outputPath)
|
||||
expect(utils.getKubeconfig).toHaveBeenCalled()
|
||||
expect(fs.writeFileSync).toHaveBeenCalled()
|
||||
expect(fs.chmodSync).toHaveBeenCalled()
|
||||
expect(utils.setContext).toHaveBeenCalled()
|
||||
expect(utils.setContext).toHaveBeenCalledWith(kubeconfig)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import {beforeEach, describe, expect, it, test, vi} from 'vitest'
|
||||
import * as core from '@actions/core'
|
||||
import * as actions from '@actions/exec'
|
||||
import * as io from '@actions/io'
|
||||
import {getRequiredInputError} from '../../tests/util'
|
||||
@ -5,6 +7,12 @@ import {getArcKubeconfig, KUBECONFIG_LOCATION} from './arc'
|
||||
import * as az from './azCommands'
|
||||
|
||||
describe('Arc kubeconfig', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
vi.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
test('it throws error without resource group', async () => {
|
||||
await expect(getArcKubeconfig()).rejects.toThrow(
|
||||
getRequiredInputError('resource-group')
|
||||
@ -28,11 +36,11 @@ describe('Arc kubeconfig', () => {
|
||||
process.env['INPUT_RESOURCE-GROUP'] = group
|
||||
process.env['INPUT_CLUSTER-NAME'] = name
|
||||
|
||||
jest.spyOn(io, 'which').mockImplementation(async () => path)
|
||||
jest.spyOn(az, 'runAzCliCommand').mockImplementation(async () => {})
|
||||
jest
|
||||
.spyOn(az, 'runAzKubeconfigCommandBlocking')
|
||||
.mockImplementation(async () => kubeconfig)
|
||||
vi.spyOn(io, 'which').mockResolvedValue(path)
|
||||
vi.spyOn(az, 'runAzCliCommand').mockResolvedValue()
|
||||
vi.spyOn(az, 'runAzKubeconfigCommandBlocking').mockResolvedValue(
|
||||
kubeconfig
|
||||
)
|
||||
})
|
||||
|
||||
it('throws an error without method', async () => {
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import {describe, expect, test, vi} from 'vitest'
|
||||
import * as core from '@actions/core'
|
||||
import * as actions from '@actions/exec'
|
||||
import {runAzCliCommand} from './azCommands'
|
||||
|
||||
describe('Az commands', () => {
|
||||
test('it runs an az cli command', async () => {
|
||||
vi.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
const path = 'path'
|
||||
const args = ['args']
|
||||
|
||||
jest.spyOn(actions, 'exec').mockImplementation(async () => 0)
|
||||
vi.spyOn(actions, 'exec').mockResolvedValue(0)
|
||||
|
||||
expect(await runAzCliCommand(path, args))
|
||||
expect(actions.exec).toHaveBeenCalledWith(path, args, {})
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'
|
||||
import * as fs from 'fs'
|
||||
import * as core from '@actions/core'
|
||||
import {getRequiredInputError} from '../../tests/util'
|
||||
import {createKubeconfig, getDefaultKubeconfig} from './default'
|
||||
|
||||
describe('Default kubeconfig', () => {
|
||||
const originalEnv = {...process.env}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
vi.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = {...originalEnv}
|
||||
})
|
||||
|
||||
test('it creates a kubeconfig with proper format', () => {
|
||||
const certAuth = 'certAuth'
|
||||
const token = 'token'
|
||||
@ -66,7 +79,7 @@ describe('Default kubeconfig', () => {
|
||||
|
||||
test('returns kubeconfig as plaintext when encoding is plaintext', () => {
|
||||
const kc = 'example kc'
|
||||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
if (name === 'method') return 'default'
|
||||
if (name === 'kubeconfig-encoding') return 'plaintext'
|
||||
if (name === 'kubeconfig') return kc
|
||||
@ -79,7 +92,7 @@ describe('Default kubeconfig', () => {
|
||||
const kc = 'example kc'
|
||||
const base64Kc = Buffer.from(kc, 'utf-8').toString('base64')
|
||||
|
||||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
if (name === 'method') return 'default'
|
||||
if (name === 'kubeconfig-encoding') return 'base64'
|
||||
if (name === 'kubeconfig') return base64Kc
|
||||
@ -93,7 +106,7 @@ describe('Default kubeconfig', () => {
|
||||
const kc = 'example kc'
|
||||
const unknownEncoding = 'foobar'
|
||||
|
||||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
vi.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
if (name === 'method') return 'default'
|
||||
if (name === 'kubeconfig-encoding') return unknownEncoding
|
||||
if (name === 'kubeconfig') return kc
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import {describe, expect, test} from 'vitest'
|
||||
import {Cluster, parseCluster} from './cluster'
|
||||
|
||||
describe('Cluster type', () => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import {describe, expect, test} from 'vitest'
|
||||
import {parseK8sSecret, K8sSecret} from './k8sSecret'
|
||||
|
||||
describe('K8sSecret type', () => {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import {describe, expect, test} from 'vitest'
|
||||
import {Method, parseMethod} from './method'
|
||||
|
||||
describe('Method type', () => {
|
||||
|
||||
@ -1,25 +1,31 @@
|
||||
import {beforeEach, describe, expect, test, vi} from 'vitest'
|
||||
import fs from 'fs'
|
||||
import * as arc from './kubeconfigs/arc'
|
||||
import * as def from './kubeconfigs/default'
|
||||
import {Cluster} from './types/cluster'
|
||||
import {getKubeconfig, setContext} from './utils'
|
||||
import * as core from '@actions/core'
|
||||
|
||||
describe('Utils', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.spyOn(core, 'warning').mockImplementation(() => {})
|
||||
vi.spyOn(core, 'debug').mockImplementation(() => {})
|
||||
})
|
||||
|
||||
describe('get kubeconfig', () => {
|
||||
test('it gets arc kubeconfig when type is arc', async () => {
|
||||
const arcKubeconfig = 'arckubeconfig'
|
||||
jest
|
||||
.spyOn(arc, 'getArcKubeconfig')
|
||||
.mockImplementation(async () => arcKubeconfig)
|
||||
vi.spyOn(arc, 'getArcKubeconfig').mockResolvedValue(arcKubeconfig)
|
||||
|
||||
expect(await getKubeconfig(Cluster.ARC)).toBe(arcKubeconfig)
|
||||
})
|
||||
|
||||
test('it defaults to default kubeconfig', async () => {
|
||||
const defaultKubeconfig = 'arckubeconfig'
|
||||
jest
|
||||
.spyOn(def, 'getDefaultKubeconfig')
|
||||
.mockImplementation(() => defaultKubeconfig)
|
||||
vi.spyOn(def, 'getDefaultKubeconfig').mockImplementation(
|
||||
() => defaultKubeconfig
|
||||
)
|
||||
|
||||
expect(await getKubeconfig(undefined)).toBe(defaultKubeconfig)
|
||||
expect(await getKubeconfig(Cluster.GENERIC)).toBe(defaultKubeconfig)
|
||||
|
||||
74
tests/bundle.integration.test.ts
Normal file
74
tests/bundle.integration.test.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {afterEach, beforeAll, describe, expect, test} from 'vitest'
|
||||
import {execFile} from 'child_process'
|
||||
import {promisify} from 'util'
|
||||
import {mkdtempSync, readFileSync, rmSync} from 'fs'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
|
||||
const sampleKubeconfig = `apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://example.com
|
||||
name: primary
|
||||
contexts:
|
||||
- context:
|
||||
cluster: primary
|
||||
namespace: default
|
||||
user: developer
|
||||
name: exp-scratch
|
||||
current-context: exp-scratch
|
||||
kind: Config
|
||||
preferences: {}
|
||||
users:
|
||||
- name: developer
|
||||
user:
|
||||
username: exp
|
||||
password: pass
|
||||
`
|
||||
|
||||
describe('Action bundle integration', () => {
|
||||
beforeAll(async () => {
|
||||
await execFileAsync('node', ['./scripts/build.mjs'])
|
||||
}, 20000)
|
||||
|
||||
const createdDirs: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
while (createdDirs.length) {
|
||||
const dir = createdDirs.pop()
|
||||
if (dir) {
|
||||
rmSync(dir, {recursive: true, force: true})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('bundle sets kubeconfig as workflow would', async () => {
|
||||
const runnerTemp = mkdtempSync(path.join(os.tmpdir(), 'bundle-test-'))
|
||||
createdDirs.push(runnerTemp)
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
RUNNER_TEMP: runnerTemp,
|
||||
GITHUB_ACTIONS: 'true',
|
||||
'INPUT_CLUSTER-TYPE': 'generic',
|
||||
INPUT_METHOD: 'kubeconfig',
|
||||
INPUT_KUBECONFIG: sampleKubeconfig,
|
||||
INPUT_CONTEXT: 'exp-scratch'
|
||||
}
|
||||
|
||||
const {stdout} = await execFileAsync('node', ['lib/index.cjs'], {
|
||||
env
|
||||
})
|
||||
|
||||
const output = stdout?.toString() ?? ''
|
||||
const match = output.match(/::set-env name=KUBECONFIG::(.+)/)
|
||||
expect(match?.[1]).toBeTruthy()
|
||||
const kubeconfigPath = match![1].trim()
|
||||
const contents = JSON.parse(readFileSync(kubeconfigPath, 'utf-8'))
|
||||
|
||||
expect(contents['current-context']).toBe('exp-scratch')
|
||||
expect(contents.clusters[0].cluster.server).toBe('https://example.com')
|
||||
}, 20000)
|
||||
})
|
||||
@ -3,5 +3,5 @@
|
||||
* @param inputName Name of input
|
||||
* @returns Error with explanation message
|
||||
*/
|
||||
export const getRequiredInputError = (inputName) =>
|
||||
export const getRequiredInputError = (inputName: string): Error =>
|
||||
Error(`Input required and not supplied: ${inputName}`)
|
||||
|
||||
@ -11,5 +11,5 @@
|
||||
"@kubernetes/client-node": ["node_modules/@kubernetes/client-node"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "tests", "src/**/*.test.ts"]
|
||||
"exclude": ["node_modules", "lib"]
|
||||
}
|
||||
|
||||
20
vitest.config.ts
Normal file
20
vitest.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import {defineConfig} from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['**/*.test.ts'],
|
||||
restoreMocks: true,
|
||||
clearMocks: true,
|
||||
mockReset: true,
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
thresholds: {
|
||||
statements: 22,
|
||||
branches: 0,
|
||||
functions: 40,
|
||||
lines: 22
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user