Files
slsDetectorPackage/.github/workflows/pr_release_notes.yaml
T
maliakal_d 0e9b54d00a
Build on RHEL9 docker image / build (push) Successful in 4m13s
Build on RHEL8 docker image / build (push) Successful in 5m4s
Run Simulator Tests on local RHEL9 / build (push) Failing after 10m5s
Run Simulator Tests on local RHEL8 / build (push) Successful in 23m27s
Merge branch 'dev/pr_automation_5' into dev/test_pr_automation_5
2026-09-16 14:41:36 +02:00

215 lines
7.9 KiB
YAML

# This reusable workflow validates the PR release notes. It ignores comments and
# <details> blocks, checks for notes under the "## Release notes" section. If
# it is not an Infrastructure PR and release notes are not found, it should find
# them in an equivalent PR or referenced PR, else validation fails. If it is an
# Infrastructure PR or release notes are not found in an equivalent or
# referenced PR, it should add label 'No Release Note'.
#test branch
name: PR Release Notes
on:
workflow_call:
inputs:
pr_number:
required: true
type: number
permissions:
pull-requests: write
contents: read
issues: write
jobs:
check-release-notes:
name: Check PR release notes
runs-on: ubuntu-latest
steps:
- name: Validate presence of release notes
uses: actions/github-script@v8
with:
script: |
const prNumber = ${{ inputs.pr_number }};
if (!prNumber) {
core.setFailed('Unable to determine PR number.');
return;
}
/**
* Strips ignored content such as comments and <details> blocks
* from the given text.
*/
const stripIgnoredContent = (text = '') => {
return (text || '')
.replace(/<!--.*?-->/gs, '\n')
.replace(/<details\b[\s\S]*?<\/details>/gi, '\n')
.trim();
};
/**
* Finds the "Release notes" section text without ignored content.
*/
const findReleaseNotesSection = (body = '') => {
const sanitized = stripIgnoredContent(body || '');
if (!sanitized) return '';
const match = sanitized.match(
/(^|\n)#{1,6}\s*Release notes\s*\n([\s\S]*?)(?=\n#{1,6}\s+\S|\s*$)/i
);
if (!match) return '';
let section = (match[2] || '')
.split('\n')
.filter(line => !/^#{1,6}\s+/.test(line.trim()))
.join('\n')
.trim();
return section;
};
/**
* Parses a list of PR numbers from the given text.
*/
const parseNumberList = (text) => {
if (!text) {
return [];
}
return Array.from(
new Set(
Array.from(text.matchAll(/#(\d+)/g), match => Number(match[1]))
)
);
};
const findReferencedPrNumbers = (body = '') => {
if (!body) {
return [];
}
const relevantSections = [
'Equivalent PRs',
'Referenced PRs'
];
const patterns = relevantSections.map(section =>
new RegExp(
`(^|\\n)#{1,6}\\s*${section.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\s*\\n([\\s\\S]*?)(?=\\n#{1,6}\\s+\\S|\\s*$)`,
'i'
)
);
const numbers = [];
for (const pattern of patterns) {
const match = body.match(pattern);
if (match && match[2]) {
numbers.push(...parseNumberList(match[2]));
}
}
return Array.from(new Set(numbers));
};
const { data: pr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
const baseBranch = pr.base.ref;
if (baseBranch === 'main') {
core.info('PR targets main branch. Release notes validation is not required.');
return;
}
const { data: labels } =
await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
const labelNames = labels.map(label => label.name);
const hasInfrastructure = labelNames.includes('Infrastructure');
core.info('Infrastructure label present: ' + hasInfrastructure);
/** check that it has only developing detector labels (Ctb, Xilinx Ctb, Matterhorn) and does not have Eiger, Mythen3, Gotthard2, Jungfrau, Moench labels */
const developingDetectorLabels = ['Ctb', 'Xilinx Ctb', 'Matterhorn'];
const otherDetectorLabels = ['Eiger', 'Mythen3', 'Gotthard2', 'Jungfrau', 'Moench'];
const hasDevelopingDetectorLabel = labelNames.some(
label => developingDetectorLabels.includes(label)
);
const hasOtherDetectorLabel = labelNames.some(
label => otherDetectorLabels.includes(label)
);
const hasOnlyDevelopingDetectors =
hasDevelopingDetectorLabel && !hasOtherDetectorLabel;
core.info('Only developing detector labels present: ' + hasOnlyDevelopingDetectors);
const releaseNotes = findReleaseNotesSection(pr.body || '');
core.info('Release notes found in current PR: ' + (releaseNotes ? 'Yes' : 'No'));
if (hasInfrastructure || hasOnlyDevelopingDetectors) {
if (releaseNotes) {
core.setFailed(
'Infrastructure PRs or only developing Detector PRs cannot include release notes. Remove the "## Release notes" section or change the PR type.'
);
return;
}
return;
}
/**
* Add AI assisted release notes and return
*/
if (releaseNotes) {
// Remove any existing details section with heading "AI Assisted Release Notes"
const updatedBody = pr.body.replace(
/(^|\n)#{1,6}\s*AI Assisted Release Notes\s*\n([\s\S]*?)(?=\n#{1,6}\s+\S|\s*$)/gi,
''
).trim();
// Add a new section with heading "AI Assisted Release Notes" and placeholder text
const newBody = `${updatedBody}\n\n## AI Assisted Release Notes\n\n**This section will be populated with AI-assisted release notes.**\n\n`;
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
body: newBody
});
core.info('AI Assisted Release Notes section added to PR body.');
return;
}
/* Find release notes in equivalent or referenced PRs */
const equivalentAndReferencedPrNumbers = findReferencedPrNumbers(pr.body || '');
for (const referencedPrNumber of equivalentAndReferencedPrNumbers) {
if (referencedPrNumber === prNumber) {
continue;
}
try {
const { data: referencedPr } =
await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: referencedPrNumber
});
const referencedNotes = findReleaseNotesSection(referencedPr.body || '');
if (referencedNotes) {
core.info(
`Release notes found: In equivalent/referenced PR #${referencedPrNumber}. ` +
`Using that as the source for this PR.`
);
return;
}
} catch (error) {
core.warning(
`Unable to inspect referenced PR #${referencedPrNumber}: ${error.message}`
);
}
}
core.setFailed(
'No release notes were found in the PR body, equivalent PRs, or referenced PRs.'
);