-
Notifications
You must be signed in to change notification settings - Fork 11
[TTAHUB-5247] Enforce approved Jira issue link on PRs via CI check #3662
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
Merged
Merged
Changes from 17 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
d877963
Bump fast-xml-builder from 1.1.5 to 1.2.0
dependabot[bot] 41c91fc
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas 342270f
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas a62c429
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas 8ae2bb7
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas 7016a1c
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas 2f9926e
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas d646a3e
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas e76ab80
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas cf71581
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas 6fc4d8d
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas 73d6bf3
Merge remote-tracking branch 'origin/main' into kw/ttahub-5247
kryswisnaskas 67ceab1
[TTAHUB-5247] Enforce a Jira issue link on PRs via CI check
kryswisnaskas e639e05
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas cd2a4fd
[TTAHUB-5247] Address PR feedback on Jira issue link validation
kryswisnaskas dc9c625
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas 3052fab
Merge branch 'main' into kw/ttahub-5247
kryswisnaskas 2b2c7f0
[TTAHUB-5247] Soften Jira issue link wording
kryswisnaskas 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
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,29 @@ | ||
| name: PR Jira Issue Link | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, edited, reopened, synchronize, ready_for_review] | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| validate_jira_issue_link: | ||
| if: ${{ github.event.pull_request.base.ref != 'production' }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Check out repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
| persist-credentials: false | ||
|
|
||
| - name: Set up Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version-file: .nvmrc | ||
|
|
||
| - name: Validate Jira issue link in PR body | ||
| env: | ||
| PR_BODY: ${{ github.event.pull_request.body }} | ||
| run: node ./tools/validate-pr-jira-issue-link.js | ||
|
kryswisnaskas marked this conversation as resolved.
|
||
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
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
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,110 @@ | ||
| const JIRA_KEY_PATTERN = /\bTTAHUB-\d+\b/gi; | ||
| const JIRA_ISSUE_LINK_PATTERN = /https:\/\/jira\.acf\.gov\/browse\/(TTAHUB-\d+)\b/gi; | ||
| const PLACEHOLDER_JIRA_LINK_PATTERN = /https:\/\/jira\.acf\.gov\/browse\/TTAHUB-0\b/i; | ||
| const ISSUES_HEADINGS = ['Jira Issue(s)', 'Jira Issue', 'Issue(s)', 'Issue']; | ||
| const PLACEHOLDER_JIRA_KEY = 'TTAHUB-0'; | ||
|
|
||
| function standardizeLineEndings(text = '') { | ||
| return text.replace(/\r\n/g, '\n'); | ||
| } | ||
|
|
||
| function stripHtmlComments(text = '') { | ||
| return text.replace(/<!--[\s\S]*?-->/g, ' '); | ||
| } | ||
|
|
||
| function extractSectionContent(body = '', heading = '') { | ||
| const bodyWithStandardLineEndings = standardizeLineEndings(body); | ||
| const headings = [...bodyWithStandardLineEndings.matchAll(/^##\s+.*$/gm)]; | ||
| const sectionHeading = headings.find((match) => match[0].trim() === `## ${heading}`); | ||
|
kryswisnaskas marked this conversation as resolved.
|
||
|
|
||
| if (!sectionHeading) { | ||
| return ''; | ||
| } | ||
|
|
||
| const currentIndex = headings.findIndex((match) => match.index === sectionHeading.index); | ||
| const nextHeading = headings[currentIndex + 1]; | ||
| const contentEnd = nextHeading ? nextHeading.index : bodyWithStandardLineEndings.length; | ||
|
|
||
| return bodyWithStandardLineEndings | ||
| .slice(sectionHeading.index + sectionHeading[0].length, contentEnd) | ||
| .trim(); | ||
| } | ||
|
|
||
| function extractJiraKeys(text = '') { | ||
| const keys = standardizeLineEndings(text).match(JIRA_KEY_PATTERN) || []; | ||
|
|
||
| return [...new Set(keys.map((key) => key.toUpperCase()))]; | ||
| } | ||
|
|
||
| function extractLinkedJiraIssues(text = '') { | ||
| const matches = [...standardizeLineEndings(text).matchAll(JIRA_ISSUE_LINK_PATTERN)]; | ||
|
|
||
| return [ | ||
| ...new Set( | ||
| matches.map((match) => match[1].toUpperCase()).filter((key) => key !== PLACEHOLDER_JIRA_KEY) | ||
| ), | ||
| ]; | ||
| } | ||
|
|
||
| function extractIssuesSection(body = '') { | ||
| return ( | ||
| ISSUES_HEADINGS.map((heading) => extractSectionContent(body, heading)).find( | ||
| (section) => section && section.trim() | ||
| ) || '' | ||
| ); | ||
| } | ||
|
|
||
| function validatePullRequestBody(body = '') { | ||
| const issuesSection = extractIssuesSection(body); | ||
| const issuesSectionWithoutComments = stripHtmlComments(issuesSection); | ||
| const bodyWithoutComments = stripHtmlComments(body); | ||
| const linkedIssues = extractLinkedJiraIssues(issuesSectionWithoutComments); | ||
| const placeholderUsed = PLACEHOLDER_JIRA_LINK_PATTERN.test(issuesSectionWithoutComments); | ||
| const linkedIssuesElsewhere = extractLinkedJiraIssues(bodyWithoutComments); | ||
| const jiraKeysInIssuesSection = extractJiraKeys(issuesSectionWithoutComments).filter( | ||
| (key) => key !== PLACEHOLDER_JIRA_KEY | ||
| ); | ||
|
|
||
| if (!issuesSection.trim()) { | ||
| return { | ||
| valid: false, | ||
| jiraKeys: [], | ||
| message: | ||
| 'PR validation failed: add the approved Jira issue link to the `Jira Issue(s)` section.', | ||
| }; | ||
| } | ||
|
|
||
| if (placeholderUsed) { | ||
| return { | ||
| valid: false, | ||
| jiraKeys: linkedIssues, | ||
| message: `PR validation failed: remove the ${PLACEHOLDER_JIRA_KEY} placeholder from the \`Jira Issue(s)\` section and keep only approved Jira issue links.`, | ||
| }; | ||
| } | ||
|
|
||
| if (!linkedIssues.length) { | ||
| const keyFormatHint = jiraKeysInIssuesSection.length | ||
| ? ` Jira key(s) found without links: ${jiraKeysInIssuesSection.join(', ')}. Use the full https://jira.acf.gov/browse/TTAHUB-#### issue link format.` | ||
| : ''; | ||
| const locationHint = linkedIssuesElsewhere.length | ||
| ? ` Jira issue link(s) found outside \`Jira Issue(s)\`: ${linkedIssuesElsewhere.join(', ')}.` | ||
| : ''; | ||
|
|
||
| return { | ||
| valid: false, | ||
| jiraKeys: [], | ||
| message: `PR validation failed: the \`Jira Issue(s)\` section must include at least one approved Jira issue link.${keyFormatHint}${locationHint}`, | ||
| }; | ||
| } | ||
|
|
||
| return { | ||
| valid: true, | ||
| jiraKeys: linkedIssues, | ||
| message: `Validated Jira issue link(s): ${linkedIssues.join(', ')}`, | ||
| }; | ||
| } | ||
|
|
||
| module.exports = { | ||
| extractIssuesSection, | ||
| validatePullRequestBody, | ||
| }; | ||
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,125 @@ | ||
| const { extractIssuesSection, validatePullRequestBody } = require('./pr-jira-issue-link'); | ||
|
|
||
| describe('PR Jira issue link validation', () => { | ||
| it('extracts the Jira Issue(s) section from the PR body', () => { | ||
| const body = `## Description of change | ||
|
kryswisnaskas marked this conversation as resolved.
|
||
|
|
||
| - Update dependencies. | ||
|
|
||
| ## Jira Issue(s) | ||
|
|
||
| * https://jira.acf.gov/browse/TTAHUB-5247 | ||
|
|
||
| ## Checklists | ||
|
|
||
| - [ ] Code is meaningfully tested | ||
| `; | ||
|
|
||
| expect(extractIssuesSection(body)).toContain('TTAHUB-5247'); | ||
| }); | ||
|
|
||
| it('passes when the Jira Issue(s) section contains a valid Jira issue link', () => { | ||
| const result = validatePullRequestBody(`## Jira Issue(s) | ||
|
|
||
| * https://jira.acf.gov/browse/TTAHUB-5247 | ||
| `); | ||
|
|
||
| expect(result).toEqual({ | ||
| valid: true, | ||
| jiraKeys: ['TTAHUB-5247'], | ||
| message: 'Validated Jira issue link(s): TTAHUB-5247', | ||
| }); | ||
| }); | ||
|
|
||
| it('fails when the Jira Issue(s) section is missing', () => { | ||
| const result = validatePullRequestBody(`## Description of change | ||
|
|
||
| - Update a dependency. | ||
| `); | ||
|
|
||
| expect(result.valid).toBe(false); | ||
| expect(result.message).toContain('add the approved Jira issue link'); | ||
| }); | ||
|
|
||
| it('fails when the placeholder is still present', () => { | ||
| const result = validatePullRequestBody(`## Jira Issue(s) | ||
|
|
||
| * https://jira.acf.gov/browse/TTAHUB-0 | ||
| `); | ||
|
|
||
| expect(result.valid).toBe(false); | ||
| expect(result.message).toContain('remove the TTAHUB-0 placeholder'); | ||
| }); | ||
|
|
||
| it('fails when the placeholder and a real issue link both appear', () => { | ||
| const result = validatePullRequestBody(`## Jira Issue(s) | ||
|
|
||
| * https://jira.acf.gov/browse/TTAHUB-0 | ||
| * https://jira.acf.gov/browse/TTAHUB-5247 | ||
| `); | ||
|
|
||
| expect(result.valid).toBe(false); | ||
| expect(result.message).toContain('remove the TTAHUB-0 placeholder'); | ||
| }); | ||
|
|
||
| it('ignores the template comment that mentions TTAHUB-0 when a real issue link is present', () => { | ||
| const result = validatePullRequestBody(`## Jira Issue(s) | ||
|
|
||
| <!-- Link the approved Jira issue for this PR. Replace TTAHUB-0 before requesting review. --> | ||
| * https://jira.acf.gov/browse/TTAHUB-5247 | ||
| `); | ||
|
|
||
| expect(result).toEqual({ | ||
| valid: true, | ||
| jiraKeys: ['TTAHUB-5247'], | ||
| message: 'Validated Jira issue link(s): TTAHUB-5247', | ||
| }); | ||
| }); | ||
|
|
||
| it('fails when the Jira Issue(s) section contains only a bare Jira key', () => { | ||
| const result = validatePullRequestBody(`## Jira Issue(s) | ||
|
|
||
| * TTAHUB-5247 | ||
| `); | ||
|
|
||
| expect(result.valid).toBe(false); | ||
| expect(result.message).toContain( | ||
| 'Use the full https://jira.acf.gov/browse/TTAHUB-#### issue link format' | ||
| ); | ||
| }); | ||
|
|
||
| it('fails when the Jira issue link is only outside the Jira Issue(s) section', () => { | ||
| const result = validatePullRequestBody(`## Description of change | ||
|
|
||
| - https://jira.acf.gov/browse/TTAHUB-5247 update dependencies. | ||
|
|
||
| ## Jira Issue(s) | ||
|
|
||
| * None provided yet | ||
| `); | ||
|
|
||
| expect(result.valid).toBe(false); | ||
| expect(result.message).toContain('Jira Issue(s)'); | ||
| expect(result.message).toContain('TTAHUB-5247'); | ||
| }); | ||
|
|
||
| it('accepts the fallback Issue heading used by some handwritten PR bodies', () => { | ||
| const result = validatePullRequestBody(`## Issue | ||
|
|
||
| * https://jira.acf.gov/browse/TTAHUB-5247 | ||
| `); | ||
|
|
||
| expect(result.valid).toBe(true); | ||
| expect(result.jiraKeys).toEqual(['TTAHUB-5247']); | ||
| }); | ||
|
|
||
| it('accepts the previous Issue(s) heading for backward compatibility', () => { | ||
| const result = validatePullRequestBody(`## Issue(s) | ||
|
|
||
| * https://jira.acf.gov/browse/TTAHUB-5247 | ||
| `); | ||
|
|
||
| expect(result.valid).toBe(true); | ||
| expect(result.jiraKeys).toEqual(['TTAHUB-5247']); | ||
| }); | ||
| }); | ||
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,10 @@ | ||
| const { validatePullRequestBody } = require('./pr-jira-issue-link'); | ||
|
|
||
| const result = validatePullRequestBody(process.env.PR_BODY || ''); | ||
|
|
||
| if (!result.valid) { | ||
| console.error(result.message); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log(result.message); |
Oops, something went wrong.
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.