From 4e71f8544b2eb84861d48582b4e0c1f30aacd365 Mon Sep 17 00:00:00 2001 From: Dhanya Thattil Date: Tue, 25 Aug 2026 11:20:54 +0200 Subject: [PATCH] workflow for detector whole word matching from changes in detectorServer folders, matching in pr title description, linked or references issues and prs, change in code with matching labels, add detector labels --- .github/workflows/pr_detector_labels.yaml | 370 ++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 .github/workflows/pr_detector_labels.yaml diff --git a/.github/workflows/pr_detector_labels.yaml b/.github/workflows/pr_detector_labels.yaml new file mode 100644 index 000000000..12f606577 --- /dev/null +++ b/.github/workflows/pr_detector_labels.yaml @@ -0,0 +1,370 @@ +name: PR Detector Labels + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - edited + +permissions: + pull-requests: write + issues: read + +jobs: + detector-labels: + name: Detect detector labels + runs-on: ubuntu-latest + + steps: + - name: Detect detector labels + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const pr = context.payload.pull_request; + + /* + * Known detector labels. + */ + const detectorLabels = [ + 'Ctb', + 'Eiger', + 'Moench', + 'Mythen3', + 'Jungfrau', + 'Gotthard2', + 'Matterhorn', + 'Xilinx_ctb' + ]; + + /* + * Escape characters that have special meaning + * in regular expressions. + */ + function escapeRegex(value) { + return value.replace( + /[.*+?^${}()|[\]\\]/g, + '\\$&' + ); + } + + /* + * Check whether a detector name appears as a + * complete word, case-insensitively. + */ + function containsDetector(text, detector) { + const regex = new RegExp( + `\\b${escapeRegex(detector)}\\b`, + 'i' + ); + + return regex.test(text); + } + + /* + * Detectors detected by any detection method. + */ + const detectedDetectors = new Set(); + + /* + * -------------------------------------------------- + * 1. Get all files changed by the PR + * -------------------------------------------------- + */ + const files = await github.paginate( + github.rest.pulls.listFiles, + { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + per_page: 100 + } + ); + + core.info( + `Found ${files.length} changed files.` + ); + + /* + * -------------------------------------------------- + * 2. Detect detector from detector-server folders + * -------------------------------------------------- + * + * Examples: + * + * slsDetectorServers/eigerDetectorServer/... + * ↓ + * Eiger + * + * slsDetectorServers/xilinx_ctbDetectorServer/... + * ↓ + * Xilinx_ctb + */ + for (const file of files) { + const match = file.filename.match( + /^slsDetectorServers\/([^/]+)DetectorServer\// + ); + + if (!match) { + continue; + } + + const detectorName = match[1]; + + /* + * Remove "DetectorServer" and capitalize + * the first character. + */ + const detector = + detectorName.charAt(0).toUpperCase() + + detectorName.slice(1); + + if (detectorLabels.includes(detector)) { + detectedDetectors.add(detector); + + core.info( + `Detected ${detector} from detector-server path: ${file.filename}` + ); + } + } + + /* + * -------------------------------------------------- + * 3. Detect detector from changed code + * -------------------------------------------------- + * + * Only the PR patch is searched. + * + * Therefore, text that already existed elsewhere + * in an unchanged file does not trigger a label. + */ + for (const file of files) { + if (!file.patch) { + continue; + } + + for (const detector of detectorLabels) { + if ( + containsDetector( + file.patch, + detector + ) + ) { + detectedDetectors.add(detector); + + core.info( + `Detected ${detector} in changed code: ${file.filename}` + ); + } + } + } + + /* + * -------------------------------------------------- + * 4. Detect detector from PR title/description + * -------------------------------------------------- + */ + const prText = [ + pr.title || '', + pr.body || '' + ].join('\n'); + + for (const detector of detectorLabels) { + if ( + containsDetector( + prText, + detector + ) + ) { + detectedDetectors.add(detector); + + core.info( + `Detected ${detector} in PR title or description.` + ); + } + } + + /* + * -------------------------------------------------- + * 5. Find any issue or PR mentioned in the PR + * -------------------------------------------------- + * + * Same repository: + * + * #123 + * Fixes #123 + * Related to #123 + * + * Cross repository: + * + * slsdetectorgroup/slsDetectorPackage#123 + */ + const references = new Map(); + + /* + * Same-repository references. + */ + for ( + const match of prText.matchAll( + /(^|[^\w])#(\d+)\b/g + ) + ) { + const owner = context.repo.owner; + const repo = context.repo.repo; + const number = Number(match[2]); + + const key = + `${owner}/${repo}#${number}`; + + references.set( + key, + { + owner, + repo, + number + } + ); + } + + /* + * Cross-repository references. + */ + for ( + const match of prText.matchAll( + /(?:^|[^\w])([\w.-]+)\/([\w.-]+)#(\d+)\b/g + ) + ) { + const owner = match[1]; + const repo = match[2]; + const number = Number(match[3]); + + const key = + `${owner}/${repo}#${number}`; + + references.set( + key, + { + owner, + repo, + number + } + ); + } + + core.info( + `Found ${references.size} referenced issue/PR(s).` + ); + + /* + * -------------------------------------------------- + * 6. Inspect referenced issues and PRs + * -------------------------------------------------- + * + * GitHub's Issues API returns both issues and PRs. + * A pull request is represented as an issue with + * additional pull-request information. + * + * We inspect: + * + * - title + * - description/body + */ + for (const reference of references.values()) { + try { + const { data: referencedItem } = + await github.rest.issues.get({ + owner: reference.owner, + repo: reference.repo, + issue_number: reference.number + }); + + const referencedText = [ + referencedItem.title || '', + referencedItem.body || '' + ].join('\n'); + + for (const detector of detectorLabels) { + if ( + containsDetector( + referencedText, + detector + ) + ) { + detectedDetectors.add(detector); + + core.info( + `Detected ${detector} in ` + + `${reference.owner}/${reference.repo}#${reference.number}.` + ); + } + } + } catch (error) { + /* + * If the referenced item cannot be accessed, + * don't fail the entire detector-label workflow. + */ + core.warning( + `Could not inspect ` + + `${reference.owner}/${reference.repo}#${reference.number}: ` + + `${error.message}` + ); + } + } + + /* + * -------------------------------------------------- + * 7. Get current PR labels + * -------------------------------------------------- + */ + const { data: currentLabels } = + await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber + }); + + const currentLabelNames = + currentLabels.map(label => label.name); + + /* + * -------------------------------------------------- + * 8. Add detected detector labels + * -------------------------------------------------- + * + * Existing labels are left untouched. + * + * Detector labels are NEVER removed automatically. + */ + for (const detector of detectedDetectors) { + if (!currentLabelNames.includes(detector)) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + labels: [detector] + }); + + core.info( + `Added detector label: ${detector}` + ); + } else { + core.info( + `Detector label already exists: ${detector}` + ); + } + } + + /* + * -------------------------------------------------- + * 9. Summary + * -------------------------------------------------- + */ + core.info( + `Detected detectors: ${ + detectedDetectors.size > 0 + ? [...detectedDetectors].join(', ') + : 'none' + }` + ); \ No newline at end of file