From 6e0cf02f04852c763bf9f51733b55d0868008c21 Mon Sep 17 00:00:00 2001 From: mt4110 <4221282+mt4110@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:29:20 +0900 Subject: [PATCH 1/3] ci: add ssh remote-up shortcuts --- README.md | 31 ++++--- docs/ci/QUICKSTART.md | 26 ++++++ ops/ci/ci_self.sh | 202 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 4e51ddf..8980a3e 100644 --- a/README.md +++ b/README.md @@ -33,18 +33,18 @@ ci-self run-focus ### ローカルネットワーク編(MacBook -> 同一LANの Mac mini) ```bash -ssh 'cd ~/dev/maakie-brainlab && ci-self register' -ssh 'cd ~/dev/maakie-brainlab && ci-self run-focus' +cd ~/dev/maakie-brainlab +ci-self remote-up --host --project-dir ~/dev/maakie-brainlab --repo mt4110/maakie-brainlab ``` ### リモートネットワーク編(外出先) ```bash -# どこからでも dispatch + All Green確認 + PRテンプレ同期 -ci-self run-focus --repo mt4110/maakie-brainlab --ref main +# どこからでも (SSHあり): Mac mini 上で register + run-focus を1コマンド実行 +ci-self remote-up --host --project-dir ~/dev/maakie-brainlab --repo mt4110/maakie-brainlab -# queued で詰まった時だけ、Mac mini 側を復旧 -ssh 'colima status || colima start' +# どこからでも (SSHなし): dispatch + All Green確認 + PRテンプレ同期のみ実行 +ci-self run-focus --repo mt4110/maakie-brainlab --ref main ``` `ci-self register` が実施すること: @@ -62,8 +62,19 @@ ssh 'colima status || colima start' 3. PR checks を All Green まで watch 4. PRテンプレートを検出して PR title/body を自動同期(テンプレートがある場合) +`ci-self remote-up` が実施すること: + +1. SSH で Mac mini に接続 +2. `ci-self register` を実行 +3. `ci-self run-focus` を実行 + これで `Copilot review` / `Codex review` に集中できます。 +補足: + +- `remote-*` は接続先Macに `ci-self`(`bash ops/ci/install_cli.sh` 実行済み)が必要です +- `--project-dir` 未指定時は `~/dev/<現在のリポジトリ名>` を使います + ## Production QuickStart(実稼働用) 詳細: `docs/ci/QUICKSTART.md` @@ -238,16 +249,14 @@ gh api repos//actions/runners --jq '.runners[] | {name,status,busy}' 外出先から検証を流す: ```bash -gh workflow run verify.yml --ref main -R -RUN_ID="$(gh run list --workflow verify.yml -R --limit 1 --json databaseId --jq '.[0].databaseId')" -gh run watch "$RUN_ID" -R --exit-status +ci-self run-focus --repo --ref main ``` runner が offline / colima停止で失敗した場合の復旧例(SSH可能時): ```bash -ssh 'colima status || colima start' -ssh 'cd ~/dev/ci-self-runner && gh api repos//actions/runners --jq ".runners[] | {name,status}"' +ci-self remote-register --host --project-dir ~/dev/ --repo +ci-self remote-run-focus --host --project-dir ~/dev/ --repo --ref main ``` ## ローカル/リモート実行 diff --git a/docs/ci/QUICKSTART.md b/docs/ci/QUICKSTART.md index 4e9fb6d..e1f1353 100644 --- a/docs/ci/QUICKSTART.md +++ b/docs/ci/QUICKSTART.md @@ -17,6 +17,25 @@ ci-self register ci-self run-focus ``` +同一LANの Mac mini で実行する場合: + +```bash +cd ~/dev/maakie-brainlab +ci-self remote-up --host --project-dir ~/dev/maakie-brainlab --repo mt4110/maakie-brainlab +``` + +外出先で SSH 可能な場合: + +```bash +ci-self remote-up --host --project-dir ~/dev/maakie-brainlab --repo mt4110/maakie-brainlab +``` + +外出先で SSH なしの場合(dispatch/watch のみ): + +```bash +ci-self run-focus --repo mt4110/maakie-brainlab --ref main +``` + ## 前提 - macOS(Mac mini 推奨) @@ -81,3 +100,10 @@ go run ./cmd/verify_full_host 1. 該当の `out/*.status` ファイルを確認 2. `reason=` 行で原因を特定 3. `docs/ci/RUNBOOK.md` の復旧手順を参照 + +SSH経由の簡易復旧: + +```bash +ci-self remote-register --host --project-dir ~/dev/ --repo +ci-self remote-run-focus --host --project-dir ~/dev/ --repo --ref main +``` diff --git a/ops/ci/ci_self.sh b/ops/ci/ci_self.sh index 68130eb..bbb6e87 100755 --- a/ops/ci/ci_self.sh +++ b/ops/ci/ci_self.sh @@ -24,6 +24,9 @@ Commands: register One-command runner registration for current repo run-watch One-command verify workflow dispatch + watch run-focus run-watch + All Green check + PR template sync + remote-register Run `register` over SSH on remote host + remote-run-focus Run `run-focus` over SSH on remote host + remote-up Run `remote-register` + `remote-run-focus` in one command watch Watch latest verify workflow run help Show this help @@ -32,6 +35,7 @@ Examples: ci-self register ci-self run-watch ci-self run-focus + ci-self remote-up --host mac-mini.local --project-dir ~/dev/maakie-brainlab USAGE } @@ -239,6 +243,201 @@ USAGE gh run watch "$run_id" -R "$repo" --exit-status } +quote_words() { + local out="" + local q="" + local arg + for arg in "$@"; do + printf -v q "%q" "$arg" + if [[ -z "$out" ]]; then + out="$q" + else + out="$out $q" + fi + done + printf '%s\n' "$out" +} + +default_remote_project_dir() { + local root + local name + root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + name="$(basename "$root")" + printf '%s\n' "~/dev/$name" +} + +run_remote_ci_self() { + local host="$1" + local project_dir="$2" + local remote_cli="$3" + shift 3 + local remote_args=("$@") + local remote_args_q + local script_q + local remote_script + + remote_args_q="$(quote_words "${remote_args[@]}")" + printf -v remote_script 'set -euo pipefail; cd %q; %q %s' "$project_dir" "$remote_cli" "$remote_args_q" + script_q="$(quote_words "$remote_script")" + echo "OK: ssh host=$host dir=$project_dir cmd=$remote_cli ${remote_args[*]}" + ssh "$host" "bash -lc $script_q" +} + +cmd_remote_register() { + local host="" + local project_dir="" + local remote_cli="ci-self" + local repo="" + local labels="" + local runner_name="" + local runner_group="" + local discord_webhook_url="" + local force_workflow=0 + local skip_workflow=0 + + while [[ $# -gt 0 ]]; do + case "$1" in + --host) host="${2:-}"; shift 2 ;; + --project-dir) project_dir="${2:-}"; shift 2 ;; + --remote-cli) remote_cli="${2:-}"; shift 2 ;; + --repo) repo="${2:-}"; shift 2 ;; + --labels) labels="${2:-}"; shift 2 ;; + --runner-name) runner_name="${2:-}"; shift 2 ;; + --runner-group) runner_group="${2:-}"; shift 2 ;; + --discord-webhook-url) discord_webhook_url="${2:-}"; shift 2 ;; + --force-workflow) force_workflow=1; shift ;; + --skip-workflow) skip_workflow=1; shift ;; + -h|--help) + cat <<'USAGE' +Usage: ci-self remote-register --host [--project-dir path] [--repo owner/repo] [--remote-cli path] + [--labels csv] [--runner-name name] [--runner-group name] + [--discord-webhook-url url] [--force-workflow] [--skip-workflow] +USAGE + return 0 + ;; + *) + echo "ERROR: unknown option for remote-register: $1" >&2 + return 2 + ;; + esac + done + + [[ -n "$host" ]] || { echo "ERROR: --host is required" >&2; return 2; } + if [[ -z "$project_dir" ]]; then + project_dir="$(default_remote_project_dir)" + fi + + local args=(register) + [[ -n "$repo" ]] && args+=(--repo "$repo") + [[ -n "$labels" ]] && args+=(--labels "$labels") + [[ -n "$runner_name" ]] && args+=(--runner-name "$runner_name") + [[ -n "$runner_group" ]] && args+=(--runner-group "$runner_group") + [[ -n "$discord_webhook_url" ]] && args+=(--discord-webhook-url "$discord_webhook_url") + [[ "$force_workflow" -eq 1 ]] && args+=(--force-workflow) + [[ "$skip_workflow" -eq 1 ]] && args+=(--skip-workflow) + + run_remote_ci_self "$host" "$project_dir" "$remote_cli" "${args[@]}" +} + +cmd_remote_run_focus() { + local host="" + local project_dir="" + local remote_cli="ci-self" + local repo="" + local ref="main" + + while [[ $# -gt 0 ]]; do + case "$1" in + --host) host="${2:-}"; shift 2 ;; + --project-dir) project_dir="${2:-}"; shift 2 ;; + --remote-cli) remote_cli="${2:-}"; shift 2 ;; + --repo) repo="${2:-}"; shift 2 ;; + --ref) ref="${2:-}"; shift 2 ;; + -h|--help) + cat <<'USAGE' +Usage: ci-self remote-run-focus --host [--project-dir path] [--repo owner/repo] [--ref branch] [--remote-cli path] +USAGE + return 0 + ;; + *) + echo "ERROR: unknown option for remote-run-focus: $1" >&2 + return 2 + ;; + esac + done + + [[ -n "$host" ]] || { echo "ERROR: --host is required" >&2; return 2; } + if [[ -z "$project_dir" ]]; then + project_dir="$(default_remote_project_dir)" + fi + + local args=(run-focus --ref "$ref") + [[ -n "$repo" ]] && args+=(--repo "$repo") + run_remote_ci_self "$host" "$project_dir" "$remote_cli" "${args[@]}" +} + +cmd_remote_up() { + local host="" + local project_dir="" + local remote_cli="ci-self" + local repo="" + local ref="main" + local labels="" + local runner_name="" + local runner_group="" + local discord_webhook_url="" + local force_workflow=0 + local skip_workflow=0 + + while [[ $# -gt 0 ]]; do + case "$1" in + --host) host="${2:-}"; shift 2 ;; + --project-dir) project_dir="${2:-}"; shift 2 ;; + --remote-cli) remote_cli="${2:-}"; shift 2 ;; + --repo) repo="${2:-}"; shift 2 ;; + --ref) ref="${2:-}"; shift 2 ;; + --labels) labels="${2:-}"; shift 2 ;; + --runner-name) runner_name="${2:-}"; shift 2 ;; + --runner-group) runner_group="${2:-}"; shift 2 ;; + --discord-webhook-url) discord_webhook_url="${2:-}"; shift 2 ;; + --force-workflow) force_workflow=1; shift ;; + --skip-workflow) skip_workflow=1; shift ;; + -h|--help) + cat <<'USAGE' +Usage: ci-self remote-up --host [--project-dir path] [--repo owner/repo] [--ref branch] + [--remote-cli path] [--labels csv] [--runner-name name] + [--runner-group name] [--discord-webhook-url url] + [--force-workflow] [--skip-workflow] +USAGE + return 0 + ;; + *) + echo "ERROR: unknown option for remote-up: $1" >&2 + return 2 + ;; + esac + done + + [[ -n "$host" ]] || { echo "ERROR: --host is required" >&2; return 2; } + if [[ -z "$project_dir" ]]; then + project_dir="$(default_remote_project_dir)" + fi + + local register_args=(--host "$host" --project-dir "$project_dir" --remote-cli "$remote_cli") + [[ -n "$repo" ]] && register_args+=(--repo "$repo") + [[ -n "$labels" ]] && register_args+=(--labels "$labels") + [[ -n "$runner_name" ]] && register_args+=(--runner-name "$runner_name") + [[ -n "$runner_group" ]] && register_args+=(--runner-group "$runner_group") + [[ -n "$discord_webhook_url" ]] && register_args+=(--discord-webhook-url "$discord_webhook_url") + [[ "$force_workflow" -eq 1 ]] && register_args+=(--force-workflow) + [[ "$skip_workflow" -eq 1 ]] && register_args+=(--skip-workflow) + cmd_remote_register "${register_args[@]}" + + local run_focus_args=(--host "$host" --project-dir "$project_dir" --remote-cli "$remote_cli" --ref "$ref") + [[ -n "$repo" ]] && run_focus_args+=(--repo "$repo") + cmd_remote_run_focus "${run_focus_args[@]}" +} + main() { local cmd="${1:-help}" shift || true @@ -246,6 +445,9 @@ main() { register) cmd_register "$@" ;; run-watch) cmd_run_watch "$@" ;; run-focus) cmd_run_watch --all-green --sync-pr-template "$@" ;; + remote-register) cmd_remote_register "$@" ;; + remote-run-focus) cmd_remote_run_focus "$@" ;; + remote-up) cmd_remote_up "$@" ;; watch) cmd_watch "$@" ;; help|-h|--help) usage ;; *) From 491f8731f5c200f3a3cddb68cc1d25a37ad32ba7 Mon Sep 17 00:00:00 2001 From: mt4110 <4221282+mt4110@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:39:43 +0900 Subject: [PATCH 2/3] ci: shorten flow with up and pr-template scaffold --- README.md | 349 +++------------------------- docs/ci/QUICKSTART.md | 7 +- ops/ci/ci_self.sh | 54 +++++ ops/ci/ci_self_test.go | 57 +++++ ops/ci/onboard_and_verify.sh | 11 +- ops/ci/scaffold_pr_template.sh | 121 ++++++++++ ops/ci/scaffold_pr_template_test.go | 89 +++++++ 7 files changed, 361 insertions(+), 327 deletions(-) create mode 100644 ops/ci/ci_self_test.go create mode 100755 ops/ci/scaffold_pr_template.sh create mode 100644 ops/ci/scaffold_pr_template_test.go diff --git a/README.md b/README.md index 8980a3e..67cdb9f 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,7 @@ bash ops/ci/install_cli.sh ```bash cd ~/dev/maakie-brainlab -ci-self register -ci-self run-focus +ci-self up ``` ### ローカルネットワーク編(MacBook -> 同一LANの Mac mini) @@ -47,355 +46,63 @@ ci-self remote-up --host --project-dir ~/dev/maakie-brain ci-self run-focus --repo mt4110/maakie-brainlab --ref main ``` -`ci-self register` が実施すること: +サブコマンド要約: -1. `colima` 起動確認 -2. runner 登録(repo指定で token 自動取得) -3. `runner_health` -4. `SELF_HOSTED_OWNER` 変数設定 -5. カレントプロジェクトに `verify.yml` を生成(必要時) +- `ci-self up`: `register` + `run-focus` を連続実行(ローカル最短) +- `ci-self register`: colima確認 + runner登録 + health + `SELF_HOSTED_OWNER` + 必要なら `verify.yml` / PRテンプレ雛形 +- `ci-self run-focus`: verify dispatch/watch + PR checks All Green待機 + PRテンプレ同期 +- `ci-self remote-up`: SSH先で `register` と `run-focus` を連続実行 -`ci-self run-focus` が実施すること: +補足: `remote-*` は接続先Macに `ci-self`(`bash ops/ci/install_cli.sh` 実行済み)が必要です。 -1. `verify.yml` dispatch -2. 実行結果 watch(失敗なら即終了) -3. PR checks を All Green まで watch -4. PRテンプレートを検出して PR title/body を自動同期(テンプレートがある場合) - -`ci-self remote-up` が実施すること: - -1. SSH で Mac mini に接続 -2. `ci-self register` を実行 -3. `ci-self run-focus` を実行 - -これで `Copilot review` / `Codex review` に集中できます。 - -補足: - -- `remote-*` は接続先Macに `ci-self`(`bash ops/ci/install_cli.sh` 実行済み)が必要です -- `--project-dir` 未指定時は `~/dev/<現在のリポジトリ名>` を使います - -## Production QuickStart(実稼働用) - -詳細: `docs/ci/QUICKSTART.md` +## 補助コマンド(3ステップ版 / CLI未導入時) ```bash -# 0) 再起動直後は docker runtime を復帰 -colima status || colima start - -# 1) Runner セットアップ(初回のみ・冪等) -# - gh auth が有効なら registration token を自動取得して登録まで実行 +# 1) runner登録 go run ./cmd/runner_setup --apply --repo # 2) 健康診断 go run ./cmd/runner_health -# 3) 軽量検証(ホストラッパ経由) +# 3) verify実行 go run ./cmd/verify_lite_host - -# 4) フル検証 dry-run(ホストラッパ経由) go run ./cmd/verify_full_host --dry-run ``` -SOT(判定の真実): `out/runner-setup.status`, `out/health.status`, `out/verify-lite.status`, `out/verify-full.status` +SOT(判定の真実): -runner を1コマンド登録(最短): - -```bash -go run ./cmd/runner_setup --apply --repo mt4110/maakie-brainlab -``` - -補足: - -- デフォルト install 先は `~/.local/ci-runner--`(repoごとに分離) -- `RUNNER_TOKEN` を渡さない場合は `gh api` で registration token を自動取得 -- オプション: `--labels`, `--name`, `--runner-group`, `--install-dir`, `--no-service` - -## 最短 1-2-3(運用手順) +- `out/runner-setup.status` +- `out/health.status` +- `out/verify-lite.status` +- `out/verify-full.status` -### 0) 初回だけ(対象リポジトリごと) +## 初回セットアップ(対象リポジトリ) ```bash # 必須: ownerガード gh variable set SELF_HOSTED_OWNER -b "$(gh repo view --json owner --jq .owner.login)" -R # 任意: 失敗通知 -printf '%s' '' | gh secret set DISCORD_WEBHOOK_URL -R -``` - -### 1) ローカル事前検証 +printf '%s' '' | gh secret set DISCORD_WEBHOOK_URL -R -```bash -mise x -- go run ./cmd/verify-lite -mkdir -p out cache -REPO_DIR='.' OUT_DIR='out' CACHE_DIR='cache' mise x -- go run ./cmd/verify-full --dry-run -``` - -失敗時は `out/verify-lite.status` / `out/verify-full.status` の `ERROR:` 行を確認。 - -### 2) GitHub Actions 実行 - -```bash -gh workflow run verify.yml --ref main -R -``` - -### 3) 完了待ち(成功/失敗を確定) - -```bash -RUN_ID="$(gh run list --workflow verify.yml -R --limit 1 --json databaseId --jq '.[0].databaseId')" -gh run watch "$RUN_ID" -R --exit-status +# verify.yml が未作成なら(404回避) +bash ops/ci/scaffold_verify_workflow.sh --repo ~/dev/ --apply ``` -複数リポジトリ運用時は毎回 `-R ` を変えるだけです。 - -注意: - -- `gh workflow run verify.yml ...` は、対象リポジトリに `.github/workflows/verify.yml` が存在しないと 404 になります。 -- 先に対象リポジトリへ workflow を作成してコミットしてください。 +## 外出先運用の要点 -workflow を1コマンドで生成(`ci-self-runner` リポジトリから実行): - -```bash -bash ops/ci/scaffold_verify_workflow.sh --repo ~/dev/maakie-brainlab --apply -``` - -生成後は対象リポジトリでコミット: - -```bash -cd ~/dev/maakie-brainlab -git add .github/workflows/verify.yml .gitignore -git commit -m "ci: add self-hosted verify workflow" -git push -``` - -## system architecture flow - -![system architecture](docs/assets/systemArchitecture.png) - -## 入口ドキュメント - -- `docs/ci/QUICKSTART.md`(実稼働 QuickStart) -- `docs/ci/QUICKSTART_PLAN.md`(設計 SOT) -- `docs/ci/RUNNER_LOCK.md`(Runner バージョン固定) -- `docs/ci/SYSTEM.md` -- `docs/ci/FLOW.md` -- `docs/ci/RUNNER_ISOLATION.md` -- `docs/ci/COLIMA_TUNING.md` -- `docs/ci/SHELL_POLICY.md` -- `docs/ci/SECRETS_POLICY.md` -- `docs/ci/DISCORD_NOTIFICATIONS.md` -- `docs/ci/GARBAGE_COLLECTION.md` -- `docs/ci/RUNBOOK.md` -- `docs/ci/SECURITY_HARDENING_PLAN.md` -- `docs/ci/SECURITY_HARDENING_TASK.md` - -## 前提セットアップ(初回) - -```bash -mise trust -mise install -``` - -## Quick Start(最短) - -```bash -# 1) 軽量検証(公式推奨: gofmt/vet/test) -mise x -- go run ./cmd/verify-lite - -# 2) フル検証(dry-run) -mkdir -p out cache -REPO_DIR='.' OUT_DIR='out' CACHE_DIR='cache' mise x -- go run ./cmd/verify-full --dry-run - -# 3) レビューパック(core) -mise x -- go run ./cmd/review-pack --profile core - -# 4) optional版レビューパック(必要時のみ) -mise x -- go run ./cmd/review-pack --profile optional - -# 5) Discord通知のdry-run(Webhook送信なし) -DISCORD_WEBHOOK_URL='https://example.invalid/webhook' mise x -- \ - go run ./cmd/notify_discord --dry-run --status out/verify-full.status --title "verify-full local" --min-level ERROR -``` - -## 実行フロー(推奨) - -1. MacBookで `verify-lite` -2. Mac miniで `verify-full`(または `remote_verify --mode remote`) -3. `review-pack --profile core` で提出パック生成 -4. 必要時のみ `review-pack --profile optional` -5. 検証後にPR作成(GitHubは証跡の公証台帳) - -## 外出先から Mac mini self-hosted runner を使う - -前提: - -- Mac mini が起動中 -- runner が `online` -- colima / docker が動作中 - -ポイント: - -- 外出先でも、GitHubへアクセスできれば `workflow_dispatch` と `PR` は実行可能です。 -- self-hosted runner は GitHub 側へ取りに行く方式なので、通常は「自宅回線への公開ポート」は不要です。 -- ただし runner 本体が停止している場合、復旧には Mac mini への遠隔管理手段(例: Tailscale + SSH)が必要です。 - -まず状態確認: - -```bash -ssh 'colima status || colima start' -gh api repos//actions/runners --jq '.runners[] | {name,status,busy}' -``` - -外出先から検証を流す: - -```bash -ci-self run-focus --repo --ref main -``` - -runner が offline / colima停止で失敗した場合の復旧例(SSH可能時): +- SSHあり: `ci-self remote-up ...` が最短 +- SSHなし: `ci-self run-focus --repo --ref main` +- runner/colima 停止時のみ、SSHで復旧 ```bash ci-self remote-register --host --project-dir ~/dev/ --repo ci-self remote-run-focus --host --project-dir ~/dev/ --repo --ref main ``` -## ローカル/リモート実行 - -### verify-full(ローカル) +## 詳細ドキュメント -`verify-full.status` に `GITHUB_RUN_ID / GITHUB_SHA / GITHUB_REF_NAME` を記録したい場合: - -```bash -go run ./cmd/remote_verify --mode local -``` - -生成物: - -- `out/verify-full.status` -- `out/logs/` -- 実行後に `out/logs` は自動で最新5件に整理 - -### verify-full(MacBook -> SSH -> Mac mini) - -```bash -go run ./cmd/remote_verify \ - --mode remote \ - --remote-host \ - --remote-repo -``` - -回収される生成物: - -- `out/remote/verify-full.status` -- `out/remote/logs/` - -## CIオーケストレーター(Go) - -```bash -go run ./cmd/ci_orch run-plan --timebox-min 20 -``` - -個別ステップ実行: - -```bash -go run ./cmd/ci_orch preflight -go run ./cmd/ci_orch verify-lite -go run ./cmd/ci_orch full-build -go run ./cmd/ci_orch full-test -go run ./cmd/ci_orch bundle-make -``` - -## Discord通知(ローカル確認) - -- 通知は外部Actionを使わず `cmd/notify_discord`(Go)で送信 -- 既定は `--min-level ERROR`(ERROR時のみ送信) - -Secret設定(GitHub Actions): - -```bash -printf '%s' '' | gh secret set DISCORD_WEBHOOK_URL -R -``` - -ownerガード変数(必須): - -```bash -gh variable set SELF_HOSTED_OWNER -b "$(gh repo view --json owner --jq .owner.login)" -R -``` - -dry-run(Webhook送信せずpayload確認): - -```bash -go run ./cmd/notify_discord --dry-run --status out/verify-full.status --title "verify-full local" --min-level ERROR -``` - -将来拡張(任意): - -- 基本はテキスト通知(run URL中心) -- 将来ログ添付を行う場合は、秘匿情報マスクとサイズ制限を先に定義してから有効化 - -## レビューパック(ChatGPT / Gemini) - -### 必須(core) - -```bash -go run ./cmd/review-pack --profile core -``` - -生成物: - -- `out/reviewpack/review-pack-.tar.gz` -- `out/reviewpack/latest.tar.gz` -- `out/reviewpack/review-pack-/PACK_SUMMARY.md` -- `latest.tar.gz` はエイリアス/シンボリックリンクではなく実体ファイル(copy)です - -### Optional(追加証跡を含む) - -```bash -go run ./cmd/review-pack --profile optional -``` - -生成物: - -- `out/reviewpack/review-pack-optional-.tar.gz` -- `out/reviewpack/latest-optional.tar.gz` -- `out/reviewpack/review-pack-optional-/PACK_SUMMARY.md` -- `latest-optional.tar.gz` も実体ファイル(copy)です -- 実行後に `out/reviewpack` は自動で最新5件に整理(`latest*.tar.gz` は保持) - -実体確認コマンド: - -```bash -ls -l out/reviewpack/latest.tar.gz out/reviewpack/latest-optional.tar.gz -file out/reviewpack/latest.tar.gz out/reviewpack/latest-optional.tar.gz -``` - -## GC(out配下の整理) - -dry-run: - -```bash -go run ./cmd/gc_out -``` - -既定: `out/logs` と `out/reviewpack` は最新5件保持。 - -実削除: - -```bash -go run ./cmd/gc_out --apply --max-delete 50 -``` - -## Git管理しないもの - -- `out/`(ログ、status、reviewpack成果物) -- `.local/`(ローカル state / 実行履歴) -- `cache/`(ローカルキャッシュ) - -## 公開前チェック(最短) - -```bash -mise x -- go test ./... -mise x -- go run ./cmd/verify-lite -REPO_DIR='.' OUT_DIR='out' CACHE_DIR='cache' mise x -- go run ./cmd/verify-full --dry-run -mise x -- go run ./cmd/review-pack --profile core -``` +- `docs/ci/QUICKSTART.md`(最短運用) +- `docs/ci/RUNBOOK.md`(障害復旧) +- `docs/ci/SECURITY_HARDENING_TASK.md`(外部コラボ時の必須対策) +- その他は `docs/ci/` 配下を参照 diff --git a/docs/ci/QUICKSTART.md b/docs/ci/QUICKSTART.md index e1f1353..ffe8813 100644 --- a/docs/ci/QUICKSTART.md +++ b/docs/ci/QUICKSTART.md @@ -1,6 +1,6 @@ # QUICKSTART(実稼働の最短導線) -## 最短2コマンド(推奨) +## 最短1コマンド(推奨) 最初の1回だけ: @@ -13,10 +13,11 @@ CI実施プロジェクトで: ```bash cd ~/dev/maakie-brainlab -ci-self register -ci-self run-focus +ci-self up ``` +`ci-self up` は `register + run-focus` を連続実行し、PRテンプレートが無ければ自動生成します。 + 同一LANの Mac mini で実行する場合: ```bash diff --git a/ops/ci/ci_self.sh b/ops/ci/ci_self.sh index bbb6e87..f6435ad 100755 --- a/ops/ci/ci_self.sh +++ b/ops/ci/ci_self.sh @@ -21,6 +21,7 @@ Usage: ci-self [options] Commands: + up One-command local register + run-focus register One-command runner registration for current repo run-watch One-command verify workflow dispatch + watch run-focus run-watch + All Green check + PR template sync @@ -32,6 +33,7 @@ Commands: Examples: cd ~/dev/maakie-brainlab + ci-self up ci-self register ci-self run-watch ci-self run-focus @@ -243,6 +245,57 @@ USAGE gh run watch "$run_id" -R "$repo" --exit-status } +cmd_up() { + local repo="" + local repo_dir="$PWD" + local ref="main" + local labels="" + local runner_name="" + local runner_group="" + local discord_webhook_url="" + local force_workflow=0 + local skip_workflow=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --repo) repo="${2:-}"; shift 2 ;; + --repo-dir) repo_dir="${2:-}"; shift 2 ;; + --ref) ref="${2:-}"; shift 2 ;; + --labels) labels="${2:-}"; shift 2 ;; + --runner-name) runner_name="${2:-}"; shift 2 ;; + --runner-group) runner_group="${2:-}"; shift 2 ;; + --discord-webhook-url) discord_webhook_url="${2:-}"; shift 2 ;; + --force-workflow) force_workflow=1; shift ;; + --skip-workflow) skip_workflow=1; shift ;; + -h|--help) + cat <<'USAGE' +Usage: ci-self up [--repo owner/repo] [--repo-dir path] [--ref branch] [--labels csv] + [--runner-name name] [--runner-group name] [--discord-webhook-url url] + [--force-workflow] [--skip-workflow] +USAGE + return 0 + ;; + *) + echo "ERROR: unknown option for up: $1" >&2 + return 2 + ;; + esac + done + + local register_args=(--repo-dir "$repo_dir" --ref "$ref") + [[ -n "$repo" ]] && register_args+=(--repo "$repo") + [[ -n "$labels" ]] && register_args+=(--labels "$labels") + [[ -n "$runner_name" ]] && register_args+=(--runner-name "$runner_name") + [[ -n "$runner_group" ]] && register_args+=(--runner-group "$runner_group") + [[ -n "$discord_webhook_url" ]] && register_args+=(--discord-webhook-url "$discord_webhook_url") + [[ "$force_workflow" -eq 1 ]] && register_args+=(--force-workflow) + [[ "$skip_workflow" -eq 1 ]] && register_args+=(--skip-workflow) + cmd_register "${register_args[@]}" + + local run_focus_args=(--ref "$ref" --project-dir "$repo_dir") + [[ -n "$repo" ]] && run_focus_args+=(--repo "$repo") + cmd_run_watch --all-green --sync-pr-template "${run_focus_args[@]}" +} + quote_words() { local out="" local q="" @@ -442,6 +495,7 @@ main() { local cmd="${1:-help}" shift || true case "$cmd" in + up) cmd_up "$@" ;; register) cmd_register "$@" ;; run-watch) cmd_run_watch "$@" ;; run-focus) cmd_run_watch --all-green --sync-pr-template "$@" ;; diff --git a/ops/ci/ci_self_test.go b/ops/ci/ci_self_test.go new file mode 100644 index 0000000..4a84788 --- /dev/null +++ b/ops/ci/ci_self_test.go @@ -0,0 +1,57 @@ +package ci_test + +import ( + "os/exec" + "strings" + "testing" +) + +func runCiSelf(t *testing.T, args ...string) (string, error) { + t.Helper() + cmd := exec.Command("bash", append([]string{"./ci_self.sh"}, args...)...) + cmd.Dir = "." + out, err := cmd.CombinedOutput() + return string(out), err +} + +func TestHelpListsRemoteCommands(t *testing.T) { + out, err := runCiSelf(t, "help") + if err != nil { + t.Fatalf("help failed: %v\noutput:\n%s", err, out) + } + for _, want := range []string{"up", "remote-register", "remote-run-focus", "remote-up"} { + if !strings.Contains(out, want) { + t.Fatalf("help output missing %q\noutput:\n%s", want, out) + } + } +} + +func TestRemoteUpRequiresHost(t *testing.T) { + out, err := runCiSelf(t, "remote-up") + if err == nil { + t.Fatalf("expected failure without --host, got success\noutput:\n%s", out) + } + if !strings.Contains(out, "ERROR: --host is required") { + t.Fatalf("unexpected error output\noutput:\n%s", out) + } +} + +func TestRemoteRegisterRequiresHost(t *testing.T) { + out, err := runCiSelf(t, "remote-register") + if err == nil { + t.Fatalf("expected failure without --host, got success\noutput:\n%s", out) + } + if !strings.Contains(out, "ERROR: --host is required") { + t.Fatalf("unexpected error output\noutput:\n%s", out) + } +} + +func TestUpHelp(t *testing.T) { + out, err := runCiSelf(t, "up", "--help") + if err != nil { + t.Fatalf("up --help failed: %v\noutput:\n%s", err, out) + } + if !strings.Contains(out, "Usage: ci-self up") { + t.Fatalf("up help output missing usage\noutput:\n%s", out) + } +} diff --git a/ops/ci/onboard_and_verify.sh b/ops/ci/onboard_and_verify.sh index c688dce..0594151 100755 --- a/ops/ci/onboard_and_verify.sh +++ b/ops/ci/onboard_and_verify.sh @@ -8,7 +8,7 @@ Usage: Options: --repo Target repository (required) - --repo-dir Local path of target repo (optional; used to scaffold verify.yml) + --repo-dir Local path of target repo (optional; used to scaffold verify.yml/pr template) --ref Branch/ref for workflow dispatch (default: main) --labels Runner labels for registration (default: self-hosted,mac-mini,colima,verify-full) --runner-name Runner name override @@ -24,7 +24,7 @@ Examples: # 最短: runner登録 + owner変数 + verify dispatch bash ops/ci/onboard_and_verify.sh --repo mt4110/maakie-brainlab - # verify.yml をローカル作成してから dispatch + # verify.yml / PR template をローカル作成してから dispatch bash ops/ci/onboard_and_verify.sh --repo mt4110/maakie-brainlab --repo-dir ~/dev/maakie-brainlab USAGE } @@ -152,7 +152,12 @@ if [[ "$SKIP_WORKFLOW" -ne 1 && -n "$REPO_DIR" ]]; then scaffold_args+=(--force) fi bash ops/ci/scaffold_verify_workflow.sh "${scaffold_args[@]}" - echo "NOTE: commit workflow changes in $REPO_DIR if needed (verify.yml/.gitignore)" +fi + +if [[ -n "$REPO_DIR" ]]; then + echo "OK: scaffold_pr_template repo_dir=$REPO_DIR" + bash ops/ci/scaffold_pr_template.sh --repo "$REPO_DIR" --apply + echo "NOTE: commit workflow/template changes in $REPO_DIR if needed (.github/workflows/verify.yml/.github/pull_request_template.md/.gitignore)" fi if [[ "$SKIP_DISPATCH" -eq 1 ]]; then diff --git a/ops/ci/scaffold_pr_template.sh b/ops/ci/scaffold_pr_template.sh new file mode 100755 index 0000000..226038c --- /dev/null +++ b/ops/ci/scaffold_pr_template.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'USAGE' +Usage: + scaffold_pr_template.sh [--repo ] [--apply] [--force] + +Options: + --repo Target repository path (default: current directory) + --apply Write PR template to target repository + --force Overwrite existing template path + -h, --help Show this help + +Behavior: + - Detects common PR template paths. + - If a template already exists, exits with SKIP unless --force is set. + - Without --apply, prints template content to stdout. +USAGE +} + +find_existing_template() { + local root="$1" + local p="" + for p in \ + ".github/pull_request_template.md" \ + ".github/PULL_REQUEST_TEMPLATE.md" \ + "PULL_REQUEST_TEMPLATE.md" \ + "docs/pull_request_template.md"; do + if [[ -f "$root/$p" ]]; then + printf '%s\n' "$root/$p" + return 0 + fi + done + if [[ -d "$root/.github/PULL_REQUEST_TEMPLATE" ]]; then + p="$(find "$root/.github/PULL_REQUEST_TEMPLATE" -maxdepth 1 -type f -name '*.md' | sort | head -n 1 || true)" + if [[ -n "$p" ]]; then + printf '%s\n' "$p" + return 0 + fi + fi + return 1 +} + +render_template() { + cat <<'MD' +## Summary +- + +## Why +- + +## Changes +- + +## Verification +- [ ] `ci-self run-focus` passed +- [ ] Logs/status files checked (`out/*.status`) + +## Risks +- [ ] Rollback path considered +MD +} + +REPO_DIR="." +APPLY=0 +FORCE=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --repo) + REPO_DIR="${2:-}" + shift 2 + ;; + --apply) + APPLY=1 + shift + ;; + --force) + FORCE=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "ERROR: unknown option: $1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ ! -d "$REPO_DIR" ]]; then + echo "ERROR: repo directory not found: $REPO_DIR" >&2 + exit 2 +fi + +REPO_DIR="$(cd "$REPO_DIR" && pwd)" +existing_template="$(find_existing_template "$REPO_DIR" || true)" +target_template="$REPO_DIR/.github/pull_request_template.md" + +if [[ -n "$existing_template" ]]; then + target_template="$existing_template" +fi + +if [[ "$APPLY" -eq 0 ]]; then + render_template + exit 0 +fi + +if [[ -n "$existing_template" && "$FORCE" -ne 1 ]]; then + echo "SKIP: pr template already exists: $existing_template (use --force to overwrite)" + exit 0 +fi + +mkdir -p "$(dirname "$target_template")" +render_template >"$target_template" +echo "OK: wrote $target_template" + diff --git a/ops/ci/scaffold_pr_template_test.go b/ops/ci/scaffold_pr_template_test.go new file mode 100644 index 0000000..edf25a5 --- /dev/null +++ b/ops/ci/scaffold_pr_template_test.go @@ -0,0 +1,89 @@ +package ci_test + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func runScaffoldPRTemplate(t *testing.T, repo string, args ...string) (string, error) { + t.Helper() + allArgs := []string{"./scaffold_pr_template.sh", "--repo", repo} + allArgs = append(allArgs, args...) + cmd := exec.Command("bash", allArgs...) + cmd.Dir = "." + out, err := cmd.CombinedOutput() + return string(out), err +} + +func TestScaffoldPRTemplateCreatesCanonicalTemplate(t *testing.T) { + repo := t.TempDir() + out, err := runScaffoldPRTemplate(t, repo, "--apply") + if err != nil { + t.Fatalf("scaffold failed: %v\noutput:\n%s", err, out) + } + target := filepath.Join(repo, ".github", "pull_request_template.md") + b, err := os.ReadFile(target) + if err != nil { + t.Fatalf("failed to read generated template: %v", err) + } + if !strings.Contains(string(b), "## Summary") { + t.Fatalf("template content missing expected section\ncontent:\n%s", string(b)) + } +} + +func TestScaffoldPRTemplateSkipsWhenTemplateExists(t *testing.T) { + repo := t.TempDir() + target := filepath.Join(repo, ".github", "pull_request_template.md") + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + t.Fatalf("mkdir failed: %v", err) + } + const keep = "KEEP_EXISTING_TEMPLATE\n" + if err := os.WriteFile(target, []byte(keep), 0o644); err != nil { + t.Fatalf("write failed: %v", err) + } + + out, err := runScaffoldPRTemplate(t, repo, "--apply") + if err != nil { + t.Fatalf("expected skip success, got error: %v\noutput:\n%s", err, out) + } + if !strings.Contains(out, "SKIP: pr template already exists") { + t.Fatalf("expected skip message\noutput:\n%s", out) + } + b, err := os.ReadFile(target) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if string(b) != keep { + t.Fatalf("existing template should be preserved\ncontent:\n%s", string(b)) + } +} + +func TestScaffoldPRTemplateForceOverwritesExisting(t *testing.T) { + repo := t.TempDir() + target := filepath.Join(repo, ".github", "pull_request_template.md") + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + t.Fatalf("mkdir failed: %v", err) + } + if err := os.WriteFile(target, []byte("OLD\n"), 0o644); err != nil { + t.Fatalf("write failed: %v", err) + } + + out, err := runScaffoldPRTemplate(t, repo, "--apply", "--force") + if err != nil { + t.Fatalf("force scaffold failed: %v\noutput:\n%s", err, out) + } + if !strings.Contains(out, "OK: wrote") { + t.Fatalf("expected write message\noutput:\n%s", out) + } + b, err := os.ReadFile(target) + if err != nil { + t.Fatalf("read failed: %v", err) + } + if !strings.Contains(string(b), "## Verification") { + t.Fatalf("forced content not written\ncontent:\n%s", string(b)) + } +} + From 503b253151c207348246f26d9c8bba34e34fe7ef Mon Sep 17 00:00:00 2001 From: mt4110 <4221282+mt4110@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:41:12 +0900 Subject: [PATCH 3/3] test: format ci scaffold tests --- ops/ci/scaffold_pr_template_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/ops/ci/scaffold_pr_template_test.go b/ops/ci/scaffold_pr_template_test.go index edf25a5..10339c1 100644 --- a/ops/ci/scaffold_pr_template_test.go +++ b/ops/ci/scaffold_pr_template_test.go @@ -86,4 +86,3 @@ func TestScaffoldPRTemplateForceOverwritesExisting(t *testing.T) { t.Fatalf("forced content not written\ncontent:\n%s", string(b)) } } -