Skip to content

Align EEST validation mappings and fixture coverage #15941

Align EEST validation mappings and fixture coverage

Align EEST validation mappings and fixture coverage #15941

Workflow file for this run

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');
}