Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 348 additions & 20 deletions openci-runner/firebase/functions/package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions openci-runner/firebase/functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -18,7 +18,7 @@
"engines": {
"node": "22"
},
"main": "lib/src/index.js",
"main": "lib/index.js",
"name": "functions",
"private": true,
"scripts": {
Expand Down
32 changes: 0 additions & 32 deletions openci-runner/firebase/functions/probot/github.ts

This file was deleted.

2 changes: 1 addition & 1 deletion openci-runner/firebase/functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
99 changes: 99 additions & 0 deletions openci-runner/firebase/functions/src/probot/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { Octokit } from "@octokit/rest";

const runnerName = "openci-runner-beta";

// biome-ignore lint/suspicious/noExplicitAny: <Fill the type later>
export function isJobRequired(context: any): boolean {
return context.payload.workflow_job.labels.includes(runnerName);
}
Comment on lines +5 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

labels の null チェックを追加してください

workflow_job.labelsundefined または null の場合、ランタイムエラーが発生します。includes を呼ぶ前に存在チェックを追加してください。

以下の diff を適用して修正してください:

 export function isJobRequired(context: any): boolean {
-	return context.payload.workflow_job.labels.includes(runnerName);
+	return context.payload.workflow_job.labels?.includes(runnerName) ?? false;
 }
📝 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.

Suggested change
// biome-ignore lint/suspicious/noExplicitAny: <Fill the type later>
export function isJobRequired(context: any): boolean {
return context.payload.workflow_job.labels.includes(runnerName);
}
// biome-ignore lint/suspicious/noExplicitAny: <Fill the type later>
export function isJobRequired(context: any): boolean {
return context.payload.workflow_job.labels?.includes(runnerName) ?? false;
}
🤖 Prompt for AI Agents
In openci-runner/firebase/functions/src/probot/github.ts around lines 5 to 8,
isJobRequired calls context.payload.workflow_job.labels.includes(...) without
guarding for null/undefined and will throw if labels is missing; update the
function to first check that context.payload?.workflow_job?.labels is an array
(e.g. Array.isArray) and return false if not, then call includes on the
validated array (or use optional chaining with a boolean fallback) so includes
is only invoked on a defined array.


const generateJitConfigPath =
"/repos/{owner}/{repo}/actions/runners/generate-jitconfig";

type JitConfigRequest = {
headers: Record<string, string>;
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<string> {
const body = jitConfigRequestBody(owner, repo, serverId);
const jitConfigRes = await octokit.request(
`POST ${generateJitConfigPath}`,
body,
);

return jitConfigRes.data.encoded_jit_config;
}

export enum ActionsRunnerOS {
osx = "osx",
linux = "linux",
}

export enum ActionsRunnerArchitecture {
x64 = "x64",
arm64 = "arm64",
}

const mkdir = "mkdir actions-runner";

const cdActionRunner = "cd actions-runner";

function downloadRunnerScriptAndUnZip(
scriptVersion: string,
os: ActionsRunnerOS,
architecture: ActionsRunnerArchitecture,
) {
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}`;
}
Comment on lines +70 to +77
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

curlコマンドにエラーハンドリングとチェックサム検証を追加してください。

現在の実装には以下の問題があります:

  1. curlの -f フラグが未指定: HTTPエラー(404, 500など)が発生してもダウンロードが成功したと判断されます
  2. チェックサム検証なし: ダウンロードしたバイナリの整合性が検証されていないため、破損したファイルや改ざんされたファイルを実行する可能性があります

以下の修正を推奨します:

 function downloadRunnerScriptAndUnZip(
 	scriptVersion: string,
 	os: ActionsRunnerOS,
 	architecture: ActionsRunnerArchitecture,
 ) {
 	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}`;
+	return `curl -fsSL -o ${fileName} https://github.com/actions/runner/releases/download/v${scriptVersion}/${fileName} && tar xzf ./${fileName}`;
 }

さらに、チェックサム検証を追加することを強く推奨します。GitHubはランナーリリースと共にSHA256チェックサムを提供しています。

📝 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.

Suggested change
function downloadRunnerScriptAndUnZip(
scriptVersion: string,
os: ActionsRunnerOS,
architecture: ActionsRunnerArchitecture,
) {
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}`;
}
function downloadRunnerScriptAndUnZip(
scriptVersion: string,
os: ActionsRunnerOS,
architecture: ActionsRunnerArchitecture,
) {
const fileName = `actions-runner-${os}-${architecture}-${scriptVersion}.tar.gz`;
return `curl -fsSL -o ${fileName} https://github.com/actions/runner/releases/download/v${scriptVersion}/${fileName} && tar xzf ./${fileName}`;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai issueを作成して。github。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Create GitHub issue for curl error handling improvement

gh issue create \
  --title "curlコマンドにエラーハンドリングとチェックサム検証を追加" \
  --body "## 概要

\`downloadRunnerScriptAndUnZip\` 関数内の curl コマンドにエラーハンドリングとチェックサム検証を追加する必要があります。

## 問題点

1. **curlの \`-f\` フラグが未指定**: HTTPエラー(404, 500など)が発生してもダウンロードが成功したと判断されます
2. **チェックサム検証なし**: ダウンロードしたバイナリの整合性が検証されていないため、破損したファイルや改ざんされたファイルを実行する可能性があります

## 推奨される修正

\`curl -fsSL\` フラグを使用し、GitHubが提供するSHA256チェックサムを使用した検証を追加することを推奨します。

## 関連リンク

- PR: https://github.com/open-ci-io/openci/pull/507
- コメント: https://github.com/open-ci-io/openci/pull/507#discussion_r2427855769
- リクエスト者: @mafreud

## 対象ファイル

\`openci-runner/firebase/functions/src/probot/github.ts\` の \`downloadRunnerScriptAndUnZip\` 関数" \
  --assignee mafreud

Length of output: 47


@mafreud、issueを作成しました: #511

curlコマンドのエラーハンドリングとチェックサム検証の追加に関する詳細を記載し、あなたにアサインしました。


function runGHAScript(runnerConfig: string): string {
return `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): string {
const command = [cdActionRunner, runGHAScript(runnerConfig)];
return command.join(" && ");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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<HetznerResponse> {
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 {
Comment on lines +29 to +35
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

crypto.randomUUID() の未インポート(Node の型解決)

TS でグローバル crypto が解決されない場合があり得ます。node:crypto から randomUUID を import して使用してください。

+import { randomUUID } from "node:crypto";
 ...
-export function createServerSpec(
+export function createServerSpec(
   image: string = "ubuntu-24.04",
   location: string = "fsn1",
-  name: string = crypto.randomUUID(),
+  name: string = randomUUID(),
   serverType: string = "cpx41",
   sshKeyName: string = "openci-runner-probot",
 ): HetznerServerSpec {
📝 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.

Suggested change
export function createServerSpec(
image: string = "ubuntu-24.04",
location: string = "fsn1",
name: string = crypto.randomUUID(),
serverType: string = "cpx41",
sshKeyName: string = "openci-runner-probot",
): HetznerServerSpec {
import { randomUUID } from "node:crypto";
export function createServerSpec(
image: string = "ubuntu-24.04",
location: string = "fsn1",
name: string = randomUUID(),
serverType: string = "cpx41",
sshKeyName: string = "openci-runner-probot",
): HetznerServerSpec {
🤖 Prompt for AI Agents
In openci-runner/firebase/functions/src/probot/hetzner.ts around lines 29 to 35,
the default value uses crypto.randomUUID() which can fail TS resolution for the
global crypto; import randomUUID from 'node:crypto' at the top of the file and
replace crypto.randomUUID() with randomUUID() (or reference the imported symbol)
so the identifier is resolved correctly by Node typings and bundlers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

どういう場合に、cryptoが解決されませんか?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Node.js version requirement and TypeScript configuration
cat openci-runner/firebase/functions/package.json | jq -r '.engines'
echo "---"
cat openci-runner/firebase/functions/tsconfig.json

Length of output: 404


🏁 Script executed:

#!/bin/bash
# Check if @types/node is present and what version
cat openci-runner/firebase/functions/package.json | jq '.dependencies["@types/node"], .devDependencies["@types/node"]'

Length of output: 9


🏁 Script executed:

#!/bin/bash
# Check if there are any TypeScript compilation errors related to crypto
cd openci-runner/firebase/functions
npx tsc --noEmit 2>&1 | grep -i "crypto" || echo "No crypto-related TypeScript errors found"

Length of output: 41


@mafreud さん、申し訳ございません。私の以前の指摘は不正確でした。

実際にコードをチェックした結果、現在の設定では crypto.randomUUID() は問題なく動作します。理由は以下の通りです:

  1. Node.js 22を使用している: crypto.randomUUID() は Node.js 16.7.0 から利用可能で、Node.js 22 では完全にサポートされています

  2. TypeScript設定が適切: tsconfig.jsonmodule: "NodeNext"moduleResolution: "nodenext" を使用しているため、TypeScript は自動的に Node.js のビルトイン型を認識します

  3. コンパイルエラーなし: TypeScript のコンパイル時に crypto 関連のエラーは発生していません

したがって、node:crypto からの import は不要です。現在のコードのままで問題ありません。

私の以前のレビューコメントは解決(resolve)させていただきます。ご迷惑をおかけして申し訳ございませんでした。


🧬 Code Graph Analysis Results

openci-runner/firebase/functions/src/probot/hetzner.ts: createServerSpec (lines 29-43)

Summary:

  • Function: createServerSpec(image, location, name, serverType, sshKeyName)
  • Notes: Uses crypto.randomUUID() to generate a default server name when none is provided. This is relevant to understanding potential runtime issues if crypto is not available in the environment.

return {
image: image,
location: location,
name: name,
server_type: serverType,
ssh_keys: [sshKeyName],
};
}

export async function createServer(
apiKey: string,
serverSpec: HetznerServerSpec,
): Promise<HetznerResponse> {
const response = await fetch(baseUrl, {
body: JSON.stringify(body),
body: JSON.stringify(serverSpec),
headers: {
Authorization: `Bearer ${apiKey}`,
},
Expand Down Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -84,9 +95,37 @@ 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-get update -y && apt-get install -y 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);
Expand Down
65 changes: 0 additions & 65 deletions openci-runner/firebase/functions/test/index.test.ts

This file was deleted.

Loading