feat(test): Add CTest infrastructure and improve UnitTestFramework #698
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] | |
| 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: Check PR title format | |
| id: title_check | |
| continue-on-error: true | |
| uses: amannn/action-semantic-pull-request@v6 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.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: | |
| 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 { data: files } = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.payload.pull_request.number, | |
| per_page: 50 | |
| }); | |
| return files.map(f => `${f.filename} (+${f.additions}/-${f.deletions})`).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 unique delimiters | |
| { | |
| echo "title<<TITLE_END_7f3a9c2e" | |
| echo "$SAFE_TITLE" | |
| echo "TITLE_END_7f3a9c2e" | |
| echo "body<<BODY_END_8b4d1e6f" | |
| echo "$SAFE_BODY" | |
| echo "BODY_END_8b4d1e6f" | |
| echo "files<<FILES_END_2c5a8d9b" | |
| echo "$SAFE_FILES" | |
| echo "FILES_END_2c5a8d9b" | |
| } >> "$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' && 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 { data: comments } = await 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.*`; | |
| 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 { data: comments } = await 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 -->', '')} | |
| </details>`; | |
| 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: Get current size label | |
| id: current | |
| run: | | |
| PR_NUMBER=${{ github.event.pull_request.number }} | |
| CURRENT_LABEL=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/labels" \ | |
| --jq '.[] | select(.name | startswith("size/")) | .name' | head -1) | |
| echo "label=${CURRENT_LABEL}" >> "$GITHUB_OUTPUT" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Label PR by size | |
| continue-on-error: true | |
| uses: codelytv/pr-size-labeler@v1 | |
| with: | |
| GITHUB_TOKEN: ${{ secrets.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 | |
| run: | | |
| PR_NUMBER=${{ github.event.pull_request.number }} | |
| OLD_LABEL="${{ steps.current.outputs.label }}" | |
| # Get all current size labels | |
| CURRENT_LABELS=$(gh api "repos/${{ github.repository }}/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=$(echo "$OLD_LABEL" | sed 's|/|%2F|g') | |
| gh api "repos/${{ github.repository }}/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 | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| label: | |
| name: Label PR by Files | |
| runs-on: ubuntu-latest | |
| if: github.event.action != 'closed' | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Label PR by changed files | |
| uses: actions/labeler@v6 | |
| with: | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| sync-labels: true | |