-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement Hetzner server management and update Probot integration #476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughHetzner Cloud と GitHub Actions ランナーの JIT プロビジョニング(作成・稼働待機・初期化)と終了処理を行う Probot ハンドラ群と Hetzner 統合モジュールを追加し、依存・start スクリプト・TS 設定・シークレット定義・スペル辞書・CI マトリクスを更新しました。 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant GH as GitHub (workflow_job)
participant Probot as Probot App
participant HZ as Hetzner API
participant VM as Hetzner Server (SSH)
participant GHAPI as GitHub API (Actions)
GH->>Probot: workflow_job.queued
Probot->>Probot: authenticate as installation (token)
Probot->>Probot: isJobRequired?
alt required
Probot->>HZ: POST /servers (createServer)
HZ-->>Probot: { serverId, ipv4 }
loop Poll (1s, retry limit)
Probot->>HZ: GET /servers/{id} (status)
HZ-->>Probot: { status }
end
Probot->>VM: SSH 接続(node-ssh)→ 必要パッケージ・tmux を準備
Probot->>GHAPI: POST /repos/{owner}/{repo}/actions/runners/generate-jitconfig
GHAPI-->>Probot: { encoded_jit_config }
Probot->>VM: 転送 & 実行 (initRunner を tmux で起動)
Note over VM,Probot: ランナーが tmux で起動
else not required
Probot-->>GH: no-op
end
sequenceDiagram
autonumber
participant GH as GitHub (workflow_job)
participant Probot as Probot App
participant HZ as Hetzner API
GH->>Probot: workflow_job.completed
Probot->>Probot: isJobRequired? -> derive runnerId
alt runnerId found
Probot->>HZ: DELETE /servers/{runnerId}
HZ-->>Probot: 削除確認
Probot-->>GH: ログ(クリーンアップ完了)
else none
Probot-->>GH: no-op
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro Disabled knowledge base sources:
📒 Files selected for processing (1)
🧰 Additional context used🧬 Code graph analysis (1)openci-runner/firebase/functions/probot/index.ts (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (3)
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (2)
openci-runner/firebase/functions/tsconfig.json (1)
7-7:noUnusedLocalsを false にする必要がありますか?未使用ローカルの検出が止まると、タイポや未使用変数が紛れ込んでもビルド時に検知できず、今回のような外部連携コードでは不具合の温床になります。
tsconfig.json側で明示的な警告抑止を入れるのではなく、該当箇所で不要な変数を削除するか抑制コメントを個別に付与する方向をご検討ください。openci-runner/firebase/functions/probot/index.ts (1)
81-90: GitHub ホストランナーの場合は早期 return してください。
runnerNameが null のとき(GitHub ホストランナー)でも、後続でdeleteServerが呼ばれ、undefinedID で API を叩いてしまいます。ログ出力後にreturnして処理を打ち切ってください。if (runnerName == null) { console.log("This runner is GitHub hosted one"); + return; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (1)
openci-runner/firebase/functions/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
openci-runner/firebase/functions/package.json(2 hunks)openci-runner/firebase/functions/probot/hetzner.ts(1 hunks)openci-runner/firebase/functions/probot/index.ts(1 hunks)openci-runner/firebase/functions/tsconfig.json(1 hunks)openci.code-workspace(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
openci-runner/firebase/functions/probot/index.ts (1)
openci-runner/firebase/functions/probot/hetzner.ts (6)
OctokitToken(5-7)createServer(23-51)getServerStatusById(53-64)getJitConfig(66-88)initRunner(90-99)deleteServer(9-16)
openci-runner/firebase/functions/package.json (2)
openci-runner/firebase/functions/test/index.test.ts (3)
probot(28-76)mock(47-70)nock(32-45)openci-runner/firebase/functions/src/index.ts (1)
req(15-31)
🪛 GitHub Actions: Deploy GitHub Apps
openci-runner/firebase/functions/probot/hetzner.ts
[error] 1-30: Cannot find module '@octokit/rest' or its corresponding type declarations.
| await sshResult.execCommand("apt install tmux"); | ||
| await sshResult.execCommand(initRunner(encodedJitConfig)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
apt install tmux に非対話オプションを付けてください。
非対話セッションで apt install tmux を実行すると、確認プロンプトで待ち続けてしまい SSH コマンドがハングします。DEBIAN_FRONTEND=noninteractive と -y を付け、必要なら apt-get update も合わせて実行してください。
- await sshResult.execCommand("apt install tmux");
+ await sshResult.execCommand(
+ "DEBIAN_FRONTEND=noninteractive apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y tmux",
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await sshResult.execCommand("apt install tmux"); | |
| await sshResult.execCommand(initRunner(encodedJitConfig)); | |
| // install tmux in non-interactive mode (update first, then install with -y) | |
| await sshResult.execCommand( | |
| "DEBIAN_FRONTEND=noninteractive apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y tmux", | |
| ); | |
| await sshResult.execCommand(initRunner(encodedJitConfig)); |
🤖 Prompt for AI Agents
In openci-runner/firebase/functions/probot/index.ts around lines 68-69, the code
runs "apt install tmux" in a non-interactive SSH session which can hang on
confirmation prompts; change it to run a non-interactive, affirmative install
(use DEBIAN_FRONTEND=noninteractive and -y) and prefer apt-get, and run apt-get
update beforehand if package lists may be stale (e.g. run apt-get update &&
DEBIAN_FRONTEND=noninteractive apt-get install -y tmux) so the SSH command won't
wait for user input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (3)
openci-runner/firebase/functions/probot/index.ts (3)
43-55: サーバー起動待ちのタイムアウト制御が未対応です。この無限ループの問題は以前のレビューで指摘されています。Hetznerサーバーが「running」状態にならない場合、関数が永遠に待ち続けてしまいます。タイムアウト制御の追加が必要です。
57-88: リトライ上限到達時のエラー処理が未対応です。SSH接続とrunner初期化のリトライが最大回数に達しても例外を投げていない問題は、以前のレビューで指摘されています。失敗時にワークフローを適切に失敗させる必要があります。
78-78: apt installの非対話オプションが未対応です。
apt install tmuxに非対話オプションが必要な問題は、以前のレビューで指摘されています。SSH セッションでプロンプト待ちによるハングを防ぐため、DEBIAN_FRONTEND=noninteractiveと-yオプションが必要です。
🧹 Nitpick comments (1)
openci-runner/firebase/functions/probot/index.ts (1)
23-101: エラーハンドリングの追加を検討してください。各イベントハンドラにtry-catchブロックがないため、予期しないエラーが発生した場合にログに記録されない可能性があります。特にworkflow_job.queuedとworkflow_job.completedハンドラでは、外部API呼び出しやSSH接続など、失敗する可能性のある操作が多数あります。
トップレベルでtry-catchを追加し、エラーを適切にログ出力することをお勧めします。
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
openci-runner/firebase/functions/probot/index.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
openci-runner/firebase/functions/probot/index.ts (1)
openci-runner/firebase/functions/probot/hetzner.ts (6)
OctokitToken(5-7)createServer(23-51)getServerStatusById(53-64)getJitConfig(66-88)initRunner(90-99)deleteServer(9-16)
🔇 Additional comments (2)
openci-runner/firebase/functions/probot/index.ts (2)
1-12: LGTM!インポートは適切です。
node:timers/promisesからのsetTimeoutの使用は正しいです。
17-21: LGTM!シンプルなコメント投稿ハンドラで、問題ありません。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (4)
openci-runner/firebase/functions/probot/index.ts (4)
40-52: サーバー起動待ちに上限時間を設けてください。この問題は以前のレビューで指摘されています。Hetzner側が「running」にならない場合、
while (true)が永遠に実行され続けます。タイムアウト制御を追加して、一定時間後にエラーを投げるようにしてください。以下の修正を適用してください:
- while (true) { + const maxStatusChecks = 60; + let statusCheckCount = 0; + let status; + while (statusCheckCount < maxStatusChecks) { - const status = await getServerStatusById( + status = await getServerStatusById( hetznerResponse.serverId, process.env.HETZNER_API_KEY, ); if (status === "running") { console.log("Server is now running!"); break; } else { console.log("Waiting for the server init. Will try again in 1 second"); await setTimeout(1000); + statusCheckCount++; } } + if (status !== "running") { + console.error(`Server ${hetznerResponse.serverId} failed to reach running state after ${maxStatusChecks} attempts`); + throw new Error(`Timed out waiting for Hetzner server ${hetznerResponse.serverId} to become running`); + }
54-84: 最大リトライ失敗時に例外を投げてください。この問題は以前のレビューで指摘されています。SSH接続やrunner初期化に最大回数失敗しても、そのまま関数が終了し、ジョブが成功したように見えます。
retryCount >= maxRetryの場合はエラーを投げてください。以下の修正を適用してください:
while (retryCount < maxRetry) { console.log("retry:", retryCount); try { // ... existing code ... console.log("Successfully start openci runner"); break; } catch (e) { console.log("error, will try again in 1 second", e); await setTimeout(1000); retryCount++; } } + if (retryCount >= maxRetry) { + console.error(`Failed to establish SSH connection and bootstrap runner after ${maxRetry} attempts for server ${hetznerResponse.serverId}`); + throw new Error(`Failed to establish SSH connection and bootstrap runner for server ${hetznerResponse.serverId}`); + } });
75-76:apt install tmuxに非対話オプションを付けてください。この問題は以前のレビューで指摘されています。非対話セッションで
apt install tmuxを実行すると、確認プロンプトで待ち続けてSSHコマンドがハングします。DEBIAN_FRONTEND=noninteractiveと-yを付けてください。以下の修正を適用してください:
- await sshResult.execCommand("apt install tmux"); + await sshResult.execCommand( + "DEBIAN_FRONTEND=noninteractive apt-get update -y && DEBIAN_FRONTEND=noninteractive apt-get install -y tmux", + );
93-102: runnerNameがnullの場合は早期returnしてください。この問題は以前のレビューで指摘されています。Line 94-96で
runnerNameがnullの場合をログ出力していますが、その後もLine 98でrunnerIdを抽出し、Line 101でdeleteServerを呼び出しています。runnerNameがnullの場合、runnerIdもundefinedになり、deleteServerにundefinedが渡されてエラーになります。以下の修正を適用してください:
const runnerName = context.payload.workflow_job.runner_name; if (runnerName == null) { console.log("This runner is GitHub hosted one"); + return; } const defaultRunnerName = "OpenCI ランナー "; const runnerId = runnerName?.replace(defaultRunnerName, ""); console.log("runner id", runnerId); + if (!runnerId) { + console.log("Failed to extract runner ID from runner name"); + return; + } + await deleteServer(runnerId, process.env.HETZNER_API_KEY); console.log("Finish cleaning up"); });
🧹 Nitpick comments (2)
openci-runner/firebase/functions/probot/index.ts (2)
24-38: 環境変数の存在確認を追加してください。初期セットアップは適切ですが、
process.env.HETZNER_API_KEYなどの環境変数が未定義の場合の処理がありません。後続の処理で使用される前に、必要な環境変数の存在を確認してください。以下の修正を適用してください:
app.on("workflow_job.queued", async (context) => { console.log("workflow_job.queued"); + + // 環境変数の検証 + if (!process.env.HETZNER_API_KEY || !process.env.HETZNER_SSH_PRIVATE_KEY || !process.env.HETZNER_SSH_PASSPHRASE) { + console.error("Missing required environment variables"); + throw new Error("HETZNER_API_KEY, HETZNER_SSH_PRIVATE_KEY, and HETZNER_SSH_PASSPHRASE must be set"); + } + const { token } = (await context.octokit.auth({ type: "installation", })) as OctokitToken;
53-84: SSH接続のクリーンアップを追加してください。SSH接続が確立された後、明示的にクローズされていません。接続が成功した場合でも失敗した場合でも、
ssh.dispose()を呼び出してリソースをクリーンアップしてください。以下の修正を適用してください:
const ssh = new NodeSSH(); const maxRetry = 10; let retryCount = 0; + let connected = false; while (retryCount < maxRetry) { console.log("retry:", retryCount); try { const sshResult = await ssh.connect({ host: hetznerResponse.ipv4, username: "root", privateKey: process.env.HETZNER_SSH_PRIVATE_KEY, passphrase: process.env.HETZNER_SSH_PASSPHRASE, }); + connected = true; const octokit = new Octokit({ auth: token, }); const encodedJitConfig = await getJitConfig( octokit, owner, repo, hetznerResponse.serverId, ); await sshResult.execCommand("apt install tmux"); await sshResult.execCommand(initRunner(encodedJitConfig)); console.log("Successfully start openci runner"); break; } catch (e) { console.log("error, will try again in 1 second", e); await setTimeout(1000); retryCount++; + } finally { + if (connected) { + ssh.dispose(); + } } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
openci-runner/firebase/functions/probot/index.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
openci-runner/firebase/functions/probot/index.ts (2)
openci-runner/firebase/functions/probot/hetzner.ts (6)
OctokitToken(5-7)createServer(23-51)getServerStatusById(53-64)getJitConfig(66-88)initRunner(90-99)deleteServer(9-16)openci-runner/firebase/functions/probot/github.ts (1)
isJobRequired(4-6)
🔇 Additional comments (2)
openci-runner/firebase/functions/probot/index.ts (2)
1-13: インポートは適切です。必要なモジュールが正しくインポートされています。Octokit、NodeSSH、Hetzner関連のユーティリティ関数が適切に読み込まれています。
18-22: LGTM!イベントハンドラーがissues.reopenedに変更され、コメント本文も更新されています。テスト用途として適切な実装です。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
openci-runner/firebase/functions/src/index.ts(2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: deploy-firebase-functions-dev / ci_and_deploy_firebase_functions
🔇 Additional comments (1)
openci-runner/firebase/functions/src/index.ts (1)
25-29: LGTM! (15 行目の修正が前提)Hetzner 認証情報を環境変数として Probot に渡す実装は適切です。ただし、15 行目の
secrets配列に Hetzner シークレットを追加する必要があります(別のコメントを参照)。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (5)
openci-runner/firebase/functions/probot/index.ts (5)
39-39: サーバー作成失敗時のエラーハンドリングが不足しています。過去のレビューで指摘された
createServerのエラーハンドリングがまだ追加されていません。Hetzner APIの呼び出しが失敗した場合、適切にエラーを投げてワークフローを失敗させる必要があります。
40-52: サーバー起動待ちに上限時間を設定してください。過去のレビューで指摘されたタイムアウト制御がまだ実装されていません。
while(true)が無限ループになる可能性があり、Hetznerサーバーが「running」ステータスにならない場合、処理が永遠に停止します。一定回数または時間で打ち切る制御を追加してください。
54-84: 最大リトライ失敗時に例外を投げてください。過去のレビューで指摘されたように、SSH接続やランナー初期化に最大回数失敗しても、そのまま関数が終了してしまいます。
retryCount >= maxRetryの場合はエラーを投げてワークフローを失敗させ、運用者が検知できるようにしてください。
75-75:apt install tmuxに非対話オプションを付けてください。過去のレビューで指摘されたように、非対話セッションで
apt install tmuxを実行すると、確認プロンプトで待ち続けてSSHコマンドがハングします。DEBIAN_FRONTEND=noninteractiveと-yオプションを付けてください。
94-104: runnerNameがnullの場合の処理に不備があります。過去のレビューで指摘されたように、Line 95-97で
runnerNameがnullの場合をログ出力していますが、その後も処理が継続し、Line 102でdeleteServerにundefinedが渡されてエラーになります。
runnerNameがnullの場合は早期returnしてください。以下の修正を適用してください:
const runnerName = context.payload.workflow_job.runner_name; if (runnerName == null) { console.log("This runner is GitHub hosted one"); + return; } const defaultRunnerName = "OpenCI ランナー ";
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
openci-runner/firebase/functions/probot/index.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
openci-runner/firebase/functions/probot/index.ts (2)
openci-runner/firebase/functions/probot/hetzner.ts (6)
OctokitToken(5-7)createServer(23-51)getServerStatusById(53-64)getJitConfig(66-88)initRunner(90-99)deleteServer(9-16)openci-runner/firebase/functions/probot/github.ts (1)
isJobRequired(4-6)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: deploy-firebase-functions-dev / ci_and_deploy_firebase_functions
| const hetznerResponse = await createServer(process.env.HETZNER_API_KEY); | ||
| while (true) { | ||
| const status = await getServerStatusById( | ||
| hetznerResponse.serverId, | ||
| process.env.HETZNER_API_KEY, | ||
| ); | ||
| if (status === "running") { | ||
| console.log("Server is now running!"); | ||
| break; | ||
| } else { | ||
| console.log("Waiting for the server init. Will try again in 1 second"); | ||
| await setTimeout(1000); | ||
| } | ||
| } | ||
| const ssh = new NodeSSH(); | ||
| const maxRetry = 10; | ||
| let retryCount = 0; | ||
| while (retryCount < maxRetry) { | ||
| console.log("retry:", retryCount); | ||
| try { | ||
| const sshResult = await ssh.connect({ | ||
| host: hetznerResponse.ipv4, | ||
| username: "root", | ||
| privateKey: process.env.HETZNER_SSH_PRIVATE_KEY, | ||
| passphrase: process.env.HETZNER_SSH_PASSPHRASE, | ||
| }); | ||
|
|
||
| const octokit = new Octokit({ | ||
| auth: token, | ||
| }); | ||
| const encodedJitConfig = await getJitConfig( | ||
| octokit, | ||
| owner, | ||
| repo, | ||
| hetznerResponse.serverId, | ||
| ); | ||
| await sshResult.execCommand("apt install tmux"); | ||
| await sshResult.execCommand(initRunner(encodedJitConfig)); | ||
| console.log("Successfully start openci runner"); | ||
| break; | ||
| } catch (e) { | ||
| console.log("error, will try again in 1 second", e); | ||
| await setTimeout(1000); | ||
| retryCount++; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
初期化失敗時にHetznerサーバーが削除されません。
サーバー作成後、SSH接続やランナー初期化に失敗した場合、作成されたHetznerサーバーが残り続けます。これはリソースリークとコスト増加につながります。
以下の修正を適用してください:
+ let hetznerResponse;
+ try {
- const hetznerResponse = await createServer(process.env.HETZNER_API_KEY);
+ hetznerResponse = await createServer(process.env.HETZNER_API_KEY);
while (true) {
// ... ステータスチェック
}
const ssh = new NodeSSH();
// ... SSH接続とランナー初期化
+ } catch (error) {
+ console.error("Failed to initialize runner, cleaning up server", error);
+ if (hetznerResponse?.serverId) {
+ await deleteServer(String(hetznerResponse.serverId), process.env.HETZNER_API_KEY);
+ }
+ throw error;
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hetznerResponse = await createServer(process.env.HETZNER_API_KEY); | |
| while (true) { | |
| const status = await getServerStatusById( | |
| hetznerResponse.serverId, | |
| process.env.HETZNER_API_KEY, | |
| ); | |
| if (status === "running") { | |
| console.log("Server is now running!"); | |
| break; | |
| } else { | |
| console.log("Waiting for the server init. Will try again in 1 second"); | |
| await setTimeout(1000); | |
| } | |
| } | |
| const ssh = new NodeSSH(); | |
| const maxRetry = 10; | |
| let retryCount = 0; | |
| while (retryCount < maxRetry) { | |
| console.log("retry:", retryCount); | |
| try { | |
| const sshResult = await ssh.connect({ | |
| host: hetznerResponse.ipv4, | |
| username: "root", | |
| privateKey: process.env.HETZNER_SSH_PRIVATE_KEY, | |
| passphrase: process.env.HETZNER_SSH_PASSPHRASE, | |
| }); | |
| const octokit = new Octokit({ | |
| auth: token, | |
| }); | |
| const encodedJitConfig = await getJitConfig( | |
| octokit, | |
| owner, | |
| repo, | |
| hetznerResponse.serverId, | |
| ); | |
| await sshResult.execCommand("apt install tmux"); | |
| await sshResult.execCommand(initRunner(encodedJitConfig)); | |
| console.log("Successfully start openci runner"); | |
| break; | |
| } catch (e) { | |
| console.log("error, will try again in 1 second", e); | |
| await setTimeout(1000); | |
| retryCount++; | |
| } | |
| } | |
| let hetznerResponse; | |
| try { | |
| hetznerResponse = await createServer(process.env.HETZNER_API_KEY); | |
| while (true) { | |
| const status = await getServerStatusById( | |
| hetznerResponse.serverId, | |
| process.env.HETZNER_API_KEY, | |
| ); | |
| if (status === "running") { | |
| console.log("Server is now running!"); | |
| break; | |
| } else { | |
| console.log("Waiting for the server init. Will try again in 1 second"); | |
| await setTimeout(1000); | |
| } | |
| } | |
| const ssh = new NodeSSH(); | |
| const maxRetry = 10; | |
| let retryCount = 0; | |
| while (retryCount < maxRetry) { | |
| console.log("retry:", retryCount); | |
| try { | |
| const sshResult = await ssh.connect({ | |
| host: hetznerResponse.ipv4, | |
| username: "root", | |
| privateKey: process.env.HETZNER_SSH_PRIVATE_KEY, | |
| passphrase: process.env.HETZNER_SSH_PASSPHRASE, | |
| }); | |
| const octokit = new Octokit({ | |
| auth: token, | |
| }); | |
| const encodedJitConfig = await getJitConfig( | |
| octokit, | |
| owner, | |
| repo, | |
| hetznerResponse.serverId, | |
| ); | |
| await sshResult.execCommand("apt install tmux"); | |
| await sshResult.execCommand(initRunner(encodedJitConfig)); | |
| console.log("Successfully start openci runner"); | |
| break; | |
| } catch (e) { | |
| console.log("error, will try again in 1 second", e); | |
| await setTimeout(1000); | |
| retryCount++; | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Failed to initialize runner, cleaning up server", error); | |
| if (hetznerResponse?.serverId) { | |
| await deleteServer( | |
| String(hetznerResponse.serverId), | |
| process.env.HETZNER_API_KEY, | |
| ); | |
| } | |
| throw error; | |
| } |
| const ssh = new NodeSSH(); | ||
| const maxRetry = 10; | ||
| let retryCount = 0; | ||
| while (retryCount < maxRetry) { | ||
| console.log("retry:", retryCount); | ||
| try { | ||
| const sshResult = await ssh.connect({ | ||
| host: hetznerResponse.ipv4, | ||
| username: "root", | ||
| privateKey: process.env.HETZNER_SSH_PRIVATE_KEY, | ||
| passphrase: process.env.HETZNER_SSH_PASSPHRASE, | ||
| }); | ||
|
|
||
| const octokit = new Octokit({ | ||
| auth: token, | ||
| }); | ||
| const encodedJitConfig = await getJitConfig( | ||
| octokit, | ||
| owner, | ||
| repo, | ||
| hetznerResponse.serverId, | ||
| ); | ||
| await sshResult.execCommand("apt install tmux"); | ||
| await sshResult.execCommand(initRunner(encodedJitConfig)); | ||
| console.log("Successfully start openci runner"); | ||
| break; | ||
| } catch (e) { | ||
| console.log("error, will try again in 1 second", e); | ||
| await setTimeout(1000); | ||
| retryCount++; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SSH接続のクリーンアップ処理が不足しています。
SSH接続成功後、ssh.dispose()などのクリーンアップ処理が行われていません。リソースリークを防ぐため、処理完了後またはエラー時に接続を適切に閉じてください。
以下のように修正してください:
const ssh = new NodeSSH();
const maxRetry = 10;
let retryCount = 0;
+ let connected = false;
while (retryCount < maxRetry) {
console.log("retry:", retryCount);
try {
const sshResult = await ssh.connect({
// ... 接続設定
});
+ connected = true;
// ... ランナー初期化
console.log("Successfully start openci runner");
+ ssh.dispose();
break;
} catch (e) {
console.log("error, will try again in 1 second", e);
await setTimeout(1000);
retryCount++;
}
}
+ if (connected) {
+ ssh.dispose();
+ }
return;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const ssh = new NodeSSH(); | |
| const maxRetry = 10; | |
| let retryCount = 0; | |
| while (retryCount < maxRetry) { | |
| console.log("retry:", retryCount); | |
| try { | |
| const sshResult = await ssh.connect({ | |
| host: hetznerResponse.ipv4, | |
| username: "root", | |
| privateKey: process.env.HETZNER_SSH_PRIVATE_KEY, | |
| passphrase: process.env.HETZNER_SSH_PASSPHRASE, | |
| }); | |
| const octokit = new Octokit({ | |
| auth: token, | |
| }); | |
| const encodedJitConfig = await getJitConfig( | |
| octokit, | |
| owner, | |
| repo, | |
| hetznerResponse.serverId, | |
| ); | |
| await sshResult.execCommand("apt install tmux"); | |
| await sshResult.execCommand(initRunner(encodedJitConfig)); | |
| console.log("Successfully start openci runner"); | |
| break; | |
| } catch (e) { | |
| console.log("error, will try again in 1 second", e); | |
| await setTimeout(1000); | |
| retryCount++; | |
| } | |
| } | |
| const ssh = new NodeSSH(); | |
| const maxRetry = 10; | |
| let retryCount = 0; | |
| let connected = false; | |
| while (retryCount < maxRetry) { | |
| console.log("retry:", retryCount); | |
| try { | |
| const sshResult = await ssh.connect({ | |
| host: hetznerResponse.ipv4, | |
| username: "root", | |
| privateKey: process.env.HETZNER_SSH_PRIVATE_KEY, | |
| passphrase: process.env.HETZNER_SSH_PASSPHRASE, | |
| }); | |
| connected = true; | |
| const octokit = new Octokit({ | |
| auth: token, | |
| }); | |
| const encodedJitConfig = await getJitConfig( | |
| octokit, | |
| owner, | |
| repo, | |
| hetznerResponse.serverId, | |
| ); | |
| await sshResult.execCommand("apt install tmux"); | |
| await sshResult.execCommand(initRunner(encodedJitConfig)); | |
| console.log("Successfully start openci runner"); | |
| ssh.dispose(); | |
| break; | |
| } catch (e) { | |
| console.log("error, will try again in 1 second", e); | |
| await setTimeout(1000); | |
| retryCount++; | |
| } | |
| } | |
| if (connected) { | |
| ssh.dispose(); | |
| } | |
| return; |
🤖 Prompt for AI Agents
In openci-runner/firebase/functions/probot/index.ts around lines 53 to 84, the
SSH connection created with new NodeSSH() is never disposed, risking resource
leaks; wrap the connect/command sequence in a try/finally (or add a nested
try/finally) so that after a successful connect and after running commands you
call await ssh.dispose(), and also ensure you call await ssh.dispose() in the
catch path before retrying (guarding the dispose call so it only runs if a
connection was established). Ensure dispose is awaited and that you still break
out of the loop after successful execution.
| const runnerId = runnerName?.replace(defaultRunnerName, ""); | ||
| console.log("runner id", runnerId); | ||
|
|
||
| await deleteServer(runnerId, process.env.HETZNER_API_KEY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleteServerのエラーハンドリングを追加してください。
deleteServerの呼び出しにエラーハンドリングがありません。Hetzner APIの削除処理が失敗した場合でも、エラーをログに記録して適切に処理する必要があります。
以下の修正を適用してください:
- await deleteServer(runnerId, process.env.HETZNER_API_KEY);
- console.log("Finish cleaning up");
+ try {
+ await deleteServer(runnerId, process.env.HETZNER_API_KEY);
+ console.log("Finish cleaning up");
+ } catch (error) {
+ console.error("Failed to delete Hetzner server:", error);
+ // サーバー削除失敗は致命的ではないため、エラーを投げずにログのみ記録
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await deleteServer(runnerId, process.env.HETZNER_API_KEY); | |
| app.on("workflow_job.completed", async (context) => { | |
| const runnerName = context.payload.workflow_job.runner_name; | |
| if (runnerName == null) { | |
| console.log("This runner is GitHub hosted one"); | |
| return; | |
| } | |
| const defaultRunnerName = "OpenCI ランナー "; | |
| const runnerId = runnerName?.replace(defaultRunnerName, ""); | |
| console.log("runner id", runnerId); | |
| if (!runnerId) { | |
| console.log("Failed to extract runner ID from runner name"); | |
| return; | |
| } | |
| - await deleteServer(runnerId, process.env.HETZNER_API_KEY); | |
| try { | |
| await deleteServer(runnerId, process.env.HETZNER_API_KEY); | |
| console.log("Finish cleaning up"); | |
| } catch (error) { | |
| console.error("Failed to delete Hetzner server:", error); | |
| // サーバー削除失敗は致命的ではないため、エラーを投げずにログのみ記録 | |
| } | |
| }); |
🤖 Prompt for AI Agents
In openci-runner/firebase/functions/probot/index.ts around line 102, the call to
await deleteServer(runnerId, process.env.HETZNER_API_KEY) lacks error handling;
wrap the await in a try/catch, log the caught error with clear context (include
runnerId and that Hetzner delete failed) using the existing logger (or
console/error logger if none available), and then handle the failure
appropriately (either rethrow the error or return a handled response/status so
upstream code doesn't assume deletion succeeded).
Summary by CodeRabbit
新機能
既存機能の変更
チョア
設定