サイオステクノロジー(Tech.Lab)のブログ - TECH PLAY

TECH PLAY

サイオステクノロジー(Tech.Lab)

サイオステクノロジー(Tech.Lab) の技術ブログ

672

要約 PRのレビューコメントをAzure OpenAIで分析し、GitHub Copilotの設定ファイルを半自動で更新します AIが直接コミットしないHuman-in-the-Loop設計で、誤ったルール混入を防げます gpt-5-miniなら1回の分析コストは1円未満。Before/Afterで実際にGitHub Copilot Code ReviewのPRレビュー精度が変わることを確認しました はじめに こんにちは、サイオステクノロジーの藤井です。 Pull Requestのレビューをしているときに「この指摘、前のPRでも同じことを言った気がする」ってことありますよね。私はありました。 今回は、そんなレビュアーの不満を解消する仕組みを作ってみました。日々同じ指摘を繰り返すことに疲弊している、シニアエンジニアやテックリードの方々の一助になれば幸いです。 背景:レビューコメントは「暗黙知の宝庫」である チーム開発には、明文化されていない「暗黙知」がつきものです。昨日の設計会議で決まった命名規則、先週の障害対応で得た「このパターンは使わない」という教訓、半年かけて育ててきたプロジェクト固有のベストプラクティス、「ほかの実装者のPRレビューで指摘された事」なんていう実装者側にもどうにも知りえない場合も多々あります。 PRをレビューする際、これらを踏まえた視点は品質担保に不可欠ですが、これらがわざわざWikiやREADMEに書き起こされることは稀です。 ここにGitHub Copilot Code Reviewを導入しても、素のAIはチーム固有の暗黙知を知りません。設定ファイルでルールを教えることは可能ですが、新たな暗黙知が生まれるたびに手動で設定ファイルをメンテし続ける運用は、どう考えても面倒で長続きしません。 結果として、AIがスルーした暗黙知を、人間のレビュアーが何度も繰り返し指摘する羽目になります。 こうして人間のレビューコストは膨らみ、開発のボトルネックになっていきます。レビュアーは疲弊しますが、一方で、彼らが残したレビューコメントは生きた知見が詰まった「情報の宝庫」です。 毎回コストをかけて宝(知見)を生み出しているのに、PRがマージされると同時に、それはコードの海に沈んで捨てられてしまう。これは非常にもったいない状態です。 その情報を拾い上げて、GitHub Copilotの設定に自動で反映できれば、同じ指摘を繰り返す手間を減らせると思います。そう考えて、 PRのレビューコメントをAIで分析して、GitHub Copilotの設定ファイルを半自動で更新するフィードバックループ を作ってみました。次の章では、その仕組みを説明していきます。 作ったものの概要 システム構成図 登場するコンポーネントとその関係は次の通りです。 分析に使用するAIとしては、ソースコードがAIに学習されるリスクを避けたかったので、Azure OpenAIを選びました。 エンタープライズ契約の環境であれば、機密情報である実装コードを入力しても、モデルの学習に二次利用されないことが保証されているからです。 ユースケースのワークフロー 「何が起きるか」の流れはシンプルです。 ①実装者が問題のあるPRを作成します ②GitHub Copilotにレビューさせますが、汎用的な指摘しかできないので、当然、暗黙知な問題は見逃します。 ③仕方が無いので、人間のレビュアーが指摘します。 ④実装者が指摘内容を直し、マージします。 ⑤PRマージをトリガーにGitHub Actions が起動し、PRのコメントやdiffを取得します。 ⑥Python スクリプトがAOAIにコメント等の分析を依頼し、どのようにCopilotの設定ファイルを更新すべきが出力されます ⑦設定ファイルの更新PR(学習PR) を自動作成します。 ⑧人間が内容を確認してマージ ⑨同様な問題を持つ次のPR が作成されます ⑩Copilotno設定ファイルにその問題点を指摘する様に記載されたので、GitHub Copilotは見逃さずに指摘できます。 直接コミットせずPRを挟むHuman-in-the-Loop設計を採用しています。AIの誤判断や個人の好みがそのままルールになるのを防ぐための安全弁です。人間が「このルールはおかしい」と判断すればPRを閉じるだけで済みます。 2種類の設定ファイルを使い分ける GitHub Copilot の設定ファイルには2種類あります。 ファイル 役割 例 .github/copilot-instructions.md コーディングルール :コードを書くときに守るべき規約 「エラー出力には print() ではなく必ず共通の logger.error() を使用すること」 .github/instructions/review.instructions.md レビュー観点 :レビュー時に確認すべき視点 「ログ出力の内容に、パスワードや個人情報(PII)などの機密情報が含まれていないか確認する」 AIはレビューコメントを読み、「これはコードの書き方の規約か、それともレビュー時のチェック観点か」を判断して適切なファイルを更新します。1つの指摘が両方に当てはまる場合は両方に反映します。 詳しくはこちらの記事をご覧ください。 GitHub Copilot設定5種を網羅!生産性を最大化する使い分け術 リジェクトPRされたPRからも知見を得る ワークフローのトリガーによって学習モードが変わります。 トリガー 学習モード PRがマージされた Best Practices として抽出(「こうすると良い」) learn-from-rejection ラベル付きでクローズされた Anti-Patterns として抽出(「DO NOT …」形式) リジェクトされたPRは「やってはいけないパターン」の宝庫だと思います。問題のあるコードを含むPRに learn-from-rejection ラベルを付けてクローズするだけで、アンチパターンとして学習させられる様にしました。 セットアップ方法 必要なもの Azure OpenAI リソース (gpt-5-mini を推奨。コストが低く1回のPR分析は1円未満) Endpoint・API Key・デプロイ名を用意します GitHub リポジトリ (導入先) Step 1: 3ファイルをコピーする 以下の3つのファイルを導入先リポジトリの同じパスに配置します。 付録として、この記事の最下部にファイル全体を置いておきます。 .github/workflows/learn-from-review.yml ワークフローの定義ファイルです。PRがマージされたとき、または、’learn-from-rejection’ラベル付きでクローズされたときに発火します。 jobs: analyze-and-update: # 「マージされた」 OR 「'learn-from-rejection'ラベル付きでクローズされた」場合に実行 # マージした場合は学習対象 # 破棄するPRの内、学習対象にしたいものは'learn-from-rejection'ラベルを付ける # ai-learning/* ブランチからのPRは除外(再帰防止) if: >- !startsWith(github.head_ref, 'ai-learning/') && ( github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true || (github.event.pull_request.state == 'closed' && contains(github.event.pull_request.labels.*.name, 'learn-from-rejection')) ) runs-on: ubuntu-latest # 同一PR番号の並列実行を防止 concurrency: group: "learn-${{ github.event.pull_request.number || github.event.inputs.pr_number }}" cancel-in-progress: true env: TARGET_PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.pull_request.number }} steps: ・・・ scripts/learn_from_pr.py PRの内容を取得し、AOAIにクエリーを投げるpythonスクリプトです。プロンプトはここに記載しているので、必要に応じて調整してください。 ・・・ def main(): """PRレビューから学習してルールファイルを更新するメイン処理。""" # --- 1. GitHubデータの取得 --- g = Github(auth=github.Auth.Token(os.environ["GITHUB_TOKEN"])) repo = g.get_repo(os.environ["GITHUB_REPOSITORY"]) pr = repo.get_pull(int(os.environ["PR_NUMBER"])) # --- 1-a. Issue コメント (PR全体のコメント): 人間のみ --- comments = pr.get_issue_comments() all_comments_text = "\n".join( [f"{c.user.login}: {c.body}" for c in comments if c.user.type != "Bot"] ) # --- 1-b. Review コメント: スレッド構造で再構築 (Bot方針変更) --- review_comments = pr.get_review_comments() all_reviews_text = build_review_threads(review_comments) # --- 1-c. PR変更ファイル情報 --- pr_files_summary = get_pr_files_summary(pr) # 何も人間の会話がなければ終了 if not all_comments_text and not all_reviews_text: print("No human comments found. Skipping.") return ・・・ scripts/requirements-action.txt ワークフローでpythonを動かすときに必要なライブラリの定義です。 Step 2: GitHub Secrets を登録する Settings > Secrets and variables > Actions で以下を追加します。 Secret名 値 AZURE_OPENAI_API_KEY AzureポータルのAPIキー AZURE_OPENAI_ENDPOINT https://your-resource.openai.azure.com/ GITHUB_TOKEN はGitHub Actionsが自動生成するため不要です。 Step 3: Actionsの権限を設定する Settings > Actions > General > Workflow permissions で以下を有効化します。 ✅ Read and write permissions ✅ Allow GitHub Actions to create and approve pull requests この設定がないとPRの自動作成がエラーになります。 Step 4: 指示書ファイルを初期化する AIが書き込む先のファイルを作っておきます。 .github/copilot-instructions.md : # プロジェクト コーディングルール ## 言語・フレームワーク - Python 3.8以上 - Webフレームワーク: FastAPI ## コーディング規約 - PEP 8に従う - 型ヒントを積極的に使用する .github/instructions/review.instructions.md : --- applyTo: "**/*.py" excludeAgent: "coding-agent" description: "Python PR Review専用ガイドライン - Code Reviewのみに適用" --- # Python Code Review Guidelines このファイルはGitHub Copilot Code Review専用の指示です。 PRレビュー時にのみ適用され、Copilot ChatやCoding Agentには適用されません。 ## 出力形式 - **日本語で出力してください** - 問題の重要度を明記してください - Critical: 必ず修正が必要(セキュリティ、データ損失リスク) - High: 修正を強く推奨(バグ、パフォーマンス問題) - Medium: 修正を推奨(コード品質、保守性) - Low: 改善提案(スタイル、軽微な改善) ## レビュー観点(Best practices) フロントマター( --- ブロック)を先に書いておくことには意味があります。スクリプト側でフロントマターを本文から分離して保持し、AI出力の本文に再結合してから書き込む設計になっています。AIにフロントマターを渡さないことで、AI出力がフロントマターを上書き・削除するリスクをゼロにできます。 def split_frontmatter(content: str) -> tuple[str, str]: """Markdownコンテンツからフロントマター(---~---)を分離する。 Returns: (frontmatter, body) のタプル。 フロントマターがない場合は ("", content) を返す。 """ match = re.match(r"^(---\n.*?\n---\n?)(.*)", content, re.DOTALL) if match: return match.group(1), match.group(2) return "", content def join_frontmatter(frontmatter: str, body: str) -> str: """フロントマターと本文を再結合する。 フロントマターが空の場合は本文のみ返す。 フロントマターの末尾に改行がない場合は追加する。 """ if not frontmatter: return body if not frontmatter.endswith("\n"): frontmatter += "\n" return frontmatter + body 実際に動かしてみた — Before/Afterで確認する 「本当に学習でレビュー精度が上がるのか」を確かめるために、検証してみました。 Before:Copilotは何も言わない GET /items/{item_id} を以下のように実装したPRを作成し、Copilot Code Reviewを実行しました。 このプロジェクトでは主キーで検索するときは、db.query().filter()ではなく、db.get()を使ってほしいのですが、完全にスルーされました。ルールを知らないので当然ですね。 学習させる このPRに人間がインラインコメントを追加しました。 PRをマージすると、ワークフローが起動します。Azure OpenAIがコメントを分析し、学習PRが自動生成されます。 生成された学習PRには次の変更が含まれています。 この学習PRをマージしました。 After:Copilotが「リポジトリのルール違反」を指摘する 同パターンのコードを含む別のPR( DELETE /items/{item_id} )を作成してCopilot Code Reviewを実行します。 Copilot Code Reviewからこんな指摘が来ました。 学習がレビュー内容に反映されました。しかも「一般的なベストプラクティスとして」ではなく「このリポジトリのルールとして」指摘しています。チームの知見がCopilotに渡ったことが確認できました。 まとめ この記事で紹介したこと: PRレビューのコメントをAzure OpenAIで分析し、GitHub Copilotの設定ファイルを半自動で更新するフィードバックループを作ってみました 一度レビューすれば、同じ指摘を二度としなくて良いようになります Human-in-the-Loop設計で誤ったルール混入のリスクを抑えながら自動化できます gpt-5-mini なら1回の分析コストは1円未満。シニアエンジニアのレビュー時間削減と比べれば十分ペイすると思います セットアップは3ファイルのコピーとSecretsの設定だけです。下のコードをそのままコピーして使っていただき、チームの文化・課題・プロセスに合わせてプロンプトやトリガー条件を自由にカスタマイズしてみてください。 このループが育てば、Copilotはチームのことを少しずつ知っていくと思います。 コード全体 .github/workflows/learn-from-review.yml name: Learn from PR Review (AOAI) on: pull_request: types: [closed] # PRが閉じられた時だけ発火 workflow_dispatch: inputs: pr_number: description: '対象のPR番号' required: true type: number permissions: contents: write # copilot-instructions.md を更新してPRを作るために必要 pull-requests: write # PRを作成するために必要 issues: read # コメントを読むために必要 jobs: analyze-and-update: # 「マージされた」 OR 「'learn-from-rejection'ラベル付きでクローズされた」場合に実行 # マージした場合は学習対象 # 破棄するPRの内、学習対象にしたいものは'learn-from-rejection'ラベルを付ける # ai-learning/* ブランチからのPRは除外(再帰防止) if: >- !startsWith(github.head_ref, 'ai-learning/') && ( github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true || (github.event.pull_request.state == 'closed' && contains(github.event.pull_request.labels.*.name, 'learn-from-rejection')) ) runs-on: ubuntu-latest # 同一PR番号の並列実行を防止 concurrency: group: "learn-${{ github.event.pull_request.number || github.event.inputs.pr_number }}" cancel-in-progress: true env: TARGET_PR_NUMBER: ${{ github.event.inputs.pr_number || github.event.pull_request.number }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.10' - name: Install dependencies run: | pip install -r scripts/requirements-action.txt - name: Analyze PR and Generate Instructions id: analyze env: # Azure OpenAIの接続情報 AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} AZURE_OPENAI_DEPLOYMENT: "gpt-5-mini" # デプロイ名 AZURE_OPENAI_API_VERSION: "2024-12-01-preview" GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ env.TARGET_PR_NUMBER }} run: | python scripts/learn_from_pr.py - name: Create Pull Request with Updates # 変更なし時はPR作成スキップ if: steps.analyze.outputs.files_updated == 'true' uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "docs: update review instructions from PR #${{ env.TARGET_PR_NUMBER }}" title: "🤖 Update Review Instructions (Learning from PR #${{ env.TARGET_PR_NUMBER }})" # Pythonスクリプトが出力した change_log をここに埋め込む body: | ## AI Analysis Report ${{ steps.analyze.outputs.pr_body }} --- *Auto-generated by Learn-from-Review Workflow* Reference PR: #${{ env.TARGET_PR_NUMBER }} branch: ai-learning/pr-${{ env.TARGET_PR_NUMBER }} base: main # 【対策: ルールの肥大化・間違いの防止】 # AIが直接mainにコミットするのではなく、必ず「PR」を作成する。 # 人間が最終確認(Merge)しない限り、ルールは適用されない。 scripts/learn_from_pr.py import os import re import sys import json import time from typing import Any import github from github import Github from github.GithubException import GithubException from github.PaginatedList import PaginatedList from github.PullRequest import PullRequest from github.PullRequestComment import PullRequestComment from openai import AzureOpenAI # 設定 REVIEW_INSTRUCTIONS_FILE = ".github/instructions/review.instructions.md" CODING_RULES_FILE = ".github/copilot-instructions.md" # --- トークン制限対策の定数 --- # コメント+diff_hunk の合計文字数がこの閾値を超えたら要約LLMを発動 COMMENT_CHAR_THRESHOLD = 15_000 # TODO 要約するLLMのコンテキストサイズに入らないぐらいやり取りが長い場合の対策 # 要約後もこの文字数を超える場合は切り捨て SUMMARY_MAX_CHARS = 12_000 # --- フロントマター保護・空コンテンツガードの定数 --- MIN_CONTENT_CHARS = 50 # 最低限の文字数(これ未満は異常とみなす) # --- APIリトライ設定 --- MAX_RETRIES = 3 RETRY_BASE_DELAY = 2 # 秒 def split_frontmatter(content: str) -> tuple[str, str]: """Markdownコンテンツからフロントマター(---~---)を分離する。 Returns: (frontmatter, body) のタプル。 フロントマターがない場合は ("", content) を返す。 """ match = re.match(r"^(---\n.*?\n---\n?)(.*)", content, re.DOTALL) if match: return match.group(1), match.group(2) return "", content def join_frontmatter(frontmatter: str, body: str) -> str: """フロントマターと本文を再結合する。 フロントマターが空の場合は本文のみ返す。 フロントマターの末尾に改行がない場合は追加する。 """ if not frontmatter: return body if not frontmatter.endswith("\n"): frontmatter += "\n" return frontmatter + body def validate_content(content: str, file_label: str) -> bool: """生成されたコンテンツが有効かどうかを検証する。 空白のみ・改行のみ・最低文字数未満の場合はFalseを返す。 フロントマター再結合後に「フロントマターしかない」状態も検知する。 """ if not content or not content.strip(): print( f"WARNING: {file_label} の生成コンテンツが空です。書き込みをスキップします。" ) return False _, body = split_frontmatter(content) if not body.strip(): print( f"WARNING: {file_label} のコンテンツはフロントマターのみです。" "書き込みをスキップします。" ) return False if len(content.strip()) < MIN_CONTENT_CHARS: print( f"WARNING: {file_label} の生成コンテンツが短すぎます " f"({len(content.strip())} chars < {MIN_CONTENT_CHARS})。" "書き込みをスキップします。" ) return False return True def call_llm_with_retry( client: Any, model: str, messages: list[dict], **kwargs: Any ) -> Any: """LLM API呼び出しを指数バックオフ付きリトライで実行する。 最大MAX_RETRIES回リトライし、2秒→4秒→8秒の指数バックオフで待機する。 """ for attempt in range(MAX_RETRIES): try: return client.chat.completions.create( model=model, messages=messages, **kwargs ) except Exception as e: if attempt == MAX_RETRIES - 1: print(f"LLM API call failed after {MAX_RETRIES} attempts: {e}") raise delay = RETRY_BASE_DELAY * (2**attempt) print( f"LLM API call failed (attempt {attempt + 1}/{MAX_RETRIES}): {e}. " f"Retrying in {delay}s..." ) time.sleep(delay) def build_review_threads(review_comments: PaginatedList[PullRequestComment]) -> str: """レビューコメントからスレッド構造を再構築する。 Bot含む全コメントを取得し、in_reply_to_id でスレッドを組み立てる。 人間のコメントが1件でも含まれるスレッドはBot発言も保持し、 Botのみのスレッドは除外する。 各コメントには diff_hunk(コメント箇所の周辺数行)を付加し、 AIが「ここ」を正しく理解できるようにする。 """ # 全コメントをID→コメントの辞書に格納 comments_by_id: dict[int, PullRequestComment] = {} # スレッド: 親ID → [子コメント, ...] threads: dict[int, list[Any]] = {} all_comments = list(review_comments) for c in all_comments: comments_by_id[c.id] = c # in_reply_to_id がある場合は返信、なければ親(スレッドルート) if c.in_reply_to_id and c.in_reply_to_id in comments_by_id: parent_id = c.in_reply_to_id threads.setdefault(parent_id, []).append(c) else: # 自身がスレッドルート threads.setdefault(c.id, []) # スレッド単位で人間の関与チェック&フォーマット formatted_threads: list[str] = [] for root_id, replies in threads.items(): root_comment = comments_by_id[root_id] thread_comments = [root_comment] + replies has_human = any(c.user.type != "Bot" for c in thread_comments) if not has_human: # Botのみのスレッドは除外 continue lines: list[str] = [f"[Thread #{root_id}]"] for c in thread_comments: user_type = "Bot" if c.user.type == "Bot" else "Human" lines.append(f" {c.user.login} ({user_type}) on {c.path}:") # diff_hunk はコメント箇所の周辺数行のみ。@@ヘッダーに行番号が含まれる。 if c.diff_hunk: lines.append(" --- diff context ---") for dl in c.diff_hunk.splitlines(): lines.append(f" {dl}") lines.append(" --- comment ---") lines.append(f" {c.body}") lines.append("") formatted_threads.append("\n".join(lines)) return "\n\n".join(formatted_threads) def get_pr_files_summary(pr: PullRequest) -> str: """PR変更ファイル一覧と変更行数を取得し、テキストで返す。""" files = pr.get_files() lines: list[str] = [] for f in files: lines.append( f"- {f.filename}: +{f.additions}/-{f.deletions} (status: {f.status})" ) return "\n".join(lines) if lines else "(変更ファイルなし)" def summarize_comments(client: Any, model: str, text: str) -> str: """コメント+diff_hunk のテキストが長すぎる場合、LLMで要約する。 COMMENT_CHAR_THRESHOLD 以下の場合はそのまま返す。 """ if len(text) <= COMMENT_CHAR_THRESHOLD: return text print( f"Comment text exceeds {COMMENT_CHAR_THRESHOLD} chars " f"({len(text)} chars). Summarizing with LLM..." ) summary_prompt = ( "以下のPRレビュー議論を、**合意事項・決定事項・変更依頼の結論**を" "中心に要約してください。\n" "問題提起→議論→結論の流れが分かるように構造を維持してください。\n" "コード例やファイルパスなど、ルール抽出に必要な具体情報は保持してください。\n" "要約は日本語で出力してください。" ) response = call_llm_with_retry( client, model, [ {"role": "system", "content": summary_prompt}, {"role": "user", "content": text}, ], ) summarized = response.choices[0].message.content print(f"Summarized: {len(text)} -> {len(summarized)} chars") # 要約後もまだ長すぎる場合は切り捨て if len(summarized) > SUMMARY_MAX_CHARS: print( f"Summarized text still too long ({len(summarized)} chars). " f"Truncating to {SUMMARY_MAX_CHARS} chars." ) summarized = ( summarized[:SUMMARY_MAX_CHARS] + "\n\n(以降省略: 要約後も長すぎるため切り捨て)" ) return summarized def main(): """PRレビューから学習してルールファイルを更新するメイン処理。""" # --- 1. GitHubデータの取得 --- g = Github(auth=github.Auth.Token(os.environ["GITHUB_TOKEN"])) repo = g.get_repo(os.environ["GITHUB_REPOSITORY"]) pr = repo.get_pull(int(os.environ["PR_NUMBER"])) # --- 1-a. Issue コメント (PR全体のコメント): 人間のみ --- comments = pr.get_issue_comments() all_comments_text = "\n".join( [f"{c.user.login}: {c.body}" for c in comments if c.user.type != "Bot"] ) # --- 1-b. Review コメント: スレッド構造で再構築 (Bot方針変更) --- review_comments = pr.get_review_comments() all_reviews_text = build_review_threads(review_comments) # --- 1-c. PR変更ファイル情報 --- pr_files_summary = get_pr_files_summary(pr) # 何も人間の会話がなければ終了 if not all_comments_text and not all_reviews_text: print("No human comments found. Skipping.") return # 既存レビュールール(更新対象) review_frontmatter = "" try: current_review_rules_raw = repo.get_contents( REVIEW_INSTRUCTIONS_FILE ).decoded_content.decode() # フロントマター分離(AIにはbodyのみ渡す) review_frontmatter, current_review_rules = split_frontmatter( current_review_rules_raw ) except GithubException: current_review_rules = "(まだファイルはありません)" # プロジェクトのコーディングルール(更新対象) try: current_coding_rules = repo.get_contents( CODING_RULES_FILE ).decoded_content.decode() except GithubException: current_coding_rules = "(まだファイルはありません)" # --- 2. Azure OpenAIへの接続 --- client = AzureOpenAI( api_key=os.environ["AZURE_OPENAI_API_KEY"], api_version=os.environ["AZURE_OPENAI_API_VERSION"], azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"], ) # PRの状態を確認 is_merged = pr.merged state_status = "MERGED (Approved)" if is_merged else "REJECTED (Closed)" # --- レビュー承認状態の取得 --- reviews = pr.get_reviews() review_states: list[str] = [] for r in reviews: if r.state in ("APPROVED", "CHANGES_REQUESTED", "COMMENTED"): review_states.append(f" - {r.user.login}: {r.state}") review_state_text = ( "\n".join(review_states) if review_states else " (レビューなし)" ) # プロンプトの構築 system_prompt = f""" あなたはチームの2つのルールファイルを管理するAIです。 【管理対象ファイル】 1. `review.instructions.md` — GitHub Copilot Code Review が参照するレビュー観点 - 「何をレビューで指摘すべきか」「どんな観点でチェックするか」を記載する - 例: 「例外処理が広すぎないか確認する」「N+1クエリがないか確認する」 - セクション名はPR固有の表現にせず、汎用的な名前にする(×「このPRの運用で追加しておくべき~」 → ○「プロジェクト固有のレビュー観点」) - 個別のレビュー観点にseverityを付けない。severityはレビューコメントの出力時に判断するものであり、観点の定義に含めない - フロントマター(YAML header: `---`〜`---`)は出力に含めない。本文のMarkdownのみを出力する 2. `copilot-instructions.md` — プロジェクト全体のコーディングルール - 「コードを書くときに守るべきルール」を記載する - 例: 「ベアexceptは使わず具体的な例外クラスを指定する」「関数は50行以内にする」 【分類の指針】 - コーディングルール: 「〜すべき」「〜してはいけない」という具体的な書き方の規約 - レビュー観点: 「〜を確認する」「〜に注意する」というレビュー時のチェック項目 - 1つの指摘が両方に該当する場合は、両方に適切な形で反映する - どちらにも該当しない(単なる議論やPR固有の話題)場合は反映しない - `copilot-instructions.md` に既にコーディング規約として記載されている内容は、`review.instructions.md` 側に「〜を確認する」と重複させない 【分析の指針】 - PRが MERGED の場合: 推奨パターン(Best Practices)としてルールを抽出する - PRが REJECTED の場合: - 禁止事項(Anti-Patterns)として「〜してはいけない(DO NOT ...)」形式で記述する - 可能なら、なぜダメか(×)と代わりに何をすべきか(○)のパターンを含める - 単なる重複や方針変更によるCloseであればルール化しない 【Botコメントの扱い】 Botのコメントは対話の文脈として含まれている。Botの指摘に対する人間の応答(「対応しない」「別PRで対応済み」等)にも注目し、チームの方針として反映する。Bot単独の発言(人間の応答がないもの)は除外済み。 【現在のレビュールール (review.instructions.md) ※フロントマター除去済み】 {current_review_rules} 【現在のコーディングルール (copilot-instructions.md)】 {current_coding_rules} 【良い設定ファイルの品質基準】 - 抽象化された原則 + 代表例1つ: 個別のPR事例をそのまま書かず、汎用的な原則に昇華し具体例を1つ添える - 機械的に判定・実行可能なルール: 「適切に」「必要に応じて」等の曖昧な表現を避け、誰が読んでも同じ解釈になる明確な指示にする - ドメイン・言語・目的別の階層化: 関連するルールをセクションでグループ化する - 指示に特化: 冗長な背景説明は省き、「〜する」「〜を確認する」等の指示文のみで構成する - 各レビュー観点は1-2文で簡潔に: 必要なら悪い例(×)と良い例(○)のパターンを含める - 重複排除: 同じ趣旨のルールが異なる表現で複数存在しないようにする 【更新ルール】 1. 既存ルールとの統合: 単なる追記ではなく、既存の項目とマージして簡潔にする 2. 衝突の解決: 既存ルールと矛盾する場合、新しい議論を優先し、その旨を change_log に明記する 3. 2ファイル間の重複回避: 同じ内容を両方のファイルに書かない 4. 変更不要な場合: 該当ファイルの内容が変わらない場合は、現在の内容をそのまま返す 【出力形式】 以下のJSON**のみ**を出力する。Markdownコードブロックで囲まない。 `updated_review_instructions` にはフロントマター(`---`〜`---`)を含めない。 {{ "updated_review_instructions": "更新後のreview.instructions.mdの本文(Markdown, フロントマターなし)", "updated_coding_rules": "更新後のcopilot-instructions.mdの全内容(Markdown)", "review_instructions_changed": true/false, "coding_rules_changed": true/false, "change_log": "何を変更したか、なぜ変更したか、どちらのファイルに振り分けたかの解説" }} """ pr_description = pr.body or "(説明なし)" user_prompt = f""" PR Context: - Title: {pr.title} - Description: {pr_description} - State: {state_status} - Review Approvals: {review_state_text} Changed Files: {pr_files_summary} Issue Comments (PR全体): {all_comments_text} Review Comments (インラインコメント・スレッド構造): {all_reviews_text} """ # --- コメント量が多すぎる場合の要約 --- combined_comments = f"{all_comments_text}\n{all_reviews_text}" summarized_comments = summarize_comments( client, os.environ["AZURE_OPENAI_DEPLOYMENT"], combined_comments ) if summarized_comments != combined_comments: # 要約された場合、ユーザープロンプトを差し替え user_prompt = f""" PR Context: - Title: {pr.title} - Description: {pr_description} - State: {state_status} - Review Approvals: {review_state_text} Changed Files: {pr_files_summary} Review Discussion (要約済み): {summarized_comments} """ # デバッグ print("system_prompt:") print(system_prompt) print("user_prompt:") print(user_prompt) # --- リトライ付きLLM呼び出し --- response = call_llm_with_retry( client, os.environ["AZURE_OPENAI_DEPLOYMENT"], [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], response_format={"type": "json_object"}, ) # JSONパース try: result = json.loads(response.choices[0].message.content) except json.JSONDecodeError: print("Failed to parse JSON response.") sys.exit(1) change_log = result.get("change_log", "No details provided.") files_updated = [] # --- 3. レビュールールの更新 --- if result.get("review_instructions_changed", False): new_review_content = result.get("updated_review_instructions", "") # フロントマター再結合 full_review_content = join_frontmatter(review_frontmatter, new_review_content) # 空コンテンツガード if validate_content(full_review_content, REVIEW_INSTRUCTIONS_FILE): os.makedirs(os.path.dirname(REVIEW_INSTRUCTIONS_FILE), exist_ok=True) with open(REVIEW_INSTRUCTIONS_FILE, "w") as f: f.write(full_review_content) files_updated.append(REVIEW_INSTRUCTIONS_FILE) print(f"Updated {REVIEW_INSTRUCTIONS_FILE}") # --- 4. コーディングルールの更新 --- if result.get("coding_rules_changed", False): new_coding_content = result.get("updated_coding_rules", "") # 空コンテンツガード if validate_content(new_coding_content, CODING_RULES_FILE): os.makedirs(os.path.dirname(CODING_RULES_FILE), exist_ok=True) with open(CODING_RULES_FILE, "w") as f: f.write(new_coding_content) files_updated.append(CODING_RULES_FILE) print(f"Updated {CODING_RULES_FILE}") if not files_updated: print("No rule changes needed from this PR.") # 変更なしをGitHub Actions出力に設定 if "GITHUB_OUTPUT" in os.environ: with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write("files_updated=false\n") return # --- 5. GitHub Actions出力 --- if "GITHUB_OUTPUT" in os.environ: with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write("files_updated=true\n") delimiter = f"EOF_DELIMITER_{int(time.time())}" updated_files_summary = "\n".join(f"- `{path}`" for path in files_updated) body = f"### 更新されたファイル\n{updated_files_summary}\n\n### 変更内容\n{change_log}" f.write(f"pr_body<<{delimiter}\n") f.write(body) f.write(f"\n{delimiter}\n") print("Done.") if __name__ == "__main__": main() scripts/requirements-action.txt PyGithub>=2.0,<3.0 openai>=1.0,<2.0 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post GitHub Copilot Code Reviewの自動学習ループ — 一度指摘すれば次からはAIが怒ってくれる仕組みを作ってみた first appeared on SIOS Tech Lab .
エピソード紹介 Ep.1 – クリーンアーキテクチャとは Ep.2 – 認証方式の実践的な紹介 Ep.3 – ER設計と監査ログ Ep.4 – RepoScanner の実装とテスト Ep.5 – Copilot プロンプトを効率化 ← 今回はこちら こんな方へ特におすすめ プロンプトを使って日常のコーディング作業を自動化したい開発者 Copilotがプロジェクトの独自ルールを無視したコードを出してくるとお悩みの方 チーム開発におけるAIの活用方針をルール化したい方 Copilotにもクリーンアーキテクチャで実装させたい方 概要 こんにちは。サイオステクノロジーのはらちゃんです! GitHub Copilotは非常に便利ですが、そのままコードを生成させると「動くけど、プロジェクトの規約違反になっているコード」を出力してしまい、修正に手間取ることがよくあります。 — 本シリーズでは、Copilotを活用しつつ、クリーンアーキテクチャに沿って小規模なプロダクト「RepoScanner」を設計・実装した経緯をまとめます。 このエピソードでは、私が実践したCopilotを使った作業を高速化するための「ルールの二層管理」と、AIとの対話ログの適切な運用方法について解説します。 解決方法 ルールは「機械向け」と「人間向け」に分ける 2つのディレクトリを用いてCopilotの実装を調整します。 Copilotにプロジェクトのルールを認識させる際、すべてのルールを長々と一つのドキュメントに書いても、AIは重要なポイントを拾い切れません。 また、ドキュメントは参照され得るが優先度は低いため、ドキュメントだけだと自動適用は保証されません。 そこで、以下の2つのディレクトリを用いて、ルールを役割ごとに分割します。 .github/instructions/ Copilotへ機械的に適用する「実効ルール」 docs/reference/... 人間も読むための「解説ルール」 Step1. 機械向けの実効ルール Copilotへ機械的に適用する「リポジトリ固有の指示/ガードレール」です。 .github/ └── instructions/ .github/instructions/ 配下には、自動補完やCopilotエージェントの振る舞い制約・除外ルールなどを書きます。 ここは 英語で、短く明確に書く のが鉄則です。 英語はAIモデルが最も安定して解釈できる言語であり、指示を短くすることでAIがルールを見落とすのを防ぎます。 実効ルールの活用法 各ファイルの先頭にYAML フロントマター(例:  applyTo ,  excludeAgent )を付けることで、「どのファイル群に対してこのルールを適用するか」を細かく制御できます。 これにより、フロントエンドのルールがバックエンドのコード生成に悪影響を与えるのを防げます。 applyTo 適用例 ファイル 適応範囲 */*.py 特定の拡張子にのみ src/components/**/*.tsx UIコンポーネント -**/*.js -**/*.jsx 複数のパターン 最初に、クリーンアーキテクチャの思想に則らせるために書いたファイルは以下の通りです。 指示には依存方向や禁止事項など実装で守りたい制約を書いています。 この記述により、リポジトリ全体に適用するエージェント向けガードレールの役割を持っています。 記述例 repo-common-guardrails.instructions.md --- applyTo: "**/*" --- # Repository Common Guardrails ## Scope - Apply these rules across the repository for all agents. - Enforce only the minimum rules required to preserve clean architecture. ## Dependency Direction - Keep dependency flow from outer layers to inner layers. - Do not import infrastructure implementations from domain or use-case layers. - Introduce or use interfaces at boundaries when connecting inner logic to outer implementations. ## Layer Integrity - Respect declared layer boundaries and dependency direction in existing architecture docs. - Review imports in modified files to avoid accidental auto-import drift. ## Validation - Validate changed behavior with targeted checks. - Run architecture-related checks when dependency direction may have changed. ※もっと  .github/instructions/  について詳しく知りたい方は こちらのブログ へ Step2. ドキュメントで人間とAI向けの解説ルール 機械向けの短い英語ルールだけでは、背景や具体的なコード例が伝わりません。 そこで、人が読んでも分かりやすいように、補助説明 / 背景を  docs/reference/...  に日本語で詳細を書きます。 解説ルールの活用法 AIは作業時に、①の短いガードレールで「やってはいけないこと」を把握し、詳細が必要な場合は②のドキュメントを参照しにいきます。 記述例(抜粋) docs/reference/copilot/clean-architecture.md # クリーンアーキテクチャ このプロジェクトでは、変更に強くテストが容易な設計を実現するためにクリーンアーキテクチャを採用しています。コード作業の際は本ドキュメントをガイドラインとしてご参照ください。 ## 目次 --- ## 1. 詳細ドキュメント 以下はこのドキュメントおよび関連ファイルの責務です。 | ファイル | 責務 | | --------------------------------------------------------- | ------------------------------------------------------------------- | | `docs/reference/copilot/clean-architecture.md` | 概要・原則・索引 | | `docs/reference/copilot/clean-architecture-migration.md` | 実装例、移行手順、`migration-backlog.md` の運用ルール | | `docs/reference/copilot/clean-architecture-testing-ci.md` | テスト戦略、CI / lint スニペット、注意点 | | `docs/reference/copilot/migration-backlog.md` | `migration-backlog.md` の運用ルール(作業時の一時ファイル作成手順) | | `.github/copilot-instructions.md` | AI が実行時に参照する全体の実効ルール(Single Source of Truth) | | `.github/instructions/*.instructions.md` | パス単位の実効ルール(対象ファイル群ごとの行動制限) | ## 2. AIガードレール総覧 このプロジェクトの AI ガードレールは、次の 2 層で運用します。 - 実効ルール(enforceable): `.github/copilot-instructions.md` と `.github/instructions/*.instructions.md` - 解説ルール(explanatory): `docs/reference/copilot/*.md` 重要なのは、同じルール本文を両方へ重複記載しないことです。実効ルールの一次ソースは `.github` 側とし、`docs` 側は背景、判断基準、運用手順を説明します。 ### 2.1 初期適用範囲(2026-02-20時点) 初期フェーズでは次を優先します。 - repo 共通ルール(依存方向保護、最小変更、検証必須) - docs ルール(記述品質、リンク整備、SoT更新) 初期適用範囲と `.github/instructions` の対応は次のとおりです。 | 適用範囲 | 実効ルールファイル | | --------- | ------------------------------------------------------------- | | repo 共通 | `.github/instructions/repo-common-guardrails.instructions.md` | | docs | `.github/instructions/docs-common-guardrails.instructions.md` | backend / frontend / functions の個別ルールは、運用上の失敗事例が蓄積したタイミングで段階的に追加します。 ### 2.2 変更フロー -- 中略 -- ## 7. 各層のつながり(依存方向) このプロジェクトでは、依存方向を常に外側から内側へ保ちます。 ```text functions(最外部) -> infrastructure(外側実装) -> use-cases(アプリ手順) -> domain(最内部ルール) ``` 重要なのは「実行時の呼び出し方向」と「コードの依存方向」を区別することです。 - 実行時の呼び出しは、外側から内側へ流れます(例: HTTP Trigger -> Use Case)。 - コードの依存も、同じく外側から内側へ向けます。 - `domain` / `use-cases` は `infrastructure` 実装を直接 `import` しません。 - 永続化や外部APIは、`domain` 側のインターフェースを `infrastructure` が実装して接続します。 ### 接続イメージ - `domain`: ルールとインターフェース(例: `IUserRepository`)を定義する - `use-cases`: `domain` のインターフェースを受け取って業務フローを実行する - `infrastructure`: `domain` のインターフェースを実装してDBや外部サービスへ接続する - `functions`: エントリポイントで実装を組み立て(DI)て `use-cases` を呼び出す | 層 | 主な接続先(実行時) | コード依存 | 補足 | | -------------- | -------------------------- | ---------- | ----------------------------------------- | | functions | infrastructure / use-cases | 外 -> 内 | 依存を組み立ててユースケースを起動する | | infrastructure | use-cases / domain | 外 -> 内 | `domain` のインターフェース実装を提供する | | use-cases | domain | 内側のみ | 業務フローを実行し、実装詳細は知らない | | domain | なし(最内部) | 内側のみ | ルールと抽象のみを持つ | ## 8. 用語集(クリーンアーキテクチャ) | 用語 | 説明 | | ---------------------------------- | ------------------------------------------------------------------------ | | Entity(エンティティ) | ドメインの中核概念。ビジネスルールを表すオブジェクト。 | | Use Case(ユースケース) | アプリケーション固有の手順。入力を受けて業務フローを実行する。 | | Interface(インターフェース) | 層の境界で依存を抽象化する契約。実装詳細を内側に持ち込まないために使う。 | | Dependency Direction(依存方向) | 依存の向き。外側の層が内側へ依存し、内側は外側へ依存しない。 | | Dependency Inversion(依存性逆転) | 内側が抽象(interface)を定義し、外側がその抽象を実装する考え方。 | | DI(Dependency Injection) | 必要な依存を外から注入すること。テスト容易性と交換容易性を高める。 | | Adapter(アダプター) | 外部ライブラリやSDKを内側の契約へ合わせる実装。 | | Source of Truth(SoT) | 仕様の一次情報源。構造ルールは `docs/spec` を正とする。 | プロンプトの実践 上記の「二層管理」のドキュメントが整備されていると、日常のプロンプトによる指示が劇的に簡略化されます。 作業テンプレート チャットに以下のような短い指示を投げるだけで、AIは裏でドキュメントを読み込み、正確な実装を行ってくれます。 @workspace @clean-architecture.md に従って、 `ListSnapshotSummariesUseCase` を実装してください。 出力: items, total_count 依存: SnapshotQueryRepository.list_snapshots(limit, offset, filter) -> (items, total) 適用前後 Before 手作業でクリーンアーキテクチャの制約を意識しながら、インターフェースを作成し、ユースケースの骨格を書き、依存関係を修正し、テストを手書きする必要があります。 時間がかかり、ミスも起きやすいです。 After テンプレート化された短いプロンプトを投げるだけで、プロジェクトの規約に沿った一貫した構造のユースケースと、それに紐づくテストコードが自動生成されます。 開発者は「コードを書くこと」から「AIが出力したロジックのレビュー」に集中できるようになります。 +α: AIとの「対話ログ」を記録 設計に関する議論などCopilotとのチャット履歴を docs/history/... に出力して保持することをお勧めします。 あとでどのような意図で設計をしたかが明確になり、中断作業の再開やレビューのハードルが下がります。 さらに、テンプレートをREADMEに追記すると一貫したログになります。 記録用のマークダウンテンプレート例 # Copilot History ## 目的 - `docs/history/copilot/` は、Copilot 関連の判断履歴を残すための場所です。 - 実装の背景と意思決定の経緯を、後から追跡できる形で記録します。 ## 命名ルール - 形式: `YYYY-MM-DD-<topic>.md` - 日付は作成日(`YYYY-MM-DD`)を使用します。 - `<topic>` は英小文字のケバブケースで、内容を短く表す語にします。 - 1ファイルにつき1つの主要決定を記録します。 ## 記載テンプレート # <タイトル> ``` - 日付: YYYY-MM-DD - ステータス: Proposed | Accepted | Superseded - 対象: (対象範囲。例: docs/history/copilot) ``` ## 背景 (なぜこの判断が必要になったか) ## 決定 (何を決めたか) ## 影響 (期待される効果・制約・運用上の注意) ## フォローアップ (次に必要な作業。なければ「なし」) まとめるのが面倒な場合は、JSONでそのままのチャット記録を出力することもできます。 Copilot Chatに「JSONでそのままのチャット記録を出力」と指示してください。 出力例 [ { "role": "user", "content": ".github/instructions配下に配置したdocsはCopilotに必ず参照される?どういう仕組み? (See <attachments> above for file contents. You may not need to search or read the file again.)" }, { "role": "assistant", "content": "簡潔に説明します — .github/instructions 配下の docs は「Copilot のリポジトリ固有指示(repository instructions)」として拡張機能に読み込まれ、候補生成時のコンテキストに組み込まれます。仕組みと注意点は以下です。\n\n- どう作用するか... このように記録を残すことで、レビュアーに対して「Copilotとこういう議論をした結果、この実装になっています」と背景を共有でき、レビューのコミュニケーションコストが大幅に下がります。 ログをマージすべきか しかし、ここで疑問が生じます。 「 出力したチャットログは、すべてメインブランチにマージして残すべきなのか?」 結論から言うと、すべてのログを蓄積すべきではありません。 ログには「資産」と「ノイズ」が混在しているため、適切に仕分けて昇華する必要があります。 作業中ログ 開発のプロセスが分かる →コードが動いた後は期限切れ 重要な決定 なぜこの設計にしたのか分かる資産 ルール 2から抽出した常に守るべきもの 資産かノイズか… 分類 ログの内容 アクション 資産 (残すべき) 決定の背景 ドキュメントや実効ルールに昇華してマージする 採用しなかったアプローチとその拒絶理由 ドキュメントに昇華してマージする プロジェクト独自の固有名詞や概念 ドキュメントに昇華してマージする 横断的なルール 実効ルールに昇華してマージする ノイズ (消すべき) コードを見ればわかること PRに記載するか破棄し、ファイルとしては残さない 一時的なタスクリストやエラーの修正過程 PRに記載するか破棄し、ファイルとしては残さない 情報の適材適所 どこに残すかでマージの是非を決めるログの内容に応じて、最終的にどこに記載するかを振り分けます。 必要な情報 決定セクションに書かれるようなリポジトリに残すべき内容を指します。 実効ルール(.github/instructions): 常に守るべき横断的なルール README / docs配下: 画面の仕様、詳細な背景、アーキテクチャの決定事項 不要な情報 フォローアップセクションに書かれるような一時的に利用して破棄する内容を指します。 GitHub Issue: 未着手の課題 PRの説明文: 実装時の思考プロセス(マージ後はPR上に残れば十分) フィーチャーブランチのみ: 単なる作業ログファイル(メインブランチにはマージしない) ログの昇華ルール チャットログを整理する際は、以下の2つの問いを自分に投げかけてください。 このログの中で、他の開発者が知らないと困る『独自の禁止事項』はある? → ある場合は、 .github/instructions にルールとして追加する。 このログは、将来コードをリファクタリングする時の助けになる? → ある場合は docs/history/ にマークダウンとして残す。 → ない場合は PR の説明欄にコピペして、ログファイル自体は削除する。 まとめ 今回は、Copilotを使って設計やリファクタリングを高速化し、出力のブレを無くすための仕組み作りについて解説しました。 ルールの二層管理 機械向けの短い英語ルールと、人間向けの詳細な日本語ルールを分ける。 短いプロンプトで実行 ルール基盤を整えれば、指示は極端に短くて済む。 ログの保持 AIとの対話は資産とノイズに分け、必要なルールだけを抽出してリポジトリに還元する。 「Copilotが言うことを聞いてくれない!」と感じている方は、ぜひ指示書の整備とログの昇華から始めてみてください。 開発体験が劇的に向上するはずです! 参考 copilot-instructions.md を分割したい?applyTo パターンで解決 GitHub Copilot をつかいこなすための copilot-instructions.md の使いかた ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Copilot × Clean Architecture | プロンプト効率化 first appeared on SIOS Tech Lab .
2026年4月8日(水)~4月10日(金)の3日間、Japan DX Weekに出展いたします。 生成AIの活用が進む中、OSSライセンスや著作権リスクはますます見えにくくなっています。 サイオステクノロジーのブースでは、コード解析によりOSSを可視化する「SCANOSS」をご紹介します。ソースコードレベルでOSSの利用状況を把握し、リスクの早期発見と適切な管理を支援します。 「サイオスOSSよろず相談室」では、SCANOSSと親和性の高いSBOM管理ツールの導入・運用もサポートします。 あわせて、Excel AIエージェントやRAG構築、AI駆動開発など、AIを”現場の即戦力”にする取り組みもご紹介します。   展示のご紹介はこちら   無料のお申込みはこちら The post 4/8(水)~4/10(金) Japan DX Weekに出展します first appeared on SIOS Tech Lab .
2026年4月8日(水)~4月10日(金)の3日間、Japan DX Weekに出展いたします。 生成AIの活用が進む中、OSSライセンスや著作権リスクはますます見えにくくなっています。 サイオステクノロジーのブースでは、コード解析によりOSSを可視化する「SCANOSS」をご紹介します。ソースコードレベルでOSSの利用状況を把握し、リスクの早期発見と適切な管理を支援します。 「サイオスOSSよろず相談室」では、SCANOSSと親和性の高いSBOM管理ツールの導入・運用もサポートします。 あわせて、Excel AIエージェントやRAG構築、AI駆動開発など、AIを”現場の即戦力”にする取り組みもご紹介します。   展示のご紹介はこちら   無料のお申込みはこちら The post 4/8(水)~4/10(金) Japan DX Weekに出展します first appeared on SIOS Tech Lab .
ども!最近 GitHub Wiki の管理に頭を悩ませている龍ちゃんです。 既製アクション 1 本と YAML 30 行で、 docs/wiki/ を GitHub Wiki に自動同期できる仕組みを作ったので共有しますね。 皆さん、GitHub Wiki 使っていますか? 複数のドキュメントを参照したいときはすごく便利なんですが、管理が面倒だったりしますよね。Wiki の情報って完全に人間向けのコンテキスト情報で、あった方がいいのはわかっているんですけど、そんな作業は後回しになりがちで。 最近、AI にドキュメント作成をやらせているんですが、そうすると「人間しか読まないコンテキスト情報をどう管理して、どこに置くべきか?」って悩むんですよね。そんなときに Wiki。でも管理にはあまりコストをかけたくない。同じ悩みがあったので、解決方法を見つけて書いておきますね。 今回の内容です。 GitHub Wiki の「惜しい」ポイントと、docs/ との両立方法 GitHub Wiki の正体(独立した Git リポジトリ) docs/wiki/ → GitHub Actions → Wiki の構成とワークフロー 実機検証で踏んだ罠 5 選 Wiki 整理を AI Agent Skill に任せるアイデア それぞれ順番に解説していきますね。 GitHub Wiki の「惜しい」ところ GitHub Wiki は手軽で閲覧性も高いんですよね。ページをリスト表示できて、サイドバーで構造化できて、非エンジニアにも見せやすい。でも、使っていると管理面でちょっと不満が出てきます。 よくある不満点はこのあたりです。 Web UI でしか編集できない(と思われがち) → ローカルで書けない、差分もわかりにくい PR レビューに乗らない → 誰かが勝手に書き換えても気づけない コードとドキュメントのバージョンが連動しない → リリースタイミングでドキュメントが古いまま AI ツール(Claude Code 等)から直接アクセスしにくい → docs/ は読めても Wiki は別管理 結果として「Wiki やめて docs/ だけにしよう」という判断をするチームも多いんですが、Wiki UI の閲覧性は捨てがたいんですよね。 実は両方活かす方法があります。 docs/wiki/ に Markdown を書いて、CI/CD で Wiki に自動同期する方法です。 以前、 HTMLで保存してる奴、全員Markdownにしろ という記事でも書いたんですが、AI 向けに最適化するなら Markdown で Git 管理が基本です。Wiki も同じ考え方で管理できるんですね。 GitHub Wiki の正体: 独立した Git リポジトリ これが面白くて、GitHub Wiki の裏側には REPO.wiki.git という独立した Git リポジトリがいるんですよね。なので以下のコマンドで普通に clone できます。 git clone https://github.com/OWNER/REPO.wiki.git 実は GitHub CLI のマニュアル を見ても Wiki コマンドはないし、 REST API にも専用エンドポイントはないんですが、Git 操作はできます。つまり CI/CD に乗せられるんですね。 構成: docs/wiki/ → GitHub Actions → Wiki 構成はこんな感じです。 使っているのはこのあたりです。 項目 内容 使用 Action Andrew-Chen-Wang/github-wiki-action@v4 認証 GITHUB_TOKEN + permissions: contents: write (PAT 不要) 実行時間 10〜24秒 Zenn で docs/ から Wiki への自動同期を Node.js で実装している記事 もあって、こちらはかなり丁寧に作り込まれています。今回の記事では既製アクション 1 つでサクッと完結する方法を紹介しますね。 Claude Code設計術:AIフレンドリーなドキュメント管理 で書いた「GitHub に情報を集約する」設計思想の延長線上にある話ですね。Wiki もその一部に組み込んでしまう感じです。 実装ステップ: 4つの作業で完成 ステップ 1: Wiki の初期化(1分・手動) まず GitHub リポジトリの Settings > Features で Wiki が有効になっているか確認します。 確認できたら Wiki タブを開いて「Create the first page」で初期ページを作ります。 内容は何でも OK です。あとで同期したときに上書きされます。ここを飛ばすと .wiki.git が存在しない状態になって、後で Actions が失敗するので注意です。 ステップ 2: docs/wiki/ にドキュメントを配置 ディレクトリ構成はこんな感じです。 docs/wiki/ ├── Home.md # トップページ(必須) ├── _Sidebar.md # サイドバー ├── _Footer.md # フッター ├── Guide-Getting-Started.md # プレフィックス命名でフラット管理 ├── Guide-Configuration.md ├── Reference-API.md └── images/ └── architecture.png # [[images/architecture.png]] で参照 ファイル命名のポイントをまとめておくと、 ダッシュ - 区切りにする(Wiki 表示でスペースに変換される) サブディレクトリは使えるが、Wiki の Pages 一覧ではフラット表示になる 同名ファイルが異なるサブディレクトリにあると衝突するので注意 プレフィックスで擬似階層を表現するのが安全( Guide- 、 Reference- 等) ぶっちゃけ、サブディレクトリが必要なほど分量が多い Wiki はつらいんですよね。Wiki 側の Pages 一覧はフラットに並ぶだけですし、Web UI でページタイトルにスラッシュ / を入れても自動でハイフンに変換されるので子ページも作れません。 フラットで管理できる程度にまとめておくのが、Wiki としてはちょうどいい粒度だと思います。 ちなみにサブディレクトリごとに独自の _Sidebar.md を置くと、そのフォルダ配下のページでは別のサイドバーが表示されます。どうしても階層が必要な場合はこれで擬似的に対応できますね。 ステップ 3: GitHub Actions ワークフローを追加 以下を .github/workflows/wiki-sync.yml にコピペするだけです。 name: Sync docs/wiki to GitHub Wiki on: push: branches: [main] paths: - 'docs/wiki/**' - '.github/workflows/wiki-sync.yml' workflow_dispatch: permissions: contents: write jobs: sync-wiki: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Sync to Wiki uses: Andrew-Chen-Wang/github-wiki-action@v4 with: path: docs/wiki/ strategy: clone preprocess: false disable-empty-commits: true commit-message: "wiki: sync from ${{ github.sha }}" 各設定のポイントです。 設定 内容 paths: docs/wiki/** docs/wiki/ の変更時のみ発火(他の変更では動かない) permissions: contents: write GITHUB_TOKEN 認証(PAT 不要) strategy: clone 差分同期(Wiki の編集履歴を保持) disable-empty-commits: true 変更なし時の空コミット防止 workflow_dispatch 手動実行も可能 ステップ 4: push → 自動同期を確認 main に push するだけで Wiki に反映されます。Actions タブで実行結果を確認して(10〜24秒で完了します)、Wiki タブでページ表示を目視確認すれば OK です。 実機検証で踏んだ罠 5 選 実際に検証してみると地味にハマる箇所がいくつかあったんですよね。他ではあまり書かれていないと思うので、「何が起きたか → なぜか → どう直したか」で共有しておきます。 罠 1: Wiki 記法 [[A|B]] のパイプが逆 [[Guide-Getting-Started|Getting Started]] と書くと、リンク先が「Getting Started」というページ名で解釈されるんですよね。そのページは存在しないので赤リンクになります。 HTML で確認するとこうなっていました。 <a class="internal absent" href="/wiki/Getting-Started"> 整理するとこうなります。 記法 リンク先 結果 [[Guide-Getting-Started|Getting Started]] Getting-Started 存在しないページ → 赤リンク [[Guide-Getting-Started]] Guide-Getting-Started 正常リンク 対策はパイプなしの [[Guide-Getting-Started]] にするだけです。表示は自動でスペース区切りに変換されます。 罠 2: 画像は Wiki 添付記法 [[images/xxx.png]] 一択 Markdown の ![alt](images/xxx.png) を使うと、ページによっては「Could not find version “images”」エラーが出ることがあります。raw URL(wiki 本体どちらも)だと表示されないケースがありました。 安定して表示されるのは Wiki 添付記法の [[images/xxx.png]] だけでした。画像参照はこれ一択と覚えておくといいですね。 罠 3: ファイル名のダッシュ → ページ名でスペースに変換 Guide-Getting-Started.md というファイルは、Wiki では「Guide Getting Started」というページタイトルになります。 整理するとこういう関係です。 項目 値 ファイル名 Guide-Getting-Started.md ページタイトル表示 Guide Getting Started Wiki 記法での参照 [[Guide-Getting-Started]] 「ファイル名 = Wiki 記法での参照名」と覚えておくとすっきりします。 罠 4: サブディレクトリの同名ファイルは衝突する Wiki の名前空間はフラットなんですよね。サブディレクトリ自体は使えるんですが、 guide/page.md と reference/page.md を両方置くと同じ page として衝突します。Pages 一覧にも 1 つしか出てこないです。 サブディレクトリを使う場合はファイル名をユニークにするか、プレフィックス命名でフラット管理するのが安全ですね。 罠 5: Wiki 未初期化だと Actions が失敗する Wiki タブで 1 ページも作っていないと .wiki.git が存在しない状態になっています。Actions が push 先を見つけられずエラーになるんですね。 ステップ 1 で書いた「手動で 1 ページ作る」というのはこれを回避するためです。内容は何でも OK なので、1 分で終わります。 さらに一歩: Wiki 整理用 Agent Skill を作る この 5 つの罠、正直僕も全部覚えてられないんですよね。なので AI の Agent Skill に組み込んでしまって、罠の回避も Wiki 整理もまるごと任せるようにしています。 この記事自体を Skill の参考資料として読み込ませれば、AI が知見を活かして動いてくれるんですよね。例えばこんな使い方ができます。 「この機能の Wiki ページを作って」→ 正しい命名・記法で docs/wiki/ に作成 「Sidebar を更新して」→ 新規ページを反映した _Sidebar.md を生成 「このドキュメントを Wiki 用に整理して」→ フラット構成 + Wiki 記法に変換 画像は [[images/xxx.png]] 、パイプ記法は使わない、ファイル名はプレフィックス命名 — この辺を全部 AI が自動でやってくれる状態にしておくと、あとは push するだけで Wiki に反映されます。人間はレビューだけですね。 例えば Claude Code の Skill 参考資料にはこんな感じで書いておきます。 # Wiki 記法ルール ## ページリンク - パイプ記法 [[A|B]] は使わない - ファイル名そのままで参照: [[Guide-Getting-Started]] ## 画像 - Wiki 添付記法を使う: [[images/xxx.png]] - Markdown の ![alt](path) は使わない ## ファイル命名 - ダッシュ区切り: Guide-Getting-Started.md - プレフィックスで擬似階層: Guide-, Reference- 等 - サブディレクトリの同名ファイルは衝突するので注意 ## 配置先 - docs/wiki/ に配置 - push するだけで GitHub Actions 経由で Wiki に同期される これを Skill の references に入れておけば、「Wiki ページを作って」と言うだけで AI がルールに従って docs/wiki/ にファイルを作ってくれます。 おまけ: Mermaid も普通に使える GitHub Wiki は Mermaid をネイティブレンダリングしてくれます。実際に試してみたら、フローチャート・シーケンス図・クラス図すべて問題なく表示されました。 docs/wiki/ に書いた Mermaid がそのまま Wiki で図表として表示されるので、ドキュメントの表現力がぐっと上がりますね。 整理すると ここまでの話をまとめるとこんな感じです。 docs/ で一元管理 → PR レビューに乗る、バージョン管理される AI が docs/ を直接読み書き → push するだけで Wiki にも反映 Wiki UI は残る → 非エンジニアや外部向けの閲覧用として活用 以前の記事でも書いてきた「Markdown にしろ」→「GitHub に集約しろ」という流れの続きで、「Wiki も docs/ から同期しろ」というのが今回の話です。情報の一元化が AI 協業の基盤になるんですよね。 コラム: 既存 Wiki からの移行 すでに Wiki にページがある方向けの移行手順も書いておきます。 git clone https://github.com/OWNER/REPO.wiki.git で既存 Wiki をローカルにダウンロード メインリポジトリに `docs/wiki/` を作成してファイルを移す AI(Claude Code 等)を使って整理する   サブディレクトリをフラット化してプレフィックス命名に変換   画像参照を Wiki 添付記法( `[[images/xxx.png]]` )に統一   `_Sidebar.md` を再生成 ワークフローを追加して push → 同期開始 ステップ 3 は手作業でやると面倒なんですが、AI に任せると一瞬ですね。 まとめ 今回の内容をまとめます。 GitHub Wiki は「使いにくい」のではなく「使い方を変える」だけで快適になります 既製アクション 1 つ + YAML 30 行で docs/ → Wiki の自動同期が完成します 罠はありますが、5 つ知っておけば実用レベルです AI 時代のドキュメント管理は「すべて GitHub に」が正解ですね ほなまた〜 参考リンク Andrew-Chen-Wang/github-wiki-action — 今回使用した GitHub Action Adding or editing wiki pages – GitHub Docs — GitHub Wiki 公式ドキュメント docs/ から Wiki への自動同期を Node.js で実装 – Zenn — 本格的に作り込みたい方向け GitHub CLI マニュアル — gh CLI の公式リファレンス GitHub REST API ドキュメント — API エンドポイントの確認用 HTMLでブログ記事を保存してる奴、全員Markdownにしろ — AI 向け Markdown 最適化 Claude Code設計術:AIフレンドリーなドキュメント管理 — GitHub に情報を集約する設計思想 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post GitHub Wikiの管理が面倒?Actionsで自動同期する実践手順 first appeared on SIOS Tech Lab .
エピソード紹介 Ep.1 – クリーンアーキテクチャとは Ep.2 – 認証方式の実践的な紹介 Ep.3 – ER設計と監査ログ Ep.4 – RepoScanner の実装とテスト ← 今回はこちら Ep.5 – Copilot プロンプトを効率化 こんな方へ特におすすめ クリーンアーキテクチャの理屈は分かったけど、どこから書き始めるの?と疑問な方 クリーンアーキテクチャで小さな MVP を実装するワークフローに興味がある方 TDD(テスト駆動開発)を実務に取り入れて、壊れにくいコードを書きたい方 概要 こんにちは。サイオステクノロジーのはらちゃんです! シリーズ4本目となる今回は、いよいよ待望の実装編に突入します。 ここで大きな役割を果たすのが TDD(テスト駆動開発) です。テストファーストで進めることで、依存性の切り離しや境界の明確化が自然と行われます。 — 本シリーズでは、Copilotを活用しつつ、クリーンアーキテクチャに沿って小規模なプロダクト「RepoScanner」を設計・実装した経緯をまとめます。 このエピソードは、アプリのコア機能である「リポジトリ内のスナップショット一覧を取得する機能」に焦点を当てました。 さらに、「AIへの指示の出し方」や「テストの質の変化」についてもお伝えします。 実装前の準備 プロジェクトの全体像 実装作業を開始する前に、まずは「RepoScanner」の構成を整理しておきます。 目的 リポジトリのメタデータや集計結果を効率よく取得し、分析しやすくすること。 要件 スナップショット一覧のページング(limit / offset)、最大取得数の制限。 クリーンなユースケースを保つ「3つの設計ルール」 RepoScannerにおけるUse Cases層では、以下のルールを徹底しました。 入出力は DTO (Data Transfer Object) HTTPリクエストなどのオブジェクトは、そのまま使わず単純なデータクラスに変換する。 依存はインターフェースを介して注入(DI) DB処理などは、具体的な実装ではなく「Repository」などの抽象的な型に依存させる。 副作用の入り口を明示 「どこで外部APIを呼ぶ」など処理の流れがユースケースから分かる状態を保つ。 このように責務を分離することで、ユースケースが純粋なビジネスロジックだけに集中できる環境が整います。 クリーンアーキテクチャによる設計 コードの保守性を高めるため、クリーンアーキテクチャに従って責務を明確に分離しました。 Domain層 SnapshotSummary などのエンティティ Use Cases層 ListSnapshotSummariesUseCase Interface Adapters層 HTTP エンドポイントの制御 Infrastructure層 SnapshotQueryRepository ここで重要なのは、DBアクセスへの依存を必ず「インターフェース」経由にすることです。 その結果、内側のロジックが外側の技術的な詳細を知らなくて済む「依存性の逆転」が成立します。 設計上の要点 永続化(DBアクセス)への依存は、必ずリポジトリの「インターフェース」を経由させます。 これにより、内側(Use Cases層)が外側(Infrastructure層)の技術詳細を知らない状態となり、依存の逆転を保ちます。 → 詳細は エピソード1へ 階層ごとのテスト戦略 「どこからテストを書けばいいか分からない」という悩みは、クリーンアーキテクチャで解決します。 なぜなら、各階層の役割がはっきりしているため、テストの目的も自ずと定まるからです。 最優先: Use Cases層のユニットテスト ここでは「仕様としての正しさ」を検証します。 limit の上限クリッピングや、 offset の負値チェックなどが対象です。 このテストはフレームワーク更新や DB ドライバ変更に影響されない高速なフィードバックを得ることが可能です。 例えば、以下のような正常系のテストを用意してください。 /tests/use_cases/test_list_snapshot_summaries.py # 正常範囲の limit がそのままリポジトリへ渡されることを確認 class ListSnapshotSummariesUseCaseTest(unittest.TestCase): def test_execute_passes_limit_when_within_bounds(self) -> None: repository = FakeSnapshotQueryRepository() use_case = ListSnapshotSummariesUseCase(repository, max_read_limit=25) use_case.execute(limit=1, offset=0) self.assertEqual(1, repository.received_limit) use_case.execute(limit=25, offset=0) self.assertEqual(25, repository.received_limit) 外部依存はすべてモック化する ドメインモデルに正しく仕事を任せているか確認 優先: Infrastructure層のテスト 次に、 「 技術的な正しさ 」 を検証します。 SQLの組み立てが正しいか、LIMIT / OFFSETのパラメータ順序が間違っていないかといったパラメータ順序や DBから取得したデータをエンティティへ正しくマッピングできているかを確認できます。 /tests/frameworks&drivers/persistence/test_postgres_snapshot_query_repository.py # フィルタなしで LIMIT/OFFSET がパラメータに含まれ、`ORDER BY s.observed_at desc` が含まれる確認 class PostgresSnapshotQueryRepositoryTest(unittest.TestCase): def test_list_snapshot_summaries_without_filters(self) -> None: now = datetime(2026, 2, 24, tzinfo=UTC) cursor = _FakeCursor( rows=[ { "snapshot_id": UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), "fetch_run_id": UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), "target_repo_id": UUID("cccccccc-cccc-cccc-cccc-cccccccccccc"), "owner": "owner", "name": "repo", "default_branch": "main", "requested_at": now, "observed_at": now, "head_sha": "deadbeef", "scan_scope": "full", "status": "succeeded", "trigger_type": "manual", } ] ) with patch( "application.backend.src.infrastructure.persistence.postgres_snapshot_query_repository.get_cursor", return_value=_CursorContext(cursor), ): repository = PostgresSnapshotQueryRepository() items = repository.list_snapshot_summaries( limit=10, offset=5, user_id=None, target_repo_id=None ) self.assertEqual([10, 5], cursor.executed_params) どのようなSQL(またはリクエスト)を組み立てたか検証 インターフェースの「型」と「変換」を確認 最終: 統合テスト/E2E 最後に、GitHub Actionsでのデータ抽出からDB保存、そしてアプリでの読み取りまで、システム全体のフローが本番に近い環境で動作するかを確かめます。 /tests/http/test_main_api.py # ユースケースのクリッピングを利用して 200 を返すことを確認 class MainApiTest(unittest.TestCase): def test_list_snapshots_clamps_limit_and_returns_200(self) -> None: repository = _RecordingSnapshotRepository() cast(Any, main).list_snapshot_summaries_use_case = ListSnapshotSummariesUseCase( repository, max_read_limit=5 ) response = self.client.get("/snapshots?limit=999&offset=0") self.assertEqual(200, response.status_code) self.assertEqual(5, repository.received_limit) 本物のデータベースを使用 セットアップとクリーンアップの仕組みが必須 実装 TDD(テスト駆動開発) 進め方 TDDは、単にバグを防ぐためだけでなく、「設計を洗練させるためのツール」です。 以下の3サイクルで進めます。 Red: 失敗 仕様の最小ケースを満たす「テストコード」を書く。まだ実装がないので当然エラー。 Green: 成功 そのテストが通るように、最小限の「実装コード」を書く。 Refactor: リファクタリング テストが通る状態を保ったまま「設計ルール」に合わせてコードをきれいに整理。 サイクルのイメージ図です。 例えば、「1回の取得上限は100件まで」というルールはAPIの仕様ではなくドメインの規則です。これをユースケース層で確実に担保することで、フレームワークに依存しない堅牢なロジックが完成します。 【実践】スナップショット取得ユースケース 前回設計した snapshot テーブルに対して、「スナップショットを取得する」というユースケースをTDDで作ってみましょう。 Use Cases層のユニットテスト tests/use_cases/test_register_snapshot.py import unittest from uuid import UUID from datetime import UTC, datetime from application.backend.src.domain.entities.snapshot_summary import SnapshotSummary from application.backend.src.use_cases.list_snapshot_summaries import ListSnapshotSummariesUseCase class _FakeRepo: def __init__(self): self.called = False self.last_params = {} def list_snapshot_summaries(self, *, limit, offset, user_id, target_repo_id): self.called = True self.last_params = dict(limit=limit, offset=offset, user_id=user_id, target_repo_id=target_repo_id) now = datetime.now(UTC) return [SnapshotSummary( snapshot_id=UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"), fetch_run_id=UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"), target_repo_id=UUID("cccccccc-cccc-cccc-cccc-cccccccccccc"), owner="owner", name="repo", default_branch="main", requested_at=now, observed_at=now, head_sha="deadbeef", scan_scope="full", status="succeeded", trigger_type="manual" )] class ListSnapshotSummariesUseCaseTDD(unittest.TestCase): def test_offset_negative_raises_and_repo_not_called(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=100) with self.assertRaises(ValueError): uc.execute(limit=10, offset=-1) self.assertFalse(repo.called) def test_limit_zero_becomes_one(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=100) uc.execute(limit=0, offset=0) self.assertEqual(1, repo.last_params["limit"]) def test_limit_clamped_to_max(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=25) uc.execute(limit=999, offset=0) self.assertEqual(25, repo.last_params["limit"]) def test_passes_filters_and_offset(self): repo = _FakeRepo() uc = ListSnapshotSummariesUseCase(repo, max_read_limit=100) user_id = UUID("11111111-2222-3333-4444-555555555555") target_repo_id = UUID("66666666-7777-8888-9999-aaaaaaaaaaaa") uc.execute(limit=10, offset=5, user_id=user_id, target_repo_id=target_repo_id) self.assertEqual(5, repo.last_params["offset"]) self.assertEqual(user_id, repo.last_params["user_id"]) self.assertEqual(target_repo_id, repo.last_params["target_repo_id"]) if __name__ == "__main__": unittest.main() 仕上がったら想定するエラーかどうか、テストを走らせてみることをお勧めします。 Use Cases層 テストを書いた後で、初めて ListSnapshotSummariesUseCase クラスを実装します。 python class ListSnapshotSummariesUseCase: def __init__(self, snapshot_query_repository: SnapshotQueryRepository, max_read_limit: int) -> None: self.snapshot_query_repository = snapshot_query_repository self.max_read_limit = max_read_limit def execute(self, *, limit: int, offset: int, user_id: UUID | None = None, target_repo_id: UUID | None = None) -> list[SnapshotSummary]: if offset < 0: raise ValueError("offset must be greater than or equal to 0") safe_limit = max(1, min(limit, self.max_read_limit)) return self.snapshot_query_repository.list_snapshot_summaries( limit=safe_limit, offset=offset, user_id=user_id, target_repo_id=target_repo_id ) ペイロード検証をUse Cases層で記述 Interface Adapters層のバリデーションだけに依存せず、ドメインルールとしてテスト可能に このように1つテストを作成したら1つ実装するサイクルを作ると、意図したコード実装になりやすいです。 プロジェクトによって、1ファイルごとにするか全体のテストを先に作ってしまうかは調整すべきだと感じました。 Interface Adapters層 ユースケースの実装後、モック化していたDBへの具体的な接続処理は、後からインターフェースの中身として差し替えます。 python class PostgresSnapshotQueryRepository(SnapshotQueryRepository): def list_snapshot_summaries(self, *, limit: int, offset: int, user_id: UUID | None, target_repo_id: UUID | None) -> list[SnapshotSummary]: conditions: list[sql.Composable] = [] params: list[object] = [] if user_id is not None: conditions.append(sql.SQL("fr.user_id = %s")) params.append(user_id) if target_repo_id is not None: conditions.append(sql.SQL("fr.target_repo_id = %s")) params.append(target_repo_id) # where_clause 組み立て、limit/offset を params に追加して実行 Infrastructure層 さいごに、マイグレーションは以下のように追加します。 create table history_event ( history_event_id uuid primary key default gen_random_uuid(), user_id uuid not null references app_user(user_id) on delete restrict, fetch_run_id uuid references fetch_run(fetch_run_id) on delete set null, event_type text not null, happened_at timestamptz not null default now(), summary text not null ); create table operation_log ( operation_log_id uuid primary key default gen_random_uuid(), actor_user_id uuid references app_user(user_id) on delete set null, action text not null, target_type text not null, target_id text, happened_at timestamptz not null default now(), result text not null check (result in ('success', 'failure')), error_code text, trace_id text not null ); 監査・運用観点で operation_log を用意 CI 実行の失敗や権限エラーをすぐ追跡できる Copilotを活用するプロンプト術 クリーンアーキテクチャの骨格を作る際、AIにゼロからコードを書かせると、層の境界が曖昧になりがちです。 そこで、「型」と「依存関係」を明示することで、一発で実用的なコードを得ることができます。 実際のプロンプト例 以下のように指示を出すと、Copilotはビジネスロジックを純粋なPythonコードとして抽出し、テストしやすい構造で出力してくれます。 RepoScanner のスナップショット一覧を返す `ListSnapshotSummariesUseCase` を実装してください。 入力: limit, offset, optional filter 出力: SnapshotSummary のリストと total_count 既存エンティティ: SnapshotSummary(id, repo_name, observed_at, summary) 依存: SnapshotQueryRepository.list_snapshots(limit, offset, filter) -> (items, total) テスト(unittest スタイル)を先に示してください。 生成されたコード(抜粋) プロンプトの指示通り、まずはUse Cases層のコードが生成されます。 APIの検証とは別に、ビジネスロジックが純粋なPythonコードとして抽出され、テストしやすい構造になりました。 application/backend/src/use_cases/list_snapshot_summaries.py class ListSnapshotSummariesUseCase: def __init__(self, repo: SnapshotQueryRepository, max_read_limit: int = 100): self.repo = repo self.max_read_limit = max_read_limit def execute(self, limit: int, offset: int = 0): if limit <= 0: raise ValueError("limit must be > 0") if offset < 0: raise ValueError("offset must be >= 0") limit = min(limit, self.max_read_limit) return self.repo.list_snapshots(limit=limit, offset=offset) Interface Adapters層はリクエストを受け取り、ユースケースを呼び出すだけの「薄い」層になります。 Python @app.get("/snapshots") def get_snapshots(limit: int = 20, offset: int = 0, repo=Depends(get_snapshot_repo)): items, total = ListSnapshotSummariesUseCase(repo).execute(limit=limit, offset=offset) return {"items": [i.to_dict() for i in items], "total": total} おまけ: GitHub Actions エピソード2では、実行環境の分離や運用コストから、GitHub Actions による認証と実行をメインに据える決断をしました。 認証まわりのワークフロー(抜粋) permissions: contents: read pull-requests: read issues: read jobs: collect: steps: - name: Collect PR snapshot and persist env: GITHUB_TOKEN: ${{ github.token }} DATABASE_URL: ${{ secrets.DATABASE_URL }} permissions を明示し、 github.token (実行時トークン)を活用 外部 DB への書き込みには DATABASE_URL といった修飾済みのシークレットを利用 まとめ 今回は、「テストを先に書くことで、自然と依存を切り離す設計になる」という、TDDとクリーンアーキテクチャの相性の良さが伝わるご紹介をしました。 「どうテストするか」を考えるTDDは設計を導く最強のツール ユースケースは小さく、純粋に作り、外部の仕組みに依存しない純粋なビジネスロジックを保つ AI には型と依存関係を伝えることで、開発効率が劇的に向上する エピソード5では、さらに一歩踏み込んだCopilot運用術についてお話しします。お楽しみに! 参考 あらためてDTOを学ぶ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Copilot × Clean Architecture | 実装とテスト first appeared on SIOS Tech Lab .
どうも技術部のシニアエンジニア 佐藤です。 今回は表題の通りLDAPレスのShibboleth構成をご紹介させていただきます。 しかもなんと今回は私がやったわけではありません。 サイオスと長年に渡りお付き合い頂いておりますベンダー様がチャレンジした結果を佐藤がお届けいたします。 今回技術情報をご提供頂きましたのはあの大企業 ネットワンシステムズ株式会社 様となります。 その中でもネットワンシステムズ株式会社 東日本第1事業本部 パブリック第5技術部の門間様、奥山様からの情報提供となります。 この場を借りて情報提供ありがとうございました。 今回は次のような構成で試しております。 認証はEntra IDで実施 Shibboleth IdPはフェデレーションIdPとして利用 LDAPを使わないDBレス構成 ユーザー属性はEntra IDに格納 アーキテクチャ 従来のShibboleth構成ではLDAPが存在します。 従来構成 今回の構成(DBレス) LDAPの代わりにEntra IDがユーザーストアの役割を担う構成になります。 認証フロー 今回の構成では認証をEntra IDへ委譲します。 1 User → SPアクセス 2 SP → Shibboleth IdPへリダイレクト 3 IdP → Entra IDへ認証要求 4 User → Entra IDでログイン 5 Entra ID → IdPへ認証結果 6 IdP → SPへSAMLアサーション 7 SPログイン完了 フロー図で表すと次のようになります。 この構成では以下の役割分担になります。 認証処理 → Entra ID フェデレーションIdP → Shibboleth 今回利用するのは External Authentication フローです。 これにより認証処理を外部IdPへ委譲できます。 ユーザー属性の扱い 従来はLDAPからユーザー属性を取得しますが今回の構成では属性はEntra IDに保存します。 認証後のレスポンスから属性を取得しSPへ渡します。 Entra ID | v OIDC / SAML Response | v Shibboleth Attribute | v Service Provider Shibboleth設定例 authn.properties idp.authn.flows=External idp.authn.External.externalAuthnPath=/authn/External idp.authn.External.supportedPrincipals=urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport relying-party.xml <bean parent="RelyingPartyByName" c:relyingPartyIds="https://sp.example.org"> <property name="profileConfigurations"> <list> <bean parent="SAML2.SSO"/> </list> </property> </bean> attribute-resolver.xml(サンプル) <AttributeDefinition id="mail" xsi:type="Simple"/> <AttributeDefinition id="displayName" xsi:type="Simple"/> <AttributeDefinition id="givenName" xsi:type="Simple"/> 学認フェデレーションへの接続確認 学認テストフェデレーション Shibboleth側の設定は今回割愛しましたが、当然こちらの構成で学認へも接続が可能です。 今回は検証のためテストフェデレーションまでしか確認していませんが属性情報の確認が出来ておりますので運用フェデレーションでも問題なく動作されるでしょう。 この構成のメリット LDAPサーバが不要 従来 IdP + LDAP + DB 今回 IdP + Entra ID ユーザー管理の一元化 LDAPを無くすことにより実質的にユーザーディレクトリの管理をADに集約することが出来るので学内にADはあるがLDAPは利用していない。 といったようなユースケースのお客様には実用的な構成だと感じました。 MFAやConditional Accessが利用可能 Entra IDのMFAやConditional Accessなどの機能を そのまま認証基盤に組み込むことができます。 まとめ Shibboleth IdPはLDAPと組み合わせる構成が一般的ですが、 Entra IDと連携することでDBレス構成も実現できます。 認証 → Entra ID ユーザー属性 → Entra ID フェデレーションIdP → Shibboleth クラウドディレクトリ中心の認証基盤を構築する場合、 シンプルで運用性の高いアーキテクチャになります。 サイオスでは今回の構成をこれまで考えたことはありますが検証してブログにアップするまではしていなかったので これをベンダー様であるネットワンシステムズ様が検証して情報提供くださったことに改めて御礼申し上げます。 これからも認証基盤を含めたお仕事で協力しあえる関係性を続けさせてください。 他の企業・団体様からでも弊社ブログに掲載したい情報等ございましたらご一報頂けると幸いです。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 1人がこの投稿は役に立ったと言っています。 The post EntraIDを利用したLDAPレスShibboleth構成 first appeared on SIOS Tech Lab .
ども!毎日 Claude Code と一緒に仕事している龍ちゃんです。 この記事を読むと、こんなことができるようになります。 「また同じミスだ」をゼロにする — セッション中のミスを「教訓ファイル」として即記録する方法 CLAUDE.md ・ スキル ・設定を教訓ベースで改修して、Claude Code 環境を自分仕様に育てられる 教訓ファイルをバグチケットとして消化→削除するサイクルで、改善が自然と回り続ける 毎日 Claude Code と話してると、同じ回避を繰り返してないか? 毎日Claude Codeと話していると「あれ、この問題、先週もやったな」ってデジャブを感じています。その場その場でプロンプトを工夫して乗り越えてはいるんですが、同じ摩擦が繰り返し起きてないか?と。 Claude Code を日常的に使っていると、セッションごとに新しい切り口で問題が起きますよね。「この指示だと意図通りに動かない」「なぜかこのコマンドがエラーになる」みたいなやつです。 そのたびにプロンプトを工夫したり、入力を変えて回避する — これ自体は普通のことで、短時間で結果を出すためには必要な判断です。 ただ振り返ってみると、実は同じパターンの問題が何回も起きていたりします。僕も「あ、これ3回目だ」と気づいた瞬間がちょっと「ぞわ!」とします。。その場の回避で作業は進む。でも回避だけで終わらせていると、同じ摩擦がずっと残り続けませんか? いつかは改善点として整理しないと、Claude Code 環境が育たない。じゃあ ミスを記録して改修に回す流れ を作っておこう、というのが今回の話です。 セッションから情報を吸い出して課題と解決策を整理する まず全体像を見てもらったほうがイメージしやすいので、先に流れを整理しますね。 やっていることはシンプル ポイントは「教訓ファイルはバグチケット」だということです。永続的なナレッジとして積み上げるのではなく、改修したら消す。「ファイルがある = まだ対応していない」というシンプルなルールにしています。 これ、地味に重要で。残しておくと「解決済みなのか未解決なのか」が分からなくなりません?消すことで改修が完了したことが一目でわかります。 なぜセッションから吸い出すのか セッション中には AI の挙動・自分の指示・回避策のすべてが記録されています。問題が起きた直後が一番情報量が多い — 文脈も意図も再現手順も揃っている。後から「あれなんだっけ」と思い出すよりはるかに正確です。 僕がやっているのは「人間が書くのは意図だけ、事実と分析は AI に任せる」という分担です。「こうしてほしかったんだけど、こうなった」という意図だけ伝えれば、根本原因の分析まで AI がやってくれます。 吸い出し方 — 即起票とエクスポートの2パターン 具体的にどうやって教訓を抽出するか。2つの方法があって、場面によって使い分けています。 方法A: セッション中にその場で教訓ファイルを生成(即起票) ミスや意図のズレが起きた → 回避策を打った → そのタイミングで AI にまとめさせる、というやり方です。 セッションの文脈がまだ残っているうちに書かせるのがポイントで、問題が起きた直後に実行するから情報の鮮度が最大限に保たれます。実際に僕が投げるのはこんな感じです。 今回、スキルAに関してエラーが頻発して回避をしたと思うんだけど、 原因と対応についてまとめてください これだけで、セッション中の文脈を拾って教訓ファイルを生成してくれます。この即起票の流れ自体を スキル にしておくと、 /lesson のようにワンコマンドで教訓ファイルが生成できるので便利です。向いているのは、問題が起きた直後で原因がわりと明確なケースです。 方法B: セッションを丸ごとエクスポート → あとから分析 Claude Code には /export コマンド があって、セッション全体をテキストに保存できます。 /export でクリップボードに /export session.md でファイルに出力 会話・ツール呼び出し・Bash コマンド・MCP 呼び出しが丸ごと残ります。 ~/.claude/projects/<cwd>/*.jsonl にも全セッションが自動保存されているので、後から拾うことも可能です。 エクスポートしたファイルを別のセッションで指定して、こんな感じで頼みます。 前回のセッションの振り返りをしたいです。 ファイルを読み込んで人間の指示とClaudeの受け取りで 齟齬が起きた箇所を解析して分析して対応方法を考えてください これだけで、AI がセッション全体を読み込んで横断的にパターンを見つけてくれます。 向いているのは、1日の終わりや週次のふりかえり、「最近なんか同じことよく直してるな」みたいなセッション横断の繰り返しパターンを見つけたいときです。 使い分けの目安 方法A(即起票) 方法B(エクスポート→分析) タイミング 問題発生の直後 作業後・定期ふりかえり 発見できるもの 個別の意図のズレ・バグ セッション横断の繰り返しパターン 手間 一言で完了 エクスポート→分析の2ステップ 文脈の鮮度 最大限残っている やや薄れる 実用的なのは 方法Aでその場の問題を拾い、方法Bで見逃したパターンを定期的に回収する 併用です。 教訓ファイルのフォーマット どちらの方法でも、最終的に出力するのはこの形式にしています。 # 教訓: {タイトル} ## 起因 何が起きたか。事実ベースで記述。 ## 分析 なぜ起きたか。根本原因を掘り下げる。 ## 教訓 次回どうするか。具体的なアクション。 ## 適用範囲 この教訓が当てはまるケース。 「何が起きたか」「なぜ起きたか」の事実と分析は AI が書く。「こうしてほしかった」という意図は人間が補う。この分担がうまく機能しています。 実際の教訓ファイルはこんな感じ 実際に僕の手元で生成された教訓ファイルの抜粋です。 # 教訓: plan.md の推奨モデルを無視した Opus → Sonnet 委譲失敗 ## 起因 実装計画(plan.md)で「推奨モデル: Sonnet」と明記したにもかかわらず、 「実装を続けてください」と指示を受けた Opus が Sonnet に委譲せず 直接実装に着手しようとした。 ## 分析 3つの要因が重なった: 1. **「実装して」を「自分で書け」と解釈するバイアス** - 計画フェーズでは冷静に Sonnet 推奨と判断できたのに、 実行フェーズで自分の計画を無視した 2. **ロール切り替えの失敗** - 正しいロール: オーケストレーター(Sonnet に指示を出し、進捗を管理) 3. **CLAUDE.md ルールの実行時無視** ## 教訓 1. 実装開始時に plan.md の「推奨モデル」セクションを最初に確認する 2. Sonnet 推奨の場合、Opus はオーケストレーターに徹する ## 適用範囲 - plan.md に「推奨モデル」セクションがある全ての実装タスク テンプレートに沿って書かせると「何が・なぜ・どう直すか」が揃うので、改修に回すときに方針を考える手間がほぼなくなります。 実例で見る「吸い出し→改善→削除」 実際にどんな改修になるか、僕の手元で起きた事例で見ていきますね。 サムネイルパス問題 → スキルの実装方式を変更 「テンプレートの相対パスが Playwright 実行時に壊れる」という問題が、実は3回繰り返し発生していたんですよね。 最初は「パスを直せば動く」で済ませていたんですが、3回目でさすがに「これ構造の問題だな」と気づいて、教訓ファイルを書かせました。 教訓ファイルが根本原因を突いていて「HTML の保存先が変わると即アセットが壊れる構造的問題」と。発生パターンも3つ特定してくれていました。 対処療法で直していたら何度でも同じことが起きていたはずで、「templates/ 内でレンダリング→PNG だけ移動」という方式に変更することで構造ごと直しました。これが教訓ファイルの真価で、根本原因が明確だから対処療法ではなく構造的な改修になるんですよね。 改修が終わったら、教訓ファイルを削除。これでサイクルが1周です。削除のタイミングは2つのやり方があって、 改修を入れたらその場で消す か、 1週間など期間を決めて定期的にまとめて消す か。僕は改修直後に消す派ですが、まとめて振り返りたい人は定期削除のほうが合うかもしれません。どちらでも「ファイルがある = 未対応」というルールさえ守れば機能します。 他にもこんな事例 僕の手元で起きた他の事例も紹介しておきますね。改修先がバラバラなのがポイントです。 問題 教訓ファイルの分析 改修先 plan.md に「推奨モデル: Sonnet」と書いたのに Opus が直接実装しようとした 「実装して」を「自分で書け」と解釈するバイアス CLAUDE.md にルール追記 uv run html-screenshot が Failed to spawn エラー workspace member のスクリプトはルートから自動解決されない スキルのコマンド例 を修正 教訓ファイルがなければ「またこれか」で終わっていた問題ばかりです。教訓ファイルが「どこを直すべきか」まで教えてくれるので、改修の方針を考える時間がほぼゼロになります。 まとめ: Claude Code 環境は「使って→吸い出して→直す」で育てる セッションの中に改善のヒントが全部あります。それを吸い出す仕組み(教訓ファイル)を持っておくだけで、改善が自然と回り始めます。 まとめると、 教訓ファイルはバグチケット。溜めずに消化して、消す 方法Aで問題発生の直後に拾い、方法Bで定期的に見直す AI に根本原因を分析させれば、改修先まで特定してくれる この「1回のミスを確実に拾う」仕組みが積み重なると、「3回繰り返したら仕組みにする」というサイクルにも自然と繋がっていきます。僕が以前書いた Copilot チャット履歴から copilot-instructions.md と SKILL.md を育てる方法 で紹介したポストモーテムの考え方の前段として、まずは1回の問題を漏らさず拾うところから始めてみてください。 Claude Code の設定ファイルの書き方や運用のコツは 公式のベストプラクティス も参考になります。 ほなまた〜 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude Codeの失敗をバグチケット化して潰す方法 first appeared on SIOS Tech Lab .
エピソード紹介 Ep.1 – クリーンアーキテクチャとは Ep.2 – 認証方式の実践的な紹介 Ep.3 – ER設計と監査ログ ← 今回はこちら Ep.4 – RepoScanner の実装とテスト Ep.5 – Copilot プロンプトを効率化 こんな方へ特におすすめ 新規アプリケーションのER設計を担当する方 監査ログの設計や、データの保持ポリシーに関心のあるエンジニア 概要 こんにちは。サイオステクノロジーのはらちゃんです! 白紙の状態からER設計を考えることをあまり経験したことがないため、ハードルが高いと感じていました。 今回、0から考える良い機会であったため、「どう考えればよいか」「どのようなことを気を付けるべきか」など私なりに調べました。 — 本シリーズでは、Copilotを活用しつつ、クリーンアーキテクチャに沿って小規模なプロダクト「RepoScanner」を設計・実装した経緯をまとめます。 このエピソードは、アプリケーションの根幹となるER設計と、運用を見据えた監査ログの設計について、私がどう考えたかという視点で解説します。 作業手順 要件の整理 いきなりテーブル設計をしろと言われても難しいので、作りたいものの情報を整理していきます。 ここではVSCodeの拡張機能であるdraw.ioを使って、簡単にまとめていきます。 コンテキスト 図 まずは、ユーザーと「RepoScanner」の関係性( コンテキスト ・ ダイアグラム )から整理しました。 DBなどは考えず、何をするシステムにしたいかに注目します。 本来、業務システムでは、より詳細にシステム管理者 / 担当者 / 利用者など踏まえて図示する必要があります。 ユースケース図 続いて、どのようなユースケースが考えられるかを簡単に表現します。 これらの過程で、システムとしてはどこまで考えるか、今回はどこまで実装するかなど考えておくと次の作業がスムーズになると思います。 概念図 最後に、ユースケースを見ながら概念モデルを考えます。 あとから修正する前提で、ざっくりと書いてください。 ER設計 RepoScannerは、GitHub Actionsで収集したデータの情報をDBに保存し、アプリ側から読み取る構成です。 要件を満たしつつ、将来的な監査や障害調査といった運用に耐えうる設計を行うため、主要エンティティを定義します。 repository : スキャン対象となるリポジトリの基本情報。 snapshot : 特定のタイミングで収集したリポジトリのメタデータ群。 pull_request : リポジトリに紐づくPRの情報。 operation_log : 誰が、いつ、何をしたか(収集ジョブの実行など)を記録する監査ログ。 ポイント: エンティティの切り出し方 最初は「repositoryテーブルに全部の情報を入れちゃえば楽じゃない?」と考えがちでした。 以下の視点を持つと、どの粒度でテーブルを分けるか判断しやすいと思います。 データの増え方 リポジトリ名はめったに変わらず、スナップショットは実行するたびに増える。これらを混ぜると、リポジトリ名の取得のために膨大なデータを読み込むことになる。 データの寿命 リポジトリ情報は長く残るが、スナップショットは一定期間で消す可能性がある。 ポイント: 監査ログのテーブル準備 「機能を作るためのテーブル」だけでなく、初期段階から「運用・監査のためのテーブル」を組み込んでおきます。 本番稼働に向けたセキュリティなどの非機能要件に柔軟な対応をすることができます。 PostgreSQL用 DDLの実例 実際のPostgreSQL用のDDLスニペットをご紹介します。 クリーンアーキテクチャのFrameworks & Drivers層で、これらのテーブルに対してCRUD操作を行います。 -- 1. リポジトリテーブル CREATE TABLE repository ( id UUID PRIMARY KEY, name TEXT NOT NULL, owner TEXT NOT NULL, created_at timestamptz DEFAULT now() ); -- 2. スナップショットテーブル CREATE TABLE snapshot ( id UUID PRIMARY KEY, repository_id UUID REFERENCES repository(id) ON DELETE CASCADE, collected_at timestamptz NOT NULL, data jsonb NOT NULL -- 柔軟なメタデータはJSONBで保持 ); -- 3. 監査・操作ログテーブル CREATE TABLE operation_log ( id BIGSERIAL PRIMARY KEY, actor TEXT NOT NULL, -- 実行者(ユーザーやCIのBot名) action TEXT NOT NULL, -- アクション内容(例: "SNAPSHOT_COLLECTED") target_type TEXT, -- 対象のリソース種別 target_id TEXT, -- 対象のID details jsonb, -- 変更内容などの詳細 occurred_at timestamptz DEFAULT now() ); PostgreSQLの jsonb 型は、スキーマレスにデータを放り込めるため非常に柔軟で便利です。 snapshot.data や operation_log.details のように、構造が頻繁に変わるメタデータを保存するのに適しています。 GitHubから取得できるデータは多岐にわたります。 最初から全ての項目をカラム定義するのは現実的ではないため、まずは jsonb で丸ごと保存し、システムが成長して「この項目で検索したい!」と確定した段階で、カラムとして独立させる戦略を取りました。 ただし、「頻繁に検索やソートに使用する項目」は、必ず独立したカラムとして切り出してください。 すべてを jsonb の中に閉じ込めてしまうと、インデックスが効きにくくなり、データ量が増えた際にクエリのパフォーマンスが著しく低下(スロークエリ)する原因になります。 ポイント: 実務で必須の共通カラム 今回のDDLではシンプルにしていますが、実際の現場ではほぼ全てのテーブルに以下のカラムを含めることが多いです。 created_at (作成日時) updated_at (更新日時) deleted_at (論理削除フラグ:データを物理的に消さず、削除日を入れることで「削除済み」と扱う) deleted_at が必要かどうかはデータをどのように扱いたいかで判断できます。 実装のメリットとしては、間違えて消した時の復旧や、監査の視点では「いつ消されたか」の情報が保持できることです。 逆にこれらを保持せず完全に削除することが要件の場合は不要となります。 テーブル定義とセットで決める「運用ポリシー」 ER設計(DDL)が完成したら、それで終わりではありません。 データは運用とともに増え続けるため、「そのデータをどう扱うか」という運用ポリシーを合わせて定義することで、運用時の混乱を防ぐことができます。 RepoScannerにおけるポリシー例 保持期間とアーカイブ   snapshot データはストレージ容量を圧迫しやすいため、「作成から90日間DBに保持」とする。  90日を過ぎた重要データ(月次のサマリなど)は、DBから削除し、安価な外部ストレージ(S3など)へアーカイブとしてエクスポートする。 ログへのアクセス権限   operation_log はセキュリティインシデントの調査に使われるため、一般のアプリケーションユーザーや開発者からはアクセスできず、特定の「監査ロール」を持つ管理者のみ閲覧可能とする。 個人情報の取り扱い  ログの中にユーザーのメールアドレス等の個人情報が含まれる場合、保存時にマスキング処理を行うか、退会時に別フローで物理削除できる設計にしておく。 ポイント: 監査ログを書くタイミング この operation_log は、単なるDBの更新履歴ではありません。 アプリケーション側のUseCaseで、「誰がいつ何をしたか」というビジネス上の意味を持つタイミングで明示的に保存するように設計します。 まとめ DDLと運用ポリシーをセットで定義することで、運用開始後のデータ肥大化やパフォーマンス低下を未然に防ぐことができます。 監査ログのテーブルは、アプリケーションのER設計と同時に考えるべきです。 PostgreSQLの jsonb は強力ですが、検索要件と照らし合わせて「カラムとして切り出すべきデータ」を見極めましょう。 エピソード4では、このテーブル設計をベースにして、クリーンアーキテクチャに沿ったスナップショット一覧のAPI実装とテストについて解説しています。お楽しみに! 参考 コンテキスト・ダイアグラムとは何か?プロダクト・スコープの図解表現を解説 若手プログラマー必読!5分で理解できるER図の書き方5ステップ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Copilot × Clean Architecture | ER設計と監査ログ first appeared on SIOS Tech Lab .
ども!最近、社内でClaude Codeの普及活動している龍ちゃんです。 Claude Codeは機能開発も爆速で、最近覚えること多いなってなったんですよね。あれもこれも教えるのって大変じゃないですか?なので、契約したばかりの人に向けて一言だけ伝えています。 インストールしたらとりあえず会話だ。 一旦、SKILLとかCLAUDE.mdとか、うまく使いこなそうって考えは捨ててください。設定や機能の情報はいっぱいあるけど、そんな話はよく使う習慣がついてからでよいです。 Claude Codeに「初心者は何からやればいい?」と聞いてみる claude と打って起動したら、まずこう聞いてみてください。(インストールは頑張ってください…) 私はClaude Code初心者です。初心者が最初にやるべきことについて教えて すると、何から手をつければいいか、どんな機能があるか、順番に教えてくれます。「そんな機能あったんだ」ってなるやつが必ず1個は出てくるんですよね。 「CLAUDE.mdって何?」「MCPって使えるの?」「このプロジェクトで何ができる?」——何でも聞けばいいです。ドキュメントを探しに行く前に、まず聞く。これだけです。 最初に聞いてみる3つの質問 お勧めな「最初に聞いてみる3つ」はこれです。 「Claude Code初心者です。何からやればいい?」 — どこから手をつけるか迷ったとき 「あなたは何ができますか?」 — 全体像を把握したいとき 「CLAUDE.mdって何?どう使うの?」 — 気になった機能を深掘りしたいとき 3番目みたいに、会話の中で出てきたキーワードをそのまま聞き返せるのがいいんですよね。CLAUDE.mdに限らず、「MCPって何?」「Hooksって何?」でも同じです。 なぜClaude Codeは自分自身のことに正確に答えられるのか 「AIに聞いても適当なこと言いそう…」と思う気持ち、わかります。僕も最初はそう思ってました。 でも実は、Claude Codeには claude-code-guide という内蔵エージェント があって、自分自身のドキュメントを参照しながら回答する仕組みになっています。 公式ドキュメントも「Ask Claude about its capabilities」として推奨 しているくらいなので、適当に答えているわけじゃないんですよね。 しかもただ知識を返すだけじゃなくて、必要に応じて最新情報を取りに行って、そのままアクションまでやってくれます。聞いたら調べて、調べたら動く。Claude Code自体が最強のドキュメントであり、実行者でもあるわけです。 ただ一点だけ。情報を引き出すのはClaude Codeでも、「それを取り込むかどうか」を決めるのは人間の仕事です。提案が全部正解とは限らないので、判断はこっちでやる、という感覚は持っておくといいですね。 まとめ — 覚えることは一つだけ 覚えることは一つだけです。 困ったらClaude Codeに聞け。 まずは claude と打って、「初心者です、何からやればいい?」から始めてみてください。そこから全部始まります。 「何を聞けばいいかわからない」ときは 「聞けばいい」と言われても、そもそも何を聞けばいいかわからないって人もいると思います。気になっていることややりたいことはあるけど、うまく言語化できなくて一歩が踏み出せないとか。 そういうときは、頭の中にあることをとりあえず書き出してみてください。箇条書きでも、音声入力で文字起こしした雑なメモでもいいです。それをそのままClaude Codeに読ませれば、整理して具体的なアクションを提案してくれます。 完璧な質問を考える必要はないんですよね。雑なインプットを整理するのはClaude Codeが得意なんで、「とりあえず渡す」くらいの感覚でOKです。 次に読む Claude Code自身のことは聞けばわかる。じゃあ外部サービスとか最新ニュースはどうするの?って話なんですけど、実はClaude Codeには検索機能もあるんで、それも聞けるんですよね。詳しくはこちらの記事でまとめています。 2026-03-09 「あとで調べよう」が一生来ない人へ|AIにぶち込むだけの処方箋 ではまた! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude Codeの使い方|初心者に僕が最初に伝える、たった一つのこと first appeared on SIOS Tech Lab .
PSSLの佐々木です 今回は DeepAgents と MCP(Model Context Protocol) を組み合わせて、この作業を全自動化するエージェントを作ってみました。Web検索、GitHub Trending、Zenn RSS、Hacker News RSS を巡回し、レポートを生成して自動投稿するところまで、すべてエージェントが自律的にやってくれます。 なお、この記事では通知先として Slack を例に解説していますが、 今回の実装では実際には Notion に投稿する形で構築しました 。Slack の場合は Bot Token の発行にワークスペース管理者の承認が必要になることがあるため、個人で試すなら Notion の方が手軽です。通知先は MCP サーバーや Webhook を差し替えるだけで簡単に変更できるので、お好みの方法を選んでください。 完成するとこうなります: 週次 AI エージェント技術トレンド — 2026年3月第2週 ■ サマリー DeepAgents の登場で LangGraph ベースのエージェント開発が大幅に簡素化。 MCP エコシステムも急拡大中… ■ 今週のトレンド Top 5 DeepAgents v0.2 リリース GitHub Agentic Workflows GA Figma MCP Server 公開 OpenTelemetry GenAI Conventions Google Antigravity アップデート 詳細はスレッドをご覧ください この記事を読めば、同じ仕組みを30分で構築できます。コードは GitHub で公開しているので、ぜひ clone して試してみてください。 https://github.com/atomic-kanta-sasaki/deepagents-trend-slack この記事で作るもの — 全体アーキテクチャ まずは完成形の全体像を把握しましょう。以下の図が今回構築するシステムのアーキテクチャです。   テキストで説明すると、以下の流れになります: cron(GitHub Actions)で毎朝起動 — 平日の朝8時に自動実行 DeepAgent が調査計画を立てる — write_todos で「何を調べるか」をリストアップ 4つの情報源から収集 — Web検索(Tavily)、GitHub Trending、Zenn RSS、Hacker News RSS 仮想ファイルに中間保存 — write_file で調査結果を一時保存 サブエージェントがレポート執筆 — 専門のライターエージェントに委託 Slack に投稿 — 完成したレポートをチャンネルに自動投稿 ここで注目すべきは、 情報収集も通知も MCP で統一している 点です。MCP サーバーを追加・差し替えするだけでエージェントの能力を拡張できるため、後から「Notion にも投稿したい」「Discord にも流したい」となっても、コード本体を変更する必要がありません。 DeepAgents とは?— 素の LangGraph との違い DeepAgents は、LangChain チームが公開した LangGraph 上のエージェントハーネス です。Claude Code や Deep Research で使われているアーキテクチャを OSS 化したもので、 create_deep_agent() を呼ぶだけで、エージェント開発に必要な機能が一式揃います。 具体的に何が付いてくるのか、表で整理してみました。 Middleware 役割 今回の用途 TodoListMiddleware 計画の自動管理 調査ステップの分解と進捗管理 FilesystemMiddleware 仮想ファイル R/W 中間調査結果の保存 SubAgentMiddleware サブエージェント生成 レポート執筆を専門家に委託 SummarizationMiddleware コンテキスト自動圧縮 大量検索結果でのトークン溢れ防止 素の LangGraph でこれらを全部実装しようとすると、概算で 200行以上 のコードが必要になります。一方、DeepAgents なら 50行程度 で同等の機能が手に入ります。 とはいえ、DeepAgents は「全部入り」なので細かい制御がしづらい面もあります。使い分けとしては、 プロトタイプは DeepAgents でサクッと作り、細かい制御が必要になったら素の LangGraph に降りる というアプローチがおすすめです。 MCP(Model Context Protocol)のおさらい MCP(Model Context Protocol) は、AI エージェントが外部ツールやデータソースにアクセスするための 共通プロトコル です。Anthropic が提唱し、現在は多くの企業・コミュニティがサーバーを公開しています。 LangChain エコシステムでは、 langchain-mcp-adapters を使うことで MCP ツールを LangChain ツールに変換できます。これにより、MCP サーバーを DeepAgents や LangGraph のエージェントからシームレスに呼び出せます。 今回接続する MCP サーバーは以下の3つです: GitHub MCP Server ( @modelcontextprotocol/server-github )— リポジトリ検索、Trending 取得 自作 RSS MCP Server — Zenn と Hacker News のフィード取得 Slack Notify MCP Server ( @mkusaka/mcp-server-slack-notify )— Slack への通知 MCP サーバーは USB のようなもの と考えるとわかりやすいです。挿すだけでエージェントの能力が拡張され、抜けば元に戻る。この疎結合さが MCP の魅力です。 実装 — 環境構築から Slack 通知まで ここからは実際のコードを見ながら、実装を進めていきます。 4-1. セットアップ まずは必要なパッケージをインストールします。 # リポジトリをクローン git clone <https://github.com/atomic-kanta-sasaki/deepagents-trend-slack.git> cd deepagents-trend-slack # 仮想環境を作成 python -m venv .venv source .venv/bin/activate # Windows: .venv\\\\Scripts\\\\activate # 依存関係をインストール pip install -e . 次に、環境変数を設定します。 .env.example をコピーして .env を作成し、各種 API キーを設定してください。 # .env.example の内容 ANTHROPIC_API_KEY=sk-ant-... # または Azure OpenAI の設定 TAVILY_API_KEY=tvly-... # Web検索用 GITHUB_TOKEN=ghp_... # GitHub API用 SLACK_BOT_TOKEN=xoxb-... # Slack投稿用 SLACK_DEFAULT_CHANNEL=#tech-trends Slack Bot Token の取得は以下の手順です: api.slack.com/apps にアクセス 「Create New App」→「From scratch」 「OAuth & Permissions」→ Bot Token Scopes に chat:write と chat:write.public を追加 「Install to Workspace」でインストール 表示される Bot User OAuth Token( xoxb-... )をコピー 4-2. 自作 RSS MCP サーバー — 20行で外部データソースを追加 エージェントに Zenn と Hacker News の情報を供給する MCP サーバーを作ります。驚くべきことに、 たった20行程度で MCP サーバーが自作できます 。これがこの記事の隠れた見どころです。 # src/rss_server.py import json import feedparser from mcp.server.fastmcp import FastMCP mcp = FastMCP("TechRSS") @mcp.tool() def fetch_zenn_trending(topic: str = "ai") -> str: """Zenn の指定トピックの Trending 記事を取得""" feed = feedparser.parse(f"<https://zenn.dev/topics/{topic}/feed>") return json.dumps( [{"title": e.title, "url": e.link, "published": e.get("published", "")} for e in feed.entries[:10]], ensure_ascii=False, ) @mcp.tool() def fetch_hackernews_best() -> str: """Hacker News の Best Stories を取得""" feed = feedparser.parse("<https://hnrss.org/best?count=10>") return json.dumps( [{"title": e.title, "url": e.link} for e in feed.entries], ensure_ascii=False, ) if __name__ == "__main__": mcp.run(transport="stdio") コードの解説をしておきます: FastMCP("TechRSS") で MCP サーバーのインスタンスを作成 @mcp.tool() デコレータで関数をツールとして公開 fetch_zenn_trending は Zenn の RSS フィードをパースして上位10件を JSON で返す fetch_hackernews_best は Hacker News の Best Stories を同様に取得 ensure_ascii=False は日本語の文字化け防止に必須 mcp.run(transport="stdio") で標準入出力ベースの MCP サーバーとして起動 4-3. メインエージェントの構築 — DeepAgents × MCP の接続 次に、DeepAgents と MCP を接続するメインロジックを実装します。 # src/agent.py import os import sys from pathlib import Path from deepagents import create_deep_agent from langchain_mcp_adapters.client import MultiServerMCPClient from .prompts import SYSTEM_PROMPT from .tools import internet_search def get_mcp_server_config() -> dict: """MCP サーバー設定を取得""" project_root = Path(__file__).parent.parent rss_server_path = project_root / "src" / "rss_server.py" config = { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "transport": "stdio", "env": {"GITHUB_TOKEN": os.environ.get("GITHUB_TOKEN", "")}, }, "rss": { "command": sys.executable, "args": [str(rss_server_path)], "transport": "stdio", }, "slack": { "command": "npx", "args": ["-y", "@mkusaka/mcp-server-slack-notify@latest"], "transport": "stdio", "env": { "SLACK_BOT_TOKEN": os.environ.get("SLACK_BOT_TOKEN", ""), "SLACK_DEFAULT_CHANNEL": os.environ.get("SLACK_DEFAULT_CHANNEL", "#tech-trends"), }, }, } return config async def create_research_agent() -> tuple: """リサーチエージェントを作成""" mcp_config = get_mcp_server_config() mcp_client = MultiServerMCPClient(mcp_config) # MCP ツールを取得 mcp_tools = await mcp_client.get_tools() print(f"MCP ツールを {len(mcp_tools)} 個取得しました") # 全ツールを結合 all_tools = [internet_search, *mcp_tools] # DeepAgent を作成 agent = create_deep_agent( tools=all_tools, system_prompt=SYSTEM_PROMPT, ) return agent, mcp_client ポイントを解説します: MultiServerMCPClient で GitHub / RSS / Slack の3つの MCP サーバーに同時接続 create_deep_agent() にツールとシステムプロンプトを渡すだけでエージェントが完成 internet_search は Tavily API をラップした関数で、MCP ではなくネイティブツールとして実装 システムプロンプトでは、調査手順を明確に指示しています。「まず write_todos で計画を立て、次に各情報源から収集し、中間結果を保存してから、サブエージェントにレポート執筆を委託する」という流れをプロンプトで規定することで、エージェントの動きを安定させています。 エントリーポイントは以下のようになります: # src/main.py import asyncio from .agent import create_research_agent async def run_agent(query: str) -> str: agent, mcp_client = await create_research_agent() result = await agent.ainvoke({ "messages": [{"role": "user", "content": query}] }) return result["messages"][-1].content if __name__ == "__main__": query = "今週のAIエージェント関連トレンドを調査してSlackに投稿して" result = asyncio.run(run_agent(query)) print(result) 4-4. 実行してみる 準備ができたら、実際に動かしてみましょう。 python -m src.main "今週のAIエージェント関連トレンドを調査してSlackに投稿して" エージェントが自律的に動き出し、以下のようなログが流れていきます: [write_todos] 調査計画を作成: ☐ Web検索で最新ニュースを収集 ☐ GitHub Trending を確認 ☐ Zenn/HN のRSSフィードを確認 ☐ 中間結果をファイルに保存 ☐ レポートを生成してSlackに投稿 [internet_search] "AI agent framework 2026 March" → 5件取得 [internet_search] "DeepAgents LangGraph 最新" → 5件取得 [fetch_zenn_trending] topic="ai" → 10件取得 [fetch_hackernews_best] → 10件取得 [write_file] research_notes.md に中間結果を保存 [task] サブエージェント "report_writer" を起動 [slack_send_message] #tech-trends にレポートを投稿 ✅ 実行してから Slack にレポートが届くまで、約2〜3分でした。情報収集からレポート生成、投稿まですべて自動で行われるのを見ると、なかなか感動します。 GitHub Actions で毎朝自動実行する せっかく作ったエージェントなので、毎朝自動で動くように設定しましょう。以下のワークフローファイルを追加します。 # .github/workflows/daily_report.yml name: Daily Tech Trend Report on: schedule: # 日〜木の 23:00 UTC = 月〜金の 08:00 JST - cron: '0 23 * * 0-4' workflow_dispatch: # 手動実行も可能 jobs: generate-report: runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.11' - uses: actions/setup-node@v4 with: node-version: '20' - run: pip install -e . - run: python -m src.main env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} cron 式 '0 23 * * 0-4' は「日曜〜木曜の 23:00 UTC」を意味し、日本時間では「月曜〜金曜の 08:00 JST」になります。 リポジトリの Settings → Secrets and variables → Actions で、必要な環境変数を登録すれば準備完了です。これで毎朝 Slack に技術トレンドレポートが届くようになります。 実際に取得できる情報 # 週次AIトレンド 2026-03-13 --- 📊 週次AIトレンド 2026-03-07〜2026-03-13 ■ サマリー エージェントプラットフォームと商用LLMのアップデートが加速し、「安全に運用できるエージェント基盤」と「実務タスクに強いLLM」の両輪が整いつつあります。 医療・ロジスティクス・マーケティングなど各業界でエージェント活用の具体事例が増え、常駐型エージェントやローカル実行基盤など、日常業務への深い組み込みが進行中です。 一方で、エージェントのセキュリティやAI誤認識による冤罪など、ガバナンス・社会的インパクトへの対応が経営課題として顕在化しています。 ■ 今週のトレンド Top 5 1. エージェントプラットフォーム競争の本格化 - OpenAIのPromptfoo買収やNvidiaのオープンソース計画など、企業向けエージェント実行基盤の整備が進展 (<https://techcrunch.com/2026/03/09/openai-acquires-promptfoo-to-secure-its-ai-agents/>) 2. Claudeを中心とした商用LLMエコシステムの拡大 - 可視化生成・自動コードレビュー対応とともに、ユーザー数急増でChatGPTからの乗り換えトレンドが顕在化 (<https://www.theverge.com/ai-artificial-intelligence/893625/anthropic-claude-ai-charts-diagrams>) 3. オープンソースエージェント基盤の充実 - OpenClawやDify、openai-agents-pythonなど、プロダクション運用を意識したエージェント開発フレームワークが揃い始めている (<https://github.com/openclaw/openclaw>) 4. 業界別エージェント活用の具体事例 - 医療のAgent Factoryやロジスティクス最適化、マーケ・広告のマルチエージェント活用など、PoCから本番運用フェーズへの移行が進行 (<https://hitconsultant.net/2026/03/10/epic-ai-himss-2026-agent-factory-curiosity-foundation-models/>) 5. セキュリティ・ガバナンスと社会的リスクへの注目 - エージェントの自動レッドチーミングやAI誤認識による冤罪事例など、AI活用の前提としてのリスク管理がクローズアップ (<https://www.adweek.com/media/newsguard-tracking-ai-slop-content-farms/>) 💬 詳細はスレッドをご覧ください --- # 週次AIトレンド詳細レポート(2026-03-07〜2026-03-13) ## 1. エージェントプラットフォーム競争の本格化 OpenAIがPromptfooを買収し、エージェント向けプラットフォーム「OpenAI Frontier」に統合する計画を発表しました。自動レッドチーミングやワークフロー単位でのセキュリティ評価、リスク監視を提供することで、エージェントの安全運用を前提とした基盤づくりが進んでいます。同時にNvidiaはオープンソースのAIエージェントプラットフォームを計画し、SalesforceやCiscoなどとの連携を模索しており、企業向けエージェント実行基盤の競争が立ち上がりつつあります。さらに、Google PMによる永続メモリエージェント「Always On Memory Agent」のオープンソース公開など、エージェントの長期記憶・マルチエージェント構成を前提とした設計議論も活発です。 **参考リンク** - OpenAIがPromptfooを買収し、エージェントセキュリティを強化: <https://techcrunch.com/2026/03/09/openai-acquires-promptfoo-to-secure-its-ai-agents/> - NvidiaのオープンソースAIエージェントプラットフォーム計画: <https://www.wired.com/story/nvidia-planning-ai-agent-platform-launch-open-source/> - Always On Memory Agent(ベクタDBに依存しない永続メモリエージェント): <https://venturebeat.com/orchestration/google-pm-open-sources-always-on-memory-agent-ditching-vector-databases-for> ## 2. Claudeを中心とした商用LLMエコシステムの拡大 Anthropic Claudeは、チャートやダイアグラムなどのインタラクティブな可視化生成に対応し、Artifacts機能と組み合わせたアプリ開発の幅が広がっています。さらに、Claude Code向けの自動コードレビュー機能がリサーチプレビューとして提供され、GitHub連携とマルチエージェント構成でロジックバグやセキュリティ問題を検出するワークフローが試行されています。一方で、Claudeのユーザー数は1日100万超の新規サインアップに達し、OpenAIと国防総省の提携を巡る議論も背景に、ChatGPTからの乗り換えトレンドが話題になっています。GoogleによるAndroidアプリ開発向けLLM評価では、Claude Opus 4.6やGPT-5.2 Codexが上位に入り、実務タスクベースのベンチマークが重視される流れが明確になっています。 **参考リンク** - Claudeのチャート・ダイアグラム生成対応: <https://www.theverge.com/ai-artificial-intelligence/893625/anthropic-claude-ai-charts-diagrams> - Claude Codeの自動コードレビュー機能: <https://zamin.uz/en/technology/193869-new-auto-code-review-feature-launched-for-claude-code.html> - Claudeのユーザー急増とChatGPTからの乗り換えトレンド: <https://9to5google.com/2026/03/06/claude-daily-signups-pass-one-million/> - Claudeダウンロード増加の背景分析: <https://www.forbes.com/sites/conormurray/2026/03/06/claude-surges-amid-defense-department-drama-downloads-up-55/> - GPT-5.4 Thinkingモデルの実務利用事例: <https://www.forbes.com/sites/rachelwells/2026/03/08/i-wrote-a-resume-for-a-180000-job-using-chatgpt-54-this-happened/> - GoogleによるAndroidアプリ開発向けLLM評価: <https://9to5google.com/2026/03/06/google-says-these-ai-models-are-best-at-coding-android-apps/> ## 3. オープンソースエージェント基盤の充実 オープンソース領域では、ローカル実行型のパーソナルAIアシスタント基盤「OpenClaw」が継続的に活発で、Slack連携やセキュリティ設計に関する実践記事が増えています。Difyは「Production-ready platform for agentic workflow development」を掲げ、UIや監視機能を含むプロダクション運用前提のエージェントワークフロー基盤として注目されています。OpenAIはマルチエージェントワークフロー向けの軽量フレームワーク「openai-agents-python」を公開し、Pythonでのエージェント構築・連携の標準化を狙っています。さらに、Latitude-LLMやMemoriなど、エージェントエンジニアリングや長期記憶インフラに特化した新興プロジェクトも台頭しており、エージェント開発の選択肢が一気に広がっています。 **参考リンク** - OpenClaw(ローカル実行型パーソナルAIアシスタント基盤): <https://github.com/openclaw/openclaw> - OpenClawのSlack連携とセキュリティ設計解説: <https://zenn.dev/t0yohei/articles/cb0670ecf0cad7> - Dify(エージェントワークフロー開発基盤): <https://github.com/langgenius/dify> - openai-agents-python(マルチエージェントワークフロー向けフレームワーク): <https://github.com/openai/openai-agents-python> - Latitude-LLM(オープンソースのエージェントエンジニアリングプラットフォーム): <https://github.com/latitude-dev/latitude-llm> - Memori(LLM・エージェント向けSQLネイティブメモリレイヤー): <https://github.com/MemoriLabs/Memori> ## 4. 業界別エージェント活用の具体事例 医療分野では、EpicがHIMSS 2026で医療向けエージェントプラットフォーム「Agent Factory」と医療特化基盤モデル「Curiosity」を発表し、患者トリアージや請求対応などで大幅な工数削減実績を示しました。ロジスティクスでは、エージェント型AIがTMS/ERPを超えた自律的なオペレーション最適化に活用され始めているとのレポートが出ており、サプライチェーン全体を跨いだ意思決定支援がテーマになっています。マーケティングや広告領域では、マルチエージェントでリサーチ・コピー生成・配信最適化を分担する構成や、Luma Agentsによる広告クリエイティブ制作の自動化など、ブリーフからキャンペーンまでをエージェントでつなぐ事例が登場しています。さらに、PerplexityがMac miniを24/7エージェントにする「Personal Computer」を発表し、クラウド版Computerと合わせて、個人・企業向けの常駐型エージェントユースケースが広がっています。 **参考リンク** - 医療向けエージェントプラットフォーム「Agent Factory」: <https://hitconsultant.net/2026/03/10/epic-ai-himss-2026-agent-factory-curiosity-foundation-models/> - ロジスティクスにおけるエージェント型AI活用: <https://www.logisticsmgmt.com/article/usps_says_it_could_run_out_of-capital_without_major_changes> - マルチエージェントによるマーケティング自動化事例: <https://zenn.dev/deflag_nakamae/articles/2026-03-12-multi-agent-marketing-automation> - Luma Agentsによる広告クリエイティブ自動化: <https://www.mediapost.com/publications/article/413264/research-lab-agents-automate-brief-to-campaign-for.html> - Perplexityの「Personal Computer」(常駐型エージェント): <https://thenextweb.com/news/perplexity-personal-computer-enterprise> ## 5. セキュリティ・ガバナンスと社会的リスクへの注目 OpenAIによるPromptfoo買収に象徴されるように、エージェントの自動レッドチーミングやリスク監視は、今後のエージェント活用の前提条件として重要性が増しています。NewsGuardはPangramと連携し、ChatGPTやClaude、GeminiなどLLM生成コンテンツによるニュースサイトのAIコンテンツファームを検出するツールを公開し、AI生成ニュースの透明性・信頼性確保に向けた取り組みが進んでいます。また、AIと暗号資産の収束も議論されており、AlibabaのROMEエージェントが無断でマイニングを行うなど、自律エージェントが暗号資産を利用する事例が報告されています。さらに、AI顔認証の誤認による冤罪逮捕事例がHacker Newsで注目を集め、AIの誤認識と司法・行政での利用リスクが改めて問題提起されています。これらは、技術導入と同時にガバナンス・ポリシー設計を進める必要性を示しています。 **参考リンク** - エージェントの自動レッドチーミング・リスク監視の重要性: <https://techcrunch.com/2026/03/09/openai-acquires-promptfoo-to-secure-its-ai-agents/> - AI生成ニュースサイトの検出ツール(NewsGuard × Pangram): <https://www.adweek.com/media/newsguard-tracking-ai-slop-content-farms/> - AIと暗号資産の収束と自律エージェントのリスク: <https://www.forbes.com/sites/digital-assets/2026/03/12/ai-seeking-out-crypto-illustrates-a-coming-convergence/> - AI誤認識による冤罪逮捕事例: <https://www.grandforksherald.com/news/north-dakota/ai-error-jails-innocent-grandmother-for-months-in-north-dakota-fraud-case> ## 6. 日本語圏コミュニティと研究・メタトレンド Zennでは、Claude Codeプラグイン59個の詳細解説や、/simplifyコマンドによるコード整理など、Claude Codeの実践活用記事が増加しています。また、マルチエージェントのマーケティング自動化、エージェントのcronジョブ無限ループ対策、エージェントの記憶喪失を防ぐ文脈インフラ設計など、エージェント運用の実務ノウハウが共有されています。ローカルLLM環境構築や自然言語での開発、LLM翻訳の本番運用など、LLM前提の開発プロセスへの移行も進んでいます。研究・メタトレンドとしては、LLMエージェント関連の必読論文リスト(LLMAgentPapers)が継続更新され、推論・計画・メモリ・マルチエージェント協調などの研究が整理されています。また、Awesome LLMアプリ・エージェント/RAG事例のキュレーションも更新が続いており、実務ユースケースのカタログとして機能しています。 **参考リンク** - Claude Codeプラグイン・/simplify活用記事: <https://zenn.dev/chmod644/articles/claude-code-plugins-all-59> - Claude Codeのコード整理活用例: <https://zenn.dev/takibilab/articles/claude-code-simplify> - マルチエージェントのマーケティング自動化: <https://zenn.dev/deflag_nakamae/articles/2026-03-12-multi-agent-marketing-automation> - エージェントのcronジョブ無限ループ対策: <https://zenn.dev/anicca/articles/2026-03-05-agent-cron-loop> - エージェントの記憶喪失を防ぐ文脈インフラ設計: <https://zenn.dev/dragon1208/articles/62a496e75ab568> - ローカルLLM環境構築・LLM前提開発プロセス: <https://zenn.dev/andyyyy64/articles/1c6d9bc87a7ad1> - LLM翻訳の本番運用事例: <https://zenn.dev/hirayuki/articles/783a518c63afd2> - LLMを前提とした開発プロセスの変化: <https://zenn.dev/lova_man/articles/a80256aa9370e3> - LLMエージェント関連必読論文リスト(LLMAgentPapers): <https://github.com/zjunlp/LLMAgentPapers> - Awesome LLMアプリ・エージェント/RAG事例集: <https://github.com/Shubhamsaboo/awesome-llm-apps>   ハマりポイントと Tips 実装中に実際にハマった点を共有します。同じ轍を踏まないよう、参考にしてください。 1. MCP サーバーの async context 管理 langchain-mcp-adapters の v0.1.0 以降、 MultiServerMCPClient は context manager として使えなくなりました。以前のコード例を参考にすると動かないので注意が必要です。現在は await client.get_tools() で直接ツールを取得します。 2. トークン消費が大きい リサーチ系エージェントは検索結果を大量に扱うため、トークン消費が激しくなりがちです。DeepAgents の SummarizationMiddleware がデフォルトで効きますが、それに加えて internet_search 側で include_raw_content=False を指定しておくと、さらにトークンを節約できます。 3. サブエージェントへの全ツール伝播 デフォルトでは親エージェントのツールがすべてサブエージェントに渡されます。レポート執筆用のサブエージェントに Slack ツールは不要なので、 subagents 設定で渡すツールを絞るのがベターです。 4. RSS の文字化け feedparser で取得したデータを JSON にする際、 json.dumps(ensure_ascii=False) を忘れると日本語が \\\\uXXXX 形式でエスケープされてしまいます。Zenn の記事タイトルが化けて読めなくなるので、必ず指定しましょう。 まとめと今後の展望 今回は DeepAgents と MCP を組み合わせて、技術トレンドを自動調査して Slack に投稿するエージェントを作りました。 DeepAgents は 「エージェント開発の Create React App」 的な存在だと感じています。複雑な Middleware を自分で組む必要がなく、 create_deep_agent() 一発で実用的なエージェントが手に入る。プロトタイピングの速度が劇的に上がります。 MCP のエコシステムも急速に拡大しており、Notion、Figma、Linear など様々なサービスの MCP サーバーが公開されています。今回の仕組みは、以下のような拡張が考えられます: 長期記憶(Memory Store) で過去のレポートを参照し、週次トレンドの精度を上げる Notion MCP を追加して、社内ドキュメントも情報源に加える A2A プロトコル で他チームのエージェントと連携する コードは GitHub で公開しています。同じ仕組みで社内ナレッジ検索や競合分析にも応用できるので、ぜひ試してみてください。 GitHub リポジトリ : https://github.com/atomic-kanta-sasaki/deepagents-trend-slack コントリビューション募集中 このプロジェクトはオープンソースで公開しています。以下のような貢献を歓迎します: Fork して自分用にカスタマイズ — 情報源や通知先を変えて、自分だけのトレンドレポーターを作ってみてください バグ報告・機能リクエスト — Issue でお知らせください Pull Request — 新機能の追加やドキュメントの改善など、どんな PR も歓迎です スター — 気に入ったらスターをいただけると励みになります 「こんな情報源も追加したい」「Discord にも対応してほしい」など、アイデアがあればぜひ Issue や PR でお寄せください。一緒にこのエージェントを育てていきましょう! https://github.com/atomic-kanta-sasaki/deepagents-trend-slack   ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post DeepAgents × MCP で技術トレンドを”勝手に”調査してSlackに流すエージェントを作りました first appeared on SIOS Tech Lab .
ども!龍ちゃんです。 「Streamlit で Google 認証がめっちゃ簡単にできるようになったらしい」って聞いて、実際にやってみました。 結論から言うと、本当にシンプルでした。 secrets.toml に接続情報を書いて、あとは数行コードを書くだけで動きます。ただ、ハマりポイントもいくつかあったので、この記事では「ローカル環境で動くところまで」を体験ベースでお伝えしていきますね。 今回の内容です。 st.login() とは何か 認証フローの全体像 Google Cloud Console の設定手順 Streamlit 側の実装コード ハマりポイントと動作確認 それぞれ順番に解説していきます。 前提条件:動作確認環境 今回の検証環境です。 [project] name = "streamlit-oidc" version = "0.1.0" requires-python = ">=3.12" dependencies = [ "streamlit>=1.42.0", "authlib>=1.3.2", ] 項目 バージョン Python 3.12 Streamlit 1.55.0 Authlib 1.3.2+ OS DevContainer (Linux) st.login() は Streamlit 1.42.0 で追加された機能で、今回は 1.55.0 で検証しています。パッケージ管理は uv を使っています。 環境構築まわりの関連記事もあわせてどうぞ。 DevContainerでStreamlit開発を始める方法:Docker+VSCode uv + Ruff + mypyで構築する超軽量Python開発環境 st.login() とは:Streamlit標準の認証API st.login() は Streamlit 1.42.0(2025年2月)で追加されたネイティブの認証 API です。 これまで Streamlit で認証を実装しようとすると、 streamlit-authenticator を入れたり、OAuth2 Proxy を別途立てたり、自前で認証ロジックを書いたりと、それなりに手間がかかっていたんですよね。それが secrets.toml に設定を書いて数行コードを追加するだけで済むようになりました。サードパーティへの依存がかなり減るのがうれしいところです。 対応プロバイダーは Google、Microsoft、Okta など OIDC に準拠しているプロバイダー であれば使えます。 全体像:st.login()のGoogle OIDC認証フロー まずフローを整理しておきます。 フローとしては「ブラウザ → st.login() → Google OAuth → /oauth2callback → st.user 」という流れになります。 登場人物の役割はこんな感じです。 登場人物 役割 .streamlit/secrets.toml Google との接続情報(クライアント ID、シークレット等)を保持 /oauth2callback Streamlit が自動でハンドリング。実装は不要 st.user 認証後のユーザー情報(名前、メール等)を格納 /oauth2callback は自分で実装しなくていいのがポイントで、Streamlit が勝手にやってくれます。 昔実装したときは、ここのリダイレクトの挙動がややこしかったんです! 準備:Google Cloud ConsoleでOAuthクライアントIDを作成 Google 側の設定手順です。コンパクトにまとめると以下の流れになります。公式チュートリアルは Use the Google Auth Platform to authenticate users にあります。 OAuth 同意画面 を作成 テストユーザーを追加(Testing ステータス中は登録済みユーザーしかログインできません) OAuth クライアント ID を作成 リダイレクト URI の設定(ハマりポイント 1) クライアント ID を作成する際、リダイレクト URI に以下を設定します。 http://localhost:8501/oauth2callback ここだけ注意してください。末尾にスラッシュを付けると動きません。 パスの完全一致が必要なので、 http://localhost:8501/oauth2callback/ のように末尾スラッシュを付けると認証コールバックが失敗します。 ここは見落とすと大変なのでコピペで行きましょう。 設定画面はこんな感じです。 実装:secrets.tomlの設定とst.login()のコード secrets.toml の設定 .streamlit/secrets.toml に以下を書きます。設定項目の詳細は 公式ドキュメント を参照してください。 [auth] client_id = "xxxxxxxxxxxx.apps.googleusercontent.com" client_secret = "GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxxxxxx" server_metadata_url = "https://accounts.google.com/.well-known/openid-configuration" redirect_uri = "http://localhost:8501/oauth2callback" cookie_secret = "ここにランダムな文字列を入れる" 各項目の意味はこんな感じです。 キー 説明 client_id Google Cloud Console で発行したクライアント ID client_secret 同じくクライアントシークレット server_metadata_url OIDC の設定情報が入った URL。Google はこの固定値で OK redirect_uri コールバック先の URL。Cloud Console の設定と完全一致させる cookie_secret セッション管理用のランダム文字列 cookie_secret はランダムな文字列であれば何でもよくて、以下のコマンドで生成できます。 python -c "import secrets; print(secrets.token_urlsafe(32))" # uv の場合 uv run python -c "import secrets; print(secrets.token_urlsafe(32))" .streamlit/secrets.toml は .gitignore に追加するのを忘れないでください。 クライアントシークレットが含まれるので、リポジトリにコミットしてはいけません。 # .gitignore .streamlit/secrets.toml Authlib のインストール(ハマりポイント 2) ここ要注意です。 st.login() を使うには authlib が別途必要で、Streamlit 本体には含まれていません。 uv を使っている場合は uv add で追加できます。 uv add "authlib>=1.3.2" pip の場合はこちら。 pip install "authlib>=1.3.2" インストールせずに実行すると、以下のエラーが出ます。 StreamlitAuthError: To use authentication features, you need to install Authlib>=1.3.2 見ればわかるエラーなんですが、うっかりすると「あれ、なんで動かないんだろう」ってなりますね(なりました)。 最小実装コード 動作確認した実装コードです。 import streamlit as st st.set_page_config(page_title="Streamlit OIDC Auth Demo", page_icon="🔐", layout="centered") if not st.user.is_logged_in: st.title("🔐 Streamlit OIDC 認証デモ") st.write("Google アカウントでログインしてください。") st.button("Google でログイン", on_click=st.login, type="primary") st.stop() st.title(f"ようこそ、{st.user.name} さん!") st.subheader("ユーザー情報") col1, col2 = st.columns([1, 3]) with col1: picture = str(st.user.get("picture", "")) if picture: st.image(picture, width=100) with col2: st.write(f"**名前**: {st.user.name}") st.write(f"**メール**: {st.user.email}") st.write(f"**メール認証済み**: {st.user.email_verified}") with st.expander("st.user の全フィールド(デバッグ用)"): fields = ["name", "email", "email_verified", "given_name", "family_name", "picture"] for key in fields: value = st.user.get(key, "N/A") st.code(f"st.user.{key} = {value!r}") st.divider() st.button("ログアウト", on_click=st.logout) 構造はシンプルで、 st.user.is_logged_in で分岐して、未ログインならボタンを出して st.stop() 、ログイン済みならユーザー情報を表示するだけです。ログアウトは st.logout を渡せば完了なのでラクですね。 動作確認とエラー対処 st.user has no attribute "is_logged_in" が出たら(ハマりポイント 3) このエラーが出る場合は、 secrets.toml が読み込まれていない可能性が高いです。 Streamlit は streamlit run を実行したディレクトリにある .streamlit/secrets.toml を参照します( Secrets management )。なので .streamlit/ フォルダがあるディレクトリから起動しているか確認してみてください。 # NG: secrets.toml がないディレクトリから起動 cd /somewhere/else streamlit run /path/to/app.py # OK: .streamlit/ があるディレクトリから起動 cd /path/to/project # ここに .streamlit/secrets.toml がある streamlit run app.py uv で起動する場合も同様です。 cd /path/to/project uv run streamlit run app.py 実際の画面 起動してログインボタンをクリックすると、Google の認証画面に飛んで、戻ってきたらユーザー情報が表示されます。 st.user で取得できるフィールド一覧 実際に取得できたフィールドをまとめておきます。取得できるフィールドはプロバイダーによって異なるので、詳細は st.user の公式ドキュメント を確認してください。 フィールド 値の例 説明 is_logged_in True ログイン状態 name "田中龍之介" 表示名 email "ry-tanaka@sios.com" メールアドレス email_verified True メール認証済みか given_name "龍之介" 名 family_name "田中" 姓 picture "https://..." プロフィール画像 URL ログアウトボタンを押すと st.user.is_logged_in が False に戻って、ログイン画面に切り替わります。動きとしては素直ですね。 まとめ st.login() を使えば、 secrets.toml の設定と数行のコードで Google 認証が実装できました。体感的にも圧倒的にシンプルに書けるようになったと思います。 ハマりポイントのおさらいです。 Authlib のインストール忘れ : pyproject.toml に authlib>=1.3.2 を追加(pip なら pip install "authlib>=1.3.2" ) secrets.toml の読み込み失敗 : streamlit run を実行するディレクトリを確認 リダイレクト URI の末尾スラッシュ : Cloud Console と secrets.toml で完全一致させる 今回はローカル環境での動作確認までですが、本番環境では redirect_uri を https:// に変更する必要があります(OAuth 2.0 の仕様上、本番では HTTPS が必須です)。次のステップとしては複数プロバイダーへの対応や、本番デプロイ時の redirect_uri 管理あたりが気になるところです。そのあたりも試したら記事にしますね。 いや~こんだけ簡単に認証ができちゃうと、認可まで踏み込んでGoogleのサービスを使い倒すみたいな可能性が広がっていいですね。 参考リンク st.login() API リファレンス st.user API リファレンス User authentication and information Google Auth チュートリアル Secrets management ではまた! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Streamlit st.login()でGoogle認証を実装する実践ガイド first appeared on SIOS Tech Lab .
エピソード紹介 Ep.1 – クリーンアーキテクチャとは Ep.2 – 認証方式の実践的な紹介 ← 今回はこちら Ep.3 – ER設計と監査ログ Ep.4 – RepoScanner の実装とテスト Ep.5 – Copilot プロンプトを効率化 こんな方へ特におすすめ 外部 API と連携するアプリケーションで安全な認証・権限設計を行いたい方 GitHubを使っているが、「トークン」や「認証」の仕組みがよく分からない方 PAT (Personal Access Token)を使っているが、セキュリティに不安がある方 概要 こんにちは。サイオステクノロジーのはらちゃんです! 「GitHubを使った開発をよくするけど、認証方式って結局どうなっているの…」 と分からなくなったことありませんか? — 本シリーズでは、Copilotを活用しつつ、クリーンアーキテクチャに沿って小規模なプロダクト「RepoScanner」を設計・実装した経緯をまとめます。 このエピソードでは、GitHub など外部 API と連携する際に実務でよく使われる認証方式(PAT、OAuth2、JWT)の比較と、GitHub APIとGitHub Actionsで使われる認証の違いを考えていきます。 外部と連携する認証方式 方式の比較   認証の主体 有効期限 管理の単位 PAT (Personal Access Token) ユーザー個人 任意 (設定可能) アカウントごと OAuth2 (OAuth App) ユーザーの代理 基本的に無期限 アプリ全体 JWT + Installation Token (GitHub App) アプリケーション自体 (Bot) 1時間以内 リポジトリ単位 それぞれの仕組み PAT 一言で言うと自分の分身として、他のツールやサービスにあなたの代わりに操作を許可する仕組みです。 イメージ 自分の「分身」 メリット 簡易で導入が早い。 デメリット 「個人のアカウント」に紐づくため、本人が退職すると動かなくなる。 権限が広くなりがちなため、長期的な運用・委譲には向かない。 OAuth2 ユーザーが外部サービスへのログインや連携を許可すると、OAuth2 はアクセストークンを生成します。このアクセストークンは、ユーザーの代わりに外部サービスで操作を実行するための許可証のようなものです。 イメージ 標準プロトコル メリット ユーザー代理での操作やサービス間連携に適する。 デメリット 「リポジトリ全部の読み取り」など、付与できる権限のスコープが大雑把。 最小権限の原則を適用しにくいため、セキュリティ要件が厳しい組織では敬遠される。 JWT + Installation Token (GitHub App) JWT の署名と検証、そして短期トークンの発行という 2 段階の認証フローが、PAT や OAuth2 のシンプルな鍵と比較して、より強固なセキュリティであることを示しています。 イメージ 最もセキュア メリット リポジトリ単位で細かく権限(Issueの読み取りだけ、など)を絞れる。 トークンの期限が1時間以内と短く、万が一漏洩しても被害が最小限。 デメリット JWTの生成や署名検証、短期トークンの都度発行など、実装と運用設計の手間が大きい。 共通の注意点: シークレット管理の鉄則 個人開発の場合は問題ない場合もありますが、常に意識しておくと習慣になるためおすすめです。 ソースコードに直書きしない 必要な権限だけを持たせたトークンを発行する 長期トークンはローテーション(再発行)を設計し、失効を検知できる仕組みを入れる GitHub APIとGitHub Actions どちらも最終的には「トークン」を使いますが、「誰として動作するか」「どこで発行されるか」に大きな違いがあります。 GitHub API スクリプトが個人のアカウント権限を模した合鍵(トークン)を使って、GitHubからデータを自動的に集約します。 イメージ 外から自由に指示を出してデータを収集 メリット 簡易で導入が早い。 デメリット 大量のリポジトリを扱う場合、APIのレート制限に引っかかりやすい。 漏洩時のリスクが集中する。 GitHub Actions PATのようにずっと使える「マスターキー」を持たせるのではなく、「その時、そのジョブだけで使える使い捨ての鍵」にすることで、万が一の漏洩リスクを最小限に抑えています。 イメージ リポジトリ内に住み込み、出来事に反応して自動で労働 メリット トークンを管理する手間がない。 有効期限が最大24時間と短く、自動消滅する。 イベントに合わせてリアルタイム処理。 デメリット スコープがワークフローが動いているリポジトリの中のみ。 行った操作をトリガーにして、別のワークフローを起動できない。 各リポジトリで独立して動くため、集める仕組みが必要。 本シリーズの認証方針 今回「RepoScanner」では、最終的にGitHub Actions による認証と実行をメインに据える決断をしました。 その理由は、運用の手軽さとセキュリティのバランスにあります。 データ取得自動化の恩恵 私がこの構成で最も大きなメリットだと感じたのは、 「 Actionsが勝手にデータを運んできてくれる 」 という点です。 プッシュや定時実行(Cron)をトリガーに実行 コードが更新された瞬間や毎日決まった時間に、ActionsがリポジトリのスナップショットやPR詳細をスキャンし、DBへ自動保存する。 ユーザーの手間が軽減 アプリ実行時にはデータが揃っているので、取得を走らせて待つ時間がない。 実行環境の分離 クリーンアーキテクチャの観点で見ると、GitHub Actions は非常に扱いやすいです。 実行基盤の最適化 GitHub Actionsが提供する一時的な環境変数 GITHUB_TOKEN を利用することで、アプリ本体は「合鍵(トークン)を使ってAPIを叩く」というシンプルな責務になる。 環境を移し替えても最小限の変更で再利用 「誰が実行するか」をActionsに委ねることで、ビジネスロジックはアクセストークンに依存しない。 セキュリティと運用コスト 前述の比較表の通り、PATは管理が漏洩リスクを伴い、JWTは実装コストが高いです。 GitHub Actions を使えば、ワークフロー実行中だけ有効なトークンが自動発行されます。 管理不要 自分でトークンの有効期限を気にしなくてよい。 安全 万が一ログから漏洩しても、数時間後にはその鍵は無効化している。 結論 「RepoScanner」が目指す形として、リポジトリのデータを自動で集約し、DBに溜め、それをアプリで閲覧することです。 このサイクルを最も低コストかつセキュアに実現できるのが、GitHub Actions という方法でした。 — リポジトリごとの権限管理の手間は「方式そのもの」だけで決まるのではなく、どの手法を採り、どのレベルで一括管理するかに依存します。 ローカルや外部サーバーから叩く場合  個人でのちょっとした検証・ツール → PAT(Personal Access Token)  チームでの本格的な運用・自動化 → GitHub App(JWTベース) GitHub Actions 内で完結する場合  GITHUB_TOKEN を使用 Actionsの中で動かしたいけど、他のリポジトリも触りたい場合  組織用のGitHub Appを作成 まとめ 今回は、GitHubを使った開発での認証方式についてご紹介しました。 認証方式の選択は、システムのセキュリティと運用コストに直結します。要件に合わせた適切な認証・実行基盤を選択していきましょう。 PATは、個人の分身である OAuth2は、ユーザー代理での操作やサービス間連携に適する JWTは、最小権限の原則に即している これらの認証方式は、クリーンアーキテクチャにおいては「Frameworks & Drivers層」の関心事です。 エピソード4で、Domain層やUse Case層が特定の認証方式に依存しないよう抽象化して実装を行うのでお楽しみに! 参考 きめ細かな個人アクセストークンに必要な権限 GitHub アプリと OAuth アプリの違い GitHub Actionsを理解する ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Copilot × Clean Architecture | 認証方式の実践的な紹介 first appeared on SIOS Tech Lab .
こんにちは、 サイオステクノロジーの 米谷俊輔です。 本日は多要素認証 ( MFA ) ツールであるMicrosoft Authenticatorの設定をバックアップ機能を使わずに移行する方法を紹介します。先に説明しておくとAppが推奨しているバックアップ機能を使わずに設定を復元することはできません。そのためこれから解説する方法はバックアップ機能が使えない状況 (デバイスの紛失、社内セキュリティ規約の制限等) においてMicrosoft Authenticator を再設定する方法を段階的に紹介します。 Microsoft Authenticatorとは Microsoft Authenticator は、パスワードを使用せずにすべてのアカウントにサインインするのに役立つ無料アプリです。アプリの認証には指紋、顔認識、または PIN のみを使用します。 Authenticator を使用して、Microsoft の個人、職場、学校、またはその他のアカウントにサインインできます。 (出典元: Microsoft Authenticator について – Microsoftサポート ) Microsoft Authenticatorを使用することで個人または組織で 強固なセキュリティ認証を導入することができる一方、 バックアップ機能を使った移行 まずは他の手順との違いを明確にするため、 Microsoftが提供する正規の移行方法を紹介します。 以下のリンクよりMicrosoftサポートの手順をご確認ください。 Microsoft Authenticator でアカウントをバックアップする – Microsoftサポート  Microsoft Authenticator以外の認証を利用 次は事前にMicrosoft Authenticator以外の認証方法を用意する方法です。 複数の認証手段を用意しておくことでMicrosoft Authenticatorに依存せず、再設定が可能になります。   Microsoft Azureを見本に多要素認証を追加する方法を紹介します。 Microsoft AzureにMicrosoft Authenticator以外の認証方法を追加 Microsoft Authenticatorを使って Microsoft Azure にサインインする。 右上のアカウント情報から「アカウントを表示」を押してマイアカウントを表示する。 左サイドメニューから「セキュリティ情報」を押す。 セキュリティ情報の「サイン方法の追加」を押す。 Microsoft Authenticator以外の認証方法を追加する。 スマホの機種変更後にMicrosoft Authenticatorを再設定 Microsoft Authenticator以外の認証方法 でMicrosoft Azure にサインインする。 右上のアカウント情報から「アカウントを表示」を押してマイアカウントを表示する。 左サイドメニューから「セキュリティ情報」を押す。 既存のMicrosoft Authenticatorを削除する。 「サイン方法の追加」から新規にMicrosoft Authenticatorを追加する。 これでMicrosoft Authenticatorを再設定が完了します。念のため、Microsoft Authenticatorを使って正常にサインインできることを確認してください。 Microsoft Authenticator以外の認証方法が設定されていない場合 最後はMicrosoft Authenticator以外の認証方法を事前に用意しておらず、Microsoft Authenticator以外では認証ができない場合です。この場合、旧機種のMicrosoft Authenticatorに設定が残ってしまっているため、新機種側のMicrosoft Authenticatorで再登録しようとしても成功しません。 サービスのアカウント管理者 (個人の場合はカスタマーサポート) に連絡して、 多要素認証のリセットまたは一時的な除外処置を実施してもらってください。   その後、サービスにログインし、Microsoft Authenticatorによる認証を再設定します。 まとめ 今回のブログの内容いかがだったでしょうか。 Microsoft Authenticator Appは 強固なセキュリティを提供してくれる反面、認証用のデバイスが手元にないと再設定自体が難しくなってしまいます。前以って別の認証方法を用意しておくことをお勧めします。また別の認証方法を用意し忘れてしまった場合には、迅速にアカウント管理者へ連絡し認証方法の回復を図ることが大切だと考えます。 参考URL [記事] Microsoft Authenticator が利用できず組織アカウントにサインインできない場合の対処方法 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post バックアップを使わずにMicrosoft Authenticatorの設定を移行する方法 first appeared on SIOS Tech Lab .
ども!GitHub Copilot のニュースレターを眺めていた龍ちゃんです。 3月11日に届いたニュースレターで、 GitHub Copilot CLI が2月に GA(一般提供)になったことを知りました。ターミナルで動くエージェント型の AI アシスタントらしいんですよね。気になったのでとりあえずセットアップして触ってみました。 今回はセットアップだけまとめておきます。基本的には公式ドキュメントの補完なので、詳しくはそちらを参照してください。 About GitHub Copilot CLI – GitHub Docs Installing GitHub Copilot CLI – GitHub Docs GitHub Copilot CLI とは — ターミナルで動くエージェント型AI 一言でいうと、 ターミナルで動くエージェント型 AI アシスタント です。VSCode の Copilot Chat がエディタ内で動くのに対し、こちらは copilot コマンドを叩くだけで起動します。IDE も拡張機能も不要です。 イメージはClaude CodeのGitHub Copilot版を想像してもらえるとわかりやすいです。 2026年2月25日に GA(一般提供)になりました。約5ヶ月のプレビュー期間を経てのリリースで、GA バージョンは v1.0.2 です。既存の Copilot サブスクリプション(Free / Pro / Pro+ / Business / Enterprise)に含まれているので、追加費用はかかりません。 ただ!大きめの注意が必要です。デフォルトでプレミアムリクエストが消費されるので!気づいたら大量消費している可能性があるので気を付けてください。 旧 gh copilot(廃止済み)との違い 検索すると古い情報がよく出てくるので注意が必要です。「GitHub Copilot CLI」と検索すると、旧来の gh copilot の情報が混ざってくることがあるんですよね。まったくの別物なので整理しておきます。 旧: gh copilot 新: copilot コマンド 正式名称 GitHub Copilot in the CLI GitHub Copilot CLI 状態 2025年10月に廃止・アーカイブ 現行・GA インストール gh extension install github/gh-copilot npm install -g @github/copilot 等 できること コマンド提案・説明のみ エージェント型(コード生成・ファイル編集・コマンド実行) 旧 gh copilot はすでに廃止済みなので、今からインストールしようとしている方は新しい copilot コマンドを使ってください。 Copilot CLI のインストール方法(npm / Homebrew / WinGet 他5種類) 方法が5種類あります。使っている環境に合わせて選んでください。公式の手順は Installing GitHub Copilot CLI を参照。 方法 コマンド npm(全プラットフォーム) npm install -g @github/copilot Homebrew(macOS / Linux) brew install copilot-cli WinGet(Windows) winget install GitHub.Copilot シェルスクリプト(macOS / Linux) curl -fsSL https://gh.io/copilot-install | bash スタンドアロン実行ファイル GitHub Releases からダウンロード Homebrew / WinGet / シェルスクリプト経由は自動更新に対応しているので、個人的には Homebrew かシェルスクリプトがおすすめです。 npm を使う場合は Node.js 22 以上 が必要です。今回の検証はnpm経由で行いました。 npm だと npm update -g @github/copilot を手動で叩かないといけないので、そこだけ注意が必要です。 Codespaces を使っている方はプリインストール済みなので何もしなくて大丈夫です。 認証と初回起動の手順 インストールが終わったら、リポジトリのディレクトリに移動して copilot を叩くだけです。 cd /path/to/your-repo copilot 初回起動時は /login で認証を済ませてください。ブラウザが開いて GitHub の認証画面に飛びます。 > /login 起動すると「Environment loaded: 2 custom instructions, 24 skills, 11 agents, Visual Studio Code connected」のような表示が出ます。VSCode が開いていれば連携も自動で認識してくれます。 Business / Enterprise プランを使っている方は、組織の管理者が Copilot CLI のポリシーを有効化している必要があります。組織の Settings → Code, planning, and automation → Copilot → Policies から有効化できるので、使えない場合は管理者に確認してみてください。 基本操作と便利なスラッシュコマンド 起動したら、あとは日本語で話しかけるだけです。 # リポジトリの構成を聞く > このリポジトリの構成を説明して # テストの失敗原因を調べて修正してもらう > テストが失敗している原因を調べて修正して # ドキュメントを日本語に翻訳する > READMEを日本語に翻訳して ファイルの中身を読んで説明してくれたり、実際にコードを修正してくれたりします。VSCode の Chat と違うのは、ターミナルで完結することとエージェントとして動いてくれる点です。 この辺の使用感は Claude Code とほぼ同じで、「次のファイルを開いて」「このテストを直して」という自然な指示で動きます。 セッション中に使えるコマンド一覧 コマンド 説明 /plan 実行計画を確認してから作業を進める(いきなり変更させたくないときに) /model 使用するモデルを切り替える /usage プレミアムリクエストの消費量を確認する /context トークン使用量の詳細内訳を表示する /exit セッション終了(Ctrl+C でも可) /plan はplanエージェントが発火する形ですね。 /model でモデルを切り替えるで、大量のモデルが出てくるのもGitHub Copilotの特徴です。 Claude Sonnet 4.6 がデフォルトで、Claude Opus 4.6 や GPT-4.1 なども選べます。用途によって使い分けられます。 プレミアムリクエストの消費に注意 — Copilot Chat と共有枠 Copilot CLI に送る1プロンプトは、 1プレミアムリクエスト を消費します。そして、このプレミアムリクエストは Copilot Chat と同じ枠を共有しています 。 /usage で使用料を確認できるので、使いすぎが心配なときはこまめにチェックしておくといいです。ガッツリ使う作業の前に確認しておくのが無難です。 モデルを GPT-4o mini のような軽量モデルに切り替えると消費量を抑えられる場合もあるので、長丁場の作業には /model と合わせて使うといいと思います。 まとめ ニュースレターで知って、とりあえずセットアップして触ってみたという記事でした。ポイントだけまとめておきます。 ターミナルで動くエージェント型 AI — IDE 不要、Copilot サブスクで追加費用なし 旧 gh copilot とは別物 — 検索で古い情報が出てくるので注意 インストールは npm / Homebrew / WinGet — 自動更新が欲しいなら Homebrew かシェルスクリプト プレミアムリクエストは Chat と共有 — /usage でこまめに確認 セットアップ自体は数分で終わるので、Copilot のサブスクを持っている方はとりあえず入れてみるといいんじゃないかなと思います。 次回は Fleet モード について書く予定です。エージェントを複数同時に走らせて並列で作業させる機能で、これが CLI ならではの目玉っぽいんですよね。使ってみたらまたまとめます。 ではまた! 関連記事 GitHub Copilot設定5種を網羅!生産性を最大化する使い分け術 — copilot-instructions.md やカスタムエージェントなど、Copilot の設定ファイル5種類を目的別フローチャートで整理した記事です。CLI を入れた後に設定まわりを整えたい方はこちらをどうぞ。 Claude Code→GitHub Copilot移行で使える設定ファイル6つの対応表 — 今回の記事で Claude Code に触れましたが、両方使っている方向けに CLAUDE.md と copilot-instructions.md など6種類の設定ファイルの対応関係を表でまとめています。 参考リンク GitHub Copilot CLI is now generally available – GitHub Changelog (2026-02-25) About GitHub Copilot CLI – GitHub Docs Installing GitHub Copilot CLI – GitHub Docs CLI Command Reference – GitHub Docs Requests in GitHub Copilot – GitHub Docs ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 2026年2月 GA:新 GitHub Copilot CLI を入れてみた — セットアップ手順まとめ first appeared on SIOS Tech Lab .
ども!最近バグ調査のたびに GitHub の Issue を行ったり来たりしている龍ちゃんです。 今回紹介するスキルは GitHub Copilot でも Claude Code でも動きます。どちらを使っていても同じ方法で導入できるので、自分の環境に合わせて読んでください。 対象読者 VS Code + GitHub Copilot(Free 以外のプラン)または Claude Code を使っている方 gh CLI の認証が済んでいる方( gh auth status で確認) Agent Skills の基本は知っている方(知らない方は「 【2026年版】Agent Skills 入門 」を先にどうぞ) 併せて読みたい GitHub Copilot デバッグの精度を上げるにはドキュメント1枚を先に書く — 今回の記事の前提になるデバッグワークフローの話 GitHub Issue の議論を AI に渡せない問題 チーム開発で Issue を使っている方は多いと思います。バグ報告、機能要望、設計の相談——Issue のコメントスレッドには、チームメンバーが議論した記録がそのまま残っているんですよね。 「ここが怪しいんじゃないか」「いや、こっちを先に調べたほうがいい」「試したけどこれは関係なかった」——こういうやりとりって、バグ調査の文脈では情報の宝庫なんです。 じゃあこの情報を AI に渡して調査させたいとなったとき、どうするか。 バグ調査のワークフローって大体こうなりますよね。 Issue で問題が報告される コメントで方針をすり合わせる (人間同士のディスカッション) 調査・改修する 結果を Issue に書き戻す ステップ3で AI(Copilot や Claude Code)を使いたいんですが、ステップ2までのディスカッション内容は GitHub 上にしかないんですよね。AI にとってはコンテキストがゼロの状態で調査を始めることになる。 以前の記事「 デバッグの精度を上げるにはドキュメント1枚を先に書く 」で、Issue.md を書いてから AI に渡すと初手の精度が上がるという話をしました。問題・仮説・関連パスを整理したファイルをローカルに置いて、AI のコンテキストに載せることで効果が絶大になります。 でも、その Issue.md を毎回手で書くのは面倒なんですよね。しかも Issue のコメントスレッドにはすでにチームの議論が蓄積されている。 これをそのままローカルに落とせれば、手書きの手間もなくなるし、チームの知見も一緒に AI に渡すことができます。 問題は、VS Code の拡張機能( GitHub Pull Requests and Issues )は参照・操作には優秀なんですが、 エクスポート機能がない こと。Issue の内容をファイルに落とし込む手段がないんです。 今回は、 gh CLI を使って Issue の検索からローカルへの書き出しまでをワンセットでスキル化 して、この穴を埋めます。 今回の内容です。 GitHub Issue の情報を AI が使える形でローカルに落とすデモ SKILL.md の設計と実装 拡張機能との使い分け デモ:gh CLI で Issue をローカルに自動出力する 作り方の前に、まず完成形を見てください。 バグ Issue → Issue.md を自動生成する 「Issue #3 の内容を確認して Issue.md を作成して」と入力すると、Agent が以下を実行します。 gh issue view 3 --json title,body,state,labels,assignees,author,createdAt,url そして、Issue.md テンプレートに変換して出力してくれます。 # Issue #3: blog_scraper が robots.txt を無視して403エラーになる - URL: https://github.com/.../issues/3 - 起票者: @Ryunosuke-Tanaka-sti - 担当: @Ryunosuke-Tanaka-sti - 状態: OPEN - ラベル: bug ## 問題 blog_scraper でブログ記事をスクレイピングする際、robots.txt の Crawl-delay を 無視してリクエストを送信し、403 Forbidden エラーが発生する。 ## 仮説 1. robots.txt のパース処理が未実装、またはパーサーが Crawl-delay ディレクティブを無視している 2. リクエスト間隔の制御(rate limiting)が実装されていない、または設定値が短すぎる 3. User-Agent ヘッダーがサーバー側でブロック対象になっている ## 関連ファイル - application/tools/src/blog_scraper/ ## 再現手順 1. `uv run python -m blog_scraper https://example.com/archives/12345` を実行 2. 403 Forbidden エラーが発生 以前の記事「 デバッグの精度を上げるにはドキュメント1枚を先に書く 」では、この Issue.md を手で書く前提で解説していました。今回はそれが GitHub Issue から1プロンプトで自動生成できるようになります。 コメント付き Issue → 進捗まとめ 「Issue #6 の本文とコメントをすべて確認して進捗をまとめて」と入力すると: gh issue view 6 --json title,body,state,labels,assignees,author,comments,createdAt,updatedAt,url 投稿者付きの進捗まとめを出してくれます。 # Issue #6: README.md にツールの使い方セクションを追加する - 起票者: @Ryunosuke-Tanaka-sti - 状態: OPEN - ラベル: documentation ## タスク概要 README.md に各ツールの使い方セクションを追加する ## 進捗状況 - **@Ryunosuke-Tanaka-sti** (2026-03-10): blog_scraper セクションから着手 - **@Ryunosuke-Tanaka-sti** (2026-03-10): blog_scraper のドラフト完成、レビュー依頼 - **@Ryunosuke-Tanaka-sti** (2026-03-10): svg_to_png, html_to_png 追記完了 - **@Ryunosuke-Tanaka-sti** (2026-03-10): cleanup_articles, thumbnail_generator の方針メモ - **@Ryunosuke-Tanaka-sti** (2026-03-10): 全ツール分ドラフト完了 ## 残タスク - 最終レビュー待ち ここがミソなんですけど、投稿者情報( @author + 日付)が入っているので、誰の発言かが一目でわかるんですよね。これを AI に渡せば、経緯を踏まえた回答が返ってきます。Issue のコメントスレッドを自分で全部読まなくていい。 検索も自然言語でできる エクスポートの前段として、Issue の検索自体も自然言語で指示できます。 プロンプト 変換されるコマンド 「Issue 一覧を見せて」 gh issue list 「bugラベルのIssueを検索して」 gh issue list --label bug --state open 「自分にアサインされたIssueは?」 gh issue list --assignee @me 「robots.txt に関連するIssue」 gh issue list --search "robots.txt" 「クローズ済みのIssue」 gh issue list --state closed 「自分にアサインされた bug ラベルのオープン Issue」みたいな複合条件も一言でいけます。 gh issue list --assignee @me --label bug --state open に変換してくれますね。 SKILL.md の導入方法(GitHub Copilot・Claude Code 両対応) SKILL.md は GitHub Copilot 版と Claude Code 版の両方を用意しました。Gist から取得して、所定のディレクトリに配置するだけで動きます。 Copilot 版 : .github/skills/gh-issue-search/ に配置 → Gist Claude Code 版 : .claude/skills/gh-issue-search/ に配置 → Gist 中身をざっくり見せます。Copilot 版と Claude Code 版で frontmatter の書き方が違うので、両方載せておきます。 Copilot 版 ( .github/skills/gh-issue-search/SKILL.md ) --- name: gh-issue-search description: GitHub IssueをghCLIで検索・取得するスキル。 Issue一覧のフィルタリング、個別Issueの詳細・コメント取得、 検索結果のIssue.md変換に対応。 Use when: Issueを検索したい、Issue一覧を見たい、 Issueの詳細を確認したい、Issue.mdを作りたい。 allowed-tools: Bash(gh:*) Bash(git:*) Bash(jq:*) Read --- Claude Code 版 ( .claude/skills/gh-issue-search/SKILL.md ) --- name: gh-issue-search description: | This skill should be used when the user asks to "Issue一覧を見せて", "Issueを検索して", "Issue確認して", "Issueの詳細を見せて", "バグ一覧", "アサインされたIssue", "Issue.mdを作って", "Issueをまとめて", "/gh-issue-search", or needs to search, view, or summarize GitHub Issues via gh CLI. allowed-tools: Bash, Read --- Copilot 版は allowed-tools で Bash(gh:*) のようにコマンド単位で制限できます。Claude Code 版はツール名だけの指定です。description の書き方もそれぞれのルーティングエンジンに合わせています。 ポイントは3つです。 description にトリガーワードを列挙する — 「Issue一覧」「Issue検索」「Issue.md」など、ユーザーが入力しそうな言い回しを書いておく。Agent がスキルを自動選択する精度に直結します allowed-tools で実行可能なコマンドを絞る — Copilot 版では Bash(gh:*) のように前方一致で制限でき、スキルが意図しないコマンドを実行するリスクを減らせます モード自動判定で1つのスキルに集約する — 検索・詳細取得・Issue.md 変換の3モードをユーザーの指示から自動判定する設計にしています。スキルを3つに分けるより、1つにまとめたほうがルーティングが安定しました 本文の後半では、モード判定テーブルとテンプレート定義が続きます。全文は Gist を参照してください。 SKILL.md の設計パターン(description の書き方、references 分離、ルーティング設定など)の詳細は CI 編 で解説しているので、カスタマイズしたい方はそちらを参照してください。 MCP でもできる ちなみに、GitHub Issue の操作は GitHub MCP Server を使う方法もあります。MCP Server は Docker コンテナで動作し、gh CLI とは独立しています。GitHub API トークン(Personal Access Token)を設定すれば Issue の取得や検索ができるので、gh CLI をインストールしたくない環境ではこちらが選択肢になります。 個人的には gh CLI 構成のほうが好みですね。理由はシンプルで、MCP サーバーの起動管理が要らないのと、ターミナルでそのまま動作確認できるから。スキルが期待通りに動かないとき、 gh issue list --label bug をターミナルで叩けばコマンド自体の問題かスキルの問題かすぐ切り分けられるんですよね。 前提条件 gh CLI の認証が必要です。 このスキルは内部で gh issue list / gh issue view を実行するので、gh CLI がインストールされていて、対象リポジトリに認証済みである必要があります。本記事の動作確認環境は gh v2.87.3 です。 # 認証状態の確認 gh auth status # 未認証の場合 gh auth login プライベートリポジトリの Issue を取得する場合は、そのリポジトリへのアクセス権限も必要です。GitHub API のトークンスコープに repo が含まれていることを確認してください。 VS Code 拡張機能と Agent Skills の使い分け 「拡張機能でよくない?」と思う方もいると思うので、整理しておきます。 やりたいこと 拡張機能 Agent Skill Issue をさっと一覧確認 サイドバーで十分 どちらでも OK 複合条件で絞り込み UI 操作が手間 自然言語で一発 Issue の内容をファイルに落とす エクスポート機能なし 1プロンプトで自動生成 コメントスレッドの要約 自分で読む AI が投稿者付きで要約 「見るだけ」なら拡張機能で十分なんですよね。でも「ローカルに落としたい」「コメントの経緯を AI に渡したい」という場面では Agent Skill の出番です。 まとめ 今回の内容を振り返ります。 GitHub Issue のディスカッション記録を AI が読める形でローカルに落とせるようになった Issue.md の自動変換・コメント付き進捗まとめ・複合条件検索が、拡張機能にはない価値 SKILL.md は Gist で公開済み。gh CLI の認証さえ済んでいればすぐ使える 次回は gh issue create / gh issue comment / gh issue close をスキル化して、Issue の書き込み操作も自然言語でできるようにする予定です。 ではまた! 関連記事 【2026年版】Agent Skills 入門:SKILL.md の基本から実践まで — Agent Skills が初めての方はここから。SKILL.md の構造と書き方を一通り解説しています gh CLI × Claude Code でブラウザなし GitHub 操作を実現する — 今回のスキルの土台になっている gh CLI 連携の基本編 gh CLI × Claude Code で Gist 操作をチャット1行で完結させる — gh CLI スキルの応用例。Gist の作成・取得・更新をチャットで完結させます GitHub Actions失敗ログ、まだ手動で読む?Copilot Agent Skills で CI デバッグを自動化する実装ガイド — SKILL.md の設計パターン(description の書き方、references 分離)を詳しく解説した記事 GitHub Copilot デバッグの精度を上げるにはドキュメント1枚を先に書く — 今回の記事の前提。Issue.md を手書きしていた頃のワークフローを解説しています ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post GitHub Issue をローカルに落として AI に渡す Agent Skills 実装ガイド first appeared on SIOS Tech Lab .
エピソード紹介 Ep.1 – クリーンアーキテクチャとは ← 今回はこちら Ep.2 – 認証方式の実践的な紹介 Ep.3 – ER設計と監査ログ Ep.4 – RepoScanner の実装とテスト Ep.5 – Copilot プロンプトを効率化 こんな方へ特におすすめ クリーンアーキテクチャが何かイメージを掴みたい方 概要 こんにちは。サイオステクノロジーのはらちゃんです! フロントエンドを開発していたとき、アトミックデザインという手法を知りました。ざっくり言うと小さな部品を用意して、使いまわせるようにする設計です。 バックエンドを開発するときに、同じような設計手法があるはずだと気になりました。 そこで知った手法こそ、クリーンアーキテクチャです。 — 本シリーズでは、Copilotを活用しつつ、クリーンアーキテクチャに沿って小規模なプロダクト「RepoScanner」を設計・実装した経緯をまとめます。 シリーズの前提知識として、このエピソードでは、クリーンアーキテクチャの基本を簡潔に説明します。 定義 はじめに、どのような方針であるのかイメージを掴みましょう。 クリーンアーキテクチャは、ソフトウェアを複数の関心ごとに分離し、変更に強くテストしやすい構造を保つ設計原則です。中心にビジネスルールとなるドメインを置き、外側にインフラやUIを置くことで、依存の向きを守ります。 クリーンアーキテクチャの解説でよくみられるイラストです。 主なレイヤー 全体は以下のような層に分かれています。 src/ ├── domain/ # 【最内部】ビジネスルール │ ├── entities/ # ここにエンティティを置く │ └── repositories/ # リポジトリインターフェースを定義する ├── use-cases/ # 【内側】アプリケーション固有の手順 │ └── createUser.ts # ユースケースの実装をする ├── infrastructure/ # 【外側】技術固有の実装 │ └── persistence/ # 永続化の実装をする └── functions/ # 【最外部】エントリポイント └── http/ # ここで依存性注入を行う 具体的にどのような役割なのか見ていきましょう。 Domain Domain層は、「コードでビジネスを記述する」場所であり、技術的な詳細(データベースやWebフレームワーク)とは無関係に、ビジネスの正解を定義する最も重要な層です。 エンティティ(概念) 「何ができるか」の定義。型やNullを許容するかなどのビジネスルールを持つ。 具体的には、以下のような定義をします。ここではユーザー情報を用意しています。 Python @dataclass(slots=True) class User: user_id: UUID display_name: str | None created_at: datetime last_login_at: datetime | None リポジトリ(ふるまい) 「どのように取得 / 保存するか」の定義。エンティティの操作を抽象化する。 具体的には、以下のような定義をします。 定義したエンティティを用いて、「ログイン時に得られた情報を元に、ユーザーを作成 / 更新し、結果として User エンティティを返す」という処理をしています。 リポジトリのメソッド引数で再び型を明記することで、Pythonの型ヒントを機能させる意味もあります。 Python class UserRepository(Protocol): def upsert_login_user(self, display_name: str | None) -> User: ... Use Cases Use Cases層は、どのようにエンティティやリポジトリを組み合わせて業務フローにするかを表現する層です。 司令塔としての役割があり、受け取った入力に対しリポジトリを使ってデータを取得 / 更新します。 例えば、以下のような具体的な機能を実現しています。 トークンでユーザー情報を取得して情報を更新する DBから項目を受け取ってデータを返す Infrastructure Infrastructure層は、Use Cases層とさらに外側の層を繋ぐための層です。 データの変換と橋渡しを行う、アダプターという役割を担って外部ライブラリやDB を利用します。 例えば、以下のようにデータの相互変換や外側への具体的なアクセス実装をしています。 db.py ファイルでSQL に接続する SQL文で操作を指定する functions Functions層はビジネスルールを組み合わせ、外側から受け取ったリクエストを処理する層です。 これにより、UIの変更(HTML→JSON)や、データベースの変更(MySQL→PostgreSQL)が、Domain層に一切影響を与えないように保護しつつ、具体的な技術要素との連携を実現しています。 例えば、以下のような具体的な機能を実現をしています。 HTTP API のスキーマを定義する main.py ファイルでエントリポイントの指定をする Pydantic を利用してバリテーションをする 依存性ルール(外側→内側) コードの依存は常に外側から内側へ向かいます。内側の層が外側の具体実装を import してはいけません。 依存性逆転の原則に従い、ドメインやユースケースがインターフェースを定義し、インフラがそれを実装します。 functions(最外部) -> infrastructure(外側実装) -> use-cases(アプリ手順) -> domain(最内部ルール) 依存を分けるメリット 大きく2つ考えられます。 影響範囲が狭い ドメイン / ユースケースを外部変更から隔離でき、要件変更など対応しやすい。 依存性注入(DI)とテスト ユースケースはリポジトリなど依存をコンストラクタやファクトリで注入して受け取る。 テストではその注入ポイントにモックやインメモリ実装を差し替えることで、ユースケース単体で即テストできる。 依存を分けるデメリット こちらも、大きく2つ考えられます。 初期コスト インターフェース定義やDIの仕組み、アダプター実装が増える。 実装の一貫性維持 境界のインターフェース設計を誤ると、後続実装で整合性を保つのが難しい。 層をまたぐ設計と抽象が増え、設計理解の学習コストが上がる。 三層構造との違い ここまで学んで、依存を分けるなら三層構造でもやっていなかったか?と疑問に思いました。 クリーンアーキテクチャと三層構造は、どちらもソフトウェアの「関心事を分離する」ための設計手法ですが、最大の違いは「依存関係の方向」と「システムにおいて何を中心と捉えるか」にあります。 三層構造は、システムを役割ごとに「上から下へ」3つの層に分けるアーキテクチャです。 プレゼンテーション層 ビジネスロジック層 データアクセス層 ここでは、プレゼンテーション層はビジネスロジック層に依存し、ビジネスロジック層はデータアクセス層に依存します。つまり、システムの中心がデータベースになることが多いです。 システムの中心がビジネスロジックであるクリーンアーキテクチャとは依存関係の方向が違うことがわかりました。 まとめ 今回は、クリーンアーキテクチャとは何かをご紹介しました。 依存は外側→内側。インフラ実装を内側に漏らさない。 エンティティは何ができるか、リポジトリはどのようにするか。 DI を使えばユースケースはテストしやすくなる。 ユースケース単体と境界・統合を優先して整備する。 これからクリーンアーキテクチャの思想に則った設計を行うのでお楽しみに! 参考 The Clean Architecture ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Copilot × Clean Architecture | クリーンアーキテクチャとは first appeared on SIOS Tech Lab .
PSSLの佐々木です。 E2Eテストは重要だとわかっていても、Playwrightのコードを書くのが面倒で後回しにしていませんか? 本記事では、 Markdownファイルに日本語で操作手順を書くだけで、Playwrightが自動実行してくれるE2Eテストフレームワーク の作り方を、実際のプロダクション事例をもとに解説します。 この記事でわかること Markdownシナリオ駆動のE2Eテストの全体アーキテクチャ 自然言語ステップをPlaywrightアクションに変換する仕組み フォーム入力の多段フォールバック戦略 動画記録・スクリーンショットによるデバッグ支援 pre-commitフックとの連携による開発フロー統合 なぜMarkdownでE2Eテストを書くのか 従来のE2Eテストには3つの問題がありました。 テストコードが仕様と乖離する — Playwrightのコードを読んでも、何のシナリオをテストしているのかひと目でわからない 非エンジニアがレビューできない — PMやデザイナーがテストケースを確認・追加できない メンテナンスコストが高い — セレクタの変更ひとつで大量のテストが壊れる Markdownシナリオ駆動テストなら、こう書けます: ## シナリオ: 管理者ログイン成功 1. <http://localhost:8055/login/> にアクセスする 2. メールアドレスに「admin@example.com」を入力する 3. パスワードに「admin123」を入力する 4. 「ログイン」ボタンをクリックする 5. 「ダッシュボード」というテキストが画面に表示されていることを確認する 6. URLに「/admin/dashboard」が含まれることを確認する 誰が読んでも何をテストしているかわかります。 全体アーキテクチャ フレームワークは4つのコンポーネントで構成されます。 Markdownシナリオファイル (.md) | [Parser] Markdownを構造化データに変換 | [Step Mapper] 自然言語 → Playwrightアクション | [Runner] ブラウザ操作の実行・動画記録・レポート 各コンポーネントのコード量は驚くほど小さく、Parser約90行、Step Mapper約280行、Runner約280行で実現できます。 それぞれ解説していきます。 Step 1: Markdownシナリオのフォーマットを定義する まず、テストシナリオを記述するMarkdownのフォーマットを決めます。 # 認証機能テスト ## 前提条件 - テスト用管理者が存在する(email: admin@example.com, password: admin123) - テスト用代理店ユーザーが存在する(email: user@example.com, password: user123) ## シナリオ: 管理者ログイン成功 → ログアウト 1. <http://localhost:8055/login/> にアクセスする 2. メールアドレスに「admin@example.com」を入力する 3. パスワードに「admin123」を入力する 4. 「ログイン」ボタンをクリックする 5. 「ダッシュボード」というテキストが画面に表示されていることを確認する 6. URLに「/admin/dashboard」が含まれることを確認する 7. ログアウトする ## シナリオ: パスワード間違い 1. <http://localhost:8055/login/> にアクセスする 2. メールアドレスに「admin@example.com」を入力する 3. パスワードに「wrongpassword」を入力する 4. 「ログイン」ボタンをクリックする 5. 「メールアドレスまたはパスワードが正しくありません」というテキストが表示されることを確認する ルールはシンプルです: 要素 記法 例 テストファイルのタイトル # タイトル # 認証機能テスト 前提条件 ## 前提条件 + 箇条書き - テスト用ユーザーが存在する シナリオ ## シナリオ: 名前 + 番号リスト ## シナリオ: ログイン成功 ステップ 1. 操作内容 1. 「ログイン」ボタンをクリックする 1ファイルに複数シナリオを書けます。前提条件セクションはドキュメントとして機能し、シードデータの仕様を明示する役割を果たします。 Step 2: Markdownパーサーを実装する Markdownファイルを解析して構造化データに変換するパーサーを作ります。 # parser.py import re from dataclasses import dataclass, field from pathlib import Path @dataclass class Scenario: name: str steps: list[str] = field(default_factory=list) @dataclass class ScenarioFile: path: Path title: str preconditions: list[str] = field(default_factory=list) scenarios: list[Scenario] = field(default_factory=list) def parse_scenario_file(filepath: Path) -> ScenarioFile: """Markdownシナリオファイルを解析する""" text = filepath.read_text(encoding="utf-8") lines = text.splitlines() title = "" preconditions: list[str] = [] scenarios: list[Scenario] = [] current_section = None current_scenario: Scenario | None = None for raw_line in lines: line = raw_line.strip() # トップレベルタイトル if line.startswith("# ") and not line.startswith("## "): title = line[2:].strip() continue # セクションヘッダー if line.startswith("## "): header = line[3:].strip() if "前提条件" in header: current_section = "preconditions" current_scenario = None continue # シナリオ検出 m = re.match(r"^シナリオ[::]\\s*(.+)", header) if m: current_scenario = Scenario(name=m.group(1).strip()) scenarios.append(current_scenario) current_section = "scenario" continue # 箇条書き(前提条件) m_bullet = re.match(r"^[-*]\\s+(.+)", line) if m_bullet: content = m_bullet.group(1).strip() if current_section == "preconditions": preconditions.append(content) elif current_section == "scenario" and current_scenario: current_scenario.steps.append(content) continue # 番号付きリスト(シナリオステップ) m_num = re.match(r"^\\d+\\.\\s+(.+)", line) if m_num and current_section == "scenario" and current_scenario: current_scenario.steps.append(m_num.group(1).strip()) return ScenarioFile( path=filepath, title=title or filepath.stem, preconditions=preconditions, scenarios=scenarios, ) ポイントは番号付きリストから番号プレフィックスを除去してステップ文字列だけを抽出していることです。 1. 「ログイン」ボタンをクリックする → 「ログイン」ボタンをクリックする Step 3: ステップマッパーを実装する(コア部分) ここがこのフレームワークの心臓部です。自然言語のステップを正規表現でパターンマッチし、対応するPlaywrightアクションを実行します。 基本構造 # step_mapper.py import re from playwright.sync_api import Page, expect def execute_step(page: Page, step: str, base_url: str, timeout: int = 30000): """自然言語ステップを解釈してPlaywrightアクションを実行する""" # --- ナビゲーション --- m = re.search(r"(https?://\\S+)\\s*(?:に|へ)アクセスする", step) if m: page.goto(m.group(1), timeout=timeout) return m = re.search(r"(/.+?)\\s*(?:に|へ)(?:アクセス|遷移|移動)する", step) if m: page.goto(base_url + m.group(1), timeout=timeout) return # ... 他のパターンが続く 対応するステップパターン一覧 フレームワークが認識するステップパターンを紹介します。 ナビゲーション # 絶対URL # 例: "<http://localhost:8055/login/> にアクセスする" m = re.search(r"(https?://\\S+)\\s*(?:に|へ)アクセスする", step) if m: page.goto(m.group(1), timeout=timeout) return # 相対パス # 例: "/agency/dashboard にアクセスする" m = re.search(r"(/.+?)\\s*(?:に|へ)(?:アクセス|遷移|移動)する", step) if m: page.goto(base_url + m.group(1), timeout=timeout) return リンク・ボタンのクリック # リンクをクリック # 例: 「物件管理」リンクをクリックする m = re.search(r"[「「](.+?)[」」](?:リンク|メニュー)をクリックする", step) if m: text = m.group(1) page.get_by_role("link", name=text).first.click(timeout=timeout) page.wait_for_load_state("networkidle") return # ボタンをクリック # 例: 「ログイン」ボタンをクリックする m = re.search(r"[「「](.+?)[」」]ボタンを(?:クリック|押)する", step) if m: text = m.group(1) # submit ボタンを優先的に探す submit_buttons = page.locator("button[type='submit']:visible") if submit_buttons.count() > 0: for i in range(submit_buttons.count()): btn = submit_buttons.nth(i) if text in (btn.inner_text() or ""): btn.click(timeout=timeout) page.wait_for_load_state("networkidle") return # テキスト完全一致がなければ最初のsubmitボタン submit_buttons.first.click(timeout=timeout) else: page.get_by_role("button", name=text).first.click(timeout=timeout) page.wait_for_load_state("networkidle") return # テキスト要素をクリック(テーブル行など) # 例: 「SKR-001」テキストをクリックする m = re.search(r"[「「](.+?)[」」](?:テキスト|文字|項目|行)をクリックする", step) if m: page.get_by_text(m.group(1), exact=False).first.click(timeout=timeout) return フォーム入力 # テキスト入力 # 例: メールアドレスに「admin@example.com」を入力する m = re.search( r"[「「]?(.+?)[」」]?(?:欄|フィールド)?に\\s*[「「](.+?)[」」]\\s*(?:を入力|と入力)する", step ) if m: label, value = m.group(1), m.group(2) _fill_by_label(page, label, value, timeout) return # セレクトボックス # 例: 「ステータス」で「通電中」を選択する m = re.search(r"[「「](.+?)[」」](?:で|から)\\s*[「「](.+?)[」」]\\s*を選択する", step) if m: label, value = m.group(1), m.group(2) page.get_by_label(label).select_option(label=value, timeout=timeout) return # 日付入力 # 例: 希望日に「2026-12-01」を入力する m = re.search( r"[「「](.+?)[」」](?:欄|フィールド)?に\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2})\\s*を(?:入力|設定)する", step ) if m: label = m.group(1) date_str = m.group(2).replace("/", "-") page.get_by_label(label).fill(date_str, timeout=timeout) return アサーション(検証) # テキストが表示されていることを確認 # 例: 「ダッシュボード」というテキストが画面に表示されていることを確認する m = re.search( r"[「「](.+?)[」」].*(?:表示されている|表示される|見える|確認する|含まれる|ある)", step ) if m: text = m.group(1) locator = page.locator( f":not(option):not(select):visible:has-text('{text}')" ).first expect(locator).to_be_visible(timeout=timeout) return # URLの確認 # 例: URLに「/admin/dashboard」が含まれることを確認する m = re.search(r"URLに\\s*[「「](.+?)[」」]\\s*が含まれる", step) if m: expect(page).to_have_url(re.compile(re.escape(m.group(1))), timeout=timeout) return # テキストが表示されていないことを確認 # 例: 「エラー」というテキストが表示されていないことを確認する m = re.search(r"[「「](.+?)[」」].*(?:表示されていない|表示されない|見えない)", step) if m: expect( page.get_by_text(m.group(1), exact=False).first ).not_to_be_visible(timeout=timeout) return その他 # ログアウト if re.search(r"ログアウトする", step): page.context.clear_cookies() page.goto(base_url + "/login/") page.wait_for_load_state("networkidle") return # 待機 # 例: 3秒待つ m = re.search(r"(\\d+)秒(?:待つ|待機する)", step) if m: page.wait_for_timeout(int(m.group(1)) * 1000) return # マッチしなかった場合 raise ValueError(f"未対応のステップ:{step}") フォーム入力の多段フォールバック戦略 フォーム入力は最もハマりやすいポイントです。実際のHTMLは <label> がない場合、 placeholder で代用している場合、CSSフレームワーク特有のマークアップなど、多様です。 そこで、 5段階のフォールバック戦略 を実装します。 def _fill_by_label(page: Page, label: str, value: str, timeout: int): """多段フォールバックでフォームフィールドを特定して入力する""" # Level 1: aria-label / <label> による特定 try: loc = page.get_by_label(label, exact=False).locator("visible=true") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 2: placeholder による特定 try: loc = page.get_by_placeholder(label, exact=False).locator("visible=true") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 3: label要素のDOM構造から辿る try: labels = page.locator(f"label:visible:has-text('{label}')") for i in range(labels.count()): label_elem = labels.nth(i) parent = label_elem.locator("..") inp = parent.locator("input:visible, textarea:visible, select:visible") if inp.count() > 0: inp.first.fill(value, timeout=timeout) return except Exception: pass # Level 4: name属性による特定(日本語ラベル → HTMLのname属性マッピング) field_map = { "メールアドレス": "email", "パスワード": "password", "物件名": "name", "郵便番号": "postal_code", "都道府県": "prefecture", "市区町村": "city", "町名番地": "address", "建物名": "building_name", "部屋番号": "room_number", "管理コード": "external_key", "メモ": "memo", # 必要に応じて追加 } name_attr = field_map.get(label) if name_attr: try: loc = page.locator(f"input[name='{name_attr}']:visible, textarea[name='{name_attr}']:visible") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 5: type属性による特定 type_map = {"メールアドレス": "email", "パスワード": "password"} type_attr = type_map.get(label) if type_attr: try: loc = page.locator(f"input[type='{type_attr}']:visible") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass raise ValueError(f"入力フィールドが見つかりません:{label}") この多段フォールバックにより、ほとんどのHTMLフォームに対応できます。 レベル 方法 対応するケース 1 get_by_label 正しく <label> がマークアップされたフォーム 2 get_by_placeholder placeholder 属性で入力ヒントを持つフォーム 3 DOM構造を辿る <label> がinputと同じ親要素内にあるフォーム 4 name 属性マッピング <label> がないがname属性は一貫しているフォーム 5 type 属性 email/passwordなど型で一意に特定できるフィールド Step 4: テストランナーを実装する シナリオファイルの解析とステップ実行を統合するランナーを作ります。 # runner.py import signal import socket import subprocess import sys import time from dataclasses import dataclass from pathlib import Path from playwright.sync_api import sync_playwright from config import APP_DIR, BASE_URL, BROWSER, HEADLESS, SCENARIOS_DIR, TIMEOUT, VIDEO_DIR from parser import parse_scenario_file from step_mapper import execute_step @dataclass class TestResult: scenario_file: str scenario_name: str passed: bool error: str = "" video_path: str = "" screenshot_path: str = "" def start_django_server(port: int = 8055): """Djangoサーバーを起動する(既に起動中ならスキップ)""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(("localhost", port)) sock.close() print(f"ポート{port} は既に使用中です。既存のサーバーを使用します。") return None except ConnectionRefusedError: sock.close() proc = subprocess.Popen( [sys.executable, str(APP_DIR / "manage.py"), "runserver", str(port), "--noreload"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # サーバーの起動を待機 for _ in range(30): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("localhost", port)) s.close() print(f"Django開発サーバーがポート{port} で起動しました") return proc except ConnectionRefusedError: time.sleep(1) raise RuntimeError("Djangoサーバーの起動がタイムアウトしました") def stop_django_server(proc): """Djangoサーバーを停止する""" if proc: proc.send_signal(signal.SIGTERM) try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() def ensure_seed_data(): """テスト用シードデータを投入する""" subprocess.run( [sys.executable, str(APP_DIR / "manage.py"), "seed", "--reset"], check=True, capture_output=True, ) print("シードデータを投入しました") def run_scenarios(scenario_files: list[Path], base_url: str, timeout: int) -> list[TestResult]: """シナリオファイルを実行して結果を返す""" results = [] with sync_playwright() as p: browser = getattr(p, BROWSER).launch(headless=HEADLESS) for scenario_path in scenario_files: sf = parse_scenario_file(scenario_path) print(f"\\n{'='*60}") print(f"実行:{sf.title} ({scenario_path.name})") print(f"{'='*60}") # ファイル単位でブラウザコンテキスト(動画記録)を作成 video_dir = VIDEO_DIR / scenario_path.stem video_dir.mkdir(parents=True, exist_ok=True) context = browser.new_context( record_video_dir=str(video_dir), record_video_size={"width": 1280, "height": 720}, viewport={"width": 1280, "height": 720}, ) page = context.new_page() for scenario in sf.scenarios: print(f"\\n シナリオ:{scenario.name}") passed = True error_msg = "" screenshot_path = "" for i, step_text in enumerate(scenario.steps, 1): try: print(f" ステップ{i}:{step_text} ... ", end="", flush=True) execute_step(page, step_text, base_url, timeout) print("OK") except Exception as e: print(f"FAILED:{e}") passed = False error_msg = f"ステップ{i}:{step_text} ->{e}" # 失敗時のスクリーンショット ss_path = video_dir / f"{scenario.name}_fail.png" try: page.screenshot(path=str(ss_path)) screenshot_path = str(ss_path) except Exception: pass break results.append(TestResult( scenario_file=scenario_path.name, scenario_name=scenario.name, passed=passed, error=error_msg, screenshot_path=screenshot_path, )) # コンテキストを閉じて動画を確定 video_path_raw = page.video.path if page.video else None page.close() context.close() # 動画をリネーム if video_path_raw: final_video = video_dir / f"{scenario_path.stem}.webm" try: Path(video_path_raw).rename(final_video) for r in results: if r.scenario_file == scenario_path.name: r.video_path = str(final_video) except Exception: pass browser.close() return results def print_summary(results: list[TestResult]): """テスト結果のサマリーを表示する""" print(f"\\n{'='*60}") print("テスト結果サマリー") print(f"{'='*60}") for r in results: icon = "PASS" if r.passed else "FAIL" print(f" [{icon}]{r.scenario_file} >{r.scenario_name}") if r.video_path: print(f" 動画:{r.video_path}") if r.error: print(f" エラー:{r.error}") if r.screenshot_path: print(f" スクリーンショット:{r.screenshot_path}") total = len(results) passed = sum(1 for r in results if r.passed) failed = total - passed print(f"\\n合計:{total} 成功:{passed} 失敗:{failed}") def main(): import argparse parser = argparse.ArgumentParser(description="Markdown E2Eテストランナー") parser.add_argument("scenarios", nargs="*", help="実行するシナリオファイル") parser.add_argument("--base-url", default=BASE_URL) parser.add_argument("--no-headless", action="store_true") parser.add_argument("--no-server", action="store_true") parser.add_argument("--no-seed", action="store_true") parser.add_argument("--timeout", type=int, default=30) args = parser.parse_args() global HEADLESS if args.no_headless: HEADLESS = False # シナリオファイルを取得 if args.scenarios: files = [Path(s) for s in args.scenarios] else: files = sorted(SCENARIOS_DIR.glob("*.md")) if not files: print("シナリオファイルが見つかりません") sys.exit(1) # テスト実行 if not args.no_seed: ensure_seed_data() server_proc = None if not args.no_server: server_proc = start_django_server() try: results = run_scenarios(files, args.base_url, args.timeout * 1000) print_summary(results) sys.exit(0 if all(r.passed for r in results) else 1) finally: stop_django_server(server_proc) if __name__ == "__main__": main() 動画記録のポイント Playwrightの動画記録は ブラウザコンテキスト単位 で行われます。1つのシナリオファイル内の全シナリオが1本の動画にまとまるため、テスト失敗時のデバッグが容易です。 # ファイルごとにコンテキストを作成 → 1ファイル = 1動画 context = browser.new_context( record_video_dir=str(video_dir), record_video_size={"width": 1280, "height": 720}, ) Step 5: シナリオファイルを書く ここまでのフレームワークを使って、実際のシナリオを書いてみましょう。 ファイル命名規則 e2e/scenarios/ ├── 01_authentication.md # 認証 ├── 02_agency_property.md # 代理店 物件管理 ├── 03_agency_request.md # 代理店 申請フロー ├── 04_admin_dashboard.md # 管理者 ダッシュボード ├── 05_admin_request.md # 管理者 申請処理 └── 06_admin_agency.md # 管理者 代理店管理 番号プレフィックスで実行順序を制御します。認証テストを最初に実行し、前提となる機能を先に検証する構成です。 シナリオの書き方のコツ 1. 1シナリオに詰め込みすぎない <!-- BAD: 長すぎるシナリオ --> ## シナリオ: ログイン → 物件登録 → 申請 → 承認 → ログアウト 1. ... (50ステップ) <!-- GOOD: 論理的なまとまりで分割 --> ## シナリオ: 物件登録 1. ... (15ステップ) ## シナリオ: 通電申請 1. ... (12ステップ) ただし、1ファイル内のシナリオは同じ動画に記録されるため、 関連する操作フロー は同じファイルにまとめると良いでしょう。 2. セレクタではなくユーザーが見えるテキストを使う <!-- BAD: 実装依存 --> 1. #login-btn をクリックする <!-- GOOD: ユーザー視点 --> 1. 「ログイン」ボタンをクリックする 3. アサーションは具体的に <!-- BAD: 曖昧 --> 1. ページが表示されることを確認する <!-- GOOD: 具体的 --> 1. 「ダッシュボード」というテキストが画面に表示されていることを確認する 2. URLに「/admin/dashboard」が含まれることを確認する 対応しているステップ表現のリファレンス カテゴリ ステップ例 ナビゲーション http://... にアクセスする 、 /path にアクセスする リンククリック 「メニュー名」リンクをクリックする ボタンクリック 「送信」ボタンをクリックする テキストクリック 「SKR-001」テキストをクリックする タブ切替 「タブ名」タブをクリックする テキスト入力 項目名に「値」を入力する セレクト 「項目名」で「値」を選択する 日付入力 「項目名」に 2026-01-01 を入力する テキスト表示確認 「テキスト」というテキストが表示されていることを確認する テキスト非表示確認 「テキスト」が表示されていないことを確認する URL確認 URLに「/path」が含まれることを確認する ログアウト ログアウトする 待機 3秒待つ Step 6: pre-commitフックで開発に組み込む 変更したファイルに応じて関連するシナリオだけを自動実行するpre-commitフックを設定できます。 #!/bin/bash # e2e/hooks/pre-commit-e2e.sh CHANGED_FILES=$(git diff --cached --name-only) SCENARIOS_TO_RUN="" for file in $CHANGED_FILES; do case "$file" in *auth* | *login* | *middleware*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/01_authentication.md" ;; *property*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/02_agency_property.md" ;; *request*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/03_agency_request.md" SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/05_admin_request.md" ;; *agency*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/06_admin_agency.md" ;; # コアモデル変更時はフルリグレッション *models* | *enums* | *config/*) python e2e/runner.py exit $? ;; esac done if [ -n "$SCENARIOS_TO_RUN" ]; then # 重複除去して実行 UNIQUE=$(echo "$SCENARIOS_TO_RUN" | tr ' ' '\\n' | sort -u | tr '\\n' ' ') python e2e/runner.py $UNIQUE exit $? fi echo "E2Eテスト対象の変更なし、スキップします" exit 0 .git/hooks/pre-commit にシンボリックリンクを貼るか、 pre-commit フレームワークで管理します。 プロジェクト構成まとめ e2e/ ├── config.py # 環境設定(URL、タイムアウト、テストアカウント等) ├── parser.py # Markdownパーサー(~90行) ├── step_mapper.py # ステップマッパー(~280行) ├── runner.py # テストランナー(~280行) ├── requirements.txt # playwright>=1.40.0 ├── hooks/ │ └── pre-commit-e2e.sh ├── scenarios/ │ ├── 01_authentication.md │ ├── 02_agency_property.md │ └── ... └── test-results/ # 動画・スクリーンショット出力先 全体で約650行のPythonコードです。 実行方法 # Playwrightのインストール pip install playwright playwright install chromium # 全シナリオ実行 python e2e/runner.py # 特定シナリオのみ python e2e/runner.py e2e/scenarios/01_authentication.md # ブラウザを表示して実行(デバッグ用) python e2e/runner.py --no-headless # サーバーが既に起動している場合 python e2e/runner.py --no-server --no-seed 実行結果: シードデータを投入しました Django開発サーバーがポート 8055 で起動しました ============================================================ 実行: 認証機能テスト (01_authentication.md) ============================================================ シナリオ: 管理者ログイン成功 → ログアウト → パスワード間違い ステップ 1: <http://localhost:8055/login/> にアクセスする ... OK ステップ 2: メールアドレスに「admin@example.com」を入力する ... OK ステップ 3: パスワードに「admin123」を入力する ... OK ステップ 4: 「ログイン」ボタンをクリックする ... OK ステップ 5: 「ダッシュボード」というテキストが表示されている ... OK ... ============================================================ テスト結果サマリー ============================================================ [PASS] 01_authentication.md > 管理者ログイン成功 動画: test-results/01_authentication/01_authentication.webm 合計: 1 成功: 1 失敗: 0 従来のE2Eテストとの比較 項目 Playwrightコード直書き Markdownシナリオ駆動 可読性 エンジニアのみ 誰でも読める 記述量 多い(セレクタ指定等) 少ない(自然言語) メンテナンス テストごとに修正 Step Mapperの1箇所を修正 柔軟性 無制限 パターン定義内に限定 デバッグ ステップ単位で追跡可能 同左 + 動画記録 学習コスト Playwright APIの理解が必要 日本語テンプレに沿うだけ CI統合 標準的 同左 拡張のアイデア 新しいステップパターンの追加 step_mapper.py に正規表現と実行ロジックを追加するだけです。 # 例: チェックボックスの操作 m = re.search(r"[「「](.+?)[」」]チェックボックスをチェックする", step) if m: page.get_by_label(m.group(1)).check() return # 例: ファイルアップロード m = re.search(r"[「「](.+?)[」」]に\\s*[「「](.+?)[」」]\\s*をアップロードする", step) if m: page.get_by_label(m.group(1)).set_input_files(m.group(2)) return 多言語対応 パターン定義を外部ファイル(YAML等)に切り出せば、英語版も容易に作れます。 # patterns_en.yaml navigation: - pattern: 'navigate to "(.*)"' action: goto link_click: - pattern: 'click "(.*)" link' action: click_link テストデータのパラメータ化 前提条件セクションからテストデータを動的に生成する仕組みを追加することもできます。 まとめ Markdownシナリオ駆動のE2Eテストは、 仕様とテストを一体化 させるアプローチです。 Markdownで書いた操作手順がそのままテストになる 非エンジニアでもテストケースをレビュー・追加できる Step Mapperの修正1箇所で全テストの挙動を変更できる 動画記録により失敗時のデバッグが直感的 全体650行程度のコードで実現可能 Playwrightの柔軟なロケーター戦略( get_by_role , get_by_label , get_by_text )と正規表現ベースのパターンマッチの組み合わせにより、少ないコード量で実用的なフレームワークを構築できます。 テストが仕様書と乖離する問題に悩んでいるなら、ぜひ試してみてください。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Markdownで書くE2Eテスト:自然言語シナリオをPlaywrightで自動実行する方法 first appeared on SIOS Tech Lab .
ども!最近Playwrightの話をいろんな文脈で聞くようになって、ちょっと混乱していた龍ちゃんです。 この記事を読むと: Playwright の3パッケージ(テストランナー / MCP / CLI)の使い分けがわかる コマンド3つで CLI が動くようになる chromium vs chrome のハマりポイントを踏まずに済む 「Playwright MCP」「Playwright CLI」「Playwright テスト」…全部Playwrightなんですけど、 それぞれ別パッケージで用途が違う んですよね。僕自身、チーム内で話していて「あれ、どのPlaywrightの話してる?」ってなったことがあったので、今回はまずそこを整理します。 んで、整理したうえで 一番お手軽な「Playwright CLI」のセットアップ を最小手順で紹介していきます。 Playwright の3つの顔 — まず整理しよう ちょっと内部で話していて混乱したので、ここで整理していきます。 テストランナーとしての Playwright 開発者が一番馴染みのあるやつです。E2Eテストを書いて自動実行する、テストフレームワークとしてのPlaywright。 npx playwright test パッケージは @playwright/test 。「Playwrightって何?」って聞かれたら、多くの人がこれを思い浮かべるんじゃないでしょうか。 MCP としての Playwright AIエージェントにブラウザ操作をさせるためのプロトコルです。JSON-RPCでツール定義をやり取りする仕組みで、ChatGPTやClaude Desktopみたいな チャット型AI 向け。 パッケージは @playwright/mcp 。 以前の記事 でセットアップ方法を紹介しました。便利なんですけど、 トークン消費が重い という課題もあるんですよね。 CLI としての Playwright (今回の主役) シェルコマンドでブラウザを直接操作するやつです。Claude CodeやCursorみたいな AIコーディングエージェント 向け。ファイルシステムにアクセスできる環境が前提になっています。 パッケージは @playwright/cli 。MCPと比べて圧倒的に手軽で、コマンド1発でスクショが撮れます。 整理するとこう 種類 パッケージ 用途 誰向け テストランナー @playwright/test E2Eテストの自動実行 テストエンジニア MCP @playwright/mcp AIにブラウザ操作させる チャット型AI CLI @playwright/cli シェルからブラウザ操作 コーディングエージェント 全部「Playwright」だけど別パッケージ。 テストランナーを入れてもCLIは動きません 。ここ、地味にハマるポイントです。開発者目線でいえば、CLI一択ですね。(MCPとCLI論争がありましたが、再現性とかの観点でも開発系ならCLI一択だと思っています…) 最小セットアップ — これだけで動く セットアップ手順(コマンド3つ) Node.js v18以上が入っている環境なら、以下の3つで動きます。 # 1. ブラウザインストール(※ chromium ではなく chrome) npx playwright install chrome # 2. Skill インストール(Claude Code 用の SKILL.md が配置される) npx @playwright/cli install --skills # 3. 動作確認 npx @playwright/cli open https://example.com 成功すれば、こんな出力が返ってきます。 ### Browser `default` opened with pid XXXXX. - default: - browser-type: chrome - user-data-dir: <in-memory> - headed: false --- ### Page - Page URL: https://example.com/ - Page Title: Example Domain ### Snapshot - [Snapshot](.playwright-cli/page-YYYY-MM-DDTHH-MM-SS.yml) ここまで出ればOK。 もしエラーが出たら、次のセクションを確認してください。 エラーが出たら 「Browser “chrome” is not installed」系のエラー CLIのデフォルトブラウザは chrome (Google Chrome Stable)です。テストランナーでよく使う chromium とは別物で、 npx playwright install chromium で入るブラウザはCLIからは参照されません 。 # NG: テストランナー用のChromiumが入る。CLIでは使えない npx playwright install chromium # OK: Google Chrome Stableがインストールされる npx playwright install chrome テストランナーのPlaywrightに慣れてる人ほど chromium でいけると思ってハマるので、ここだけ注意です。 「Failed to move to new namespace」エラー(DevContainer / Docker 環境) コンテナ環境ではChromeのnamespace作成に権限が足りず、起動に失敗することがあります。プロジェクトルートに .playwright/cli.config.json を作成して --no-sandbox を設定すれば解決します。 { "browser": { "launchOptions": { "args": ["--no-sandbox", "--disable-setuid-sandbox"] } } } これはDevContainerやDocker環境に限った話なので、ローカル環境で動かしてる人は気にしなくて大丈夫です。 基本操作 — snapshot 駆動でページを操る CLI はセッションベースで動きます。 open でブラウザを開いて、操作して、 close で閉じる。この間、ブラウザの状態は保持されます。 npx @playwright/cli open https://example.com # セッション開始 # ... ここで色々操作 ... npx @playwright/cli close # セッション終了 では実際にやってみます。 ページ取得 — snapshot でページ構造を見る open した状態で snapshot を実行すると、ページの構造がYAML形式で返ってきます。 npx @playwright/cli snapshot - generic [ref=e2]: - heading "Example Domain" [level=1] [ref=e3] - paragraph [ref=e4]: This domain is for use in documentation examples... - paragraph [ref=e5]: - link "Learn more" [ref=e6] [cursor=pointer]: - /url: https://iana.org/domains/example 各要素に ref=e2 、 ref=e6 みたいな参照IDが振られていますよね。これがCLIの核心機能です。 ref で操作する snapshotで取得した参照IDを使って、要素を直接操作できます。 # "Learn more" リンク(ref=e6)をクリック npx @playwright/cli click e6 ### Ran Playwright code await page.getByRole('link', { name: 'Learn more' }).click(); ### Page - Page URL: https://www.iana.org/help/example-domains - Page Title: Example Domains コマンド1発でページが遷移しました。 snapshot → ref で操作、この繰り返しが CLI の基本です。 スクリーンショットを撮る スクリーンショットも1コマンドで撮れます。 npx @playwright/cli screenshot --filename=screenshot.png ひと通り操作が終わったら close でセッション終了です。 snapshot で構造を見て、 ref で操作して、 screenshot で結果を確認。 この3ステップが CLI の基本ワークフロー です。 Claude Code の Skill として使う セットアップの手順2で npx @playwright/cli install --skills を実行していれば、もう準備完了です。 .claude/skills/playwright-cli/ にSKILL.mdとreferencesディレクトリ(7ファイル)が自動配置されています。 これで「スクショ撮って」「Webページ開いて」みたいな指示をClaude Codeに出すだけで、CLIを呼んでくれるようになります。 前回の記事 で「CLI + Skills」パターンを提唱しましたが、あのとき自作していたものが 公式サポートになった 形ですね。Microsoft が Skill を用意してくれるので、メンテナンスも楽になりました。 まとめ 今回は、Playwrightの3つの顔を整理したうえで、CLI の最小セットアップ手順を紹介しました。 Playwrightは「テストランナー」「MCP」「CLI」の3つがあって全部別パッケージ。AIコーディングエージェントで使うなら CLI が一番お手軽です。セットアップは playwright install chrome → install --skills → open のコマンド3つだけ。 ハマりポイントは chromium じゃなくて chrome が必要なところ。テストランナーに慣れてる人ほど引っかかるので、ここだけ覚えておけば大丈夫です。あと install --skills で Claude Code 用の SKILL.md が公式から自動配置されるのも地味にありがたい。 MCPとCLIのトークン消費比較や、ユースケース別の使い分けについては別記事で詳しく書く予定です。コメント欄で気軽に話しかけてください。 参考リンク GitHub – microsoft/playwright-cli @playwright/cli – npm Playwright MCP GitHub 前回記事: Playwright MCPで始めるブラウザ自動化|環境別セットアップガイド 前回記事: Claude Code MCP が遅い・重い問題、CLI + Skills で解決 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Playwright MCP より手軽な公式 CLI — コマンド3つでセットアップ first appeared on SIOS Tech Lab .
ども!気になった単語や技術をAIにぶち込み続けている龍ちゃんです。 Xを眺めていると、知らない単語が流れてきますよね。「ふーん、なんか面白そう」で終わって、次の瞬間には忘れている。みんなもそういう経験ないですか? 自分もずっとそうで、Draw.ioのMCPが1ヶ月ほど「気になるリスト」に眠ったままでした。 ある日、Claude Codeで検索のSKILLを作ったんです。そしたら効率が爆上がりです。やったことは「気になった単語をぶち込んだ」だけなのに。 実際にやっている手法の紹介です。 Claude Code版とGemini版、どちらもすぐ試せるようにプロンプト付きで載せておきますね。 最初の一歩は小さくていい 「ちゃんと調べなきゃ」と思うから腰が重くなるんですよね。ブラウザ開いて、検索して、記事をいくつか読んで、要点をまとめて……って考えるだけで面倒ですよね。 でもAIにぶち込んでおけば、あとは出てきた結果を読むだけです。概要が分かれば「もっと知りたい」か「今は要らない」かの判断フェーズに代わります。 完璧な調査じゃなくていい。最初の一歩が踏めれば、あとは手を動かすだけです。 逆に一歩だけ踏み出しておくと、「動けばいい」になるので気になっちゃって試さずにいられないんです! Claude Code版 — 1ファイルで試せる /research Claude Codeを使っている人なら、 .claude/commands/research.md にファイルを1つ置くだけで試せます。 今だと SKILL として設定したほうが良いですね。commandsで紹介しているブログなので、コマンドで紹介しておきます。(今から作るならSKILL一択ですよ。) 以下が簡易版のコマンドファイルです。これを置けば /research [気になったこと] で即実行できます。 # /research コマンド(簡易版) トピック: $ARGUMENTS ## 調査手順 1. 以下の観点でWeb検索を実行してください - 公式ドキュメント・リファレンス - 最新の技術ブログ・記事(6ヶ月以内を優先) - GitHub リポジトリ・Issues 2. 調査結果を以下のフォーマットで `docs/research/` に保存してください ## 出力フォーマット ### 概要 (3〜5行で要点をまとめる) ### 詳細 (調査結果を構造化して記載) ### 情報源 (参照したURLをリストアップ) ### 所感 (調査から得られた気づき・次のアクション候補) とりあえず、出力される成果物を確認すると指定した範囲から検索した何かしらのドキュメントが出力されます。指定すれば、公式ドキュメントも呼んでくれますし、比較表も出力してくれます。 「Playwrightのテスト設計パターン」でも「OpenTelemetryの導入ステップ」でも、とりあえずぶち込めば最初の地図が手に入るんですよね。 もっとガッツリ使いたい人向けに、調査深度の選択やソース評価(CRAAP Test)を組み込んだ版や、バックグラウンドで並列実行する構成も作っています。 2026-01-19 Claude Codeの調査品質がバラバラ?:/researchで解決する方法 2026-02-05 Claude Code /research の待ち時間をゼロに:Skill × サブエージェント構成 Gemini版 — ブラウザで今すぐ試せるリサーチGem Claude Codeを使っていなくても、ブラウザだけで同じことができます。 Google Workspaceを契約されているから、Geminiは使えるけど…使ってないって方いるんじゃないでしょうか? Geminiの リサーチGem は、よく使う調査の指示を保存しておいて、選ぶだけで実行できるんですよね。 Gemの作成画面 で以下のカスタム指示を貼り付ければ、リサーチ専用のGemが完成します。 あなたは最新情報を専門に扱うリサーチアシスタントです。 実行前に情報の深度を確認してください。 - クイック:概要 - ディープ:深掘検索 ディープの場合であれば、一回の調査で終了せずにレポート内で重要な発見・不足している情報などを判断して再帰的に検索をお願いします。 検索に関しては、本日から6カ月以内の情報ソースもしくは、公式リファレンスの情報をGoogle検索によって取得して回答を生成してください。 回答の最後には、参照したソースのリンクをリストアップしてください。 Xで見かけた単語をそのままコピペして投げると、1分くらいで概要・関連する文脈・自分に関係あるかの判断材料が出てきます。「MCPって何?」「DORAメトリクスって聞いたけど」みたいなやつを、ひとまず処理するのにちょうどいいです。僕は出先で気になったことはこっちで調べますね。 あとから調べた資料をClaude Codeで再度精査しています。 Geminiに検索させるコツやGemの作り方はこちらで詳しく書いています。 2026-01-28 Geminiに検索させるプロンプト術|リサーチGemの作り方 実践 — 「ぶち込んだ」先にあったもの 最初の一歩さえ踏めれば、そこから転がっていくんですよね。自分の実例を3つ紹介します。 Draw.ioのMCP → ブログ2本 冒頭でも触れたDraw.ioのMCPの話をもう少し詳しく書きますね。 Xで「Draw.ioが 公式のMCPサーバー を出した」というポストを見かけたんですよね。「へー、面白そう」とは思ったものの、業務に直結しないので「時間できたら触ろう」リストに入れてそのまま1ヶ月。 ある日、別の調査をしているときに思い出して /research Draw.io MCP Server とぶち込んでみました。5分くらいで「Mermaid・XML・CSVの3フォーマットに対応していて、Apache 2.0ライセンスで公開されている」という概要と、公式リポジトリのリンクが返ってきたんですよね。 概要を読んだら「あ、これ仕様書×AI の文脈で使えるかも」となって、実際にセットアップして検証してみました。結果、MCP検証記事とSkill化記事の2本に育ちましたね。放置していた1ヶ月が嘘みたいです。実際に1時間で検証までスタートして、ブログにスムーズに移行しましたね。 Draw.io公式MCPでできること・できないこと Draw.io公式Skillで設計図をGitHub管理 専門外の知識をAIに持たせる → 「一般論」が消えた AIに「レビューして」と投げると、一般論しか返ってこないことってありませんか?あれ、AIに観点がないから起きるんですよね。/research で専門外の知識を調査して、構造化してSkillやAgentに注入してみたら、出力が変わりました。どの分野でも同じパターンが使えます。 調査→構造化→注入の設計パターン 要求分析・プレゼン心理学 → 業務の専門外をカバー 最近は「要求分析の手法」「仕様書作成のベストプラクティス」「プレゼンで使える心理学」あたりも調査してSkill化しています。業務に関連しているけど自分の専門外、というやつですね。一度調べてしまえば、次からAIがその知識を持った状態で動いてくれます。 ここはまとまったらブログにします。 まとめ — 調査の質より「動けること」 AIリサーチの価値って、「調査の質が上がる」ことじゃないと思っていて。 「気になった瞬間に動ける自分になれる」 ことなんですよね。 最初の一歩を踏めれば、深掘りするかやめるかを自分で判断できます。でも放置しているかぎりは判断すらできないので、放置が一番もったいないんですよね。 ぶち込むだけでいい。さぼり癖がある人・ずぼらな人ほど、AIリサーチとの相性がいいです。 ほなまた〜 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 「あとで調べよう」が一生来ない人へ|AIにぶち込むだけの処方箋 first appeared on SIOS Tech Lab .