Skip to content

fix: changelog-generator.sh の複雑度を 35 → 23 に低減 - #699

Merged
keito4 merged 3 commits into
mainfrom
fix/changelog-generator-complexity
Apr 27, 2026
Merged

fix: changelog-generator.sh の複雑度を 35 → 23 に低減#699
keito4 merged 3 commits into
mainfrom
fix/changelog-generator-complexity

Conversation

@keito4

@keito4 keito4 commented Apr 27, 2026

Copy link
Copy Markdown
Owner

Summary

script/changelog-generator.sh のリファクタリングで循環的複雑度を 35 → 23 に低減(Issue #646 に部分対応)。

主な変更

  • 6 つのセクション出力ブロック (BREAKING / FEATURES / FIXES / PERF / DOCS / OTHER) を共通 render_section 関数化。各ブロックの if + for を 6 重複から 1 つに集約
  • 引数パースの --all/--include-all を case の | で統合
  • format_message を抽出し、PR 番号置換を sed → bash パラメータ展開 (SC2001 解消)
  • categorize_commit を抽出して種別ごとの代入を関数化
  • resolve_since_tag を抽出
  • nameref + set -u の組み合わせはバグるため、indirect expansion で配列参照

機能変更なし、出力フォーマット変更なし。

Test plan

  • npm run shellcheck
  • npm run lint
  • npm test 95 件パス
  • bash script/changelog-generator.sh --since 7d93477 --include-all --dry-run で旧版と同じ出力フォーマット
  • pre-commit hook 全パス

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Refactored changelog generation script by consolidating repeated logic into reusable functions and streamlining argument parsing, commit categorization, and changelog assembly processes.

主な変更:
- 6 つの section 出力ブロック (BREAKING / FEATURES / FIXES / PERF / DOCS / OTHER)
  を共通 `render_section` 関数化。各ブロックの `if + for` を 6 重複から 1 つに集約
- 引数パースの単純な `--all`/`--include-all` を case の `|` で統合
- `format_message` を抽出し、PR 番号置換を sed → bash パラメータ展開 (SC2001 解消)
- `categorize_commit` を抽出して種別ごとの代入を関数化
- `resolve_since_tag` 抽出
- nameref と `set -u` の組み合わせがバグるため、indirect expansion で配列を参照

機能変更なし、出力フォーマット変更なし。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Refactors the changelog generator script by extracting repetitive logic into reusable functions (show_help(), resolve_since_tag(), categorize_commit(), format_message(), render_section(), build_changelog()), improving code organization and reducing duplication while maintaining equivalent functionality.

Changes

Cohort / File(s) Summary
Changelog Generator Refactoring
script/changelog-generator.sh
Introduces helper functions for CLI handling, tag resolution, commit categorization, and changelog building; consolidates section rendering and contributors logic into build_changelog(); replaces inline argument parsing and commit processing with structured function calls.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested labels

size/S

Poem

🐰 Scripts once tangled, now they bloom so neat,
Functions refactored, making logic sweet!
From chaos to order, the changelog now sings,
Less duplication brings cleaner things! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly indicates the main refactoring objective: reducing cyclomatic complexity in changelog-generator.sh from 35 to 23, which directly matches the primary change in the changeset.
Description check ✅ Passed The description comprehensively covers all template sections with concrete details about changes, testing approach, and checklist completion, meeting the repository's requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/changelog-generator-complexity

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @keito4's task in 1m 36s —— View job


コードレビュー: script/changelog-generator.sh

  • コンテキスト収集
  • script/changelog-generator.sh の変更内容を読む
  • コードレビュー実施
  • フィードバックを投稿

総評

リファクタリングの品質は高く、DRY 原則の適用・関数分離・複雑度低減ともに妥当です。機能変更なし・出力フォーマット変更なしという制約も守られており、APPROVE 相当の変更です。

以下、いくつかの観点でコメントします。


Minor: size_var が不要な dead code (L115, L128)

# render_section 内
local size_var="${name}[@]"   # L115 — 宣言のみ
...
: "${size_var}"  # silence shellcheck "unused"  # L128 — no-op で参照

size_var${#keys[@]} -eq 0 によるチェックに置き換えられており、実際には使われていません。shellcheck を黙らせるための参照を入れるより、変数ごと削除するほうがシンプルです。

Fix this →


Minor: eval の使用 (L117)

eval "keys=(\"\${!${name}[@]}\")"

PR 説明通り nameref + set -u の組み合わせバグへの回避策であり、name は常に内部リテラル(BREAKING, FEATURES 等)のため注入リスクはありません。コメントも付いており理解可能ですが、将来 name の出所が広がった場合のリスクとして認識しておくと良いでしょう。現状は許容範囲です。


Recommendation: format_message は最初の PR 番号のみ処理 (L105–109)

format_message() {
  local msg=$1
  if [[ "$msg" =~ \(#([0-9]+)\) ]]; then
    local pr="${BASH_REMATCH[1]}"
    msg="${msg//(#$pr)/([#$pr]($REPO_URL/pull/$pr))}"
  fi
  echo "$msg"
}

=~ による正規表現マッチは最初にマッチした番号しか BASH_REMATCH[1] に入れません。1 つのコミットメッセージに (#123)(#456) が共存する場合、#456 は未処理のまま残ります。これは旧 sed 実装でも同様の挙動だったため、今回の PR で導入した問題ではありませんが、将来の改善候補として記録しておく価値があります。


肯定的な観点

観点 評価
render_section による 6 重複ブロックの統合 ✓ 明確な複雑度削減
--all|--include-all の case 統合 ✓ KISS 原則に沿った簡素化
categorize_commit の抽出 ✓ 単一責任の明確化
sed → bash パラメータ展開 (SC2001 解消) ✓ shellcheck 準拠
resolve_since_tag の抽出 ✓ グローバル状態の変更箇所が明確
set -euo pipefail との相互作用 &&grep の非ゼロ終了を適切にハンドリング

Verdict: APPROVEminor / recommendation のみで、機能・安全性に影響する問題はありません。

@keito4

keito4 commented Apr 27, 2026

Copy link
Copy Markdown
Owner Author

🔍 AI Code Review (Local Hook)

Models: Codex (default) / Gemini (default)

✨ Gemini Review

MCP issues detected. Run /mcp list for status.## 指摘事項

  1. [正確性] format_message 関数における echo の使用

    • 説明: echo "$msg" は、コミットメッセージが -n-e などのハイフンで始まる文字列であった場合に、それを echo コマンドのオプションとして解釈してしまい、メッセージが正しく出力されません。任意の文字列を安全に出力するために printf '%s\n' "$msg" を使用することを推奨します。
    • 箇所: script/changelog-generator.sh: 96行目
  2. [保守性] render_section における eval の使用

    • 説明: 動的な変数名(配列名)を参照するために eval を使用していますが、Bash 4.3以降で利用可能な local -n (nameref) を使用することで、より安全かつ簡潔に実装できます。eval は引用符の扱いが複雑になりやすく、保守性を低下させる要因となります。
    • 箇所: script/changelog-generator.sh: 122行目
  3. [正確性] 引数パース時の境界条件チェック不足

    • 説明: --since--output オプションが引数の最後に値なしで指定された場合、変数 $2 が空のまま shift 2 が実行されます。これにより、意図しないエラーが発生したり、パースロジックが不正な状態になったりする可能性があります。$2 の存在確認を行うバリデーションを追加すべきです。
    • 箇所: script/changelog-generator.sh: 42, 44行目

全体的な正確性の判定:
patch is correct

理由:
この変更は、複雑だったシングルスクリプトを論理的な関数に分割する優れたリファクタリングです。可読性と保守性が大幅に向上しており、主要なロジックは正しく移植されています。指摘した事項はエッジケースやBashのベストプラクティスに関するものであり、リファクタリング自体の正当性を否定するものではありません。

信頼度スコア: 0.9


🤖 Generated by post_pr_ai_review.py hook

@keito4 keito4 self-assigned this Apr 27, 2026
@keito4

keito4 commented Apr 27, 2026

Copy link
Copy Markdown
Owner Author

(aside)
@claude PRがbaseにマージする際にCIが落ちています。
修正してください。

@claude

claude Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @keito4's task in 7m 6s —— View job


CI修正作業 ✅

  • CIの失敗状況を確認
  • マージ競合を確認 → 競合なし(mainは .sh ファイルに変更なし)
  • mainの変更をマージ — gh pr update-branch 699 で解決(BEHIND → MERGEABLE)
  • CIが再起動されたことを確認 — 新しいCI実行が開始

実施内容

PR #700 (Windows ネイティブ環境サポート) が main にマージされたことで、このブランチが BEHIND 状態になっていました。

$ gh pr update-branch 699
✓ PR branch updated

gh pr update-branch コマンドで GitHub 側でマージを実行し、ブランチを最新の main に追いつかせました。競合はありませんでした(main は script/changelog-generator.sh を変更していないため)。

現在の状況

項目 状態
mergeable MERGEABLE ✅
mergeStateStatus BLOCKED(CI再実行待ち)
CI 再実行中 🔄

CI が完了すれば BLOCKED から CLEAN になり、マージ可能になる見込みです。
| PR branch CI runs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
script/changelog-generator.sh (1)

113-129: Drop the dead size_var and its no-op silencer.

size_var is defined (line 115) and immediately referenced via : "${size_var}" (line 128) only to suppress a shellcheck warning — it is never actually consumed; the section size is taken from ${#keys[@]} on line 118. Removing both lines makes the intent clearer.

♻️ Proposed cleanup
 render_section() {
   local title=$1 name=$2
-  local size_var="${name}[@]"
   local -a keys=()
   eval "keys=(\"\${!${name}[@]}\")"
   [[ ${`#keys`[@]} -eq 0 ]] && return 0
   printf '### %s\n\n' "$title"
   local hash short msg val_var
   for hash in "${keys[@]}"; do
     short=${hash:0:7}
     val_var="${name}[$hash]"
     msg=$(format_message "${!val_var}")
     printf -- '- %s ([%s](%s/commit/%s))\n' "$msg" "$short" "$REPO_URL" "$hash"
   done
   printf '\n'
-  : "${size_var}"  # silence shellcheck "unused"
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@script/changelog-generator.sh` around lines 113 - 129, In render_section(),
remove the unused local variable size_var and the trailing no-op silencer line
': "${size_var}"' so the function relies solely on keys array length;
specifically delete the declaration "local size_var=\"${name}[@]\"" and the
final ": \"${size_var}\"" reference, leaving the rest of render_section
(including keys, loop, and printf) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@script/changelog-generator.sh`:
- Around line 75-91: The categorize_commit function can yield a non-zero exit
(via the case default branch when [[ "$INCLUDE_ALL" == "true" ]] is false) which
trips set -e and aborts the script; fix it by ensuring categorize_commit always
returns success: after the case block in categorize_commit (the case handling
feat/fix/perf/docs/* and writing to FEATURES/FIXES/PERF/DOCS/OTHER), add an
explicit success return (e.g., return 0) so that calls to categorize_commit
never propagate a non-zero status; reference categorize_commit, the case default
clause that writes to OTHER["$hash"], and the INCLUDE_ALL variable when making
the change.

---

Nitpick comments:
In `@script/changelog-generator.sh`:
- Around line 113-129: In render_section(), remove the unused local variable
size_var and the trailing no-op silencer line ': "${size_var}"' so the function
relies solely on keys array length; specifically delete the declaration "local
size_var=\"${name}[@]\"" and the final ": \"${size_var}\"" reference, leaving
the rest of render_section (including keys, loop, and printf) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8479ce20-109c-4527-ae7f-21d409d82c8b

📥 Commits

Reviewing files that changed from the base of the PR and between e1ec41e and 82dc069.

📒 Files selected for processing (1)
  • script/changelog-generator.sh

Comment on lines +75 to +91
categorize_commit() {
local hash=$1 subject=$2 body=$3
local regex='^([a-z]+)(\([^)]+\))?: (.+)$'
[[ "$subject" =~ $regex ]] || return 0
local type="${BASH_REMATCH[1]}"
local message="${BASH_REMATCH[3]}"

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Conventional Commits](https://conventionalcommits.org/).
echo "$body" | grep -q "BREAKING CHANGE" && BREAKING["$hash"]="$message"

"
case "$type" in
feat) FEATURES["$hash"]="$message" ;;
fix) FIXES["$hash"]="$message" ;;
perf) PERF["$hash"]="$message" ;;
docs) DOCS["$hash"]="$message" ;;
*) [[ "$INCLUDE_ALL" == "true" ]] && OTHER["$hash"]="$message ($type)" ;;
esac
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Repro: categorize_commit-style function returning non-zero in a while-loop body under set -e.
cat >/tmp/repro.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
INCLUDE_ALL=false
declare -A OTHER
f() {
  local type="chore" hash="$1" message="m"
  case "$type" in
    feat) :;;
    *) [[ "$INCLUDE_ALL" == "true" ]] && OTHER["$hash"]="$message ($type)" ;;
  esac
}
i=0
while IFS= read -r line; do
  i=$((i+1))
  f "$i"
  echo "processed: $line"
done <<< $'a\nb\nc'
echo "done, processed=$i"
EOF
bash /tmp/repro.sh; echo "exit=$?"

Repository: keito4/config

Length of output: 64


🏁 Script executed:

cat -n script/changelog-generator.sh | sed -n '75,91p'

Repository: keito4/config

Length of output: 737


🏁 Script executed:

cat -n script/changelog-generator.sh | head -5 && echo "---" && cat -n script/changelog-generator.sh | sed -n '97,105p'

Repository: keito4/config

Length of output: 459


🏁 Script executed:

if [ -f test/integration/development-tools.bats ]; then
  cat -n test/integration/development-tools.bats | sed -n '35,52p'
else
  find . -name "development-tools.bats" -o -name "*changelog*test*" -o -name "*changelog*bats" 2>/dev/null | head -5
fi

Repository: keito4/config

Length of output: 700


categorize_commit can return non-zero and trip set -e, aborting changelog generation early.

The last statement of categorize_commit is the case, whose exit status is whatever the matched clause returns. The default branch on line 89 is an &&-list:

*)     [[ "$INCLUDE_ALL" == "true" ]] && OTHER["$hash"]="$message ($type)" ;;

When INCLUDE_ALL=false (the default) and $type is anything other than feat|fix|perf|docs (e.g. chore, refactor, style, ci, build, test, revert), the [[ ]] test fails, the &&-list returns 1, the case returns 1, and the function returns 1. The call site at line 100 sits in the body of a while loop (not in a test, &&/||, or ! context), so set -e (line 4) propagates that failure and the script exits before build_changelog ever runs — silently skipping any tail of commits and writing a truncated CHANGELOG.md.

The integration test at test/integration/development-tools.bats:35-52 only exercises feat: commits, so it doesn't catch this.

Suggested fix: ensure the function always returns 0
 categorize_commit() {
   local hash=$1 subject=$2 body=$3
   local regex='^([a-z]+)(\([^)]+\))?: (.+)$'
   [[ "$subject" =~ $regex ]] || return 0
   local type="${BASH_REMATCH[1]}"
   local message="${BASH_REMATCH[3]}"
 
-  echo "$body" | grep -q "BREAKING CHANGE" && BREAKING["$hash"]="$message"
+  if [[ "$body" == *"BREAKING CHANGE"* ]]; then
+    BREAKING["$hash"]="$message"
+  fi
 
   case "$type" in
     feat)  FEATURES["$hash"]="$message" ;;
     fix)   FIXES["$hash"]="$message" ;;
     perf)  PERF["$hash"]="$message" ;;
     docs)  DOCS["$hash"]="$message" ;;
-    *)     [[ "$INCLUDE_ALL" == "true" ]] && OTHER["$hash"]="$message ($type)" ;;
+    *)     if [[ "$INCLUDE_ALL" == "true" ]]; then OTHER["$hash"]="$message ($type)"; fi ;;
   esac
+  return 0
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
categorize_commit() {
local hash=$1 subject=$2 body=$3
local regex='^([a-z]+)(\([^)]+\))?: (.+)$'
[[ "$subject" =~ $regex ]] || return 0
local type="${BASH_REMATCH[1]}"
local message="${BASH_REMATCH[3]}"
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Conventional Commits](https://conventionalcommits.org/).
echo "$body" | grep -q "BREAKING CHANGE" && BREAKING["$hash"]="$message"
"
case "$type" in
feat) FEATURES["$hash"]="$message" ;;
fix) FIXES["$hash"]="$message" ;;
perf) PERF["$hash"]="$message" ;;
docs) DOCS["$hash"]="$message" ;;
*) [[ "$INCLUDE_ALL" == "true" ]] && OTHER["$hash"]="$message ($type)" ;;
esac
}
categorize_commit() {
local hash=$1 subject=$2 body=$3
local regex='^([a-z]+)(\([^)]+\))?: (.+)$'
[[ "$subject" =~ $regex ]] || return 0
local type="${BASH_REMATCH[1]}"
local message="${BASH_REMATCH[3]}"
if [[ "$body" == *"BREAKING CHANGE"* ]]; then
BREAKING["$hash"]="$message"
fi
case "$type" in
feat) FEATURES["$hash"]="$message" ;;
fix) FIXES["$hash"]="$message" ;;
perf) PERF["$hash"]="$message" ;;
docs) DOCS["$hash"]="$message" ;;
*) if [[ "$INCLUDE_ALL" == "true" ]]; then OTHER["$hash"]="$message ($type)"; fi ;;
esac
return 0
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@script/changelog-generator.sh` around lines 75 - 91, The categorize_commit
function can yield a non-zero exit (via the case default branch when [[
"$INCLUDE_ALL" == "true" ]] is false) which trips set -e and aborts the script;
fix it by ensuring categorize_commit always returns success: after the case
block in categorize_commit (the case handling feat/fix/perf/docs/* and writing
to FEATURES/FIXES/PERF/DOCS/OTHER), add an explicit success return (e.g., return
0) so that calls to categorize_commit never propagate a non-zero status;
reference categorize_commit, the case default clause that writes to
OTHER["$hash"], and the INCLUDE_ALL variable when making the change.

@keito4
keito4 merged commit bb7a86a into main Apr 27, 2026
18 checks passed
@keito4
keito4 deleted the fix/changelog-generator-complexity branch April 27, 2026 03:40
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.111.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions github-actions Bot added the released リリース済み label Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

released リリース済み

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant