Skip to content
Merged
Show file tree
Hide file tree
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] May 8, 2026
41c91fc
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 11, 2026
342270f
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 12, 2026
a62c429
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 12, 2026
8ae2bb7
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 13, 2026
7016a1c
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 14, 2026
2f9926e
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 19, 2026
d646a3e
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 20, 2026
e76ab80
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 26, 2026
cf71581
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 27, 2026
6fc4d8d
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas May 27, 2026
73d6bf3
Merge remote-tracking branch 'origin/main' into kw/ttahub-5247
kryswisnaskas May 28, 2026
67ceab1
[TTAHUB-5247] Enforce a Jira issue link on PRs via CI check
kryswisnaskas May 29, 2026
e639e05
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas Jun 1, 2026
cd2a4fd
[TTAHUB-5247] Address PR feedback on Jira issue link validation
kryswisnaskas Jun 1, 2026
dc9c625
Merge branch 'main' of https://github.com/HHS/Head-Start-TTADP
kryswisnaskas Jun 1, 2026
3052fab
Merge branch 'main' into kw/ttahub-5247
kryswisnaskas Jun 1, 2026
2b2c7f0
[TTAHUB-5247] Soften Jira issue link wording
kryswisnaskas Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
## How to test


## Issue(s)
## Jira Issue(s)

<!-- Link the approved Jira issue for this PR. Replace TTAHUB-0 before requesting review. -->
<!-- Maintenance, dependency, docs, and CI/CD changes still require a dedicated Jira issue. -->
* https://jira.acf.gov/browse/TTAHUB-0


Expand All @@ -15,8 +17,8 @@
### Every PR

<!-- Add details to each completed item -->
- [ ] Meets issue criteria
- [ ] JIRA ticket status updated
- [ ] Linked approved Jira issue
- [ ] JIRA issue status updated
- [ ] Code is meaningfully tested
- [ ] Meets accessibility standards (WCAG 2.1 Levels A, AA)
- [ ] API Documentation updated
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/pr-jira-issue-link.yml
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
Comment thread
kryswisnaskas marked this conversation as resolved.
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
Comment thread
kryswisnaskas marked this conversation as resolved.
2 changes: 1 addition & 1 deletion .github/workflows/pr-notifications.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
}

DESC=$(extract_section "^## Description of change")
ISSUES=$(extract_section "^## Issue")
ISSUES=$(extract_section "^## (Jira )?Issue")
DESC_SLACK=$(truncate_for_slack "$DESC")
ISSUES_SLACK=$(truncate_for_slack "$ISSUES")

Expand Down
9 changes: 5 additions & 4 deletions docs/guides/engineering-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Most work should begin with a tracked request and enough detail for an engineer
- **Feature work**: requires a JIRA ticket with acceptance criteria and links to relevant UI designs; the work is expected to have been refined during story refinement
- **Bugs and support requests**: typically require a JIRA ticket
- **Spikes**: may be tracked as investigation work when the outcome is learning or scoping rather than direct delivery
- **Small maintenance work**: some documentation updates, pipeline changes, and other minor DevOps tasks may not have a dedicated ticket; the team is moving toward linking these items to an epic when a dedicated ticket is not created
- **Small maintenance work**: documentation updates, pipeline changes, dependency updates, and other minor DevOps tasks still require a dedicated JIRA issue; related work may be grouped under an epic, but each PR should link its own issue

### Ready-for-development guidance

Expand All @@ -59,7 +59,7 @@ If these inputs are missing, the engineer should seek clarification before imple

### 1. Start from tracked work

Engineers normally begin from a JIRA ticket or other traceable request. Branches are created from `main`.
Engineers normally begin from an approved JIRA issue or other traceable request. Branches are created from `main`.

### 2. Implement and self-check

Expand All @@ -80,7 +80,7 @@ Each PR should include:

- a description of the change
- how the change was tested
- a link to the associated JIRA issue when one exists
- a link to the associated approved JIRA issue in the `Jira Issue(s)` section
- any required follow-up notes, risks, or deployment considerations

### 4. Human review
Expand Down Expand Up @@ -117,6 +117,7 @@ The team uses a combination of repository artifacts, CI checks, and JIRA workflo

- feature work is expected to start from a refined JIRA ticket with acceptance criteria and linked designs
- at least one human reviewer approval is required before merge
- each PR merged through the standard workflow must link at least one approved JIRA issue before merge
- QA validation is required before release progression
- production deployment approval is restricted to the System Owner unless a waiver delegates that authority
- accessibility and security scans run in CI/CD on every check-in
Expand Down Expand Up @@ -165,7 +166,7 @@ Urgent production fixes may use an accelerated path.

### Small maintenance work

Not every low risk maintenance change currently starts with a dedicated JIRA ticket. For these cases, the team should still preserve traceability through an epic link, PR documentation, or another agreed tracking mechanism.
Low risk maintenance changes still require a dedicated JIRA issue. An epic may group related maintenance work, but it does not replace the per-PR issue link required for traceability and merge approval.

## Tribal knowledge and operational notes

Expand Down
110 changes: 110 additions & 0 deletions tools/pr-jira-issue-link.js
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}`);
Comment thread
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,
};
125 changes: 125 additions & 0 deletions tools/pr-jira-issue-link.test.js
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
Comment thread
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']);
});
});
10 changes: 10 additions & 0 deletions tools/validate-pr-jira-issue-link.js
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);
Loading