From d75362ace4412643d4d30fef9ce959b7edef9f02 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 00:36:48 +0900 Subject: [PATCH 1/8] Refactor Probot app structure and update dependencies - Removed outdated files related to GitHub Actions runner and Hetzner server management. - Introduced new implementations for GitHub and Hetzner functionalities in the `src/probot` directory. - Updated `package.json` to include `msw` for mocking API requests in tests. - Reorganized test files and added mocks for GitHub and Hetzner APIs. - Enhanced server creation and management logic with improved error handling and logging. - Updated TypeScript configuration to reflect new file structure. --- .../firebase/functions/package-lock.json | 368 +++++++++++++++++- openci-runner/firebase/functions/package.json | 2 +- .../firebase/functions/probot/github.ts | 32 -- openci-runner/firebase/functions/src/index.ts | 2 +- .../firebase/functions/src/probot/github.ts | 75 ++++ .../functions/{ => src}/probot/hetzner.ts | 56 +-- .../functions/{ => src}/probot/index.ts | 49 ++- .../firebase/functions/test/index.test.ts | 65 ---- .../firebase/functions/test/src/index.test.ts | 51 +++ .../test/src/mocks/github_api_handlers.ts | 14 + .../test/src/mocks/hetzner_api_handlers.ts | 30 ++ .../firebase/functions/test/src/mocks/node.ts | 5 + .../functions/test/src/probot/hetzner.test.ts | 41 ++ .../firebase/functions/test/vitest.setup.ts | 6 + .../firebase/functions/tsconfig.json | 2 +- .../firebase/functions/vitest.config.ts | 7 + 16 files changed, 655 insertions(+), 150 deletions(-) delete mode 100644 openci-runner/firebase/functions/probot/github.ts create mode 100644 openci-runner/firebase/functions/src/probot/github.ts rename openci-runner/firebase/functions/{ => src}/probot/hetzner.ts (56%) rename openci-runner/firebase/functions/{ => src}/probot/index.ts (77%) delete mode 100644 openci-runner/firebase/functions/test/index.test.ts create mode 100644 openci-runner/firebase/functions/test/src/index.test.ts create mode 100644 openci-runner/firebase/functions/test/src/mocks/github_api_handlers.ts create mode 100644 openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts create mode 100644 openci-runner/firebase/functions/test/src/mocks/node.ts create mode 100644 openci-runner/firebase/functions/test/src/probot/hetzner.test.ts create mode 100644 openci-runner/firebase/functions/test/vitest.setup.ts create mode 100644 openci-runner/firebase/functions/vitest.config.ts diff --git a/openci-runner/firebase/functions/package-lock.json b/openci-runner/firebase/functions/package-lock.json index 2e0c3577..d74a67fa 100644 --- a/openci-runner/firebase/functions/package-lock.json +++ b/openci-runner/firebase/functions/package-lock.json @@ -9,13 +9,13 @@ "@octokit/rest": "^22.0.0", "firebase-admin": "^13.5.0", "firebase-functions": "^6.5.0", - "nock": "^15.0.0", "node-ssh": "^13.2.1", "probot": "^14.1.0" }, "devDependencies": { "@vitest/coverage-v8": "^3.2.4", "firebase-functions-test": "^3.1.0", + "msw": "^2.11.5", "smee-client": "^4.3.1", "supertest": "^7.0.0", "typescript": "^5.9.3", @@ -1346,6 +1346,154 @@ "node": ">=6" } }, + "node_modules/@inquirer/ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz", + "integrity": "sha512-JWaTfCxI1eTmJ1BIv86vUfjVatOdxwD0DAVKYevY8SazeUUZtW+tNbsdejVO1GYE0GXJW1N1ahmiC3TFd+7wZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.18", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.18.tgz", + "integrity": "sha512-MilmWOzHa3Ks11tzvuAmFoAd/wRuaP3SwlT1IZhyMke31FKLxPiuDWcGXhU+PKveNOpAc4axzAgrgxuIJJRmLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.2.2", + "@inquirer/type": "^3.0.8" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.2.2.tgz", + "integrity": "sha512-yXq/4QUnk4sHMtmbd7irwiepjB8jXU0kkFRL4nr/aDBA2mDz13cMakEWdDwX3eSCTkk03kwcndD1zfRAIlELxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.0", + "@inquirer/figures": "^1.0.13", + "@inquirer/type": "^3.0.8", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/core/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.13.tgz", + "integrity": "sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.8.tgz", + "integrity": "sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1818,6 +1966,7 @@ "version": "0.39.7", "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.7.tgz", "integrity": "sha512-sURvQbbKsq5f8INV54YJgJEdk8oxBanqkTiXXd33rKmofFCwZLhLRszPduMZ9TA9b8/1CHc/IJmOlBHJk2Q5AQ==", + "dev": true, "license": "MIT", "dependencies": { "@open-draft/deferred-promise": "^2.2.0", @@ -2194,12 +2343,14 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, "license": "MIT" }, "node_modules/@open-draft/logger": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, "license": "MIT", "dependencies": { "is-node-process": "^1.2.0", @@ -2210,6 +2361,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, "license": "MIT" }, "node_modules/@opentelemetry/api": { @@ -3035,6 +3187,13 @@ "license": "MIT", "peer": true }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -4212,6 +4371,16 @@ "license": "MIT", "peer": true }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -5571,6 +5740,16 @@ "license": "ISC", "peer": true }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, "node_modules/gtoken": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", @@ -5634,6 +5813,13 @@ "node": ">= 0.4" } }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/help-me": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", @@ -5901,6 +6087,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, "license": "MIT" }, "node_modules/is-number": { @@ -6767,12 +6954,6 @@ "license": "MIT", "peer": true }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "license": "ISC" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7280,6 +7461,101 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/msw": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.11.5.tgz", + "integrity": "sha512-atFI4GjKSJComxcigz273honh8h4j5zzpk5kwG4tGm0TPcYne6bqmVrufeRll6auBeouIkXqZYXxVbWSWxM3RA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.39.1", + "@open-draft/deferred-promise": "^2.2.0", + "@types/statuses": "^2.0.4", + "cookie": "^1.0.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.7.0", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.0", + "type-fest": "^4.26.1", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/msw/node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/msw/node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/msw/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/nan": { "version": "2.23.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz", @@ -7340,19 +7616,6 @@ "node": ">= 0.6" } }, - "node_modules/nock": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/nock/-/nock-15.0.0.tgz", - "integrity": "sha512-EoAVk4Y8Yv4JUQz62sv8zmv+DoBblD/pht/q7aW/td1WietaFWrivzhMGGCLmx2qjpLcOrbyammedW8IRUU5TA==", - "license": "MIT", - "dependencies": { - "@mswjs/interceptors": "^0.39.5", - "json-stringify-safe": "^5.0.1" - }, - "engines": { - "node": ">=18.20.0 <20 || >=20.12.1" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -7561,6 +7824,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, "license": "MIT" }, "node_modules/p-limit": { @@ -8228,6 +8492,13 @@ "node": ">=14" } }, + "node_modules/rettime": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", + "integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", @@ -8713,6 +8984,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, "license": "MIT" }, "node_modules/string_decoder": { @@ -9297,6 +9569,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -9337,6 +9629,19 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9487,6 +9792,16 @@ "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -10151,6 +10466,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/openci-runner/firebase/functions/package.json b/openci-runner/firebase/functions/package.json index 9cdb059b..2c81412a 100644 --- a/openci-runner/firebase/functions/package.json +++ b/openci-runner/firebase/functions/package.json @@ -3,13 +3,13 @@ "@octokit/rest": "^22.0.0", "firebase-admin": "^13.5.0", "firebase-functions": "^6.5.0", - "nock": "^15.0.0", "node-ssh": "^13.2.1", "probot": "^14.1.0" }, "devDependencies": { "@vitest/coverage-v8": "^3.2.4", "firebase-functions-test": "^3.1.0", + "msw": "^2.11.5", "smee-client": "^4.3.1", "supertest": "^7.0.0", "typescript": "^5.9.3", diff --git a/openci-runner/firebase/functions/probot/github.ts b/openci-runner/firebase/functions/probot/github.ts deleted file mode 100644 index cbdfc930..00000000 --- a/openci-runner/firebase/functions/probot/github.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { Octokit } from "@octokit/rest"; - -const runnerName = "openci-runner-beta"; - -// biome-ignore lint/suspicious/noExplicitAny: -export function isJobRequired(context: any): boolean { - return context.payload.workflow_job.labels.includes(runnerName); -} - -export async function getJitConfig( - octokit: Octokit, - owner: string, - repo: string, - serverId: number, -): Promise { - const jitConfigRes = await octokit.request( - `POST /repos/{owner}/{repo}/actions/runners/generate-jitconfig`, - { - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - labels: ["openci-runner-beta"], - name: `OpenCI ランナー ${serverId}`, - owner: `${owner}`, - repo: `${repo}`, - runner_group_id: 1, - work_folder: "_work", - }, - ); - - return jitConfigRes.data.encoded_jit_config; -} diff --git a/openci-runner/firebase/functions/src/index.ts b/openci-runner/firebase/functions/src/index.ts index 29eaaa1c..4f0a5609 100644 --- a/openci-runner/firebase/functions/src/index.ts +++ b/openci-runner/firebase/functions/src/index.ts @@ -2,7 +2,7 @@ import { onRequest } from "firebase-functions/https"; import { log } from "firebase-functions/logger"; import { defineSecret } from "firebase-functions/params"; import { createNodeMiddleware, createProbot } from "probot"; -import { appFn } from "../probot/index.js"; +import { appFn } from "./probot/index.js"; const githubAppId = defineSecret("GITHUB_APP_ID"); const githubPrivateKey = defineSecret("GITHUB_PRIVATE_KEY"); diff --git a/openci-runner/firebase/functions/src/probot/github.ts b/openci-runner/firebase/functions/src/probot/github.ts new file mode 100644 index 00000000..3fc06103 --- /dev/null +++ b/openci-runner/firebase/functions/src/probot/github.ts @@ -0,0 +1,75 @@ +import type { Octokit } from "@octokit/rest"; + +const runnerName = "openci-runner-beta"; + +// biome-ignore lint/suspicious/noExplicitAny: +export function isJobRequired(context: any): boolean { + return context.payload.workflow_job.labels.includes(runnerName); +} + +export async function getJitConfig( + octokit: Octokit, + owner: string, + repo: string, + serverId: number, +): Promise { + const jitConfigRes = await octokit.request( + `POST /repos/{owner}/{repo}/actions/runners/generate-jitconfig`, + { + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + labels: ["openci-runner-beta"], + name: `OpenCI ランナー ${serverId}`, + owner: `${owner}`, + repo: `${repo}`, + runner_group_id: 1, + work_folder: "_work", + }, + ); + + return jitConfigRes.data.encoded_jit_config; +} + +export enum ActionsRunnerOS { + osx, + linux, +} + +export enum ActionsRunnerArchitecture { + x64, + arm64, +} + +const mkdir = "mkdir actions-runner"; +const cdActionRunner = "cd actions-runner"; +function downloadRunnerScriptAndUnZip( + scriptVersion: string, + os: ActionsRunnerOS, + architecture: ActionsRunnerArchitecture, +) { + const fileName = `actions-runner-${os.toString()}-${architecture.toString()}-${scriptVersion}.tar.gz`; + return `curl -o ${fileName} -L https://github.com/actions/runner/releases/download/v${scriptVersion}/${fileName} && tar xzf ./${fileName}`; +} + +function runGHAScript(runnerConfig: string) { + `tmux new -d -s runner "RUNNER_ALLOW_RUNASROOT=true ./run.sh --jitconfig ${runnerConfig}"`; +} + +export function initRunner( + scriptVersion: string, + os: ActionsRunnerOS, + architecture: ActionsRunnerArchitecture, +): string { + const command = [ + mkdir, + cdActionRunner, + downloadRunnerScriptAndUnZip(scriptVersion, os, architecture), + ]; + return command.join("&&"); +} + +export function startRunner(runnerConfig: string) { + const command = [cdActionRunner, runGHAScript(runnerConfig)]; + return command.join("&&"); +} diff --git a/openci-runner/firebase/functions/probot/hetzner.ts b/openci-runner/firebase/functions/src/probot/hetzner.ts similarity index 56% rename from openci-runner/firebase/functions/probot/hetzner.ts rename to openci-runner/firebase/functions/src/probot/hetzner.ts index 5bf99282..e3bb6191 100644 --- a/openci-runner/firebase/functions/probot/hetzner.ts +++ b/openci-runner/firebase/functions/src/probot/hetzner.ts @@ -4,6 +4,19 @@ export interface OctokitToken { token: string; } +export interface HetznerResponse { + serverId: number; + ipv4: string; +} + +export interface HetznerServerSpec { + image: string; + location: string; + name: string; + server_type: string; + ssh_keys: [string]; +} + export async function deleteServer(id: string, apiKey: string) { await fetch(`${baseUrl}/${id}`, { headers: { @@ -13,22 +26,28 @@ export async function deleteServer(id: string, apiKey: string) { }); } -export type HetznerResponse = { - serverId: number; - ipv4: string; -}; - -export async function createServer(apiKey: string): Promise { - const body = { - image: "ubuntu-24.04", - location: "fsn1", - name: crypto.randomUUID(), - server_type: "cpx41", - ssh_keys: ["openci-runner-probot"], +export function createServerSpec( + image: string = "ubuntu-24.04", + location: string = "fsn1", + name: string = crypto.randomUUID(), + serverType: string = "cpx41", + sshKeyName: string = "openci-runner-probot", +): HetznerServerSpec { + return { + image: image, + location: location, + name: name, + server_type: serverType, + ssh_keys: [sshKeyName], }; +} +export async function createServer( + apiKey: string, + serverSpec: HetznerServerSpec, +): Promise { const response = await fetch(baseUrl, { - body: JSON.stringify(body), + body: JSON.stringify(serverSpec), headers: { Authorization: `Bearer ${apiKey}`, }, @@ -60,14 +79,3 @@ export async function getServerStatusById( const jsonRes = await _response.json(); return jsonRes.server.status; } - -export function initRunner(runnerConfig: string): string { - const command = ` -mkdir actions-runner -cd actions-runner -curl -o actions-runner-linux-x64-2.328.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.328.0/actions-runner-linux-x64-2.328.0.tar.gz -tar xzf ./actions-runner-linux-x64-2.328.0.tar.gz -tmux new -d -s runner "RUNNER_ALLOW_RUNASROOT=true ./run.sh --jitconfig ${runnerConfig}" -`; - return command; -} diff --git a/openci-runner/firebase/functions/probot/index.ts b/openci-runner/firebase/functions/src/probot/index.ts similarity index 77% rename from openci-runner/firebase/functions/probot/index.ts rename to openci-runner/firebase/functions/src/probot/index.ts index 1df160c6..a4c2087e 100644 --- a/openci-runner/firebase/functions/probot/index.ts +++ b/openci-runner/firebase/functions/src/probot/index.ts @@ -2,12 +2,19 @@ import { setTimeout } from "node:timers/promises"; import { Octokit } from "@octokit/rest"; import { NodeSSH } from "node-ssh"; import type { ApplicationFunction, Context, Probot } from "probot"; -import { getJitConfig, isJobRequired } from "./github.js"; +import { + ActionsRunnerArchitecture, + ActionsRunnerOS, + getJitConfig, + initRunner, + isJobRequired, + startRunner, +} from "./github.js"; import { createServer, + createServerSpec, deleteServer, getServerStatusById, - initRunner, type OctokitToken, } from "./hetzner.js"; @@ -47,7 +54,11 @@ export const appFn: ApplicationFunction = (app: Probot) => { const owner = repository.owner.login; const repo = repository.name; - const hetznerResponse = await createServer(process.env.HETZNER_API_KEY); + const serverSpec = createServerSpec(); + const hetznerResponse = await createServer( + process.env.HETZNER_API_KEY, + serverSpec, + ); console.info("Runner server has been created"); while (true) { const status = await getServerStatusById( @@ -84,9 +95,35 @@ export const appFn: ApplicationFunction = (app: Probot) => { repo, hetznerResponse.serverId, ); - await sshResult.execCommand("apt install tmux"); - await sshResult.execCommand(initRunner(encodedJitConfig)); - console.info("Successfully start openci runner"); + + const resTmux = await sshResult.execCommand("apt install tmux"); + if (resTmux.code === 0) { + console.log("Successfully installed tmux"); + } else { + throw Error("Failed to install tmux"); + } + + const resInitRunner = await sshResult.execCommand( + initRunner( + "2.328.0", + ActionsRunnerOS.linux, + ActionsRunnerArchitecture.x64, + ), + ); + if (resInitRunner.code === 0) { + console.log("Successfully initiated GHA Runner"); + } else { + throw Error("Failed to initiate GHA Runner"); + } + + const startRunnerRes = await sshResult.execCommand( + startRunner(encodedJitConfig), + ); + if (startRunnerRes.code === 0) { + console.log("Successfully start GHA Runner"); + } else { + throw Error("Failed to start GHA Runner"); + } break; } catch (e) { console.log("error, will try again in 1 second", e); diff --git a/openci-runner/firebase/functions/test/index.test.ts b/openci-runner/firebase/functions/test/index.test.ts deleted file mode 100644 index 72731fef..00000000 --- a/openci-runner/firebase/functions/test/index.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import nock from "nock"; -import { Probot } from "probot"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { appFn } from "../lib/probot/index.js"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const privateKey = fs.readFileSync( - path.join(__dirname, "fixtures/mock-cert.pem"), - "utf-8", -); - -const payload = JSON.parse( - fs.readFileSync(path.join(__dirname, "fixtures/issues.opened.json"), "utf-8"), -); - -nock.disableNetConnect(); - -describe("My Probot app", () => { - let probot: Probot; - - const appId = 123; - - beforeEach(() => { - nock.disableNetConnect(); - probot = new Probot({ - appId: appId, - privateKey, - }); - probot.load(appFn); - }); - - test("creates a comment when an issue is opened", async () => { - const issueCreatedBody = { body: "Hello, World!" }; - - nock("https://api.github.com") - .post("/app/installations/2/access_tokens") - .reply(200, { token: "test" }); - - // Test that a comment is posted - nock("https://api.github.com") - .post("/repos/hiimbex/testing-things/issues/1/comments", (body) => { - expect(body).toEqual(issueCreatedBody); - return true; - }) - .reply(200); - - // Receive a webhook event - await probot.receive({ - id: "", - name: "issues", - payload, - }); - - expect(nock.isDone()).toBe(true); - }); - - afterEach(() => { - nock.cleanAll(); - nock.enableNetConnect(); - }); -}); diff --git a/openci-runner/firebase/functions/test/src/index.test.ts b/openci-runner/firebase/functions/test/src/index.test.ts new file mode 100644 index 00000000..e16488db --- /dev/null +++ b/openci-runner/firebase/functions/test/src/index.test.ts @@ -0,0 +1,51 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Probot } from "probot"; +import { afterEach, beforeEach, describe, test } from "vitest"; +import { appFn } from "../../lib/probot/index.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const privateKey = fs.readFileSync( + path.join(__dirname, "../fixtures/mock-cert.pem"), + "utf-8", +); + +const payload = JSON.parse( + fs.readFileSync( + path.join(__dirname, "../fixtures/issues.opened.json"), + "utf-8", + ), +); + +describe("Probot App", () => { + let probot: Probot; + + beforeEach(() => { + probot = new Probot({ + appId: 123, + privateKey, + }); + probot.load(appFn); + }); + + test("creates a comment when an issue is opened", async () => { + await probot.receive({ + id: "", + name: "issues", + payload, + }); + }); + + afterEach(() => {}); +}); + +// export async function deleteServer(id: string, apiKey: string) { +// await fetch(`${baseUrl}/${id}`, { +// headers: { +// Authorization: `Bearer ${apiKey}`, +// }, +// method: "DELETE", +// }); +// } diff --git a/openci-runner/firebase/functions/test/src/mocks/github_api_handlers.ts b/openci-runner/firebase/functions/test/src/mocks/github_api_handlers.ts new file mode 100644 index 00000000..90c57192 --- /dev/null +++ b/openci-runner/firebase/functions/test/src/mocks/github_api_handlers.ts @@ -0,0 +1,14 @@ +import { HttpResponse, http } from "msw"; + +export const githubApiHandlers = [ + http.post("https://api.github.com/app/installations/2/access_tokens", () => { + return HttpResponse.json({}); + }), + + http.post( + "https://api.github.com/repos/hiimbex/testing-things/issues/1/comments", + () => { + return HttpResponse.json({}); + }, + ), +]; diff --git a/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts b/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts new file mode 100644 index 00000000..d2028968 --- /dev/null +++ b/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts @@ -0,0 +1,30 @@ +import { HttpResponse, http } from "msw"; + +const baseUrl = "https://api.hetzner.cloud/v1/servers"; + +export const hetznerApiHandlers = [ + http.delete(`${baseUrl}/id`, () => { + return HttpResponse.json({}); + }), + + http.post(`${baseUrl}`, () => { + return HttpResponse.json({ + server: { + id: "0", + public_net: { + ipv4: { + ip: "1.1.1.1", + }, + }, + }, + }); + }), + + http.get(`${baseUrl}/id`, () => { + return HttpResponse.json({ + server: { + status: "running", + }, + }); + }), +]; diff --git a/openci-runner/firebase/functions/test/src/mocks/node.ts b/openci-runner/firebase/functions/test/src/mocks/node.ts new file mode 100644 index 00000000..2d311816 --- /dev/null +++ b/openci-runner/firebase/functions/test/src/mocks/node.ts @@ -0,0 +1,5 @@ +import { setupServer } from "msw/node"; +import { githubApiHandlers } from "./github_api_handlers.js"; +import { hetznerApiHandlers } from "./hetzner_api_handlers.js"; + +export const server = setupServer(...githubApiHandlers, ...hetznerApiHandlers); diff --git a/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts b/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts new file mode 100644 index 00000000..6eef67fa --- /dev/null +++ b/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from "vitest"; +import { + createServer, + createServerSpec, + deleteServer, + getServerStatusById, +} from "../../../lib/probot/hetzner.js"; + +describe("Hetzner", () => { + test("delete a server", async () => { + await deleteServer("id", "apiKey"); + }); + + test("create a server spec", async () => { + const imageName = "ubuntu-24.04"; + const location = "fsn1"; + const serverType = "cpx41"; + const sshKeyName = "openci-runner-probot"; + + const spec = createServerSpec(); + expect(spec.image).toStrictEqual(imageName); + expect(spec.location).toStrictEqual(location); + expect(spec.name).toMatch( + /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, + ); + expect(spec.server_type).toStrictEqual(serverType); + expect(spec.ssh_keys[0]).toStrictEqual(sshKeyName); + }); + + test("create a server", async () => { + const serverSpec = createServerSpec(); + const result = await createServer("api_key", serverSpec); + expect(result.ipv4).toBe("1.1.1.1"); + expect(result.serverId).toBe("0"); + }); + + test("get a server status", async () => { + const result = await getServerStatusById("id", "apiKey"); + expect(result).toBe("running"); + }); +}); diff --git a/openci-runner/firebase/functions/test/vitest.setup.ts b/openci-runner/firebase/functions/test/vitest.setup.ts new file mode 100644 index 00000000..71665c45 --- /dev/null +++ b/openci-runner/firebase/functions/test/vitest.setup.ts @@ -0,0 +1,6 @@ +import { afterAll, afterEach, beforeAll } from "vitest"; +import { server } from "./src/mocks/node.js"; + +beforeAll(() => server.listen()); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/openci-runner/firebase/functions/tsconfig.json b/openci-runner/firebase/functions/tsconfig.json index 3c4b9a97..0c39d901 100644 --- a/openci-runner/firebase/functions/tsconfig.json +++ b/openci-runner/firebase/functions/tsconfig.json @@ -14,6 +14,6 @@ }, "include": [ "src", - "probot" + "src/probot" ] } \ No newline at end of file diff --git a/openci-runner/firebase/functions/vitest.config.ts b/openci-runner/firebase/functions/vitest.config.ts new file mode 100644 index 00000000..26cd10b4 --- /dev/null +++ b/openci-runner/firebase/functions/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + setupFiles: ["./test/vitest.setup.ts"], + }, +}); From 9a724d311943b8a6d9e3b4a29682779054837931 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 00:48:47 +0900 Subject: [PATCH 2/8] Update main entry point in package.json to lib/index.js --- openci-runner/firebase/functions/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openci-runner/firebase/functions/package.json b/openci-runner/firebase/functions/package.json index 2c81412a..3d679c1a 100644 --- a/openci-runner/firebase/functions/package.json +++ b/openci-runner/firebase/functions/package.json @@ -18,7 +18,7 @@ "engines": { "node": "22" }, - "main": "lib/src/index.js", + "main": "lib/index.js", "name": "functions", "private": true, "scripts": { From 3fe08a416e1ac2f1a9626ded584b9017f6a07385 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 03:58:09 +0900 Subject: [PATCH 3/8] Refactor ActionsRunner enums and improve file name generation in downloadRunnerScriptAndUnZip function --- openci-runner/firebase/functions/src/probot/github.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openci-runner/firebase/functions/src/probot/github.ts b/openci-runner/firebase/functions/src/probot/github.ts index 3fc06103..43d5cca5 100644 --- a/openci-runner/firebase/functions/src/probot/github.ts +++ b/openci-runner/firebase/functions/src/probot/github.ts @@ -32,13 +32,13 @@ export async function getJitConfig( } export enum ActionsRunnerOS { - osx, - linux, + osx = "osx", + linux = "linux", } export enum ActionsRunnerArchitecture { - x64, - arm64, + x64 = "x64", + arm64 = "arm64", } const mkdir = "mkdir actions-runner"; @@ -48,7 +48,7 @@ function downloadRunnerScriptAndUnZip( os: ActionsRunnerOS, architecture: ActionsRunnerArchitecture, ) { - const fileName = `actions-runner-${os.toString()}-${architecture.toString()}-${scriptVersion}.tar.gz`; + const fileName = `actions-runner-${os}-${architecture}-${scriptVersion}.tar.gz`; return `curl -o ${fileName} -L https://github.com/actions/runner/releases/download/v${scriptVersion}/${fileName} && tar xzf ./${fileName}`; } From 063fefe038df0f209db162836bf860ed2d936d83 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 04:13:31 +0900 Subject: [PATCH 4/8] Fix runGHAScript function to return command string and add unit tests for ActionsRunner enums --- .../firebase/functions/src/probot/github.ts | 2 +- .../functions/test/src/probot/github.test.ts | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 openci-runner/firebase/functions/test/src/probot/github.test.ts diff --git a/openci-runner/firebase/functions/src/probot/github.ts b/openci-runner/firebase/functions/src/probot/github.ts index 43d5cca5..deabf92d 100644 --- a/openci-runner/firebase/functions/src/probot/github.ts +++ b/openci-runner/firebase/functions/src/probot/github.ts @@ -53,7 +53,7 @@ function downloadRunnerScriptAndUnZip( } function runGHAScript(runnerConfig: string) { - `tmux new -d -s runner "RUNNER_ALLOW_RUNASROOT=true ./run.sh --jitconfig ${runnerConfig}"`; + return `tmux new -d -s runner "RUNNER_ALLOW_RUNASROOT=true ./run.sh --jitconfig ${runnerConfig}"`; } export function initRunner( diff --git a/openci-runner/firebase/functions/test/src/probot/github.test.ts b/openci-runner/firebase/functions/test/src/probot/github.test.ts new file mode 100644 index 00000000..00f80bc7 --- /dev/null +++ b/openci-runner/firebase/functions/test/src/probot/github.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test } from "vitest"; +import { + ActionsRunnerArchitecture, + ActionsRunnerOS, +} from "../../../src/probot/github"; + +describe("github", () => { + test("ActionsRunnerOS", () => { + const osx = ActionsRunnerOS.osx; + expect(osx).toBe("osx"); + + const linux = ActionsRunnerOS.linux; + expect(linux).toBe("linux"); + }); + + test("ActionsRunnerArchitecture", () => { + const arm64 = ActionsRunnerArchitecture.arm64; + expect(arm64).toBe("arm64"); + + const x64 = ActionsRunnerArchitecture.x64; + expect(x64).toBe("x64"); + }); +}); From 55136db7622d8783fa06e0569c74b19043dcafc8 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 05:08:56 +0900 Subject: [PATCH 5/8] Refactor JitConfig request handling and add unit tests for jitConfigRequestBody --- .../firebase/functions/src/probot/github.ts | 48 ++++++++++++++----- .../firebase/functions/test/src/index.test.ts | 13 +---- .../functions/test/src/probot/github.test.ts | 20 ++++++++ .../functions/test/src/probot/hetzner.test.ts | 4 +- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/openci-runner/firebase/functions/src/probot/github.ts b/openci-runner/firebase/functions/src/probot/github.ts index deabf92d..480448eb 100644 --- a/openci-runner/firebase/functions/src/probot/github.ts +++ b/openci-runner/firebase/functions/src/probot/github.ts @@ -7,25 +7,47 @@ export function isJobRequired(context: any): boolean { return context.payload.workflow_job.labels.includes(runnerName); } +const generateJitConfigPath = + "/repos/{owner}/{repo}/actions/runners/generate-jitconfig"; + +type JitConfigRequest = { + headers: Record; + labels: string[]; + name: string; + owner: string; + repo: string; + runner_group_id: number; + work_folder: string; +}; + +export function jitConfigRequestBody( + owner: string, + repo: string, + serverId: number, +): JitConfigRequest { + return { + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + labels: ["openci-runner-beta"], + name: `OpenCI ランナー ${serverId}`, + owner: `${owner}`, + repo: `${repo}`, + runner_group_id: 1, + work_folder: "_work", + }; +} + export async function getJitConfig( octokit: Octokit, owner: string, repo: string, serverId: number, ): Promise { + const body = jitConfigRequestBody(owner, repo, serverId); const jitConfigRes = await octokit.request( - `POST /repos/{owner}/{repo}/actions/runners/generate-jitconfig`, - { - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - labels: ["openci-runner-beta"], - name: `OpenCI ランナー ${serverId}`, - owner: `${owner}`, - repo: `${repo}`, - runner_group_id: 1, - work_folder: "_work", - }, + `POST ${generateJitConfigPath}`, + body, ); return jitConfigRes.data.encoded_jit_config; @@ -42,7 +64,9 @@ export enum ActionsRunnerArchitecture { } const mkdir = "mkdir actions-runner"; + const cdActionRunner = "cd actions-runner"; + function downloadRunnerScriptAndUnZip( scriptVersion: string, os: ActionsRunnerOS, diff --git a/openci-runner/firebase/functions/test/src/index.test.ts b/openci-runner/firebase/functions/test/src/index.test.ts index e16488db..3c2680b1 100644 --- a/openci-runner/firebase/functions/test/src/index.test.ts +++ b/openci-runner/firebase/functions/test/src/index.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { Probot } from "probot"; -import { afterEach, beforeEach, describe, test } from "vitest"; +import { beforeEach, describe, test } from "vitest"; import { appFn } from "../../lib/probot/index.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -37,15 +37,4 @@ describe("Probot App", () => { payload, }); }); - - afterEach(() => {}); }); - -// export async function deleteServer(id: string, apiKey: string) { -// await fetch(`${baseUrl}/${id}`, { -// headers: { -// Authorization: `Bearer ${apiKey}`, -// }, -// method: "DELETE", -// }); -// } diff --git a/openci-runner/firebase/functions/test/src/probot/github.test.ts b/openci-runner/firebase/functions/test/src/probot/github.test.ts index 00f80bc7..7dae2d4d 100644 --- a/openci-runner/firebase/functions/test/src/probot/github.test.ts +++ b/openci-runner/firebase/functions/test/src/probot/github.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest"; import { ActionsRunnerArchitecture, ActionsRunnerOS, + jitConfigRequestBody, } from "../../../src/probot/github"; describe("github", () => { @@ -20,4 +21,23 @@ describe("github", () => { const x64 = ActionsRunnerArchitecture.x64; expect(x64).toBe("x64"); }); + + describe("generate a jit-config for self-hosted runner", () => { + const owner = "open-ci-io"; + const repo = "openci"; + const serverId = 0; + test("jitConfigRequestBody", () => { + const res = jitConfigRequestBody(owner, repo, serverId); + + expect(res.headers).toStrictEqual({ + "X-GitHub-Api-Version": "2022-11-28", + }); + expect(res.labels).toStrictEqual(["openci-runner-beta"]); + expect(res.name).toBe(`OpenCI ランナー ${serverId}`); + expect(res.owner).toBe(owner); + expect(res.repo).toBe(repo); + expect(res.runner_group_id).toBe(1); + expect(res.work_folder).toBe("_work"); + }); + }); }); diff --git a/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts b/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts index 6eef67fa..5898e66d 100644 --- a/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts +++ b/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts @@ -4,7 +4,7 @@ import { createServerSpec, deleteServer, getServerStatusById, -} from "../../../lib/probot/hetzner.js"; +} from "../../../src/probot/hetzner"; describe("Hetzner", () => { test("delete a server", async () => { @@ -35,7 +35,7 @@ describe("Hetzner", () => { }); test("get a server status", async () => { - const result = await getServerStatusById("id", "apiKey"); + const result = await getServerStatusById(0, "apiKey"); expect(result).toBe("running"); }); }); From 3c58b3ef317ae744595a291894672bab65b181a6 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 11:53:06 +0900 Subject: [PATCH 6/8] Refactor hetznerApiHandlers to use correct path parameters and clean up unused imports in github.test.ts --- .../test/src/mocks/hetzner_api_handlers.ts | 4 ++-- .../functions/test/src/probot/github.test.ts | 22 +------------------ 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts b/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts index d2028968..50ab3237 100644 --- a/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts +++ b/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts @@ -3,7 +3,7 @@ import { HttpResponse, http } from "msw"; const baseUrl = "https://api.hetzner.cloud/v1/servers"; export const hetznerApiHandlers = [ - http.delete(`${baseUrl}/id`, () => { + http.delete<{ id: string }>(`${baseUrl}/:id`, () => { return HttpResponse.json({}); }), @@ -20,7 +20,7 @@ export const hetznerApiHandlers = [ }); }), - http.get(`${baseUrl}/id`, () => { + http.get<{ id: string }>(`${baseUrl}/:id`, () => { return HttpResponse.json({ server: { status: "running", diff --git a/openci-runner/firebase/functions/test/src/probot/github.test.ts b/openci-runner/firebase/functions/test/src/probot/github.test.ts index 7dae2d4d..5687f524 100644 --- a/openci-runner/firebase/functions/test/src/probot/github.test.ts +++ b/openci-runner/firebase/functions/test/src/probot/github.test.ts @@ -1,27 +1,7 @@ import { describe, expect, test } from "vitest"; -import { - ActionsRunnerArchitecture, - ActionsRunnerOS, - jitConfigRequestBody, -} from "../../../src/probot/github"; +import { jitConfigRequestBody } from "../../../src/probot/github"; describe("github", () => { - test("ActionsRunnerOS", () => { - const osx = ActionsRunnerOS.osx; - expect(osx).toBe("osx"); - - const linux = ActionsRunnerOS.linux; - expect(linux).toBe("linux"); - }); - - test("ActionsRunnerArchitecture", () => { - const arm64 = ActionsRunnerArchitecture.arm64; - expect(arm64).toBe("arm64"); - - const x64 = ActionsRunnerArchitecture.x64; - expect(x64).toBe("x64"); - }); - describe("generate a jit-config for self-hosted runner", () => { const owner = "open-ci-io"; const repo = "openci"; From 978f19c594cdb1dba8cda39d5ad9a41e547491d8 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 11:58:08 +0900 Subject: [PATCH 7/8] Fix server ID type in hetznerApiHandlers and related tests to use number instead of string --- .../firebase/functions/test/src/mocks/hetzner_api_handlers.ts | 2 +- .../firebase/functions/test/src/probot/hetzner.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts b/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts index 50ab3237..14bcbf45 100644 --- a/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts +++ b/openci-runner/firebase/functions/test/src/mocks/hetzner_api_handlers.ts @@ -10,7 +10,7 @@ export const hetznerApiHandlers = [ http.post(`${baseUrl}`, () => { return HttpResponse.json({ server: { - id: "0", + id: 0, public_net: { ipv4: { ip: "1.1.1.1", diff --git a/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts b/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts index 5898e66d..b89810e4 100644 --- a/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts +++ b/openci-runner/firebase/functions/test/src/probot/hetzner.test.ts @@ -31,7 +31,7 @@ describe("Hetzner", () => { const serverSpec = createServerSpec(); const result = await createServer("api_key", serverSpec); expect(result.ipv4).toBe("1.1.1.1"); - expect(result.serverId).toBe("0"); + expect(result.serverId).toBe(0); }); test("get a server status", async () => { From 2e565f136b44592f04e889f8d4a8e5e85ade2dc0 Mon Sep 17 00:00:00 2001 From: mafreud Date: Tue, 14 Oct 2025 13:02:52 +0900 Subject: [PATCH 8/8] Refactor runGHAScript and startRunner functions to specify return type as string, and update command concatenation for clarity. Update ssh command in appFn to include package update before installation. --- openci-runner/firebase/functions/src/probot/github.ts | 8 ++++---- openci-runner/firebase/functions/src/probot/hetzner.ts | 2 +- openci-runner/firebase/functions/src/probot/index.ts | 4 +++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/openci-runner/firebase/functions/src/probot/github.ts b/openci-runner/firebase/functions/src/probot/github.ts index 480448eb..a4506eb6 100644 --- a/openci-runner/firebase/functions/src/probot/github.ts +++ b/openci-runner/firebase/functions/src/probot/github.ts @@ -76,7 +76,7 @@ function downloadRunnerScriptAndUnZip( return `curl -o ${fileName} -L https://github.com/actions/runner/releases/download/v${scriptVersion}/${fileName} && tar xzf ./${fileName}`; } -function runGHAScript(runnerConfig: string) { +function runGHAScript(runnerConfig: string): string { return `tmux new -d -s runner "RUNNER_ALLOW_RUNASROOT=true ./run.sh --jitconfig ${runnerConfig}"`; } @@ -90,10 +90,10 @@ export function initRunner( cdActionRunner, downloadRunnerScriptAndUnZip(scriptVersion, os, architecture), ]; - return command.join("&&"); + return command.join(" && "); } -export function startRunner(runnerConfig: string) { +export function startRunner(runnerConfig: string): string { const command = [cdActionRunner, runGHAScript(runnerConfig)]; - return command.join("&&"); + return command.join(" && "); } diff --git a/openci-runner/firebase/functions/src/probot/hetzner.ts b/openci-runner/firebase/functions/src/probot/hetzner.ts index e3bb6191..989c579d 100644 --- a/openci-runner/firebase/functions/src/probot/hetzner.ts +++ b/openci-runner/firebase/functions/src/probot/hetzner.ts @@ -14,7 +14,7 @@ export interface HetznerServerSpec { location: string; name: string; server_type: string; - ssh_keys: [string]; + ssh_keys: string[]; } export async function deleteServer(id: string, apiKey: string) { diff --git a/openci-runner/firebase/functions/src/probot/index.ts b/openci-runner/firebase/functions/src/probot/index.ts index a4c2087e..b1545625 100644 --- a/openci-runner/firebase/functions/src/probot/index.ts +++ b/openci-runner/firebase/functions/src/probot/index.ts @@ -96,7 +96,9 @@ export const appFn: ApplicationFunction = (app: Probot) => { hetznerResponse.serverId, ); - const resTmux = await sshResult.execCommand("apt install tmux"); + const resTmux = await sshResult.execCommand( + "apt-get update -y && apt-get install -y tmux", + ); if (resTmux.code === 0) { console.log("Successfully installed tmux"); } else {