feat(github,dotfiles): add CI to check PR title scope against changed files #2
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| }); |