Skip to content

feat(github,dotfiles): add CI to check PR title scope against changed files #2

feat(github,dotfiles): add CI to check PR title scope against changed files

feat(github,dotfiles): add CI to check PR title scope against changed files #2

Workflow file for this run

name: Check PR Scope
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
check_scope:
runs-on: ubuntu-latest
outputs: # Define outputs for the job
outcome: ${{ steps.check_scope_step.outcome }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Fetch all history for all branches and tags
- name: Get PR title
id: pr_title
run: echo "title=$(jq -r .pull_request.title \"$GITHUB_EVENT_PATH\")" >> $GITHUB_OUTPUT
- name: Get changed files
id: changed_files
run: |
files=$(git diff --name-only origin/${{ github.base_ref }}...origin/${{ github.head_ref }})
echo "files<<EOF" >> $GITHUB_OUTPUT
echo "$files" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Check PR scope
id: check_scope_step # Add id to this step
run: |
pr_title="${{ steps.pr_title.outputs.title }}"
changed_files="${{ steps.changed_files.outputs.files }}"
echo "PR Title: $pr_title"
echo "Changed Files:"
echo "$changed_files"
# 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
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\nExample: \`feat(my-scope): add new feature\` or \`fix(*): resolve an issue\`."
echo "comment_body=$comment_body" >> $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=$(cat scope-rules.json | jq -r '.rules')
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
# BASH_REMATCH[0] is the full match, BASH_REMATCH[1] is the first capture group
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[@]}"
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
missing_scopes+=("$req_scope")
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"
comment_body+="Missing required scopes in PR title: \`${missing_scopes[@]}\`\n\n"
comment_body+="Please update your PR title to include these scopes. For example: \`type(${pr_scopes_str:+${pr_scopes_str},}${missing_scopes[@]}): your message\` or \`type(*): your message\` if a wildcard is appropriate.\n\n"
comment_body+="<details><summary>Details</summary>\n"
comment_body+="PR Title Scopes: \`${pr_scopes[@]}\`\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_body" >> $GITHUB_ENV
exit 1
else
echo "PR title scopes are valid."
fi
comment_on_pr:
if: failure() && needs.check_scope.outputs.outcome == 'failure'
needs: check_scope
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Comment on PR
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const body = `${process.env.comment_body}`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: body
});