JenkinsのPRレビューでテストとセキュリティスキャンも回す ― AIのAPPROVEを機械的に覆すMineWatchのCI
これまでに、GitHub Actionsの失敗を自己修復するパイプラインと、JenkinsでGitHub Copilot CLIにPRを自動レビューさせる仕組みを書きました。どちらも「AIにレビューさせる仕組みを、どう安全に回すか」が中心でした。
ただ、AIレビューだけでは足りません。LLMはコードとして読める範囲の指摘は得意ですが、テストが実際に通るか、依存に既知の脆弱性がないか、イメージにCVEが残っていないかは、動かしてみないと分かりません。
この記事は続編として、個人開発しているMinecraftサーバー監視アプリ「MineWatch」で、PRごとにJenkinsが何を実行しているか、そしてその結果でAIの判定をどう覆しているかをまとめたものです。MineWatchの公式サイトはこちらです。アプリの全体像はこちらの記事にあります。
TL;DR
- PRごとにJenkinsが、ビルド・ユニットテスト・APIテスト・E2E・SAST・シークレットスキャン・イメージ脆弱性スキャン・依存脆弱性チェック・SonarQubeを回します。触った層だけを対象にします。
- 検証が1つでもFAILなら、AIがAPPROVEと言っても
VERDICT: REQUEST_CHANGESに機械的に倒します。判定をLLMの裁量に委ねません。 - 手元は2段構えです。commit時に、整形・lint・型チェック・シークレット・SASTを差分だけ先に潰します。push時に、テスト全件・カバレッジ下限・依存の脆弱性チェックをまとめて回します。CIはPRが持ち込む全体を深く見る最終防衛線にします。
- 開発機と同じMac 1台でCIを回すので、CI用のcomposeを分離し、ホストポートを使わない設計にしました。
- 自動マージはGitHub Actions側で行い、
reviewed-shaでレビューした版だけをマージします。mainへのPRは人がマージし、Claudeによる自動修正はしません。 - 負荷テストの自動化は、まだありません。今後の課題として、末尾に検討内容を書きました。
全体像
検証はすべて同じJenkinsジョブの中で直列に走ります。ジョブは開発機を兼ねたMac 1台に固定しています。iOSのビルドにmacOSが必要なのと、そもそも他にMacがないためです。
手元で先に潰す:pre-commitとpre-push
PRを出す前に、pre-commit で手元のチェックを済ませます。.pre-commit-config.yaml で管理していて、make hooks(中身は pre-commit install)で登録します。make check で全ファイルにまとめて実行することもできます。
フックは、走らせるタイミングで2段階に分けています。
commit時:差分だけを見る軽量なもの
| フック | 対象 | 内容 |
|---|---|---|
| gitleaks | リポジトリ全体 | 秘密情報の混入検知 |
| opengrep | 変更ファイル | SAST(p/ci ルール + --taint-intrafile) |
| ruff | api/ |
Pythonのlintと整形 |
| ty |
api/ のPython |
型チェック |
| swift-format / swiftlint | Swift | 整形と --strict のlint |
| prettier / eslint |
web/ admin/
|
整形とlint |
push時:時間のかかるものをまとめて
| フック | コマンド | 目的 |
|---|---|---|
| pytest | make ci-test |
テスト全件 |
| カバレッジ下限 | make coverage-check |
カバレッジの静かな低下を防ぐ |
| pip-audit | make audit-api |
依存の既知脆弱性 |
テスト全件は数十秒以上かかるので、commitのたびに走らせるとフックが外されがちになります。そこで、commit時は軽いものだけにして、重いものはpush時に回します。push時のコマンドはJenkinsと同一です。CIを待たずに、手元で先に気付けます。
1回の pre-commit install で両方のフックが登録されるよう、設定ファイルの先頭で登録するフックの種類を指定しています。各フックの既定の実行タイミングはcommit時にして、push時だけ走らせたいものに stages: [pre-push] を付けます。
default_install_hook_types: [pre-commit, pre-push]
default_stages: [pre-commit]
repos:
- repo: local
hooks:
- id: pytest-api
name: pytest (api)
entry: make ci-test
language: system
files: ^api/
pass_filenames: false
stages: [pre-push]
すでに pre-commit install 済みの環境では、push用のフックを登録するために make hooks をもう一度実行する必要があります。
pre-commitは「差分だけ・速い」、pre-pushは「重いものをまとめて」を担当します。CIは「PRが持ち込む全部・深い」を担当します。gitleaks・opengrep・型チェックやテストを手元とCIの両方に置いているのは、手元のフックは各自の環境に依存する上に、--no-verify で回避もできるからです。強制力があるのはCIだけです。逆に整形とlintはCIでは重複して回さず、手元で潰す前提にしています。
差分だけを見る設計なので、触っていないファイルの違反は残り続けます。実際に全ファイルへ流してみると、Python・Swift・web・adminのどこにも、以前からの違反が残っていました。手元のチェックは「これから積むコミットを壊さない」ためのもので、リポジトリ全体の健全性を保証するものではありません。
black・isort・flake8をruffにまとめた
Pythonの整形とlintは、以前はblack・isort・flake8の3本でしたが、別の自作ライブラリの構成を参考に、ruff 1本にまとめました。設定が pyproject.toml に集約されるのと、下で書くflake8の罠が消えるのが大きな利点です。
[tool.ruff]
line-length = 100
target-version = "py313"
# migrations/versions は Alembic の自動生成物。手で直しても再生成で戻るため、
# 整形・lint のどちらも当てない。
extend-exclude = ["migrations/versions"]
[tool.ruff.lint]
# pyflakes(F) + pycodestyle(E/W) + isort(I)
select = ["E", "F", "W", "I"]
ignore = ["E203"]
移行してみて、次の差が出ました。
- 既存コードに整形の差分が出た: ruffのformatは旧blackとほぼ互換ですが、完全には一致しません。7ファイルが再整形されました。
- 日本語の長い文字列がE501になった: ruffは全角文字を幅2で数えます。文字数で数えていた旧flake8では通っていた行が、ruffでは行長超過になりました。文字列の値は変えず、整形結果に従って直しました。
-
# ty: ignore[...]の行は長くてもE501にならない: ruffは行末の型チェッカー向けpragmaコメントを、行長の判定から除外します。これは実際にレビューで問題になったので、下の「AIの指摘が誤りだったとき」で書きます。
カバレッジに下限を置く
push時のフックには、カバレッジの下限の検証も入れました。pyproject.toml に下限を書き、--cov 付きでpytestを実行したときに、その値を下回ると非ゼロ終了します。
[tool.coverage.report]
fail_under = 85
現状のカバレッジは89%なので、下限は85%にしました。下限を現状ぎりぎりに置くと、些細なテストの追加や削除で落ちて、フックが外されがちになります。約4ポイントのバッファを持たせて、「静かな低下」だけを防ぎます。
下限が本当に効くかも確認しました。一時的に下限を95%に上げて実行すると、非ゼロ終了しました。Sonar用のカバレッジXMLを作る既存のターゲットとは別に、下限の判定だけを行う make coverage-check を用意しています。
手元のフックで踏んだ罠
pre-commit側で踏んだ罠が3つありました。1つ目は、上のruff移行で解消しました。
Opengrepはlocal hookにするしかなかった
SASTには、Semgrep OSSのLGPLフォークである Opengrep を使っています。Semgrepが有料のPro Engineに切り出した、関数をまたぐデータフロー追跡(--taint-intrafile)を無料で使えるためです。
ただ、公式のpre-commit hookはpip版のsemgrepを呼ぶ構成のまま更新されていなくて使えませんでした。そのため、ローカルにインストール済みのバイナリを呼ぶlocal hookにしています。代償として、バージョンがpre-commitで固定されなくなるので、各自でインストール状況を揃える必要があります。
- repo: local
hooks:
- id: opengrep
name: opengrep (SAST)
entry: opengrep scan --config p/ci --taint-intrafile --error --skip-unknown-extensions
language: system
flake8が設定ファイルを読まなかった(ruffで解消)
pre-commit実行時のカレントディレクトリはリポジトリルートです。flake8は設定ファイルを親方向にしか探さないので、api/.flake8 が読まれず、既定の79桁で誤検知し続けていました。当時は --config=api/.flake8 を明示して回避していました。
ruffは、対象ファイルから親方向に pyproject.toml を探します。そのため、ルートから実行しても api/pyproject.toml の設定が使われ、--config を明示する必要がなくなりました。
除外設定がバッチ実行で安定しなかった
pre-commitはファイルをバッチに分割して実行します。このとき .semgrepignore の適用が安定しないことがあったので、hook自体の exclude でも二重に除外しています。ローカル専用ページがlocalhostへのHTTPリンクを意図的に持っている、という誤検知への対処です。
PRごとにJenkinsが回す検証
PRのWebhookを受けると、Jenkinsはまず git diff --name-only origin/<base>...HEAD で、どの層が変更されたかを判定します。
prReviewVerification.detectTouchedLayers(
baseRef: baseRef,
layers: [
API : ['api/'],
IOS : ['ios/'],
WEB : ['web/'],
ADMIN: ['admin/'],
INFRA: ['infra/', 'ci/', 'docker-compose.yml'],
],
)
パターンが / で終われば前方一致、そうでなければ完全一致で判定します。結果は env.TOUCHED_API のような '1' / '0' の環境変数に入り、各ステージの when で使います。docsだけのPRなら、実際に走るのはシークレットスキャンとSASTだけです。
| 検証 | コマンド | 実行条件 |
|---|---|---|
| シークレットスキャン | gitleaks git --log-opts="origin/<base>...HEAD" |
常に(全層) |
| SAST | opengrep scan --config p/ci --taint-intrafile --error |
常に(全層) |
| バックエンドのテスト |
make ci-test(pytest) |
api |
| 型チェック |
make typecheck(ty) |
api |
| APIテスト |
make postman-test(Postman/Newman) |
api |
| iOSのテスト |
make ios-test(ビルド + Tests / UITests) |
ios |
| ユニットテスト |
npm ci && npm test(vitest) |
web / admin |
| E2E |
make e2e-ci(Playwright) |
web / admin |
| 静的解析 |
make sonar-*(Quality Gate) |
触った層 |
| イメージの脆弱性 | make trivy-scan |
api / web / admin |
| 依存の脆弱性 |
make audit-*(pip-audit / npm audit) |
api / web / admin |
シークレットとSASTだけ層を問わず全ファイルを見るのは、秘密情報がどのディレクトリにも混入しうるからです。
失敗しても止めず、結果だけ記録する
各検証は、次のように実行して結果を環境変数に記録します。
def result = (sh(script: script, returnStatus: true) == 0) ? 'PASS' : 'FAIL'
env[envVar] = result
returnStatus: true なので、失敗してもビルドは止まりません。テストが落ちていても、AIレビューは投稿して「何が悪いか」を見えるようにしたいからです。実行しなかった層は SKIP として扱い、判定には影響させません。最終的なVERDICTでまとめて効かせます。
検証ごとの補足
- APIテスト: Postman/Newmanで全エンドポイントを実HTTPで叩きます。DB・API・nginxを起動してマイグレーションまで流し、Newmanを実行して片付けるところまでが1つのターゲットです。
-
iOS: 以前は
make build(ビルドのみ)で、テストターゲットを一切実行していませんでした。今はmake ios-testでMineWatchTestsとMineWatchUITestsを明示し、間欠的な取りこぼしをなくしています。 -
E2E: web/adminにはiOSのようなバイパス経路がなく、ブラウザ経由で本物のFirebase Auth SDKを通ります。そのため実際のFirebase鍵が要ります。鍵はVaultで管理し、JCasC経由でJenkinsのCredential(Secret file)として登録し、実行後は
rm -fで消します。 -
SonarQube: api・ios・web・admin・infraの5プロジェクトに分けています。Quality Gateが赤ならスキャナが非ゼロ終了する設定(
sonar.qualitygate.wait=true)にして、FAIL扱いにします。Community Buildはブランチ・PR分析を持たないため、サーバー上の表示は「直近にスキャンした状態」になりますが、ゲートの判定自体はスキャンごとに効くので、マージ制御には支障ありません。 - Trivy: OSパッケージのCRITICAL/HIGHだけを対象にします。アプリの依存(npm/uv)は、次の依存脆弱性チェックの役割です。
-
依存の脆弱性: pip-auditと
npm audit --audit-level=highです。
AIのAPPROVEを機械的に覆す
ここがこの記事の中心です。
MineWatchのレビューは、Copilot CLIとClaude Code CLIがそれぞれ別々にレビューを生成し、3回目のClaude Codeの呼び出しで両者を1本に統合します。ただし、VERDICT: APPROVE か VERDICT: REQUEST_CHANGES かは、統合するLLMには決めさせません。次の3段階で機械的に決めています。
1. 個別の判定と成否から決める
def computeEffectiveVerdict(Map args = [:]) {
if (args.copilotFailed && args.claudeFailed) {
return 'REQUEST_CHANGES'
}
if (args.copilotFailed) {
return args.claudeVerdict
}
if (args.claudeFailed) {
return args.copilotVerdict
}
return (args.copilotVerdict == 'REQUEST_CHANGES' || args.claudeVerdict == 'REQUEST_CHANGES')
? 'REQUEST_CHANGES' : 'APPROVE'
}
片方のCLIが落ちても、もう片方の判定で続けます。両方が落ちたら止めます。両方が成功したときは、どちらか一方でも懸念を示したら止めます。安全側に倒す規則です。
2. 統合LLMの出力を、一方向にだけ上書きする
統合LLMがAPPROVEを出しても、手順1で決めた値がREQUEST_CHANGESなら強制的に書き換えます。逆に、LLMがREQUEST_CHANGESと言ったなら、常にそれを尊重して上書きしません。
awk '{ if ($0 == "VERDICT: APPROVE") print "VERDICT: REQUEST_CHANGES"; else print }' review.txt > review.txt.tmp
mv review.txt.tmp review.txt
3. 検証結果で、もう一段かける
最後に、検証がどれか1つでも FAIL なら、APPROVEの行をREQUEST_CHANGESに置き換えます。
def anyFail = envVarNames.any { (env[it] ?: 'PASS') == 'FAIL' }
if (anyFail) {
content = content.replaceAll(/(?m)^VERDICT: APPROVE$/, 'VERDICT: REQUEST_CHANGES')
}
これは、Copilotが落ちているときに、検証のFAILを無視してClaude側のAPPROVEが通ってしまう事態を防ぐための最終防波堤です。未実行(SKIP)の項目はFAIL扱いにしません。
実際にあった例
あるリリースPRで、AIレビューの本文は「軽微な指摘のみ」と好意的でしたが、最終的なVERDICTは REQUEST_CHANGES でした。投稿されたコメントの自動検証欄は、次のようになっていました(抜粋)。
### 自動検証
- secret scan: PASS
- opengrep (p/ci, taint-intrafile): PASS
- backend test: PASS
- type check (ty): PASS
- sonar (minewatch-api): PASS
- trivy image scan: FAIL
- dependency audit (pip-audit/npm audit): PASS
AIがコードとして読める範囲では問題が見えなくても、イメージにCVEが残っているかどうかは、スキャンしないと分かりません。AIが好意的でも、機械が止めてくれた例です。
AIの指摘が誤りだったとき
逆の例もあります。上で書いたruff移行のPRで、AIレビューは REQUEST_CHANGES を返しました。自動検証は全項目PASSでした。理由はレビュアーの指摘で、要約すると次のとおりです。
ruff formatが、行末の# ty: ignore[...]コメントを含めて100桁を超える行を作った。selectにE501が含まれているので、ruff checkで弾かれるはずだ。
指摘された2行の長さは、実測で112桁と121桁でした。桁数は正確です。それでも ruff check は通っていました。実際に確かめるため、同じ長さの行を2本並べたファイルを作りました。1本は行末が # ty: ignore[...]、もう1本は通常のコメントです。
x = 1 # ty: ignore[invalid-argument-type] xxxx……(120桁)
y = 1 # plain comment xxxx……(120桁)
ruff check --select E501 の結果は、通常コメントの行だけがE501になり、# ty: ignore[...] の行は除外されました。ruffは、行末のpragmaコメントを行長の判定から外す仕様です。
この根拠をPRのコメントに書いて、再レビューを依頼しました。再レビューは APPROVE になり、自動マージされました。
判定を機械に決めさせるのと同じ考え方で、AIの指摘も、事実かどうかを機械で確かめてから受け入れるようにしています。AIの指摘には推測が混ざります。指摘が正しければ直し、誤りなら、実測した根拠を返して再レビューさせます。
GitHub Actionsへの引き渡し
投稿するコメントには、GitHub Actionsが解釈する契約があります。
- 本文中のマーカー(
## AI統合レビュー (MineWatch)) - 最終行に、行頭完全一致で
VERDICT: APPROVEかVERDICT: REQUEST_CHANGES - 投稿者が、リポジトリ変数
JENKINS_BOT_USERNAMEと一致すること(同じ文字列を誰かが書いても動かないように)
CLI自体の実行が失敗した場合も、フォールバックのコメントを投稿し、VERDICTが無ければREQUEST_CHANGESに倒します。検証されないままマージされることを防ぐためです。
自動マージは「レビューした版」だけに縛る
レビューの最中に追加のpushが入ると、古い版へのレビューで新しい版をマージしてしまう恐れがあります。これを防ぐために、コメントに <!-- reviewed-sha: ... --> を埋め込み、GitHub Actions側で現在のPRのheadと一致したときだけ自動マージします。
gh pr merge "${PR_NUMBER}" \
--squash \
--delete-branch \
--auto \
--match-head-commit "${REVIEWED_SHA}"
--match-head-commit は、自動マージを予約する瞬間のheadがレビュー対象と一致しない限り、予約を許可しません。ただしこれが防げるのは予約時点の競合だけです。予約に成功したあとにpushが入っても自動で無効になる保証はないので、その場合は新しいpushが起こす次のJenkinsビルドの再レビューに委ねています。
マージの扱いは、baseブランチで分けています。
| VERDICT | base | 動作 |
|---|---|---|
| APPROVE | main以外 | squashで自動マージ |
| APPROVE | main | 手動マージの手順をコメント |
| REQUEST_CHANGES | 任意 |
needs-human-review ラベルを付与 |
mainへのPRは、リリースのマージです。squashせずにマージコミットを残し、タグ付けやdevelopへのback-mergeとセットで人が行います。また、1本目の記事の自己修復ループとは違い、MineWatchではClaudeによる自動修正はしていません。AIレビュー、AI修正、AIレビュー……というループを作らないためです。REQUEST_CHANGESは人が直します。
踏んだ罠
開発機兼用のMacで、自動マージが永久に発火しなかった
JenkinsのエージェントはMac 1台で、開発機と同じです。素のdocker composeは、DBなどのポートをホストへ公開します。開発環境を立ち上げたままCIが走ると、次のエラーでDBが起動できませんでした。
Bind for 0.0.0.0:55432 failed: port is already allocated
DBが起動できないので、APIもnginxも連鎖して落ち、バックエンドのテストが常にFAILします。その結果、AIの判定に関係なく検証FAILでVERDICTが握り潰され続け、自動マージが永久に発火しない状態になりました。
安全側に倒す設計が、検証環境の不具合をそのまま「常に止まる」に変えてしまった例です。対策として、CI専用のcompose overrideを用意しました。
services:
db:
# ホストへ公開しない。CI では api コンテナから db:5432 で引く。
ports: !reset []
nginx:
profiles: ["full"]
mc:
profiles: ["full"]
ポイントは3つあります。
- composeのリスト系は既定で追記マージされるので、上書きではポートを消せません。
!resetが必要です。 - pytestはapiコンテナの中で実行し、DBへはcomposeネットワーク経由の
db:5432で繋ぎます。 - プロジェクト名は
-p minewatch-ciで固定します。既定はカレントディレクトリ名なので、開発環境と同名になり、CI用の設定で開発環境のコンテナを作り直してしまうことがありました。
Postmanのテストとe2eも、それぞれ別のプロジェクト名・別のポートにしてあり、同じノード上で共存できます。
この分離は、手元のpre-pushにも効いています。push時のフックも同じ make ci-test を使うので、開発環境を起動したままpushしても、ポートは衝突しません。
docker compose run は古いイメージを再ビルドしない
run は、イメージが既にあると、Dockerfileや依存が変わっていても自動では再ビルドしません。依存に新しいパッケージを追加したとき、古いイメージ(必要なコマンドが入っていない)のまま、再現性なく失敗したことがありました。今は --build を明示しています。
docker compose run --build --rm api uv run pytest -q
npm audit が社内のミラー経由だと400になる
パッケージの取得は社内のNexusミラー経由にしていますが、このミラーは脆弱性のアドバイザリAPIを中継しません(Nexus Lifecycle must be configured で400になります)。そこで、npm audit を実行するときだけ、公式のレジストリを直接向けます。パッケージの取得元(.npmrc の設定)は変えません。
npm audit --audit-level=high --registry https://registry.npmjs.org
E2EのApp Checkは、バックエンドの設定では防げなかった
Firebase App CheckのWeb SDKは、クライアント側から直接Googleのサーバーへ検証を投げます。そのため、バックエンドを APP_CHECK_ENFORCED=false にしても防げません。設定ファイルのサンプルのプレースホルダ(REPLACE_ME)のままだと、appCheck/fetch-status-error(403)でE2Eが落ちます。
対策として、Firebase Consoleで発行したdebug tokenもVaultで管理し、実行時だけJenkinsのCredentialから設定ファイルへ埋め込み、終わったら消しています。
30分ではタイムアウトした
web/adminを変更すると、npm ci、vitest、E2E(Dockerのビルドを含む)、SonarQube(Quality Gateの待ちを含む)、Trivy、依存のチェック、AIレビューの生成までが直列で走ります。実測30分では、検証の途中(依存のチェック)でタイムアウトし、レビューの投稿まで到達しませんでした。タイムアウトを60分に広げています。検証を増やすほど、この直列の重さが効いてきます。
今後の課題:負荷テストの自動化
ここまでの検証は、正しさ(テスト・E2E)、安全性(SAST・シークレット・脆弱性スキャン)、品質(SonarQube)を見ています。負荷や長時間稼働に対する挙動は、自動では見ていません。
実は、負荷を掛けて確かめるスクリプトが1本だけあります。APNs(プッシュ通知の送信)クライアントを、workerと同じ形で繰り返し呼び出し、エラーが出ないか、プロセスのメモリ使用量が増え続けないかを確認するものです。ただし手元で手動実行するだけで、MakefileにもCIにも組み込んでいません。
負荷や長時間稼働の検証が欲しい理由は、実際にそういう不具合を踏んだからです。
-
プッシュ通知の約半数が失敗していた: workerはタスクごとにイベントループを作り直しますが、APNsクライアントの内部のHTTP接続は、生成したときのループに紐づいたまま使い回されていました。そのため、
Event loop is closedという例外で、送信の約半数が失敗していました。上のスクリプトで再現できました。 - 監視が全滅した: Celeryのworkerが、Redis接続をリークしていました。10秒間隔の監視タスクを繰り返すうちに、数時間でファイルディスクリプタの上限に達し、監視が全滅しました。
どちらも、1回だけ動かすテストでは出ません。繰り返し実行や長時間の稼働で初めて顕在化する種類の不具合です。単体テストやE2Eが全項目PASSでも、こうした不具合は見逃します。
今後、次の方向で自動化を検討したいと考えています。
- 長時間稼働(ソーク)テスト: 監視タスクを繰り返し実行し、エラー率とメモリ、ファイルディスクリプタが増え続けないかを見る。上のAPNsのスクリプトを、この形に広げるのが最初の一歩になりそうです
- APIの負荷テスト: 同時リクエスト時のレイテンシとエラー率を見る
- SSEの同時接続: サーバー状態のリアルタイム配信を、多数のクライアントが同時に受けたときの挙動を見る
ただし、PRごとに回すのは現実的ではなさそうです。すでに、検証を増やした結果、直列の実行時間が伸びてタイムアウトを60分に広げた経緯があります。実行コストの大きい負荷テストは、常時ではなく、リリース前に回す層に置くのが妥当だと考えています。方式やツールは、まだ決めていません。
まとめ
- AIレビューは「読めば分かる範囲」、テストとスキャンは「動かして分かる範囲」を見ます。両方を並べて、最終的な判定は機械に決めさせます。AIの指摘も、事実かどうかを実測で確かめてから受け入れます。
- 手元は2段構えです。commit時は差分だけを速く、push時は重いものをまとめて回します。CIは深く(PRの全体)見ます。ただし、手元のチェックは回避できるので、大事なものはCIで必ず再検証します。
- カバレッジは、現状から少しバッファを持たせた下限を置いて、静かな低下を防ぎます。下限が本当に効くかも、わざと上げて確かめます。
- 安全側に倒す設計は、検証環境が不安定だと「常に止まる」に変わります。ホストポート・プロジェクト名・
--buildなど、環境の分離と再現性が大切です。 - 検証を増やすと直列の時間が伸びます。タイムアウトは実測で決めます。
- 自動マージは、
reviewed-shaと--match-head-commitで「レビューした版」にだけ許可します。 - 負荷や長時間稼働の検証は、まだ自動化できていません。繰り返し実行で初めて出る不具合を踏んだので、リリース前に回す層として、今後検討します。
個人開発の環境での構成なので、そのまま組織に持ち込む場合は、Credentialの管理やAI連携の社内ポリシーを先に確認してください。
JQITのエンジニアの95%以上は未経験からの採用です。
よければコーポレートサイトにも遊びに来てください。
未経験から学べます!一緒に挑戦していきましょう![]()
noteやXもやってます↓