Skip to content

Commit 678f314

Browse files
committed
[SPARK-54860][INFRA] Add JIRA Ticket Validating in GHA
### What changes were proposed in this pull request? This PR adds a new GitHub Action workflow that automatically validates pull request titles and extracts JIRA ticket information. The workflow includes: 1. **JIRA ID Extraction**: Automatically extracts JIRA IDs (e.g., `SPARK-12345`) from PR titles 2. **[MINOR] Tag Support**: Allows PRs without JIRA IDs if they are prefixed with `[MINOR]` for minor changes 3. **JIRA Information Display**: Fetches and displays JIRA ticket details (type, summary, assignee, status, affected versions) as a PR comment 4. **Title Validation**: Posts a reminder comment when PR titles lack both JIRA IDs and [MINOR] tags The workflow runs on `pull_request_target` events (opened, edited, reopened) and uses the public Apache JIRA API (no authentication required). **Output Format Example:** ``` === Task SPARK-54859 === Summary Arrow-optimized Python UD(T)F Docs Assignee None Status Open Affected ["4.2.0"] ``` ### Why are the changes needed? Currently, Apache Spark PRs require manual verification of JIRA ticket associations. This automation: - **Improves contributor experience**: Provides immediate feedback on PR title format - **Streamlines review process**: Displays JIRA context directly in the PR, eliminating manual lookups - **Enforces consistency**: Ensures all non-minor PRs are linked to JIRA tickets - **Reduces review overhead**: Reviewers can quickly understand the context without switching to JIRA This is particularly useful for new contributors who may not be familiar with Apache's PR conventions. It's very likely for them to make mistakes like #53445 (comment) ### Does this PR introduce _any_ user-facing change? No. This is an infrastructure change that only affects the GitHub PR workflow. ### How was this patch tested? 1. **Local Testing**: Validated the logic using `test-jira-action.py` which simulates the GitHub Action behavior - Tested JIRA ID extraction for various title formats - Verified [MINOR] tag detection (case-insensitive) - Confirmed JIRA API calls return expected data 2. **Test Cases**: - `[SPARK-54859] Title` → Displays JIRA info (✓ tested successfully) - `[SPARK-111][SPARK-222] Multiple` → Displays multiple JIRA infos - `[MINOR] Fix typo` → Silently skips validation - `Fix bug` → Posts reminder to add JIRA ID or [MINOR] tag 3. **Real JIRA Verification**: Tested with actual Apache JIRA tickets (SPARK-54859, SPARK-50000) to confirm API responses, e.g. https://github.com/yaooqinn/spark/actions/runs/20567427354/job/59067970526?pr=4 ### Was this patch authored or co-authored using generative AI tooling? Generated-by: GitHub Copilot (Claude Sonnet 4.5) Closes #53633 from yaooqinn/SPARK-54860. Lead-authored-by: Kent Yao <[email protected]> Co-authored-by: Kent Yao <[email protected]> Signed-off-by: Kent Yao <[email protected]>
1 parent 082de7f commit 678f314

File tree

1 file changed

+153
-1
lines changed

1 file changed

+153
-1
lines changed

.github/workflows/labeler.yml

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
# See also https://github.community/t/specify-check-suite-when-creating-a-checkrun/118380/10
2525

2626
name: "On pull requests"
27-
on: pull_request_target
27+
on:
28+
pull_request_target:
29+
types: [opened, edited, reopened]
2830

2931
jobs:
3032
label:
@@ -38,3 +40,153 @@ jobs:
3840
with:
3941
repo-token: "${{ secrets.GITHUB_TOKEN }}"
4042
sync-labels: true
43+
44+
jira-info:
45+
name: Comment JIRA information
46+
runs-on: ubuntu-latest
47+
permissions:
48+
pull-requests: write
49+
steps:
50+
- name: Extract JIRA IDs and comment
51+
uses: actions/github-script@v7
52+
with:
53+
github-token: ${{ secrets.GITHUB_TOKEN }}
54+
script: |
55+
const prTitle = context.payload.pull_request.title;
56+
const prNumber = context.payload.pull_request.number;
57+
58+
// Extract JIRA IDs from PR title
59+
const jiraIdRegex = /\bSPARK-\d+\b/g;
60+
const jiraIds = prTitle.match(jiraIdRegex);
61+
62+
// If no JIRA IDs found, check for [MINOR] tag
63+
if (!jiraIds || jiraIds.length === 0) {
64+
const minorRegex = /^\[MINOR\]/i;
65+
if (minorRegex.test(prTitle)) {
66+
console.log('PR title has [MINOR] tag, skipping');
67+
return;
68+
}
69+
70+
// Post reminder comment
71+
const reminderComment = `## ⚠️ Pull Request Title Validation\n\nThis pull request title does not contain a JIRA issue ID.\n\nPlease update the title to either:\n- Include a JIRA ID: \`[SPARK-12345] Your description\`\n- Mark as minor change: \`[MINOR] Your description\`\n\nFor minor changes that don't require a JIRA ticket (e.g., typo fixes), please prefix the title with \`[MINOR]\`.\n\n---\n*This comment was automatically generated by GitHub Actions*`;
72+
73+
const comments = await github.rest.issues.listComments({
74+
owner: context.repo.owner,
75+
repo: context.repo.repo,
76+
issue_number: prNumber
77+
});
78+
79+
const botComment = comments.data.find(comment =>
80+
comment.user.type === 'Bot' &&
81+
(comment.body.includes('## JIRA Issue Information') || comment.body.includes('## ⚠️ Pull Request Title Validation'))
82+
);
83+
84+
if (botComment) {
85+
await github.rest.issues.updateComment({
86+
owner: context.repo.owner,
87+
repo: context.repo.repo,
88+
comment_id: botComment.id,
89+
body: reminderComment
90+
});
91+
console.log('Updated reminder comment');
92+
} else {
93+
await github.rest.issues.createComment({
94+
owner: context.repo.owner,
95+
repo: context.repo.repo,
96+
issue_number: prNumber,
97+
body: reminderComment
98+
});
99+
console.log('Created reminder comment');
100+
}
101+
return;
102+
}
103+
104+
// Remove duplicates
105+
const uniqueJiraIds = [...new Set(jiraIds)];
106+
console.log(`Found JIRA IDs: ${uniqueJiraIds.join(', ')}`);
107+
108+
// Fetch JIRA information for each ID
109+
const jiraBaseUrl = 'https://issues.apache.org/jira';
110+
const jiraInfos = [];
111+
112+
for (const jiraId of uniqueJiraIds) {
113+
try {
114+
const response = await fetch(`${jiraBaseUrl}/rest/api/2/issue/${jiraId}`);
115+
116+
if (!response.ok) {
117+
jiraInfos.push({
118+
id: jiraId,
119+
type: 'Unknown',
120+
error: `Failed to fetch (HTTP ${response.status})`
121+
});
122+
continue;
123+
}
124+
125+
const data = await response.json();
126+
const fields = data.fields;
127+
128+
jiraInfos.push({
129+
id: jiraId,
130+
type: fields.issuetype?.name || 'Unknown',
131+
summary: fields.summary || 'N/A',
132+
assignee: fields.assignee ? fields.assignee.displayName : 'None',
133+
status: fields.status ? fields.status.name : 'Unknown',
134+
affected: fields.versions ? fields.versions.map(v => v.name) : []
135+
});
136+
} catch (error) {
137+
console.error(`Error fetching ${jiraId}:`, error);
138+
jiraInfos.push({
139+
id: jiraId,
140+
type: 'Unknown',
141+
error: error.message
142+
});
143+
}
144+
}
145+
146+
// Format comment
147+
let commentBody = '## JIRA Issue Information\n\n';
148+
149+
for (const info of jiraInfos) {
150+
if (info.error) {
151+
commentBody += `=== ${info.type} ${info.id} ===\n`;
152+
commentBody += `Error: ${info.error}\n\n`;
153+
} else {
154+
commentBody += `=== ${info.type} ${info.id} ===\n`;
155+
commentBody += `Summary: ${info.summary}\n`;
156+
commentBody += `Assignee: ${info.assignee}\n`;
157+
commentBody += `Status: ${info.status}\n`;
158+
commentBody += `Affected: ${JSON.stringify(info.affected)}\n\n`;
159+
}
160+
}
161+
162+
commentBody += '---\n*This comment was automatically generated by GitHub Actions*';
163+
164+
// Check if there's an existing comment from this action
165+
const comments = await github.rest.issues.listComments({
166+
owner: context.repo.owner,
167+
repo: context.repo.repo,
168+
issue_number: prNumber
169+
});
170+
171+
const botComment = comments.data.find(comment =>
172+
comment.user.type === 'Bot' &&
173+
(comment.body.includes('## JIRA Issue Information') || comment.body.includes('## ⚠️ Pull Request Title Validation'))
174+
);
175+
176+
if (botComment) {
177+
await github.rest.issues.updateComment({
178+
owner: context.repo.owner,
179+
repo: context.repo.repo,
180+
comment_id: botComment.id,
181+
body: commentBody
182+
});
183+
console.log('Updated existing comment');
184+
} else {
185+
await github.rest.issues.createComment({
186+
owner: context.repo.owner,
187+
repo: context.repo.repo,
188+
issue_number: prNumber,
189+
body: commentBody
190+
});
191+
console.log('Created new comment');
192+
}

0 commit comments

Comments
 (0)