Compare commits

...

7 Commits

Author SHA1 Message Date
Nikola Jokic
70841b6972 bump docker dep as well 2025-04-16 14:43:46 +02:00
Nikola Jokic
1c2ae5d20a rework 2025-04-16 14:25:04 +02:00
Nikola Jokic
928f63d88a exclude eslint.config.js 2025-04-16 13:42:50 +02:00
Nikola Jokic
3bda7ef21e wip 2025-04-16 10:26:22 +02:00
Nikola Jokic
3b0e87c9a7 fmt 2025-04-15 14:53:53 +02:00
Nikola Jokic
a7349e7d70 fix errors and bump client node to stable version 2025-04-15 14:53:21 +02:00
Nikola Jokic
ff583c8917 bump all dependencies 2025-04-15 14:29:29 +02:00
20 changed files with 9627 additions and 18503 deletions

View File

@@ -1,4 +0,0 @@
dist/
lib/
node_modules/
**/tests/**

View File

@@ -1,56 +0,0 @@
{
"plugins": ["@typescript-eslint"],
"extends": ["plugin:github/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"eslint-comments/no-use": "off",
"import/no-namespace": "off",
"no-constant-condition": "off",
"no-unused-vars": "off",
"i18n-text/no-en": "off",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/await-thenable": "error",
"camelcase": "off",
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
"@typescript-eslint/func-call-spacing": ["error", "never"],
"@typescript-eslint/no-array-constructor": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "warn",
"@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error",
"@typescript-eslint/no-var-requires": "error",
"@typescript-eslint/prefer-for-of": "warn",
"@typescript-eslint/prefer-function-type": "warn",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-string-starts-ends-with": "error",
"@typescript-eslint/promise-function-async": "error",
"@typescript-eslint/require-array-sort-compare": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"semi": "off",
"@typescript-eslint/semi": ["error", "never"],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unbound-method": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"]
},
"env": {
"node": true,
"es6": true
}
}

82
eslint.config.js Normal file
View File

@@ -0,0 +1,82 @@
import { defineConfig, globalIgnores } from 'eslint/config'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import globals from 'globals'
import tsParser from '@typescript-eslint/parser'
import github from 'eslint-plugin-github'
export default defineConfig([
globalIgnores(['**/dist/', '**/lib/', '**/node_modules/', '**/tests/**/*', 'eslint.config.js']),
github.getFlatConfigs().recommended,
{
plugins: {
'@typescript-eslint': typescriptEslint
},
files: ['packages/**/*.ts'],
languageOptions: {
globals: globals.node,
parser: tsParser,
ecmaVersion: 9,
sourceType: 'module',
parserOptions: {
project: './tsconfig.json'
}
},
rules: {
'eslint-comments/no-use': 'off',
'import/no-namespace': 'off',
'no-constant-condition': 'off',
'no-unused-vars': 'off',
'i18n-text/no-en': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/explicit-member-accessibility': [
'error',
{
accessibility: 'no-public'
}
],
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
camelcase: 'off',
'@typescript-eslint/explicit-function-return-type': [
'error',
{
allowExpressions: true
}
],
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-for-in-array': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-var-requires': 'error',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-function-type': 'warn',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/restrict-plus-operands': 'error',
'@typescript-eslint/unbound-method': 'error',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': ['error']
}
}
])

5673
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
"bootstrap": "npm install --prefix packages/hooklib && npm install --prefix packages/k8s && npm install --prefix packages/docker",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint packages/**/*.ts",
"lint": "eslint",
"build-all": "npm run build --prefix packages/hooklib && npm run build --prefix packages/k8s && npm run build --prefix packages/docker"
},
"repository": {
@@ -25,12 +25,12 @@
},
"homepage": "https://github.com/actions/runner-container-hooks#readme",
"devDependencies": {
"@types/jest": "^27.5.1",
"@types/node": "^17.0.23",
"@typescript-eslint/parser": "^5.18.0",
"eslint": "^8.12.0",
"eslint-plugin-github": "^4.3.6",
"prettier": "^2.6.2",
"typescript": "^4.6.3"
"@types/jest": "^29.5.14",
"@types/node": "^22.14.1",
"@typescript-eslint/parser": "^8.30.1",
"eslint": "^9.24.0",
"eslint-plugin-github": "^6.0.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
}
}

View File

@@ -1,13 +0,0 @@
// eslint-disable-next-line import/no-commonjs
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*-test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
setupFilesAfterEnv: ['./jest.setup.js'],
verbose: true
}

View File

@@ -0,0 +1,12 @@
{
"clearMocks": true,
"moduleFileExtensions": ["js", "ts"],
"testEnvironment": "node",
"testMatch": ["**/*-test.ts"],
"testRunner": "jest-circus/runner",
"transform": {
"^.+\\.ts$": "ts-jest"
},
"verbose": true,
"testTimeout": 500000
}

View File

@@ -1 +0,0 @@
jest.setTimeout(500000)

File diff suppressed because it is too large Load Diff

View File

@@ -10,21 +10,21 @@
"author": "",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"hooklib": "file:../hooklib",
"shlex": "^2.1.2",
"uuid": "^8.3.2"
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/node": "^17.0.23",
"@typescript-eslint/parser": "^5.18.0",
"@vercel/ncc": "^0.33.4",
"jest": "^27.5.1",
"ts-jest": "^27.1.4",
"ts-node": "^10.7.0",
"tsconfig-paths": "^3.14.1",
"typescript": "^4.6.3"
"@types/jest": "^29.5.14",
"@types/node": "^22.14.1",
"@typescript-eslint/parser": "^8.30.1",
"@vercel/ncc": "^0.38.3",
"jest": "^29.7.0",
"ts-jest": "^29.3.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.8.3"
}
}

View File

@@ -45,9 +45,8 @@ export default class TestSetup {
public initialize(): void {
env['GITHUB_WORKSPACE'] = this.workingDirectory
env['RUNNER_NAME'] = 'test'
env[
'RUNNER_TEMP'
] = `${this.runnerMockDir}/${this.runnerMockSubdirs.workTemp}`
env['RUNNER_TEMP'] =
`${this.runnerMockDir}/${this.runnerMockSubdirs.workTemp}`
for (const dir of this.allTestDirectories) {
fs.mkdirSync(dir, { recursive: true })

File diff suppressed because it is too large Load Diff

View File

@@ -14,15 +14,15 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@types/node": "^17.0.23",
"@typescript-eslint/parser": "^5.18.0",
"@types/node": "^22.14.1",
"@typescript-eslint/parser": "^8.30.1",
"@zeit/ncc": "^0.22.3",
"eslint": "^8.12.0",
"eslint-plugin-github": "^4.3.6",
"prettier": "^2.6.2",
"typescript": "^4.6.3"
"eslint": "^9.24.0",
"eslint-plugin-github": "^6.0.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3"
},
"dependencies": {
"@actions/core": "^1.9.1"
"@actions/core": "^1.11.1"
}
}

View File

@@ -1,13 +0,0 @@
// eslint-disable-next-line import/no-commonjs
module.exports = {
clearMocks: true,
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
testMatch: ['**/*-test.ts'],
testRunner: 'jest-circus/runner',
transform: {
'^.+\\.ts$': 'ts-jest'
},
setupFilesAfterEnv: ['./jest.setup.js'],
verbose: true
}

View File

@@ -0,0 +1,12 @@
{
"clearMocks": true,
"moduleFileExtensions": ["js", "ts"],
"testEnvironment": "node",
"testMatch": ["**/*-test.ts"],
"testRunner": "jest-circus/runner",
"transform": {
"^.+\\.ts$": "ts-jest"
},
"verbose": true,
"testTimeout": 500000
}

View File

@@ -1 +0,0 @@
jest.setTimeout(500000)

File diff suppressed because it is too large Load Diff

View File

@@ -8,25 +8,27 @@
"build": "tsc && npx ncc build",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts"
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix"
},
"author": "",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.9.1",
"@actions/core": "^1.11.1",
"@actions/exec": "^1.1.1",
"@actions/io": "^1.1.2",
"@kubernetes/client-node": "^0.22.2",
"@actions/io": "^1.1.3",
"@kubernetes/client-node": "^1.1.2",
"hooklib": "file:../hooklib",
"js-yaml": "^4.1.0",
"shlex": "^2.1.2"
"shlex": "^2.1.2",
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/node": "^17.0.23",
"@vercel/ncc": "^0.33.4",
"jest": "^27.5.1",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
"@types/jest": "^29.5.14",
"@types/node": "^22.14.1",
"@vercel/ncc": "^0.38.3",
"jest": "^29.7.0",
"ts-jest": "^29.3.2",
"typescript": "^5.8.3"
}
}

View File

@@ -127,8 +127,10 @@ export async function createPod(
mergePodSpecWithOptions(appPod.spec, extension.spec)
}
const { body } = await k8sApi.createNamespacedPod(namespace(), appPod)
return body
return await k8sApi.createNamespacedPod({
namespace: namespace(),
body: appPod
})
}
export async function createJob(
@@ -183,46 +185,42 @@ export async function createJob(
}
}
const { body } = await k8sBatchV1Api.createNamespacedJob(namespace(), job)
return body
return await k8sBatchV1Api.createNamespacedJob({
namespace: namespace(),
body: job
})
}
export async function getContainerJobPodName(jobName: string): Promise<string> {
const selector = `job-name=${jobName}`
const backOffManager = new BackOffManager(60)
while (true) {
const podList = await k8sApi.listNamespacedPod(
namespace(),
undefined,
undefined,
undefined,
undefined,
selector,
1
)
const podList = await k8sApi.listNamespacedPod({
namespace: namespace(),
labelSelector: selector,
limit: 1
})
if (!podList.body.items?.length) {
if (!podList.items?.length) {
await backOffManager.backOff()
continue
}
if (!podList.body.items[0].metadata?.name) {
if (!podList.items[0].metadata?.name) {
throw new Error(
`Failed to determine the name of the pod for job ${jobName}`
)
}
return podList.body.items[0].metadata.name
return podList.items[0].metadata.name
}
}
export async function deletePod(podName: string): Promise<void> {
await k8sApi.deleteNamespacedPod(
podName,
namespace(),
undefined,
undefined,
0
)
await k8sApi.deleteNamespacedPod({
name: podName,
namespace: namespace(),
gracePeriodSeconds: 0
})
}
export async function execPodStep(
@@ -274,7 +272,7 @@ export async function waitForJobToComplete(jobName: string): Promise<void> {
return
}
} catch (error) {
throw new Error(`job ${jobName} has failed`)
throw new Error(`job ${jobName} has failed: ${JSON.stringify(error)}`)
}
await backOffManager.backOff()
}
@@ -315,8 +313,10 @@ export async function createDockerSecret(
)
}
const { body } = await k8sApi.createNamespacedSecret(namespace(), secret)
return body
return await k8sApi.createNamespacedSecret({
namespace: namespace(),
body: secret
})
}
export async function createSecretForEnvs(envs: {
@@ -340,30 +340,30 @@ export async function createSecretForEnvs(envs: {
secret.data[key] = Buffer.from(value).toString('base64')
}
await k8sApi.createNamespacedSecret(namespace(), secret)
await k8sApi.createNamespacedSecret({ namespace: namespace(), body: secret })
return secretName
}
export async function deleteSecret(secretName: string): Promise<void> {
await k8sApi.deleteNamespacedSecret(secretName, namespace())
await k8sApi.deleteNamespacedSecret({
name: secretName,
namespace: namespace()
})
}
export async function pruneSecrets(): Promise<void> {
const secretList = await k8sApi.listNamespacedSecret(
namespace(),
undefined,
undefined,
undefined,
undefined,
new RunnerInstanceLabel().toString()
)
if (!secretList.body.items.length) {
const secretList = await k8sApi.listNamespacedSecret({
namespace: namespace(),
labelSelector: new RunnerInstanceLabel().toString()
})
if (!secretList.items.length) {
return
}
await Promise.all(
secretList.body.items.map(
secret => secret.metadata?.name && deleteSecret(secret.metadata.name)
secretList.items.map(
async secret =>
secret.metadata?.name && deleteSecret(secret.metadata.name)
)
)
}
@@ -391,7 +391,9 @@ export async function waitForPodPhases(
await backOffManager.backOff()
}
} catch (error) {
throw new Error(`Pod ${podName} is unhealthy with phase status ${phase}`)
throw new Error(
`Pod ${podName} is unhealthy with phase status ${phase}: ${JSON.stringify(error)}`
)
}
}
@@ -422,8 +424,10 @@ async function getPodPhase(podName: string): Promise<PodPhase> {
PodPhase.FAILED,
PodPhase.UNKNOWN
])
const { body } = await k8sApi.readNamespacedPod(podName, namespace())
const pod = body
const pod = await k8sApi.readNamespacedPod({
name: podName,
namespace: namespace()
})
if (!pod.status?.phase || !podPhaseLookup.has(pod.status.phase)) {
return PodPhase.UNKNOWN
@@ -432,8 +436,10 @@ async function getPodPhase(podName: string): Promise<PodPhase> {
}
async function isJobSucceeded(jobName: string): Promise<boolean> {
const { body } = await k8sBatchV1Api.readNamespacedJob(jobName, namespace())
const job = body
const job = await k8sBatchV1Api.readNamespacedJob({
name: jobName,
namespace: namespace()
})
if (job.status?.failed) {
throw new Error(`job ${jobName} has failed`)
}
@@ -455,30 +461,26 @@ export async function getPodLogs(
process.stderr.write(err.message)
})
const r = await log.log(namespace(), podName, containerName, logStream, {
await log.log(namespace(), podName, containerName, logStream, {
follow: true,
pretty: false,
timestamps: false
})
await new Promise(resolve => r.on('close', () => resolve(null)))
await new Promise(resolve => logStream.on('close', () => resolve(null)))
}
export async function prunePods(): Promise<void> {
const podList = await k8sApi.listNamespacedPod(
namespace(),
undefined,
undefined,
undefined,
undefined,
new RunnerInstanceLabel().toString()
)
if (!podList.body.items.length) {
const podList = await k8sApi.listNamespacedPod({
namespace: namespace(),
labelSelector: new RunnerInstanceLabel().toString()
})
if (!podList.items.length) {
return
}
await Promise.all(
podList.body.items.map(
pod => pod.metadata?.name && deletePod(pod.metadata.name)
podList.items.map(
async pod => pod.metadata?.name && deletePod(pod.metadata.name)
)
)
}
@@ -486,16 +488,13 @@ export async function prunePods(): Promise<void> {
export async function getPodStatus(
name: string
): Promise<k8s.V1PodStatus | undefined> {
const { body } = await k8sApi.readNamespacedPod(name, namespace())
return body.status
const pod = await k8sApi.readNamespacedPod({ name, namespace: namespace() })
return pod.status
}
export async function isAuthPermissionsOK(): Promise<boolean> {
const sar = new k8s.V1SelfSubjectAccessReview()
const asyncs: Promise<{
response: unknown
body: k8s.V1SelfSubjectAccessReview
}>[] = []
const asyncs: Promise<k8s.V1SelfSubjectAccessReview>[] = []
for (const resource of requiredPermissions) {
for (const verb of resource.verbs) {
sar.spec = new k8s.V1SelfSubjectAccessReviewSpec()
@@ -505,11 +504,13 @@ export async function isAuthPermissionsOK(): Promise<boolean> {
sar.spec.resourceAttributes.group = resource.group
sar.spec.resourceAttributes.resource = resource.resource
sar.spec.resourceAttributes.subresource = resource.subresource
asyncs.push(k8sAuthorizationV1Api.createSelfSubjectAccessReview(sar))
asyncs.push(
k8sAuthorizationV1Api.createSelfSubjectAccessReview({ body: sar })
)
}
}
const responses = await Promise.all(asyncs)
return responses.every(resp => resp.body.status?.allowed)
return responses.every(resp => resp.status?.allowed)
}
export async function isPodContainerAlpine(
@@ -527,7 +528,7 @@ export async function isPodContainerAlpine(
podName,
containerName
)
} catch (err) {
} catch {
isAlpine = false
}
@@ -535,9 +536,12 @@ export async function isPodContainerAlpine(
}
async function getCurrentNodeName(): Promise<string> {
const resp = await k8sApi.readNamespacedPod(getRunnerPodName(), namespace())
const resp = await k8sApi.readNamespacedPod({
name: getRunnerPodName(),
namespace: namespace()
})
const nodeName = resp.body.spec?.nodeName
const nodeName = resp.spec?.nodeName
if (!nodeName) {
throw new Error('Failed to determine node name')
}
@@ -647,6 +651,5 @@ export function containerPorts(
}
export async function getPodByName(name): Promise<k8s.V1Pod> {
const { body } = await k8sApi.readNamespacedPod(name, namespace())
return body
return await k8sApi.readNamespacedPod({ name, namespace: namespace() })
}

View File

@@ -32,16 +32,12 @@ describe('Cleanup Job', () => {
kc.loadFromDefault()
const k8sApi = kc.makeApiClient(k8s.CoreV1Api)
const podList = await k8sApi.listNamespacedPod(
namespace(),
undefined,
undefined,
undefined,
undefined,
new RunnerInstanceLabel().toString()
)
const podList = await k8sApi.listNamespacedPod({
namespace: namespace(),
labelSelector: new RunnerInstanceLabel().toString()
})
expect(podList.body.items.length).toBe(0)
expect(podList.items.length).toBe(0)
})
it('should have no runner linked secrets', async () => {
@@ -51,15 +47,11 @@ describe('Cleanup Job', () => {
kc.loadFromDefault()
const k8sApi = kc.makeApiClient(k8s.CoreV1Api)
const secretList = await k8sApi.listNamespacedSecret(
namespace(),
undefined,
undefined,
undefined,
undefined,
new RunnerInstanceLabel().toString()
)
const secretList = await k8sApi.listNamespacedSecret({
namespace: namespace(),
labelSelector: new RunnerInstanceLabel().toString()
})
expect(secretList.body.items.length).toBe(0)
expect(secretList.items.length).toBe(0)
})
})