# This reusable workflow validates the PR release notes. It ignores comments and #
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
blocks * from the given text. */ const stripIgnoredContent = (text = '') => { return (text || '') .replace(//gs, '\n') .replace(//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.' );