Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/check_pr_scope.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: Check PR Scope

on:
pull_request:
types: [opened, synchronize, reopened, edited]

jobs:
check_scope:
runs-on: ubuntu-latest
outputs:
outcome: ${{ steps.check_scope_step.outcome }}
pr_scopes: ${{ steps.check_scope_step.outputs.pr_scopes }}
files: ${{ steps.changed_files.outputs.files }}
required_scopes: ${{ steps.check_scope_step.outputs.required_scopes }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for all branches and tags

- name: Get PR title
id: pr_title
run: |

Check warning on line 22 in .github/workflows/check_pr_scope.yml

View workflow job for this annotation

GitHub Actions / action-lint

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2086:info:3:24: Double quote to prevent globbing and word splitting [shellcheck] Raw Output: i:.github/workflows/check_pr_scope.yml:22:9: shellcheck reported issue in this script: SC2086:info:3:24: Double quote to prevent globbing and word splitting [shellcheck]
title=$(jq -r .pull_request.title "$GITHUB_EVENT_PATH")
echo "$title"
echo "title=$title" >> $GITHUB_OUTPUT

- name: Debug PR title output
run: |
echo "PR title from output: ${{ steps.pr_title.outputs.title }}"

- name: Get changed files
id: changed_files
env:
BASE_REF: ${{ github.base_ref }}
HEAD_REF: ${{ github.head_ref }}
run: |
files=$(git diff --name-only origin/"$BASE_REF"...origin/"$HEAD_REF")
{
echo "files<<EOF"
echo "$files"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Debug before Check PR scope
run: |
echo "PR title just before check: ${{ steps.pr_title.outputs.title }}"
echo "Changed files: ${{ steps.changed_files.outputs.files }}"

- name: Check PR scope
id: check_scope_step # Add id to this step
run: |
chmod +x scripts/check_pr_scope.sh
./scripts/check_pr_scope.sh "${{ steps.pr_title.outputs.title }}" "${{ steps.changed_files.outputs.files }}"

comment_on_pr:
if: failure() && needs.check_scope.outputs.outcome == 'failure'
needs: check_scope
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Set comment body
id: set_comment_body
run: |

Check warning on line 64 in .github/workflows/check_pr_scope.yml

View workflow job for this annotation

GitHub Actions / action-lint

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2086:info:5:6: Double quote to prevent globbing and word splitting [shellcheck] Raw Output: i:.github/workflows/check_pr_scope.yml:64:9: shellcheck reported issue in this script: SC2086:info:5:6: Double quote to prevent globbing and word splitting [shellcheck]

Check warning on line 64 in .github/workflows/check_pr_scope.yml

View workflow job for this annotation

GitHub Actions / action-lint

[actionlint] reported by reviewdog 🐶 shellcheck reported issue in this script: SC2028:info:3:8: echo may not expand escape sequences. Use printf [shellcheck] Raw Output: i:.github/workflows/check_pr_scope.yml:64:9: shellcheck reported issue in this script: SC2028:info:3:8: echo may not expand escape sequences. Use printf [shellcheck]
{
echo 'comment_body<<EOF'
echo "PRのスコープが正しくありません。\n現在のスコープ: ${{ needs.check_scope.outputs.pr_scopes }}\n検知したスコープ: ${{ needs.check_scope.outputs.required_scopes }}"
echo 'EOF'
} >> $GITHUB_OUTPUT

- name: Comment on PR
uses: actions/github-script@v7
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const body = `${{ steps.set_comment_body.outputs.comment_body }}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});
24 changes: 24 additions & 0 deletions scope-rules.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"rules": [
{
"pattern": "^config/(.+)/.*",
"scope": "$1"
},
{
"pattern": "^scripts/(.+)/.*",
"scope": "$1"
},
{
"pattern": "^data/(.+)/.*",
"scope": "$1"
},
{
"pattern": "^\\.github/.*",
"scope": "github"
},
{
"pattern": ".*",
"scope": "dotfiles"
}
]
}
156 changes: 156 additions & 0 deletions scripts/check_pr_scope.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -euo pipefail

pr_title_raw="$1"
changed_files="$2"

# Sanitize pr_title_raw by removing CR/LF characters that might come from jq output
pr_title=$(echo "$pr_title_raw" | tr -d '\r\n')

if [ -z "$pr_title" ]; then
error_message="PR title is empty. Please provide a title in the format 'type(scope): message'."
echo "::error::$error_message"
comment_body="**Scope Check Failed!** 🚨\n\n${error_message}\n\nExample: \`feat(my-scope): add new feature\`."
echo "$comment_body" > comment_file.txt
{
echo "comment_body<<EOF_CMT"
cat comment_file.txt
echo "EOF_CMT"
} >> "$GITHUB_ENV"
exit 1
fi

# Extract scope from PR title (e.g., "fix(scope1,scope2): ...")
pr_scopes_str=$(echo "$pr_title" | grep -oP '^\w+\(\K[^\)]+' || echo "")
IFS=',' read -r -a pr_scopes <<< "$pr_scopes_str"
# Trim whitespace from scopes
mapfile -t pr_scopes < <(for scope in "${pr_scopes[@]}"; do echo "$scope" | xargs; done)

if [ ${#pr_scopes[@]} -eq 0 ] && [[ "$pr_scopes_str" != "*" ]]; then
error_message="PR title does not contain a valid scope. Please use the format 'type(scope): message' or 'type(*): message'."
echo "::error::$error_message"
comment_body="**Scope Check Failed!** 🚨\n\n${error_message}\n\nTitle received: \`$pr_title\`\n\nExample: \`feat(my-scope): add new feature\` or \`fix(*): resolve an issue\`."
echo "$comment_body" > comment_file.txt
{
echo "comment_body<<EOF_CMT"
cat comment_file.txt
echo "EOF_CMT"
} >> "$GITHUB_ENV"
exit 1
fi

echo "PR Scopes: ${pr_scopes[*]}"

# Load scope rules
if [ ! -f "scope-rules.json" ]; then
echo "::error::scope-rules.json not found."
exit 1
fi
rules=$(jq -r '.rules' < scope-rules.json)

required_scopes=()
while IFS= read -r file; do
matched_scope=""
for i in $(seq 0 $(($(echo "$rules" | jq length) - 1))); do
pattern=$(echo "$rules" | jq -r ".[$i].pattern")
scope_template=$(echo "$rules" | jq -r ".[$i].scope")

if [[ "$file" =~ $pattern ]]; then
# Handle scope template with capture groups (e.g., $1)
if [[ "$scope_template" == "\$1" ]]; then
matched_scope="${BASH_REMATCH[1]}"
else
matched_scope="$scope_template"
fi
break
fi
done

if [ -n "$matched_scope" ]; then
# Add to required_scopes if not already present
if [[ ! " ${required_scopes[*]} " =~ ${matched_scope} ]]; then
required_scopes+=("$matched_scope")
fi
else
# If no rule matches, consider it an error or a default scope based on requirements
# For now, let's assume if a file doesn't match any rule, it's an issue.
# Or, define a default scope in scope-rules.json like { "pattern": ".*", "scope": "default" }
echo "::warning::No matching scope rule for file: $file"
fi
done <<< "$changed_files"

echo "Required Scopes based on changed files: ${required_scopes[*]}"

# Output required_scopes for GitHub Actions output
if [ -n "${GITHUB_OUTPUT:-}" ]; then
echo "required_scopes=${required_scopes[*]}" >> "$GITHUB_OUTPUT"
echo "pr_scopes=${pr_scopes[*]}" >> "$GITHUB_OUTPUT"
fi

if [[ ${pr_scopes[*]} =~ \* ]]; then
echo "Wildcard scope '*' in PR title allows all changes."
exit 0
fi

missing_scopes=()
for req_scope in "${required_scopes[@]}"; do
is_covered=false
for pr_scope in "${pr_scopes[@]}"; do
if [ "$req_scope" == "$pr_scope" ]; then
is_covered=true
break
fi
done
if [ "$is_covered" == false ]; then
if [[ ! " ${pr_scopes[*]} " =~ $req_scope ]]; then
missing_scopes+=("$req_scope")
fi
fi
done

if [ ${#missing_scopes[@]} -gt 0 ]; then
echo "::error::PR title scopes do not cover all changed files. Missing scopes for: ${missing_scopes[*]}"
# Prepare message for PR comment
comment_body="**Scope Check Failed!** 🚨\n\nPR title scopes do not cover all changed files.\n\n"
if [ ${#missing_scopes[@]} -gt 0 ]; then
comment_body+="Missing required scopes in PR title: \`${missing_scopes[*]}\`\n\n" # Use [*] for space separated, or loop for comma separated

suggestion_scopes=""
if [ -n "$pr_scopes_str" ] && [[ "$pr_scopes_str" != "*" ]]; then
suggestion_scopes="${pr_scopes_str}"
fi

# Build comma-separated list for suggestion_scopes
for missing_scope in "${missing_scopes[@]}"; do
if [ -n "$suggestion_scopes" ]; then
suggestion_scopes+=",";
fi
suggestion_scopes+="$missing_scope"
done

comment_body+="Please update your PR title to include these scopes. For example: \`type($suggestion_scopes): your message\` or \`type(*): your message\` if a wildcard is appropriate.\n\n"
fi
comment_body+="<details><summary>Details</summary>\n"
# Ensure pr_scopes are displayed correctly, even if empty or just wildcard
pr_scopes_display="${pr_scopes[*]}"
if [ -z "$pr_scopes_display" ] && [ "$pr_scopes_str" == "*" ]; then
pr_scopes_display="*"
elif [ -z "$pr_scopes_display" ]; then
pr_scopes_display="(none)"
fi
comment_body+="PR Title Scopes: \`${pr_scopes_display}\`\n"
comment_body+="Required Scopes from changed files: \`${required_scopes[*]}\`\n"
comment_body+="Changed Files:\n"
comment_body+="\`\`\`\n${changed_files}\n\`\`\`\n"
comment_body+="</details>"

echo "$comment_body" > comment_file.txt
{
echo "comment_body<<EOF_CMT"
cat comment_file.txt
echo "EOF_CMT"
} >> "$GITHUB_ENV"
exit 1
else
echo "PR title scopes are valid."
fi
Loading