Skip to content

Commit 2600406

Browse files
authored
ci: Add PR auto labeling workflows (#488)
## Description This PR adds auto labeling GitHub actions: - uses a github's labeler that adds labels based on changed files, - uses a custom labeler action that adds labels based on the PR title prefix.
1 parent 246bb03 commit 2600406

8 files changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: 'PR Title Labeler'
2+
description: 'Automatically label PRs based on their title prefix'
3+
inputs:
4+
github-token:
5+
description: 'GitHub token for API access'
6+
required: true
7+
config-file:
8+
description: 'Path to YAML config file with label mappings (default: pr-title-labeler-config.yml)'
9+
required: false
10+
default: 'pr-title-labeler-config.yml'
11+
runs:
12+
using: 'composite'
13+
steps:
14+
- name: 📦 Install dependencies
15+
run: npm install
16+
shell: bash
17+
working-directory: ${{ github.action_path }}
18+
19+
- name: 🏷️ Run PR Title Labeler
20+
run: node index.js
21+
shell: bash
22+
working-directory: ${{ github.action_path }}
23+
env:
24+
GITHUB_TOKEN: ${{ inputs.github-token }}
25+
CONFIG_FILE: ${{ inputs.config-file }}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* PR Title Labeler Action
5+
* Labels PRs based on the title prefix
6+
*/
7+
8+
const { Octokit } = require('@octokit/rest');
9+
const yaml = require('js-yaml');
10+
const fs = require('fs');
11+
const path = require('path');
12+
13+
async function run() {
14+
try {
15+
// Get environment variables
16+
const githubToken = process.env.GITHUB_TOKEN;
17+
const configFilePath =
18+
process.env.CONFIG_FILE || 'pr-title-labeler-config.yml';
19+
const eventPath = process.env.GITHUB_EVENT_PATH;
20+
const repository = process.env.GITHUB_REPOSITORY;
21+
22+
if (!githubToken) {
23+
console.error('❌ GITHUB_TOKEN is required');
24+
process.exit(1);
25+
}
26+
27+
if (!eventPath) {
28+
console.error('❌ GITHUB_EVENT_PATH is required');
29+
process.exit(1);
30+
}
31+
32+
// Read GitHub event data
33+
const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
34+
const pr = eventData.pull_request;
35+
36+
if (!pr) {
37+
console.error('❌ No pull request found in event data');
38+
process.exit(1);
39+
}
40+
41+
// Get config file path from environment variable
42+
const configPath = path.resolve(__dirname, configFilePath);
43+
44+
let labelMappings;
45+
try {
46+
const configContent = fs.readFileSync(configPath, 'utf8');
47+
labelMappings = yaml.load(configContent);
48+
} catch (error) {
49+
console.error(
50+
`❌ Error reading config file "${configFilePath}": ${error.message}`
51+
);
52+
process.exit(1);
53+
}
54+
55+
const title = pr.title.toLowerCase();
56+
57+
// Find matching prefix and label
58+
let label = null;
59+
for (const mapping of labelMappings) {
60+
for (const prefix of mapping.prefixes) {
61+
// Try both with and without colon for flexibility
62+
const prefixWithColon = `${prefix}:`;
63+
if (title.startsWith(prefixWithColon)) {
64+
label = mapping.label;
65+
console.log(
66+
`Found prefix "${prefixWithColon}" -> applying label "${label}"`
67+
);
68+
break;
69+
}
70+
}
71+
if (label) break;
72+
}
73+
74+
// Create GitHub client
75+
const octokit = new Octokit({
76+
auth: githubToken
77+
});
78+
79+
// Parse repository owner and name
80+
const [owner, repo] = repository.split('/');
81+
82+
// Apply label if found
83+
if (label) {
84+
await octokit.rest.issues.addLabels({
85+
owner,
86+
repo,
87+
issue_number: pr.number,
88+
labels: [label]
89+
});
90+
91+
console.log(`✅ Successfully applied label: ${label}`);
92+
} else {
93+
console.log('ℹ️ No matching prefix found in PR title');
94+
console.log(`PR title: "${title}"`);
95+
const allPrefixes = labelMappings.flatMap(mapping => mapping.prefixes);
96+
console.log('Available prefixes:', allPrefixes.join(', '));
97+
}
98+
} catch (error) {
99+
console.error(`❌ Error labeling PR: ${error.message}`);
100+
process.exit(1);
101+
}
102+
}
103+
104+
run();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "pr-title-labeler-action",
3+
"version": "1.0.0",
4+
"description": "GitHub Action for labeling PRs based on title prefix",
5+
"main": "index.js",
6+
"dependencies": {
7+
"@octokit/rest": "^20.0.2",
8+
"js-yaml": "^4.1.0"
9+
},
10+
"engines": {
11+
"node": ">=20"
12+
}
13+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# PR Title Labeler Configuration
2+
# Each label can have multiple prefixes that will map to it
3+
4+
- label: feature
5+
prefixes:
6+
- feat
7+
- feature
8+
9+
- label: fix
10+
prefixes:
11+
- fix
12+
- bugfix
13+
14+
- label: documentation
15+
prefixes:
16+
- docs
17+
- doc
18+
19+
- label: cleanup
20+
prefixes:
21+
- style
22+
- format
23+
24+
- label: refactor
25+
prefixes:
26+
- refactor
27+
28+
- label: performance
29+
prefixes:
30+
- perf
31+
- performance
32+
33+
- label: test
34+
prefixes:
35+
- test
36+
- tests
37+
38+
- label: chore
39+
prefixes:
40+
- chore
41+
- build
42+
43+
- label: ci
44+
prefixes:
45+
- ci
46+
47+
- label: dependencies
48+
prefixes:
49+
- deps
50+
- dependencies
51+
52+
- label: example
53+
prefixes:
54+
- example
55+
- demo
56+
57+
- label: enhancement
58+
prefixes:
59+
- enhancement
60+
61+
- label: critical
62+
prefixes:
63+
- critical
64+
65+
- label: released
66+
prefixes:
67+
- released

.github/labeler.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Documentation changes
2+
'documentation':
3+
- changed-files:
4+
- any-glob-to-any-file: ['packages/docs/**', 'README.md']
5+
6+
# Example applications
7+
'example':
8+
- changed-files:
9+
- any-glob-to-any-file: ['example/**']
10+
11+
# Test files
12+
'test':
13+
- changed-files:
14+
- any-glob-to-any-file:
15+
[
16+
'**/*.test.*',
17+
'**/*.spec.*',
18+
'**/__tests__/**',
19+
'**/tests/**',
20+
'**/__mocks__/**'
21+
]
22+
23+
# CI/CD changes
24+
'ci':
25+
- changed-files:
26+
- any-glob-to-any-file: ['.github/**', '**/workflows/**']
27+
28+
# Dependencies
29+
'dependencies':
30+
- changed-files:
31+
- any-glob-to-any-file:
32+
[
33+
'**/package.json',
34+
'**/yarn.lock',
35+
'**/package-lock.json',
36+
'**/pnpm-lock.yaml'
37+
]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: '🏷️ PR File Labeler'
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, edited]
6+
7+
jobs:
8+
labeler:
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Check out
15+
uses: actions/checkout@v4
16+
17+
- name: Label PR based on file changes
18+
uses: actions/labeler@v5
19+
with:
20+
configuration-path: '.github/labeler.yml'
21+
repo-token: ${{ secrets.GH_TOKEN }}
22+
sync-labels: true
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: '🏷️ PR Title Labeler'
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, edited]
6+
7+
jobs:
8+
labeler:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout repository
12+
uses: actions/checkout@v4
13+
14+
- name: Label PR based on title prefix
15+
uses: ./.github/actions/pr-title-labeler
16+
with:
17+
github-token: ${{ secrets.GH_TOKEN }}
18+
# Optional: Use custom config file (default: pr-title-labeler-config.yml)
19+
# config-file: 'custom-label-mappings.yml'

eslint.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export default [
77
'example/**',
88
'!example/app/**',
99
'packages/docs/**',
10+
'scripts/**',
11+
'.github/actions/**',
1012
'**/bob.config.cjs',
1113
'**/babel.config.cjs',
1214
'**/jest.setup.js',

0 commit comments

Comments
 (0)