generated from adobe/aem-boilerplate
-
Notifications
You must be signed in to change notification settings - Fork 15
MWPW-184913: Auto-link PRs to Jira tickets #504
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
antonio-rmrz
wants to merge
4
commits into
main
Choose a base branch
from
MWPW-184913
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| import { readFileSync } from 'fs'; | ||
|
|
||
| const JIRA_TICKET_PATTERN = /MWPW-\d+/gi; | ||
|
|
||
| /** | ||
| * Extract Jira ticket IDs from PR title and branch name | ||
| * @param {string} title - PR title | ||
| * @param {string} branch - Branch name | ||
| * @returns {string[]} - Array of unique ticket IDs (uppercase) | ||
| */ | ||
| const extractTicketIds = (title, branch) => { | ||
| const titleMatches = title?.match(JIRA_TICKET_PATTERN) || []; | ||
| const branchMatches = branch?.match(JIRA_TICKET_PATTERN) || []; | ||
|
|
||
| const allMatches = [...titleMatches, ...branchMatches].map((id) => id.toUpperCase()); | ||
|
|
||
| return [...new Set(allMatches)]; | ||
| }; | ||
|
|
||
| /** | ||
| * Fetch IMS access token for iPaaS authentication | ||
| * @returns {Promise<string>} - IMS access token | ||
| */ | ||
| const getImsToken = async () => { | ||
| const imsUrl = process.env.JIRA_SYNC_IMS_URL; | ||
| const clientId = process.env.JIRA_SYNC_IMS_CLIENT_ID; | ||
| const clientSecret = process.env.JIRA_SYNC_IMS_CLIENT_SECRET; | ||
| const authCode = process.env.JIRA_SYNC_IMS_AUTH_CODE; | ||
|
|
||
| const formData = new URLSearchParams({ | ||
| client_id: clientId, | ||
| client_secret: clientSecret, | ||
| grant_type: 'authorization_code', | ||
| code: authCode, | ||
| }); | ||
|
|
||
| const response = await fetch(imsUrl, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
| body: formData, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| throw new Error(`IMS token request failed: ${response.status} - ${errorText}`); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| return data.access_token; | ||
| }; | ||
|
|
||
| /** | ||
| * Get headers for iPaaS Jira API requests (JiraProxyV2 with PAT auth) | ||
| * @param {string} imsToken - IMS access token | ||
| * @returns {object} - Headers object | ||
| */ | ||
| const getJiraHeaders = (imsToken) => ({ | ||
| Authorization: imsToken, | ||
| 'x-authorization': `Bearer ${process.env.JIRA_SYNC_PAT}`, | ||
| Api_key: process.env.JIRA_SYNC_IPAAS_KEY, | ||
| Accept: 'application/json', | ||
| 'Content-Type': 'application/json', | ||
| }); | ||
|
|
||
| /** | ||
| * Verify that a Jira ticket exists | ||
| * @param {string} ticketId - Jira ticket ID | ||
| * @param {string} imsToken - IMS access token | ||
| * @returns {Promise<boolean>} | ||
| */ | ||
| const verifyTicketExists = async (ticketId, imsToken) => { | ||
| const ipaasUrl = process.env.JIRA_SYNC_IPAAS_URL; | ||
|
|
||
| const response = await fetch(`${ipaasUrl}/issue/${ticketId}?fields=key`, { | ||
| method: 'GET', | ||
| headers: getJiraHeaders(imsToken), | ||
| }); | ||
|
|
||
| return response.ok; | ||
| }; | ||
|
|
||
| /** | ||
| * Create a remote link in Jira for the PR | ||
| * @param {string} ticketId - Jira ticket ID (e.g., MWPW-123456) | ||
| * @param {object} prData - PR data from GitHub | ||
| * @param {string} repoName - Repository name | ||
| * @param {string} imsToken - IMS access token | ||
| * @returns {Promise<Response>} | ||
| */ | ||
| const createRemoteLink = async (ticketId, prData, repoName, imsToken) => { | ||
| const { html_url: htmlUrl, number, title } = prData; | ||
| const ipaasUrl = process.env.JIRA_SYNC_IPAAS_URL; | ||
|
|
||
| const response = await fetch(`${ipaasUrl}/issue/${ticketId}/remotelink`, { | ||
| method: 'POST', | ||
| headers: getJiraHeaders(imsToken), | ||
| body: JSON.stringify({ | ||
| globalId: `github-pr-${repoName}-${number}`, | ||
| application: { | ||
| type: 'com.github', | ||
| name: 'GitHub', | ||
| }, | ||
| relationship: 'Pull Request', | ||
| object: { | ||
| url: htmlUrl, | ||
| title: `[${repoName}] PR #${number}: ${title}`, | ||
| icon: { | ||
| url16x16: 'https://github.com/favicon.ico', | ||
| title: 'GitHub Pull Request', | ||
| }, | ||
| }, | ||
| }), | ||
| }); | ||
|
|
||
| return response; | ||
| }; | ||
|
|
||
| const main = async ({ context }) => { | ||
| try { | ||
| const { pull_request: pullRequest, repository } = context.payload; | ||
| if (!pullRequest) { | ||
| console.log('No pull request found in context. Exiting.'); | ||
| return; | ||
| } | ||
|
|
||
| const { title, head, html_url: htmlUrl, number } = pullRequest; | ||
| const branch = head?.ref || ''; | ||
| const repoName = repository?.name || 'unknown'; | ||
|
|
||
| console.log(`Processing PR #${number}: ${title}`); | ||
| console.log(`Repository: ${repoName}`); | ||
| console.log(`Branch: ${branch}`); | ||
|
|
||
| const ticketIds = extractTicketIds(title, branch); | ||
|
|
||
| if (ticketIds.length === 0) { | ||
| console.log('No Jira ticket ID found in PR title or branch name. Skipping.'); | ||
| return; | ||
| } | ||
|
|
||
| console.log(`Found ticket IDs: ${ticketIds.join(', ')}`); | ||
|
|
||
| console.log('Fetching IMS token...'); | ||
| const imsToken = await getImsToken(); | ||
| console.log('IMS token obtained successfully.'); | ||
|
|
||
| for (const ticketId of ticketIds) { | ||
| try { | ||
| const exists = await verifyTicketExists(ticketId, imsToken); | ||
| if (!exists) { | ||
| console.log(`Ticket ${ticketId} not found or not accessible. Skipping.`); | ||
| continue; | ||
| } | ||
|
|
||
| const response = await createRemoteLink(ticketId, pullRequest, repoName, imsToken); | ||
|
|
||
| if (response.ok) { | ||
| console.log(`Successfully linked PR #${number} to ${ticketId}`); | ||
| } else if (response.status === 400) { | ||
| const errorData = await response.json(); | ||
| const alreadyExists = errorData.errors?.some((e) => e.toLowerCase().includes('already exists')); | ||
| if (alreadyExists) { | ||
| console.log(`Remote link already exists for ${ticketId}. Skipping.`); | ||
| } else { | ||
| console.error(`Failed to link to ${ticketId}: ${JSON.stringify(errorData)}`); | ||
| } | ||
| } else { | ||
| const errorText = await response.text(); | ||
| console.error(`Failed to link to ${ticketId}: ${response.status} - ${errorText}`); | ||
| } | ||
| } catch (error) { | ||
| console.error(`Error processing ticket ${ticketId}: ${error.message}`); | ||
| } | ||
| } | ||
|
|
||
| console.log('Jira link process completed.'); | ||
| } catch (error) { | ||
| console.error('Error in jira-link-pr:', error); | ||
| } | ||
| }; | ||
|
|
||
| // Read GitHub event payload and run | ||
| const eventPath = process.env.GITHUB_EVENT_PATH; | ||
| const payload = JSON.parse(readFileSync(eventPath, 'utf8')); | ||
| main({ context: { payload } }); |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| name: Link PR to Jira | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened] | ||
|
|
||
| env: | ||
| JIRA_SYNC_IPAAS_URL: ${{ vars.JIRA_SYNC_IPAAS_URL }} | ||
| JIRA_SYNC_IMS_URL: ${{ vars.JIRA_SYNC_IMS_URL }} | ||
| JIRA_SYNC_IMS_CLIENT_ID: ${{ secrets.JIRA_SYNC_IMS_CLIENT_ID }} | ||
| JIRA_SYNC_IMS_CLIENT_SECRET: ${{ secrets.JIRA_SYNC_IMS_CLIENT_SECRET }} | ||
| JIRA_SYNC_IMS_AUTH_CODE: ${{ secrets.JIRA_SYNC_IMS_AUTH_CODE }} | ||
| JIRA_SYNC_IPAAS_KEY: ${{ secrets.JIRA_SYNC_IPAAS_KEY }} | ||
| JIRA_SYNC_PAT: ${{ secrets.JIRA_SYNC_PAT }} | ||
|
|
||
| jobs: | ||
| link-to-jira: | ||
| if: github.repository_owner == 'adobecom' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| with: | ||
| ref: ${{ github.event.pull_request.base.ref }} | ||
|
|
||
| - name: Link PR to Jira ticket | ||
| run: node .github/workflows/jira-link-pr.js | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.