2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tips: Open Code ReviewをGitHub Actions上で実行する際、severity high以上のもののみを指摘させるようにする

2
Last updated at Posted at 2026-07-12

この記事は何

以前、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 以下の指摘がなかなか収束しない」という点です。

mediumlow の指摘は、いわゆる「直してもいいけど必須ではない」ような細かい指摘が中心です。この手の指摘は、対応しても対応しても、少しコードを変えるたびに別の細かい指摘が湧いてきて、なかなか終わりが見えません。結果として、PRのコメント欄が細かい指摘で埋まってしまい、肝心の「本当に直すべき指摘」が埋もれてしまう、ということが起きがちです。

そこで今回は、思い切って「high 以上(highcritical)の指摘だけをPRにコメントさせる」ようにしてみます。まず優先度の高い指摘に集中できるようにする、という作戦です。

Open Code ReviewをGitHub Actionsで動かす構成

具体的な実装に入る前に、Open Code ReviewをGitHub Actionsで動かす構成を軽くおさらいしておきます。

Open Code Reviewは、GitHub Actions用のサンプルワークフローを公式リポジトリで公開しています。

この記事のワークフローも、基本的にはこの公式サンプルをベースにしています。ただし公式サンプルには severity でフィルタする仕組みは無いので、今回はそこに「high 以上だけを投稿する」フィルタを足していく、という流れになります。まずはベースとなる構成を押さえておきましょう。ざっくり、以下のような流れになっています。

  1. PRの作成や、/open-code-review のようなコメントをトリガーにワークフローを起動する
  2. ocr review --from origin/<base> --to <head-sha> --format json でレビューを実行する
  3. 出力されたJSONをパースし、actions/github-script を使ってPRにインラインコメントとして投稿する

ポイントは 2 の --format json です。このオプションを付けると、レビュー結果がJSONで出力され、その中の各コメントに severity フィールドが含まれます。だいたい以下のような構造をイメージしてもらえればよいです。

ocr review --format json の出力(イメージ)
{
  "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 のように大文字混じりで来ても、あるいは undefinednull であっても落ちないように、文字列として正規化してから引くためです。

間引いた件数をログに残す

最後に、間引いた件数を数えてログに出しています。

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 の全体を見る
.github/workflows/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に組み込んでいて、指摘の多さに悩んでいる方の助けになれば幸いです。最後までお読みいただきありがとうございました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?