この記事は何
以前、Alibaba製のAIコードレビューOSSである「Open Code Review」を、ai& Inferenceで動かす記事を書きました。
このOpen Code Review、最近のアップデートで、それぞれの指摘に severity(重要度)が付くようになりました。とても便利なのですが、GitHub Actionsに組み込んで毎回レビューを走らせていると、細かい指摘まで全部PRにコメントされてしまい、少し賑やかすぎるなと感じる場面が出てきました。
そこでこの記事では、Open Code ReviewをGitHub Actions上で動かすときに、severityが「high」以上の指摘だけをPRにコメントさせる方法を紹介します。
この記事は執筆時点(2026年7月)の情報をもとにしています。Open Code Review側のアップデートによって、出力フォーマットやseverityの仕様が変わる可能性があります。
レビュー結果に severity が付くようになった
前提として、Open Code Reviewのレビュー結果には、指摘ごとに severity(重要度)が付きます。severityは以下の4段階です。
-
critical: 致命的なバグやセキュリティ上の問題など、最も深刻なもの -
high: 重要度が高い問題 -
medium: 中程度の問題 -
low: 軽微な問題
指摘に重要度が付くようになったことで、「どこから直せばいいのか」がひと目でわかるようになりました。ここまでは嬉しい話です。
課題: medium 以下の指摘はなかなか収束しない
ところが、CIに組み込んで毎回全件レビューさせていると、ある悩みが出てきます。それは「medium 以下の指摘がなかなか収束しない」という点です。
medium や low の指摘は、いわゆる「直してもいいけど必須ではない」ような細かい指摘が中心です。この手の指摘は、対応しても対応しても、少しコードを変えるたびに別の細かい指摘が湧いてきて、なかなか終わりが見えません。結果として、PRのコメント欄が細かい指摘で埋まってしまい、肝心の「本当に直すべき指摘」が埋もれてしまう、ということが起きがちです。
そこで今回は、思い切って「high 以上(high と critical)の指摘だけをPRにコメントさせる」ようにしてみます。まず優先度の高い指摘に集中できるようにする、という作戦です。
Open Code ReviewをGitHub Actionsで動かす構成
具体的な実装に入る前に、Open Code ReviewをGitHub Actionsで動かす構成を軽くおさらいしておきます。
Open Code Reviewは、GitHub Actions用のサンプルワークフローを公式リポジトリで公開しています。
この記事のワークフローも、基本的にはこの公式サンプルをベースにしています。ただし公式サンプルには severity でフィルタする仕組みは無いので、今回はそこに「high 以上だけを投稿する」フィルタを足していく、という流れになります。まずはベースとなる構成を押さえておきましょう。ざっくり、以下のような流れになっています。
- PRの作成や、
/open-code-reviewのようなコメントをトリガーにワークフローを起動する -
ocr review --from origin/<base> --to <head-sha> --format jsonでレビューを実行する - 出力されたJSONをパースし、
actions/github-scriptを使ってPRにインラインコメントとして投稿する
ポイントは 2 の --format json です。このオプションを付けると、レビュー結果がJSONで出力され、その中の各コメントに severity フィールドが含まれます。だいたい以下のような構造をイメージしてもらえればよいです。
{
"comments": [
{
"path": "src/user.ts",
"line": 42,
"severity": "high",
"body": "..."
}
]
}
やりたいことは「レビュー自体は全件走らせつつ、PRに投稿する直前で high 未満のコメントを間引く」ことです。つまり、上記の 3(投稿)の手前にフィルタを1枚挟むのがポイントになります。
解決策: PRに投稿する直前で severity でフィルタする
それでは本題です。actions/github-script の「Post review comments to PR」ステップの中で、パース済みのコメント配列(ここでは rawComments とします)に対して、次のようにフィルタをかけます。
// Only surface issues whose severity is "high" or above ("critical").
// Comments with an unknown/missing severity are kept (fail-open) so that
// feedback OCR could not classify is not silently dropped.
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
const MIN_SEVERITY_RANK = SEVERITY_RANK.high;
const comments = rawComments.filter((c) => {
const rank = SEVERITY_RANK[String(c.severity || '').toLowerCase()];
return rank === undefined || rank >= MIN_SEVERITY_RANK;
});
const filteredOutCount = rawComments.length - comments.length;
if (filteredOutCount > 0) {
console.log(`Filtered out ${filteredOutCount} comment(s) below "high" severity.`);
}
このコードのポイントを順番に見ていきます。
severity を数値ランクに変換して大小比較する
severityは critical / high / medium / low という文字列なので、そのままでは「high 以上か?」という大小比較ができません。そこで、SEVERITY_RANK で各severityを数値に変換しています。
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
こうしておけば、あとは「ランクが閾値以上か」という素直な数値比較でフィルタできます。閾値は MIN_SEVERITY_RANK に切り出してあるので、たとえば「やっぱり medium 以上にしたい」と思ったときも、次のように1行変えるだけで済みます。
- const MIN_SEVERITY_RANK = SEVERITY_RANK.high;
+ const MIN_SEVERITY_RANK = SEVERITY_RANK.medium;
閾値の意味が名前から読み取れるので、後から見返したときにも「何を基準にフィルタしているのか」が一目でわかります。
severity が未知・欠損なら「あえて残す」フェイルオープン設計
もう一つのポイントが、filter の中の次の条件です。
const rank = SEVERITY_RANK[String(c.severity || '').toLowerCase()];
return rank === undefined || rank >= MIN_SEVERITY_RANK;
ここでは rank === undefined の場合、つまりseverityが未知の値だったり、そもそも欠損していたりする場合に、そのコメントを「あえて残す」ようにしています。いわゆる「フェイルオープン」の設計です。
なぜこうしているかというと、severityが取れなかったからといってコメントを握りつぶしてしまうと、「Open Code Reviewは何か言おうとしていたのに、こちらが勝手に捨ててしまった」という状況になりかねないからです。フィルタの目的はあくまで「細かい指摘を間引くこと」であって、「分類できなかった指摘を消すこと」ではありません。判断に迷うものは残しておく、という方針です。
なお、String(c.severity || '').toLowerCase() としているのは、severityが High のように大文字混じりで来ても、あるいは undefined や null であっても落ちないように、文字列として正規化してから引くためです。
間引いた件数をログに残す
最後に、間引いた件数を数えてログに出しています。
const filteredOutCount = rawComments.length - comments.length;
if (filteredOutCount > 0) {
console.log(`Filtered out ${filteredOutCount} comment(s) below "high" severity.`);
}
フィルタは便利な反面、「気づかないうちに指摘が消えている」という状態になりがちです。何件間引いたのかをActionsのログに残しておくと、後から「あのとき何件の指摘が省かれたんだろう」と確認できて安心です。
high 以上の指摘が無かった場合は LGTM を投げる
フィルタの結果、high 以上の指摘が1件も残らなかった場合は、「対応すべき指摘は無かった」ということなので、LGTMのコメントを投げて終わりにします。このとき、閾値未満で間引いた件数も添えておくと、「指摘がゼロだったのか、それとも間引かれた結果ゼロになったのか」がわかって親切です。
// If nothing meets the "high"+ severity bar, there is nothing to address: LGTM.
if (comments.length === 0) {
let body = '✅ **OpenCodeReview**: LGTM 👍';
if (filteredOutCount > 0) {
body += `\n\n_(${filteredOutCount} comment(s) below the "high" severity threshold were omitted.)_`;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
return;
}
comments.length === 0 を判定して、残った指摘がゼロならLGTMコメントを投稿し、return で処理を打ち切っています。filteredOutCount が1件以上あるときだけ「(high 未満の指摘を N 件省きました)」という注記を添えるようにしているので、間引きが発生していないときは余計な文言が出ません。
これで、high 以上の指摘があるときはその指摘だけがPRにコメントされ、無いときはLGTMが返る、という挙動になります。
ワークフロー全体
フィルタ部分だけを抜き出して紹介してきましたが、トリガーの設定やレビューの実行ステップも含めた、アップデート後のワークフロー全体は以下のとおりです。呼び出し元のワークフローから workflow_call で呼び出して使う、再利用可能ワークフローとして書いています。少し長いので折りたたんでいますが、実際に組み込む際の参考にしてみてください。
ocr-review.yml の全体を見る
# OpenCodeReview - Reusable PR Auto-Review Workflow
#
# This reusable workflow automatically reviews pull requests using OpenCodeReview
# and posts review comments directly on the PR.
#
# Triggers are defined by the CALLER workflow (e.g. pull_request, issue_comment).
# This workflow inherits the caller's `github` event context, so the job-level
# `if:` and the github-script logic below behave the same as a standalone workflow.
#
# Required secrets:
# OCR_LLM_URL - LLM API endpoint (e.g., https://api.openai.com/v1/chat/completions)
# OCR_LLM_AUTH_TOKEN - Authentication token for the LLM API
#
# Optional secrets:
# OCR_LLM_MODEL - Model name (default: gpt-4o)
# OCR_LLM_USE_ANTHROPIC - Use Anthropic-compatible API ('true' / 'false')
#
# Note: GITHUB_TOKEN is automatically provided by GitHub Actions.
name: OpenCodeReview PR Review
on:
workflow_call:
secrets:
OCR_LLM_URL:
description: 'LLM API endpoint'
required: true
OCR_LLM_AUTH_TOKEN:
description: 'Authentication token for the LLM API'
required: true
OCR_LLM_MODEL:
description: 'Model name (default: gpt-4o)'
required: false
OCR_LLM_USE_ANTHROPIC:
description: "Use Anthropic-compatible API ('true' / 'false')"
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
code-review:
runs-on: ubuntu-latest
# Run on PR events, or on comments starting with trigger keywords
if: |
github.event_name == 'pull_request' ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/open-code-review')) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '@open-code-review'))
steps:
- name: Get PR context
id: pr-context
if: github.event_name != 'pull_request'
uses: actions/github-script@v7
with:
script: |
// For issue_comment events, get PR info
const prNumber = context.issue.number;
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('base_ref', pullRequest.base.ref);
core.setOutput('head_ref', pullRequest.head.ref);
core.setOutput('head_sha', pullRequest.head.sha);
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for merge-base diff
ref: ${{ github.event.pull_request.head.sha || steps.pr-context.outputs.head_sha }}
- name: Fetch PR head ref (ensures fork commits are available)
run: git fetch origin pull/${{ github.event.pull_request.number || github.event.issue.number }}/head
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Install OpenCodeReview
run: npm install -g @alibaba-group/open-code-review
- name: Configure OCR
run: |
ocr config set llm.url ${{ secrets.OCR_LLM_URL }}
ocr config set llm.auth_token ${{ secrets.OCR_LLM_AUTH_TOKEN }}
ocr config set llm.model ${{ secrets.OCR_LLM_MODEL }}
ocr config set llm.use_anthropic ${{ secrets.OCR_LLM_USE_ANTHROPIC }}
ocr config set language Japanese
- name: Collect review background context
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const prNumber = context.payload.pull_request?.number || context.issue.number;
// Fetch PR details for title and description
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
// Extract linked issue numbers from the PR body
// Pick up both GitHub "closing" keywords (close/fix/resolve [#N]) and bare #N references
const prBody = pr.body || '';
const issueNumbers = new Set();
const closingPattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi;
const refPattern = /(?:^|[^&\w])#(\d+)\b/g;
for (const re of [closingPattern, refPattern]) {
let m;
while ((m = re.exec(prBody)) !== null) {
issueNumbers.add(Number(m[1]));
}
}
issueNumbers.delete(prNumber);
// Fetch each referenced issue (skip PR references and anything we can't read)
const linkedIssues = [];
for (const n of issueNumbers) {
try {
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: n,
});
if (issue.pull_request) continue;
linkedIssues.push(issue);
} catch (e) {
console.log(`Skipping #${n}: ${e.message}`);
}
}
// Fetch all existing inline review comments across pages
const comments = await github.paginate(
github.rest.pulls.listReviewComments,
{
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
}
);
// NOTE: This block is injected under OCR's `### Requirement Background (Optional)`
// heading, so top-level sections start at h4 (####) and sub-entries at h5 (#####).
const sections = [
'Below is contextual information for this code review:',
'- The pull request description',
'- Linked issue descriptions',
'- Review comments that have already been posted (do not duplicate them)',
'',
'#### Pull Request Description',
'',
`##### ${pr.title || ''}`,
'',
(prBody || '(no description)').trim(),
'',
];
if (linkedIssues.length > 0) {
sections.push('#### Linked Issues');
sections.push('');
for (const issue of linkedIssues) {
sections.push(`##### #${issue.number}: ${issue.title || ''}`);
sections.push('');
sections.push((issue.body || '(no description)').trim());
sections.push('');
}
}
if (comments.length > 0) {
sections.push('#### Already-posted review comments');
sections.push('');
sections.push('The following comments have ALREADY been posted on this pull request.');
sections.push('Do NOT generate the same or similar comments for issues already covered below.');
sections.push('Focus on identifying NEW issues that have not yet been pointed out.');
sections.push('');
for (const c of comments) {
const path = c.path || '(unknown)';
const start = c.start_line;
const end = c.line ?? c.original_line;
const lineInfo = start && end && start !== end
? `L${start}-L${end}`
: `L${end ?? start ?? '?'}`;
sections.push(`##### ${path} (${lineInfo})`);
sections.push('');
sections.push((c.body || '').trim());
sections.push('');
}
}
fs.writeFileSync('/tmp/ocr-background.txt', sections.join('\n'));
console.log(
`Background context: PR description + ${linkedIssues.length} linked issue(s) + ${comments.length} existing comment(s).`
);
- name: Run OpenCodeReview
id: review
run: |
# Get base ref and head SHA from PR context (different for comment triggers)
# Note: We use HEAD_SHA instead of origin/${HEAD_REF} to support fork PRs,
# because fork branches don't exist on the origin remote.
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE_REF="${{ github.event.pull_request.base.ref }}"
HEAD_SHA="${{ github.event.pull_request.head.sha }}"
else
BASE_REF="${{ steps.pr-context.outputs.base_ref }}"
HEAD_SHA="${{ steps.pr-context.outputs.head_sha }}"
fi
echo "Reviewing PR: ${HEAD_SHA} against origin/${BASE_REF}"
# Pass PR description, linked issue descriptions, and already-posted review
# comments as background context so OCR has full context and skips duplicates.
BACKGROUND_ARGS=()
if [ -s /tmp/ocr-background.txt ]; then
BACKGROUND_ARGS+=(--background "$(cat /tmp/ocr-background.txt)")
echo "Including $(wc -l < /tmp/ocr-background.txt) lines of background context."
fi
# Run OCR in range mode with JSON output
ocr review \
--from "origin/${BASE_REF}" \
--to "${HEAD_SHA}" \
--format json \
"${BACKGROUND_ARGS[@]}" \
> /tmp/ocr-result.json 2>/tmp/ocr-stderr.log || true
echo "OCR review completed. Output:"
cat /tmp/ocr-result.json
echo "OCR review completed. Error log:"
cat /tmp/ocr-stderr.log
- name: Post review comments to PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require('fs');
const path = '/tmp/ocr-result.json';
// Read OCR output
let result;
try {
const raw = fs.readFileSync(path, 'utf8');
result = JSON.parse(raw);
} catch (e) {
console.log('Failed to parse OCR output:', e.message);
// Post a simple comment if parsing fails
const stderr = fs.readFileSync('/tmp/ocr-stderr.log', 'utf8').trim();
if (stderr) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `⚠️ **OpenCodeReview** encountered an error:\n${fencedBlock(stderr)}`
});
}
return;
}
const rawComments = result.comments || [];
const warnings = result.warnings || [];
// Only surface issues whose severity is "high" or above ("critical").
// Comments with an unknown/missing severity are kept (fail-open) so that
// feedback OCR could not classify is not silently dropped.
const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
const MIN_SEVERITY_RANK = SEVERITY_RANK.high;
const comments = rawComments.filter((c) => {
const rank = SEVERITY_RANK[String(c.severity || '').toLowerCase()];
return rank === undefined || rank >= MIN_SEVERITY_RANK;
});
const filteredOutCount = rawComments.length - comments.length;
if (filteredOutCount > 0) {
console.log(`Filtered out ${filteredOutCount} comment(s) below "high" severity.`);
}
// If nothing meets the "high"+ severity bar, there is nothing to address: LGTM.
if (comments.length === 0) {
let body = '✅ **OpenCodeReview**: LGTM 👍';
if (filteredOutCount > 0) {
body += `\n\n_(${filteredOutCount} comment(s) below the "high" severity threshold were omitted.)_`;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body
});
return;
}
// Prepare PR review with inline comments
const prNumber = context.issue.number;
let commitSha;
// Get commit SHA from event context
if (context.eventName === 'pull_request') {
commitSha = context.payload.pull_request.head.sha;
} else {
// For comment events, we need to fetch the PR
const { data: pullRequest } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
commitSha = pullRequest.head.sha;
}
// Build review comments array for the PR review API
// Only inline comments with line info can be posted via createReview
const reviewComments = [];
const commentsWithoutLine = [];
for (const comment of comments) {
const body = formatComment(comment);
// Check if comment has valid line information for inline comment (line >= 1)
const hasValidLine = (comment.start_line >= 1) || (comment.end_line >= 1);
if (!hasValidLine) {
commentsWithoutLine.push({ comment, body });
continue;
}
const reviewComment = {
path: comment.path,
body: body
};
// Use line range if available
if (comment.start_line >= 1 && comment.end_line >= 1 && comment.start_line !== comment.end_line) {
reviewComment.start_line = comment.start_line;
reviewComment.line = comment.end_line;
reviewComment.start_side = 'RIGHT';
reviewComment.side = 'RIGHT';
} else if (comment.end_line >= 1) {
reviewComment.line = comment.end_line;
reviewComment.side = 'RIGHT';
} else if (comment.start_line >= 1) {
reviewComment.line = comment.start_line;
reviewComment.side = 'RIGHT';
}
reviewComments.push({ comment, reviewComment });
}
// Submit as a single PR review with all comments
const totalCount = comments.length;
const inlineCount = reviewComments.length;
const summaryCount = commentsWithoutLine.length;
let summaryBody = buildSummaryBody(totalCount, inlineCount, summaryCount, warnings);
// Add comments without line info to summary body
summaryBody += formatSummaryComments(commentsWithoutLine);
// Statistics tracking
let successCount = 0;
let failedCount = 0;
const failedComments = [];
try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
commit_id: commitSha,
body: summaryBody,
event: 'COMMENT',
comments: reviewComments.map(({ reviewComment }) => reviewComment)
});
successCount = reviewComments.length;
console.log(`Successfully posted review with ${successCount} inline comments (${commentsWithoutLine.length} in summary)`);
} catch (e) {
console.log('Failed to post review with inline comments:', e.message);
console.log('Falling back to posting comments individually...');
// Fallback: post comments one by one
for (const { comment, reviewComment } of reviewComments) {
try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
commit_id: commitSha,
body: '',
event: 'COMMENT',
comments: [reviewComment]
});
successCount++;
console.log(`Successfully posted comment for ${reviewComment.path}`);
} catch (innerE) {
failedCount++;
failedComments.push({ comment, error: innerE.message });
console.log(`Failed to post comment for ${reviewComment.path}: ${innerE.message}`);
}
}
// Post summary comment with statistics
let finalBody = buildSummaryBody(totalCount, successCount, commentsWithoutLine.length + failedComments.length, warnings);
finalBody += formatSummaryComments(commentsWithoutLine);
finalBody += `\n\n---\n\n📊 **Posting Statistics:**`;
finalBody += `\n- ✅ Successfully posted: ${successCount} comment(s)`;
if (failedCount > 0) {
finalBody += `\n- ❌ Failed to post: ${failedCount} comment(s)`;
}
// Add failed comments as summary content so review feedback is not lost.
if (failedComments.length > 0) {
finalBody += '\n\n---\n\n### ⚠️ Inline comments shown in summary';
for (const { comment, error } of failedComments) {
finalBody += '\n\n---\n\n';
finalBody += formatCommentMarkdown(comment, error);
}
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: finalBody
});
}
function formatComment(comment) {
let body = '';
// Prepend a category / severity badge so reviewers can triage at a glance
const badge = formatBadge(comment);
if (badge) body += badge + '\n\n';
body += comment.content || '';
// Add code suggestion if available
if (comment.suggestion_code && comment.existing_code) {
body += '\n\n**Suggestion:**\n';
body += fencedBlock(comment.suggestion_code, 'suggestion');
}
return body;
}
function formatCommentMarkdown(comment, error) {
let md = `### 📄 \`${comment.path}\``;
if (comment.start_line && comment.end_line) {
md += ` (L${comment.start_line}-L${comment.end_line})`;
}
md += '\n\n';
const badge = formatBadge(comment);
if (badge) md += badge + '\n\n';
if (error) {
md += `⚠️ GitHub could not post this as an inline comment: ${error}\n\n`;
}
md += comment.content || '';
if (comment.suggestion_code && comment.existing_code) {
md += '\n\n<details><summary>💡 Suggested Change</summary>\n\n';
md += '**Before:**\n' + fencedBlock(comment.existing_code) + '\n\n';
md += '**After:**\n' + fencedBlock(comment.suggestion_code) + '\n\n';
md += '</details>';
}
return md;
}
function buildSummaryBody(totalCount, inlineCount, summaryCount, warnings) {
let body = `🔍 **OpenCodeReview** found **${totalCount}** issue(s) in this PR.`;
if (totalCount > 0) {
body += `\n- ✅ ${inlineCount} posted as inline comment(s)`;
body += `\n- 📝 ${summaryCount} posted as summary`;
}
if (warnings.length > 0) {
body += `\n\n⚠️ ${warnings.length} warning(s) occurred during review.`;
}
return body;
}
function formatSummaryComments(summaryComments) {
let body = '';
for (const { comment } of summaryComments) {
body += '\n\n---\n\n';
body += formatCommentMarkdown(comment);
}
return body;
}
// Build a `severity · category` badge line from OCR's structured fields.
// Both fields are optional, so render whichever are present (or nothing).
function formatBadge(comment) {
const category = comment.category ? String(comment.category).trim() : '';
const severity = comment.severity ? String(comment.severity).trim() : '';
const parts = [];
if (severity) {
parts.push(`${severityIcon(severity)} **${severity.toUpperCase()}**`);
}
if (category) {
parts.push(`\`${category}\``);
}
return parts.join(' · ');
}
function severityIcon(severity) {
switch (String(severity).toLowerCase()) {
case 'critical': return '🟥';
case 'high': return '🟧';
case 'medium': return '🟨';
case 'low': return '🟦';
default: return '⬜';
}
}
function fencedBlock(content, language = '') {
const text = String(content || '');
const fence = safeFence(text);
let block = fence + language + '\n' + text;
if (!text.endsWith('\n')) block += '\n';
return block + fence;
}
function safeFence(content) {
const matches = String(content || '').match(/`+/g) || [];
const maxTicks = matches.reduce((max, ticks) => Math.max(max, ticks.length), 0);
return '`'.repeat(Math.max(3, maxTicks + 1));
}
最後に
今回は、Open Code ReviewをGitHub Actions上で動かすときに、severityが high 以上の指摘だけをPRにコメントさせる方法を紹介しました。改めて整理すると、以下のようになります。
- レビューは全件走らせつつ、PRに投稿する直前でseverityによるフィルタを1枚挟む
- severityを数値ランクに変換し、閾値(
MIN_SEVERITY_RANK)以上のものだけ残す - severityが未知・欠損の場合はあえて残す「フェイルオープン」にして、分類できなかった指摘を握りつぶさない
-
high以上の指摘が無ければ、間引いた件数を添えてLGTMを返す
medium 以下の細かい指摘に埋もれず、まずは重要な指摘に集中したい、という場面ではなかなか便利な設定かなと思います。閾値は1行で変えられるので、チームの温度感に合わせて medium 以上に緩めたりと、運用しながら調整してみるのがおすすめです。
同じようにOpen Code ReviewをCIに組み込んでいて、指摘の多さに悩んでいる方の助けになれば幸いです。最後までお読みいただきありがとうございました。