Align EEST validation mappings and fixture coverage #15941
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 labeler | |
| on: | |
| pull_request_target: | |
| types: [opened, edited, ready_for_review, synchronize] | |
| jobs: | |
| label: | |
| name: Label PR by type | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Create GitHub app token | |
| id: gh-app | |
| uses: actions/create-github-app-token@v3 | |
| with: | |
| app-id: ${{ vars.APP_ID }} | |
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | |
| # Labels added with the default GITHUB_TOKEN do not emit workflow-triggering events, so | |
| # label-triggered runs (e.g. the reproducible benchmarks) never fired on auto-labeling. | |
| - name: Apply labels from PR template checkboxes | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ steps.gh-app.outputs.token }} | |
| script: | | |
| // --- Checkbox rules --- | |
| const checkboxToLabel = { | |
| 'Bugfix (a non-breaking change that fixes an issue)': 'bug fix + reliability', | |
| 'New feature (a non-breaking change that adds functionality)': 'new feature', | |
| 'Breaking change (a change that causes existing functionality not to work as expected)': 'BREAKING', | |
| 'Optimization': 'performance is good', | |
| 'Refactoring': 'refactoring', | |
| 'Documentation update': 'docs', | |
| 'Build-related changes': 'build changes', | |
| }; | |
| // --- Title prefix rules (conventional commits) --- | |
| const prefixToLabel = { | |
| 'fix': 'bug fix + reliability', | |
| 'feat': 'new feature', | |
| 'perf': 'performance is good', | |
| 'refactor': 'refactoring', | |
| 'chore': 'minor', | |
| 'docs': 'docs', | |
| 'test': 'test', | |
| 'ci': 'build changes', | |
| 'build': 'build changes', | |
| }; | |
| // --- Path rules --- | |
| const pathToLabel = [ | |
| { pattern: 'src/Nethermind/Nethermind.Optimism', label: 'optimism' }, | |
| { pattern: 'src/Nethermind/Nethermind.Taiko', label: 'taiko' }, | |
| { pattern: 'src/Nethermind/Nethermind.Xdc', label: 'XDC' }, | |
| { pattern: 'src/Nethermind/Nethermind.Network', label: 'network' }, | |
| { pattern: 'src/Nethermind/Nethermind.Evm', label: 'evm' }, | |
| { pattern: 'src/Nethermind/Nethermind.Trie', label: 'trie' }, | |
| { pattern: 'src/Nethermind/Nethermind.State', label: 'state+storage' }, | |
| { pattern: 'src/Nethermind/Nethermind.Synchronization', label: 'sync' }, | |
| { pattern: 'src/Nethermind/Nethermind.JsonRpc', label: 'rpc' }, | |
| { pattern: 'src/Nethermind/Nethermind.Db.Rocks', label: 'rocksdb' }, | |
| { pattern: 'src/Nethermind/Nethermind.Db', label: 'database' }, | |
| { pattern: 'src/Nethermind/Nethermind.Init/Modules/DbModule.cs', label: 'database' }, | |
| { pattern: 'src/Nethermind/Nethermind.Runner/configs', label: 'configuration' }, | |
| { pattern: 'src/Nethermind/Nethermind.Config', label: 'configuration' }, | |
| { pattern: 'README.md', label: 'docs' }, | |
| { pattern: 'AGENTS.md', label: 'agentic 🤖' }, | |
| { pattern: '.github/', label: 'devops' }, | |
| { pattern: 'Directory.Packages.props', label: 'dependencies' }, | |
| { pattern: 'tools/', label: 'tools' }, | |
| ]; | |
| // test label: applied when ALL changed files are in a .Test project | |
| const testOnlyLabel = 'test'; | |
| const checkboxLabels = new Set(Object.values(checkboxToLabel)); | |
| const prefixLabels = new Set(Object.values(prefixToLabel)); | |
| const pathLabels = new Set(pathToLabel.map(r => r.label)); | |
| const managedLabels = new Set([...checkboxLabels, ...prefixLabels, ...pathLabels, testOnlyLabel, 'cleanup', 'snap sync']); | |
| const body = context.payload.pull_request.body || ''; | |
| const title = context.payload.pull_request.title || ''; | |
| const desiredLabels = new Set(); | |
| // Evaluate checkboxes | |
| for (const [text, label] of Object.entries(checkboxToLabel)) { | |
| const escaped = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| if (new RegExp(`-\\s*\\[\\s*[xX]\\s*\\]\\s*${escaped}`).test(body)) { | |
| desiredLabels.add(label); | |
| } | |
| } | |
| // Evaluate "Other" checkbox with keyword matching | |
| const otherMatch = body.match(/-\s*\[\s*[xX]\s*\]\s*Other:\s*(.+)/); | |
| if (otherMatch) { | |
| const otherText = otherMatch[1].toLowerCase(); | |
| if (/\btest/.test(otherText)) desiredLabels.add('test'); | |
| if (/\btool/.test(otherText)) desiredLabels.add('tools'); | |
| if (/\bagent/.test(otherText)) desiredLabels.add('agentic 🤖'); | |
| if (/\bdoc/.test(otherText)) desiredLabels.add('docs'); | |
| } | |
| // Evaluate title prefix: supports "fix:", "fix(scope):", "(fix)", "[fix]" | |
| const prefixMatch = title.match(/^[(\[]?(\w+)[)\]]?[\s(:/]/) | |
| if (prefixMatch) { | |
| const prefix = prefixMatch[1].toLowerCase(); | |
| if (prefixToLabel[prefix]) { | |
| desiredLabels.add(prefixToLabel[prefix]); | |
| } | |
| } | |
| // Evaluate title for EIP mentions (eip-1234, EIP 1234, eip1234, etc.) | |
| if (/eip[-\s]?\d+/i.test(title)) { | |
| desiredLabels.add('eip'); | |
| } | |
| // Evaluate title for optimization keyword | |
| if (/\boptimiz/i.test(title)) { | |
| desiredLabels.add('performance is good'); | |
| } | |
| // Evaluate changed file paths | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: context.issue.number, | |
| per_page: 100, | |
| }); | |
| for (const { pattern, label } of pathToLabel) { | |
| if (files.some(f => f.filename.startsWith(pattern))) { | |
| desiredLabels.add(label); | |
| } | |
| } | |
| // SnapSync in path | |
| if (files.some(f => /SnapSync/i.test(f.filename))) { | |
| desiredLabels.add('snap sync'); | |
| } | |
| // Dockerfile changes | |
| if (files.some(f => /(?:^|\/)[Dd]ockerfile/.test(f.filename))) { | |
| desiredLabels.add('devops'); | |
| } | |
| // Chain config files with integration keywords | |
| const chainKeywordToLabel = { | |
| 'taiko': 'taiko', | |
| 'optimism': 'optimism', | |
| 'op-': 'optimism', | |
| 'xdc': 'XDC', | |
| }; | |
| for (const f of files) { | |
| if (/^src\/Nethermind\/Chains\//.test(f.filename)) { | |
| const name = f.filename.toLowerCase(); | |
| for (const [keyword, label] of Object.entries(chainKeywordToLabel)) { | |
| if (name.includes(keyword)) { | |
| desiredLabels.add(label); | |
| } | |
| } | |
| } | |
| } | |
| // Apply test label when all changed files are in Test projects | |
| if (files.length > 0 && files.every(f => /\.Test[s]?\//.test(f.filename))) { | |
| desiredLabels.add(testOnlyLabel); | |
| } | |
| // Apply cleanup label when PR only removes code | |
| if (files.length > 0 && files.every(f => f.additions === 0 && f.deletions > 0)) { | |
| desiredLabels.add('cleanup'); | |
| } | |
| // Diff against current labels | |
| const currentLabels = new Set( | |
| context.payload.pull_request.labels.map(l => l.name) | |
| ); | |
| const toAdd = [...desiredLabels].filter(l => !currentLabels.has(l)); | |
| const toRemove = [...currentLabels].filter(l => managedLabels.has(l) && !desiredLabels.has(l)); | |
| if (toAdd.length > 0) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| labels: toAdd, | |
| }); | |
| core.info(`Added labels: ${toAdd.join(', ')}`); | |
| } | |
| for (const label of toRemove) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| name: label, | |
| }); | |
| core.info(`Removed label: ${label}`); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| if (toAdd.length === 0 && toRemove.length === 0) { | |
| core.info('No label changes needed'); | |
| } |