From 2a5672a138605c1dccb08219f511907f990e3424 Mon Sep 17 00:00:00 2001 From: Dhanya Thattil Date: Wed, 2 Sep 2026 10:37:04 +0200 Subject: [PATCH] Dev/pr automation part 2 (#1542) * 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 * comment * do not look at linked issues or prs * removed warning of deprecated node 20 github maybe? * ignore for infrastructure prs * ignore infrastrucutr prs * makign detector labels a reusable workflow and pr synchronize trigger trigger at synchronize and wait for validation worfklow to be done first * synchronize triger just checks if pr validation is complete and successfull, if not it fails, else it will run detector labesl workflow. it will not wait for pr validation * pr validation should also call detector labels workflow * picked up pr_validation from developer and modified * test sync * removed commit * trigger is just for code sync * look at head branch PRs to find it * ignoring some other workflows for now * no need of if as anything failing before the consequent jobs are skipped anyway * ignore tests fo rnow * make detector labels run all * without pr validate * fix error pr * renaming to make sense * naming * all the triggers are in a different workflow and all the big jobs in separate resuable workflows * validate pr type also separated * validation doesnt have any output anymore because its either setfailed or not, the unrelated labesl output have been moved to edit trigger workflow anyway * minor * combine for loop of changed files --- .github/workflows/build_wheel.yml | 3 + .github/workflows/cmake.yaml | 13 +- .github/workflows/conda_library.yaml | 8 +- .github/workflows/conda_python.yaml | 8 +- .github/workflows/pr_detector_labels.yaml | 260 ++++++++++++++++++ .github/workflows/pr_edit_trigger.yaml | 83 ++++++ .github/workflows/pr_synchronize_trigger.yaml | 21 ++ ...alidation.yaml => pr_type_validation.yaml} | 67 +---- .github/workflows/run_tests.yaml | 5 +- 9 files changed, 406 insertions(+), 62 deletions(-) create mode 100644 .github/workflows/pr_detector_labels.yaml create mode 100644 .github/workflows/pr_edit_trigger.yaml create mode 100644 .github/workflows/pr_synchronize_trigger.yaml rename .github/workflows/{pr_validation.yaml => pr_type_validation.yaml} (73%) diff --git a/.github/workflows/build_wheel.yml b/.github/workflows/build_wheel.yml index 9e1d2df20..53a7e456c 100644 --- a/.github/workflows/build_wheel.yml +++ b/.github/workflows/build_wheel.yml @@ -3,6 +3,9 @@ name: Build wheel on: workflow_dispatch: pull_request: + branches-ignore: + - 'dev/pr_automation*' + - 'dev/test_pr_automation*' push: branches: - main diff --git a/.github/workflows/cmake.yaml b/.github/workflows/cmake.yaml index 2b3512644..a49f08140 100644 --- a/.github/workflows/cmake.yaml +++ b/.github/workflows/cmake.yaml @@ -1,7 +1,16 @@ name: Native CMake Build -on: [push, pull_request, workflow_dispatch] - +on: + push: + branches-ignore: + - 'dev/pr_automation*' + - 'dev/test_pr_automation*' + pull_request: + branches-ignore: + - 'dev/pr_automation*' + - 'dev/test_pr_automation*' + workflow_dispatch: + env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Debug diff --git a/.github/workflows/conda_library.yaml b/.github/workflows/conda_library.yaml index 1086021c7..8d39bfad9 100644 --- a/.github/workflows/conda_library.yaml +++ b/.github/workflows/conda_library.yaml @@ -1,7 +1,11 @@ name: Build slsdetlib -on: [pull_request] - +on: + pull_request: + branches-ignore: + - 'dev/pr_automation*' + - 'dev/test_pr_automation*' + jobs: build: strategy: diff --git a/.github/workflows/conda_python.yaml b/.github/workflows/conda_python.yaml index 18d18e62e..733cb263d 100644 --- a/.github/workflows/conda_python.yaml +++ b/.github/workflows/conda_python.yaml @@ -1,7 +1,11 @@ name: slsdet -on: [pull_request] - +on: + pull_request: + branches-ignore: + - 'dev/pr_automation*' + - 'dev/test_pr_automation*' + jobs: build: strategy: diff --git a/.github/workflows/pr_detector_labels.yaml b/.github/workflows/pr_detector_labels.yaml new file mode 100644 index 000000000..06261dffb --- /dev/null +++ b/.github/workflows/pr_detector_labels.yaml @@ -0,0 +1,260 @@ +# This reusable workflow detects detector labels for pull requests. This workflow is ignored for Infrastructure Prs. It checks whole word matching of detector names in server folders, changed code and PR title/description. If a detector is detected, the corresponding label is added to the PR. Existing labels are left untouched. Detector labels are never removed automatically. + +name: PR Detector Labels + +on: + workflow_call: + inputs: + pr_number: + required: true + type: number + +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@v8 + with: + script: | + const prNumber = ${{ inputs.pr_number }}; + + /* + * Get the 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); + + if (currentLabelNames.includes('Infrastructure')) { + core.info( + 'Infrastructure label found. Skipping detector label detection.' + ); + return; + } + + core.info( + `Infrastructure label not found. Proceeding with detector label detection.` + ); + + /* + * Known detector labels. + */ + const detectorLabels = [ + 'Ctb', + 'Eiger', + 'Moench', + 'Mythen3', + 'Jungfrau', + 'Gotthard2', + 'Matterhorn', + 'Xilinx_ctb' + ]; + + + /* + * Check whether a detector name appears as a + * complete word, case-insensitively. + */ + function containsDetector(text, detector) { + const regex = new RegExp( + `\\b${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 changed files + * -------------------------------------------------- + * + * We inspect each file once and short-circuit as soon + * as a detector-server folder confirms a label. That + * avoids scanning the same detector folder again and + * avoids checking the rest of the file for unrelated + * detector names. + * + * Examples: + * + * slsDetectorServers/eigerDetectorServer/... + * ↓ + * Eiger + * + * slsDetectorServers/xilinx_ctbDetectorServer/... + * ↓ + * Xilinx_ctb + */ + const ignoredDetectorServerFolders = new Set(); + + for (const file of files) { + const serverMatch = file.filename.match( + /^slsDetectorServers\/([^/]+)DetectorServer\// + ); + + if (serverMatch) { + const detectorFolder = serverMatch[0]; + + // Skip all later files under a detected detector folder. + if (ignoredDetectorServerFolders.has(detectorFolder)) { + core.info( + `Skipping file in ignored detector-server folder: ${file.filename}` + ); + continue; + } + + const detectorName = serverMatch[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}` + ); + + // Once a detector is confirmed from the server folder, + // do not inspect this file or the rest of that folder. + ignoredDetectorServerFolders.add(detectorFolder); + continue; + } + } + + // Only inspect the PR patch, never the full file. + 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}` + ); + } + } + } + + /* + * -------------------------------------------------- + * 3. Detect detector from PR title/description + * -------------------------------------------------- + */ + + const { data: pr } = + await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + 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. 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}` + ); + } + } + + /* + * -------------------------------------------------- + * 6. Summary + * -------------------------------------------------- + */ + core.info( + `Detected detectors: ${ + detectedDetectors.size > 0 + ? [...detectedDetectors].join(', ') + : 'none' + }` + ); \ No newline at end of file diff --git a/.github/workflows/pr_edit_trigger.yaml b/.github/workflows/pr_edit_trigger.yaml new file mode 100644 index 000000000..e3cf726fa --- /dev/null +++ b/.github/workflows/pr_edit_trigger.yaml @@ -0,0 +1,83 @@ +# This workflow is triggered when opening/reopening PR, editing the PR description or changing labels. It ignores unrelated label changes. It runs PR type validation and if it succeeds, it runs PR milestone and detector labels workflow in parallel. +name: PR Edit Trigger + +on: + pull_request: + types: + - opened + - reopened + - edited + - labeled + - unlabeled + +permissions: + pull-requests: write + contents: read + +jobs: + check-pr-trigger: + name: Check PR Trigger + runs-on: ubuntu-latest + + outputs: + require_validation: ${{ steps.check.outputs.require_validation }} + + steps: + - name: Check whether PR Validation is required + id: check + uses: actions/github-script@v8 + with: + script: | + /* + * If triggered by label change, ignore unrelated labels. + */ + const action = context.payload.action; + if (action === 'labeled' || action === 'unlabeled') { + const changedLabel = context.payload.label?.name; + + const relevantLabels = [ + 'Feature', + 'Bug Fix', + 'Infrastructure', + 'Breaking API' + ]; + + if (!relevantLabels.includes(changedLabel)) { + core.info( + `Ignoring unrelated label change: ${changedLabel}` + ); + core.setOutput('require_validation', 'false'); + return; + } + } + + core.info('PR type validation required.'); + core.setOutput('require_validation', 'true'); + + validate-pr-type: + needs: check-pr-trigger + if: ${{ needs.check-pr-trigger.outputs.require_validation == 'true' }} + uses: ./.github/workflows/pr_type_validation.yaml + with: + pr_number: ${{ github.event.pull_request.number }} + permissions: + pull-requests: write + contents: read + + milestone: + needs: validate-pr-type + uses: ./.github/workflows/pr_milestone.yaml + with: + pr_number: ${{ github.event.pull_request.number }} + permissions: + pull-requests: write + contents: read + + detector-labels: + needs: validate-pr-type + uses: ./.github/workflows/pr_detector_labels.yaml + with: + pr_number: ${{ github.event.pull_request.number }} + permissions: + pull-requests: write + issues: read \ No newline at end of file diff --git a/.github/workflows/pr_synchronize_trigger.yaml b/.github/workflows/pr_synchronize_trigger.yaml new file mode 100644 index 000000000..d455d254c --- /dev/null +++ b/.github/workflows/pr_synchronize_trigger.yaml @@ -0,0 +1,21 @@ +# This workflow is triggered when code is pushed to a pull request. It runs the reusable detector label workflow. + +name: PR Synchronize Trigger + +on: + pull_request: + types: + - synchronize + +permissions: + actions: read + pull-requests: read + +jobs: + detector-labels: + uses: ./.github/workflows/pr_detector_labels.yaml + with: + pr_number: ${{ github.event.pull_request.number }} + permissions: + pull-requests: write + issues: read diff --git a/.github/workflows/pr_validation.yaml b/.github/workflows/pr_type_validation.yaml similarity index 73% rename from .github/workflows/pr_validation.yaml rename to .github/workflows/pr_type_validation.yaml index 13382ffad..f9a04bc97 100644 --- a/.github/workflows/pr_validation.yaml +++ b/.github/workflows/pr_type_validation.yaml @@ -1,64 +1,33 @@ -# This workflow is triggered when opening/reopening PR or editing the Pr description or changing labels. It ignores unrelated label changes. It validates the PR type and Breaking API, then sets the labels. It also runs the PR milestone workflow to set the milestone based on the PR type label. -name: PR Validation +# This reusable workflow reads the primary type and breaking api selection from the PR description and validates the PR labels. It ensures that exactly one primary type is selected, and that the Breaking API label is consistent with the checkbox in the PR description. It also adds or removes labels as necessary to match the PR description. + +name: PR Type Validation on: - pull_request: - types: - - opened - - reopened - - edited - - labeled - - unlabeled + workflow_call: + inputs: + pr_number: + required: true + type: number permissions: pull-requests: write contents: read jobs: - validate-pr: - name: Validate PR + validate_pr_type: + name: PR Type Validation runs-on: ubuntu-latest - outputs: - set_milestone: ${{ steps.validate.outputs.set_milestone }} steps: - - name: Validate PR type and Breaking API + - name: PR Type Validation and Breaking API id: validate uses: actions/github-script@v8 with: script: | - /* - * Set it to run milestone at the end by default - */ - core.setOutput('set_milestone', 'true'); - - /* - * If triggered by label change, ignore unrelated labels. - */ - const action = context.payload.action; - if (action === 'labeled' || action === 'unlabeled') { - const changedLabel = context.payload.label?.name; - - const relevantLabels = [ - 'Feature', - 'Bug Fix', - 'Infrastructure', - 'Breaking API' - ]; - - if (!relevantLabels.includes(changedLabel)) { - core.info( - `Ignoring unrelated label change: ${changedLabel}` - ); - core.setOutput('set_milestone', 'false'); - return; - } - } - /* * Read the body contents of PR */ - const prNumber = context.payload.pull_request?.number; + const prNumber = ${{ inputs.pr_number }}; if (!prNumber) { core.setFailed('Unable to determine PR number.'); @@ -205,14 +174,4 @@ jobs: core.info(`Added label: ${primaryType}`); } - core.info('PR validation passed.'); - - milestone: - needs: validate-pr - if: ${{ needs.validate-pr.outputs.set_milestone == 'true' }} - uses: ./.github/workflows/pr_milestone.yaml - with: - pr_number: ${{ github.event.pull_request.number }} - permissions: - pull-requests: write - contents: read \ No newline at end of file + core.info('PR validation passed.'); \ No newline at end of file diff --git a/.github/workflows/run_tests.yaml b/.github/workflows/run_tests.yaml index f5ee18c63..19d7a7f03 100644 --- a/.github/workflows/run_tests.yaml +++ b/.github/workflows/run_tests.yaml @@ -2,8 +2,9 @@ name: Run Simulator Tests on: push: - - + branches-ignore: + - 'dev/pr_automation*' + - 'dev/test_pr_automation*' env: # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)