Skip to content

Commit 0c0eb3d

Browse files
committed
Improve labeler action
1 parent 6d11bf7 commit 0c0eb3d

File tree

2 files changed

+223
-68
lines changed

2 files changed

+223
-68
lines changed

.github/actions/pr-title-labeler/index.js

Lines changed: 221 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -10,93 +10,248 @@ const yaml = require('js-yaml');
1010
const fs = require('fs');
1111
const path = require('path');
1212

13-
async function run() {
13+
/**
14+
* Get labels that were added by this action by checking issue events
15+
*/
16+
async function getLabelsAddedByAction(octokit, owner, repo, issueNumber) {
1417
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;
18+
const events = await octokit.rest.issues.listEvents({
19+
owner,
20+
repo,
21+
issue_number: issueNumber,
22+
per_page: 100
23+
});
2124

22-
if (!githubToken) {
23-
console.error('❌ GITHUB_TOKEN is required');
24-
process.exit(1);
25+
const labelsAddedByAction = new Set();
26+
27+
// Look for "labeled" events that were created by the GitHub Actions bot
28+
for (const event of events.data) {
29+
if (
30+
event.event === 'labeled' &&
31+
event.actor?.login === 'github-actions[bot]' &&
32+
event.label?.name
33+
) {
34+
labelsAddedByAction.add(event.label.name);
35+
}
2536
}
2637

27-
if (!eventPath) {
28-
console.error('❌ GITHUB_EVENT_PATH is required');
29-
process.exit(1);
38+
return labelsAddedByAction;
39+
} catch (error) {
40+
console.log('⚠️ Could not fetch issue events:', error.message);
41+
return new Set();
42+
}
43+
}
44+
45+
/**
46+
* Get current labels on the PR
47+
*/
48+
async function getCurrentLabels(octokit, owner, repo, issueNumber) {
49+
try {
50+
const response = await octokit.rest.issues.listLabelsOnIssue({
51+
owner,
52+
repo,
53+
issue_number: issueNumber
54+
});
55+
return new Set(response.data.map(label => label.name));
56+
} catch (error) {
57+
console.log('⚠️ Could not fetch current labels:', error.message);
58+
return new Set();
59+
}
60+
}
61+
62+
/**
63+
* Find matching label based on PR title prefix
64+
*/
65+
function findMatchingLabel(title, labelMappings) {
66+
const normalizedTitle = title.toLowerCase();
67+
68+
for (const mapping of labelMappings) {
69+
for (const prefix of mapping.prefixes) {
70+
const prefixWithColon = `${prefix}:`;
71+
if (normalizedTitle.startsWith(prefixWithColon)) {
72+
return mapping.label;
73+
}
3074
}
75+
}
3176

32-
// Read GitHub event data
33-
const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
34-
const pr = eventData.pull_request;
77+
return null;
78+
}
79+
80+
/**
81+
* Determine which labels to remove
82+
*/
83+
function getLabelsToRemove(labelsAddedByAction, currentLabels, targetLabel) {
84+
const labelsToRemove = new Set();
3585

36-
if (!pr) {
37-
console.error('❌ No pull request found in event data');
38-
process.exit(1);
86+
for (const addedLabel of labelsAddedByAction) {
87+
if (currentLabels.has(addedLabel) && addedLabel !== targetLabel) {
88+
labelsToRemove.add(addedLabel);
3989
}
90+
}
4091

41-
// Get config file path from environment variable
42-
const configPath = path.resolve(__dirname, configFilePath);
92+
return labelsToRemove;
93+
}
4394

44-
let labelMappings;
95+
/**
96+
* Remove labels from the PR
97+
*/
98+
async function removeLabels(octokit, owner, repo, issueNumber, labelsToRemove) {
99+
for (const labelToRemove of labelsToRemove) {
45100
try {
46-
const configContent = fs.readFileSync(configPath, 'utf8');
47-
labelMappings = yaml.load(configContent);
101+
await octokit.rest.issues.removeLabel({
102+
owner,
103+
repo,
104+
issue_number: issueNumber,
105+
name: labelToRemove
106+
});
107+
console.log(`🗑️ Removed old label: ${labelToRemove}`);
48108
} 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;
109+
console.log(`⚠️ Could not remove label ${labelToRemove}:`, error.message);
72110
}
111+
}
112+
}
73113

74-
// Create GitHub client
75-
const octokit = new Octokit({
76-
auth: githubToken
114+
/**
115+
* Add label to the PR
116+
*/
117+
async function addLabel(octokit, owner, repo, issueNumber, label) {
118+
try {
119+
await octokit.rest.issues.addLabels({
120+
owner,
121+
repo,
122+
issue_number: issueNumber,
123+
labels: [label]
77124
});
125+
console.log(`✅ Successfully applied label: ${label}`);
126+
} catch (error) {
127+
console.log(`⚠️ Could not add label ${label}:`, error.message);
128+
}
129+
}
130+
131+
/**
132+
* Log information when no matching prefix is found
133+
*/
134+
function logNoMatchingPrefix(title, labelMappings) {
135+
console.log('❓ No matching prefix found in PR title');
136+
console.log(`📝 PR title: "${title}"`);
137+
const allPrefixes = labelMappings.flatMap(mapping => mapping.prefixes);
138+
console.log('📋 Available prefixes:', allPrefixes.join(', '));
139+
}
140+
141+
/**
142+
* Load configuration from YAML file
143+
*/
144+
function loadConfiguration(configFilePath) {
145+
const configPath = path.resolve(__dirname, configFilePath);
146+
147+
try {
148+
const configContent = fs.readFileSync(configPath, 'utf8');
149+
return yaml.load(configContent);
150+
} catch (error) {
151+
console.error(
152+
`❌ Error reading config file "${configFilePath}": ${error.message}`
153+
);
154+
process.exit(1);
155+
}
156+
}
157+
158+
/**
159+
* Validate required environment variables
160+
*/
161+
function validateEnvironment() {
162+
const githubToken = process.env.GITHUB_TOKEN;
163+
const eventPath = process.env.GITHUB_EVENT_PATH;
164+
165+
if (!githubToken) {
166+
console.error('❌ GITHUB_TOKEN is required');
167+
process.exit(1);
168+
}
169+
170+
if (!eventPath) {
171+
console.error('❌ GITHUB_EVENT_PATH is required');
172+
process.exit(1);
173+
}
174+
175+
return { githubToken, eventPath };
176+
}
177+
178+
/**
179+
* Parse PR data from GitHub event
180+
*/
181+
function parsePullRequestData(eventPath) {
182+
const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
183+
const pr = eventData.pull_request;
184+
185+
if (!pr) {
186+
console.error('❌ No pull request found in event data');
187+
process.exit(1);
188+
}
189+
190+
return pr;
191+
}
78192

79-
// Parse repository owner and name
193+
/**
194+
* Main execution function
195+
*/
196+
async function run() {
197+
try {
198+
// Validate environment and load configuration
199+
const { githubToken, eventPath } = validateEnvironment();
200+
const configFilePath =
201+
process.env.CONFIG_FILE || 'pr-title-labeler-config.yml';
202+
const repository = process.env.GITHUB_REPOSITORY;
203+
204+
const labelMappings = loadConfiguration(configFilePath);
205+
const pr = parsePullRequestData(eventPath);
206+
207+
// Create GitHub client and parse repository info
208+
const octokit = new Octokit({ auth: githubToken });
80209
const [owner, repo] = repository.split('/');
81210

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-
});
211+
// Find matching label based on title
212+
const targetLabel = findMatchingLabel(pr.title, labelMappings);
90213

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(', '));
214+
if (targetLabel) {
215+
console.log(
216+
`🎯 Found matching prefix -> applying label "${targetLabel}"`
217+
);
218+
}
219+
220+
// Get current state
221+
const [currentLabels, labelsAddedByAction] = await Promise.all([
222+
getCurrentLabels(octokit, owner, repo, pr.number),
223+
getLabelsAddedByAction(octokit, owner, repo, pr.number)
224+
]);
225+
226+
// Check if target label already exists
227+
if (targetLabel && currentLabels.has(targetLabel)) {
228+
console.log(
229+
`⏭️ Skipping label "${targetLabel}" because it's already added to the PR`
230+
);
97231
}
232+
233+
// Determine which labels to remove
234+
const labelsToRemove = getLabelsToRemove(
235+
labelsAddedByAction,
236+
currentLabels,
237+
targetLabel
238+
);
239+
240+
// Remove old labels if any
241+
if (labelsToRemove.size > 0) {
242+
await removeLabels(octokit, owner, repo, pr.number, labelsToRemove);
243+
}
244+
245+
// Add new label if needed
246+
if (targetLabel && !currentLabels.has(targetLabel)) {
247+
await addLabel(octokit, owner, repo, pr.number, targetLabel);
248+
} else if (!targetLabel) {
249+
logNoMatchingPrefix(pr.title, labelMappings);
250+
}
251+
252+
console.log('🎉 PR title labeler completed successfully!');
98253
} catch (error) {
99-
console.error(` Error labeling PR: ${error.message}`);
254+
console.error(`💥 Error labeling PR: ${error.message}`);
100255
process.exit(1);
101256
}
102257
}

.github/workflows/build-examples.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ jobs:
8989

9090
- name: 🍎 Setup Ruby (iOS only)
9191
if: matrix.platform == 'ios'
92-
uses: ruby/setup-ruby@v2
92+
uses: ruby/setup-ruby@v1
9393
with:
9494
ruby-version: '3.3'
9595
bundler-cache: true
@@ -107,7 +107,7 @@ jobs:
107107

108108
- name: 🤖 Setup Android SDK (Android only)
109109
if: matrix.platform == 'android'
110-
uses: android-actions/setup-android@v4
110+
uses: android-actions/setup-android@v3
111111

112112
- name: 🍎 Install pods (iOS only)
113113
if: matrix.platform == 'ios'

0 commit comments

Comments
 (0)