Plan toolbar: add currentPlanFileName property and improve toolbar UX #1475
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: PR Checks | |
| on: | |
| pull_request_target: | |
| types: [opened, edited, synchronize, reopened, closed] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| models: read | |
| jobs: | |
| title: | |
| name: Validate PR Title | |
| runs-on: ubuntu-latest | |
| if: github.event.action != 'closed' | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - name: Check PR title format | |
| id: title_check | |
| continue-on-error: true | |
| uses: amannn/action-semantic-pull-request@v6 | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| with: | |
| types: | | |
| feat | |
| fix | |
| docs | |
| style | |
| refactor | |
| perf | |
| test | |
| build | |
| ci | |
| chore | |
| revert | |
| requireScope: false | |
| subjectPattern: ^.{1,80}$ | |
| subjectPatternError: PR title must be 80 characters or less. | |
| wip: true | |
| validateSingleCommit: false | |
| - name: Checkout prompt file | |
| if: | | |
| steps.title_check.outcome == 'failure' && | |
| github.event.pull_request.user.type != 'Bot' | |
| uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.base.sha }} | |
| sparse-checkout: .github/prompts | |
| sparse-checkout-cone-mode: false | |
| - name: Get changed files | |
| id: files | |
| if: | | |
| steps.title_check.outcome == 'failure' && | |
| github.event.pull_request.user.type != 'Bot' | |
| uses: actions/github-script@v8 | |
| with: | |
| result-encoding: string | |
| script: | | |
| const files = await github.paginate( | |
| github.rest.pulls.listFiles, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.payload.pull_request.number, | |
| per_page: 100 | |
| } | |
| ); | |
| const maxFiles = 200; | |
| const lines = files | |
| .slice(0, maxFiles) | |
| .map(f => `${f.filename} (+${f.additions}/-${f.deletions})`); | |
| if (files.length > maxFiles) { | |
| lines.push(`... (${files.length - maxFiles} more files)`); | |
| } | |
| return lines.join('\n'); | |
| - name: Sanitize inputs for AI | |
| id: sanitize | |
| if: | | |
| steps.title_check.outcome == 'failure' && | |
| github.event.pull_request.user.type != 'Bot' | |
| shell: bash | |
| env: | |
| PR_TITLE: ${{ github.event.pull_request.title }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| CHANGED_FILES: ${{ steps.files.outputs.result }} | |
| run: | | |
| # Truncate inputs to prevent token flooding | |
| SAFE_TITLE=$(echo "$PR_TITLE" | head -c 200 | tr -d '\r') | |
| SAFE_BODY=$(echo "$PR_BODY" | head -c 2000 | tr -d '\r') | |
| SAFE_FILES=$(echo "$CHANGED_FILES" | head -30) | |
| # Write to outputs using heredoc with dynamic delimiters to prevent injection | |
| TITLE_DELIM="TITLE_EOF_$(echo "$SAFE_TITLE" | sha256sum | head -c 16)" | |
| BODY_DELIM="BODY_EOF_$(echo "$SAFE_BODY" | sha256sum | head -c 16)" | |
| FILES_DELIM="FILES_EOF_$(echo "$SAFE_FILES" | sha256sum | head -c 16)" | |
| { | |
| echo "title<<${TITLE_DELIM}" | |
| echo "$SAFE_TITLE" | |
| echo "${TITLE_DELIM}" | |
| echo "body<<${BODY_DELIM}" | |
| echo "$SAFE_BODY" | |
| echo "${BODY_DELIM}" | |
| echo "files<<${FILES_DELIM}" | |
| echo "$SAFE_FILES" | |
| echo "${FILES_DELIM}" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Suggest title with GitHub Models | |
| id: suggest | |
| if: | | |
| steps.title_check.outcome == 'failure' && | |
| github.event.pull_request.user.type != 'Bot' | |
| continue-on-error: true | |
| uses: actions/ai-inference@v2 | |
| with: | |
| prompt-file: .github/prompts/pr-title-suggestion.prompt.yml | |
| max-tokens: 100 | |
| input: | | |
| current_title: ${{ steps.sanitize.outputs.title }} | |
| pr_body: ${{ steps.sanitize.outputs.body }} | |
| changed_files: ${{ steps.sanitize.outputs.files }} | |
| - name: Comment title suggestion | |
| if: steps.title_check.outcome == 'failure' && github.event.pull_request.user.type != 'Bot' && steps.suggest.outputs.response | |
| uses: actions/github-script@v8 | |
| env: | |
| SUGGESTED_TITLE: ${{ steps.suggest.outputs.response }} | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| let suggestedTitle = process.env.SUGGESTED_TITLE.trim(); | |
| // Validate AI output format - must be conventional commit | |
| const validFormat = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-zA-Z0-9\-\/]+\))?:\s.{1,80}$/; | |
| if (!validFormat.test(suggestedTitle)) { | |
| console.log('AI response did not match conventional commit format, skipping'); | |
| return; | |
| } | |
| // Escape HTML entities for safety | |
| const escapeHtml = (str) => str | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/`/g, '`'); | |
| const currentTitle = escapeHtml(pr.title); | |
| suggestedTitle = escapeHtml(suggestedTitle); | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| per_page: 100 | |
| } | |
| ); | |
| const botComment = comments.find(c => | |
| c.user?.type === 'Bot' && c.body?.includes('<!-- pr-title-suggestion -->') | |
| ); | |
| const commentBody = [ | |
| '<!-- pr-title-suggestion -->', | |
| '## PR Title Suggestion', | |
| '', | |
| "Your PR title doesn't follow the [conventional commit](https://www.conventionalcommits.org/) format.", | |
| '', | |
| '| Current Title | Suggested Title |', | |
| '|---------------|-----------------|', | |
| `| \`${currentTitle}\` | \`${suggestedTitle}\` |`, | |
| '', | |
| '### Format', | |
| '`<type>(<scope>): <description>`', | |
| '', | |
| '**Types:** `feat` `fix` `docs` `refactor` `perf` `test` `ci` `chore` `build` `style` `revert`', | |
| '', | |
| '*Suggested by GitHub Models. Update your title to dismiss this suggestion.*' | |
| ].join('\n'); | |
| if (botComment) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: commentBody | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: commentBody | |
| }); | |
| } | |
| - name: Minimize suggestion if title valid | |
| if: steps.title_check.outcome == 'success' | |
| uses: actions/github-script@v8 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| per_page: 100 | |
| } | |
| ); | |
| const botComment = comments.find(c => | |
| c.user?.type === 'Bot' && c.body?.includes('<!-- pr-title-suggestion -->') | |
| ); | |
| if (botComment && !botComment.body.includes('(resolved)')) { | |
| const minimizedBody = [ | |
| '<!-- pr-title-suggestion -->', | |
| '<details>', | |
| '<summary>PR Title Suggestion (resolved)</summary>', | |
| '', | |
| botComment.body.replace('<!-- pr-title-suggestion -->', '').trim(), | |
| '</details>' | |
| ].join('\n'); | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: minimizedBody | |
| }); | |
| } | |
| size: | |
| name: Label PR Size | |
| runs-on: ubuntu-latest | |
| if: github.event.action != 'closed' | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - name: Get current size label | |
| id: current | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| CURRENT_LABEL=$(gh api "repos/${GH_REPO}/issues/${PR_NUMBER}/labels" \ | |
| --jq '.[] | select(.name | startswith("size/")) | .name' | head -1) | |
| echo "label=${CURRENT_LABEL}" >> "$GITHUB_OUTPUT" | |
| - name: Label PR by size | |
| continue-on-error: true | |
| uses: codelytv/pr-size-labeler@v1 | |
| with: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| xs_label: 'size/XS' | |
| xs_max_size: 150 | |
| s_label: 'size/S' | |
| s_max_size: 500 | |
| m_label: 'size/M' | |
| m_max_size: 1000 | |
| l_label: 'size/L' | |
| l_max_size: 1500 | |
| xl_label: 'size/XL' | |
| fail_if_xl: false | |
| message_if_xl: | | |
| ## Large PR Warning | |
| This PR has **more than 1500 lines** changed, which can be difficult to review thoroughly. | |
| Consider: | |
| - Splitting into smaller, focused PRs | |
| - Separating refactoring from feature changes | |
| - Breaking out unrelated changes into separate PRs | |
| Smaller PRs get reviewed faster and reduce the risk of bugs slipping through. | |
| files_to_ignore: | | |
| package-lock.json | |
| *.lock | |
| translations/* | |
| - name: Remove outdated size labels | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| OLD_LABEL: ${{ steps.current.outputs.label }} | |
| GH_REPO: ${{ github.repository }} | |
| run: | | |
| # Get all current size labels | |
| CURRENT_LABELS=$(gh api "repos/${GH_REPO}/issues/${PR_NUMBER}/labels" \ | |
| --jq '.[] | select(.name | startswith("size/")) | .name') | |
| # Count size labels - if more than one, remove the old one | |
| LABEL_COUNT=$(echo "$CURRENT_LABELS" | grep -c . || echo 0) | |
| if [[ "$LABEL_COUNT" -gt 1 && -n "$OLD_LABEL" ]]; then | |
| # Multiple size labels exist - remove the old one | |
| ENCODED_LABEL="${OLD_LABEL//\//%2F}" | |
| gh api "repos/${GH_REPO}/issues/${PR_NUMBER}/labels/${ENCODED_LABEL}" \ | |
| -X DELETE 2>/dev/null || true | |
| fi | |
| # If only one label exists, size didn't change - nothing to remove | |
| label: | |
| name: Label PR by Files | |
| runs-on: ubuntu-latest | |
| if: github.event.action != 'closed' | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@v2 | |
| with: | |
| egress-policy: audit | |
| - name: Label PR by changed files | |
| uses: actions/labeler@v6 | |
| with: | |
| repo-token: ${{ github.token }} | |
| sync-labels: true |