From 5798d9fbed12b2b57624c066409850a405a7dbf6 Mon Sep 17 00:00:00 2001 From: thinkyou0714 Date: Sat, 13 Jun 2026 18:36:55 +0900 Subject: [PATCH 1/5] G071: add .gitattributes (eol=lf) + .editorconfig (propagate hygiene template) --- .editorconfig | 19 +++++++++++++++++++ .gitattributes | 22 ++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..828eeb2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.py] +indent_size = 4 + +[*.md] +# Markdown は行末2スペースが改行の意味を持つため、末尾空白を削らない +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a2dc52a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# 改行コードを LF に固定し、CRLF 混入の再発を防ぐ(ADR-005) +* text=auto eol=lf + +*.py text eol=lf +*.md text eol=lf +*.json text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.toml text eol=lf +*.cfg text eol=lf +*.ini text eol=lf +*.sh text eol=lf +.gitignore text eol=lf +.gitattributes text eol=lf +.editorconfig text eol=lf + +# バイナリ(差分・改行変換の対象外) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.pdf binary From 5ad20aeb41f49340cc794eed5678e629fafaa470 Mon Sep 17 00:00:00 2001 From: thinkyou0714 Date: Sat, 13 Jun 2026 19:31:50 +0900 Subject: [PATCH 2/5] G072: extract issue-triage scoring into scripts/triage_scoring.py (was triplicated; workflow + tests now import it) --- .github/workflows/weekly-triage.yml | 72 +------------- scripts/triage_scoring.py | 142 ++++++++++++++++++++++++++++ tests/test_scoring.py | 18 +--- 3 files changed, 145 insertions(+), 87 deletions(-) create mode 100644 scripts/triage_scoring.py diff --git a/.github/workflows/weekly-triage.yml b/.github/workflows/weekly-triage.yml index 4acd4e6..bfb857d 100644 --- a/.github/workflows/weekly-triage.yml +++ b/.github/workflows/weekly-triage.yml @@ -36,77 +36,7 @@ jobs: - name: Score and rank issues id: scoring run: | - python3 - <<'PYEOF' - import json, datetime - - with open('/tmp/issues.json') as f: - issues = json.load(f) - - now = datetime.datetime.now(datetime.timezone.utc) - - def impact(labels): - names = [l['name'].lower() for l in labels] - if any('bug' in n or 'crash' in n or 'security' in n for n in names): return 5 - if any('enhancement' in n or 'feature' in n for n in names): return 3 - if any('docs' in n or 'refactor' in n for n in names): return 1 - return 2 - - def effort(body): - if not body: return 3 - l = len(body) - if l < 200: return 1 - if l < 600: return 2 - if l < 1500: return 3 - return 4 - - def thumbs_up(reaction_groups): - # The rubric counts 👍 only, not every reaction type. - for rg in reaction_groups: - if rg.get('content') == 'THUMBS_UP': - return rg.get('users', {}).get('totalCount', 0) - return 0 - - # Urgency mirrors issue-triage/references/scoring-rubric.md (max +7). - def urgency_bonus(issue, age, comments): - names = [l['name'].lower() for l in issue.get('labels', [])] - bonus = 0 - if age > 30 and comments >= 3: - bonus += 2 - if issue.get('milestone'): - bonus += 1 - if thumbs_up(issue.get('reactionGroups', [])) >= 5: - bonus += 1 - if any(n in ('urgent', 'hotfix', 'p0') for n in names): - bonus += 3 - return bonus - - scored = [] - for i in issues: - imp = impact(i.get('labels', [])) - eff = effort(i.get('body', '')) - age = (now - datetime.datetime.fromisoformat(i['createdAt'].replace('Z','+00:00'))).days - comments = i.get('comments', 0) - urgency = urgency_bonus(i, age, comments) - score = imp * (6 - eff) + urgency - scored.append({**i, 'score': score, 'impact': imp, 'effort': eff}) - - scored.sort(key=lambda x: -x['score']) - top10 = scored[:10] - - lines = ['## 🔢 Top Issues This Week\n'] - lines.append('| # | Title | Score |') - lines.append('|---|---|---|') - for i in top10: - lines.append(f"| #{i['number']} | {i['title'][:60]} | {i['score']} |") - - if scored[10:]: - lines.append(f'\n_Plus {len(scored) - 10} more issues in backlog._') - - with open('/tmp/triage_output.md', 'w') as f: - f.write('\n'.join(lines)) - - print('\n'.join(lines)) - PYEOF + python3 scripts/triage_scoring.py /tmp/issues.json - name: Generate AI summary and publish report env: diff --git a/scripts/triage_scoring.py b/scripts/triage_scoring.py new file mode 100644 index 0000000..924be0f --- /dev/null +++ b/scripts/triage_scoring.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Shared issue-triage scoring logic. + +The weekly workflow and rubric golden tests both import this module so the +formula has one executable source of truth. +""" +from __future__ import annotations + +import datetime as dt +import json +import sys +from pathlib import Path +from typing import Any, Iterable + + +def _label_names(labels: Iterable[dict[str, Any]] | None) -> list[str]: + return [ + str(label.get("name", "")).lower() + for label in (labels or []) + if isinstance(label, dict) + ] + + +def impact(labels: Iterable[dict[str, Any]] | None) -> int: + names = _label_names(labels) + if any("bug" in name or "crash" in name or "security" in name for name in names): + return 5 + if any("enhancement" in name or "feature" in name for name in names): + return 3 + if any("docs" in name or "refactor" in name for name in names): + return 1 + return 2 + + +def effort(body: str | None) -> int: + if not body: + return 3 + length = len(body) + if length < 200: + return 1 + if length < 600: + return 2 + if length < 1500: + return 3 + return 4 + + +def thumbs_up(reaction_groups: Iterable[dict[str, Any]] | None) -> int: + """Return only the thumbs-up reaction count used by the rubric.""" + for reaction_group in reaction_groups or []: + if reaction_group.get("content") == "THUMBS_UP": + users = reaction_group.get("users") or {} + return int(users.get("totalCount") or 0) + return 0 + + +def urgency_bonus(issue: dict[str, Any], age: int, comments: int) -> int: + names = _label_names(issue.get("labels", [])) + bonus = 0 + if age > 30 and comments >= 3: + bonus += 2 + if issue.get("milestone"): + bonus += 1 + if thumbs_up(issue.get("reactionGroups", [])) >= 5: + bonus += 1 + if any(name in ("urgent", "hotfix", "p0") for name in names): + bonus += 3 + return bonus + + +def score(impact_value: int, effort_value: int, urgency: int) -> int: + return impact_value * (6 - effort_value) + urgency + + +def band(score_value: int) -> str: + if score_value >= 15: + return "🔴 Sprint必須" + if score_value >= 10: + return "🟡 Sprint推奨" + if score_value >= 5: + return "🟢 Backlog" + return "⚪ 低優先度" + + +def _parse_created_at(value: str) -> dt.datetime: + return dt.datetime.fromisoformat(value.replace("Z", "+00:00")) + + +def rank_issues( + issues: Iterable[dict[str, Any]], + now: dt.datetime | None = None, +) -> list[dict[str, Any]]: + now = now or dt.datetime.now(dt.timezone.utc) + scored = [] + + for issue in issues: + impact_value = impact(issue.get("labels", [])) + effort_value = effort(issue.get("body", "")) + age = (now - _parse_created_at(issue["createdAt"])).days + comments = int(issue.get("comments", 0) or 0) + urgency = urgency_bonus(issue, age, comments) + total = score(impact_value, effort_value, urgency) + scored.append( + {**issue, "score": total, "impact": impact_value, "effort": effort_value} + ) + + return sorted(scored, key=lambda issue: -issue["score"]) + + +def render_top_issues(scored: list[dict[str, Any]]) -> str: + top10 = scored[:10] + lines = ["## 🔢 Top Issues This Week", "", "| # | Title | Score |", "|---|---|---|"] + + for issue in top10: + lines.append(f"| #{issue['number']} | {issue['title'][:60]} | {issue['score']} |") + + if scored[10:]: + lines.append("") + lines.append(f"_Plus {len(scored) - 10} more issues in backlog._") + + return "\n".join(lines) + + +def main(argv: list[str]) -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + + if len(argv) not in (2, 3): + print("Usage: triage_scoring.py ISSUES_JSON [OUTPUT_MD]", file=sys.stderr) + return 2 + + issues_path = Path(argv[1]) + output_path = Path(argv[2]) if len(argv) == 3 else Path("/tmp/triage_output.md") + issues = json.loads(issues_path.read_text(encoding="utf-8")) + output = render_top_issues(rank_issues(issues)) + output_path.write_text(output, encoding="utf-8") + print(output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/tests/test_scoring.py b/tests/test_scoring.py index 6fef1ee..fa9ebdb 100644 --- a/tests/test_scoring.py +++ b/tests/test_scoring.py @@ -11,27 +11,13 @@ import unittest from pathlib import Path +from scripts.triage_scoring import band, score + REPO = Path(__file__).resolve().parents[1] RUBRIC = REPO / "issue-triage" / "references" / "scoring-rubric.md" SKILL = REPO / "issue-triage" / "SKILL.md" -# score = impact * (6 - effort) + urgency (see issue-triage/SKILL.md) -def score(impact: int, effort: int, urgency: int) -> int: - return impact * (6 - effort) + urgency - - -# Display bands (issue-triage/SKILL.md Step 3 + scoring-rubric.md) -def band(s: int) -> str: - if s >= 15: - return "🔴 Sprint必須" - if s >= 10: - return "🟡 Sprint推奨" - if s >= 5: - return "🟢 Backlog" - return "⚪ 低優先度" - - def parse_examples(text: str): """Yield (impact, effort, urgency, claimed_score, band) from the example table.""" section = text.split("## スコアリング例", 1) From aeaf46da0aa224284854a5a6f56f4a0301433ee4 Mon Sep 17 00:00:00 2001 From: thinkyou0714 Date: Sat, 13 Jun 2026 19:45:08 +0900 Subject: [PATCH 3/5] G073: package.json add private:true + test script (keep zenn-cli, verified functional) + CONTRIBUTING note --- CONTRIBUTING.md | 12 ++++++++++++ package.json | 2 ++ 2 files changed, 14 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f953a43..e1efda1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,6 +25,18 @@ Claude Code loads `references/` as context automatically. --- +## Running Tests / Previewing the Promo Articles + +Run the Python unit tests before opening a PR: + +```bash +npm test +``` + +`zenn-cli` is kept as a devDependency because this repository owns local `articles/` drafts and the `books/claude-code-skill-pack` content. Use `npm run preview` to check those promo articles locally, and keep `npm run new:article` available for new Zenn drafts. + +--- + ## Improve references/ (Most Valuable Contribution) Found a comment pattern that was misclassified? Open an Issue with label `references-improvement`: diff --git a/package.json b/package.json index 13b1769..88cabdc 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,10 @@ { "name": "github-flow-kit", "version": "0.3.0", + "private": true, "description": "Claude Code skill pack that automates GitHub workflows: PR review responses, release notes, issue triage, repo onboarding, and security audits.", "scripts": { + "test": "python3 -m unittest discover -s tests -p 'test_*.py'", "new:article": "zenn new:article", "preview": "zenn preview" }, From 84514e3319cb03b914a732a6beb8705f2a1d6794 Mon Sep 17 00:00:00 2001 From: thinkyou0714 Date: Sat, 13 Jun 2026 19:53:55 +0900 Subject: [PATCH 4/5] G074: add CHANGELOG sync guard test (Unreleased + version heading present) --- tests/test_changelog_sync.py | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_changelog_sync.py diff --git a/tests/test_changelog_sync.py b/tests/test_changelog_sync.py new file mode 100644 index 0000000..4da7b74 --- /dev/null +++ b/tests/test_changelog_sync.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Guard package.json version and CHANGELOG.md release headings.""" +import json +import re +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +PACKAGE_JSON = REPO / "package.json" +CHANGELOG = REPO / "CHANGELOG.md" + + +def package_version(): + return json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))["version"] + + +class TestChangelogSync(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.version = package_version() + cls.changelog = CHANGELOG.read_text(encoding="utf-8") + + def test_unreleased_section_exists(self): + self.assertRegex( + self.changelog, + r"(?m)^## \[Unreleased\](?:\s|$)", + "CHANGELOG.md must keep an [Unreleased] section", + ) + + def test_package_version_has_changelog_heading(self): + self.assertRegex( + self.changelog, + rf"(?m)^## \[{re.escape(self.version)}\](?:\s|$)", + f"CHANGELOG.md must include a heading for package.json version {self.version}", + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From 561a3a083774326ca2b06851d9ee1e823928a89f Mon Sep 17 00:00:00 2001 From: thinkyou0714 Date: Sat, 13 Jun 2026 20:07:44 +0900 Subject: [PATCH 5/5] G075: align governance-workflow action majors to codex-toolkit (dependency-review v5, gitleaks v3) --- .github/workflows/dependency-review.yml | 4 ++-- .github/workflows/secrets-scan.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 304ed64..c80ef54 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -16,8 +16,8 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Dependency Review - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@v5 with: fail-on-severity: high diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml index 1329095..c22a188 100644 --- a/.github/workflows/secrets-scan.yml +++ b/.github/workflows/secrets-scan.yml @@ -18,10 +18,10 @@ jobs: timeout-minutes: 10 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Run gitleaks - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}