From d0af05e592dfb1181709d9858afeae85b211d372 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:14:50 +0000 Subject: [PATCH 001/165] update checkout action version to v6 in CLA workflow --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 57aa270..ce14de9 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -18,7 +18,7 @@ jobs: steps: # --- Step 1: Check Base Branch --- - name: Checkout base branch and check for contributor status - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.base.sha }} From fe826dccf712a991ef38d31c44ff68e2c534e6be Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:24:10 +0000 Subject: [PATCH 002/165] refactor: enhance CLA check for contributor status in PR branch --- .github/workflows/cla-check.yaml | 67 +++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index ce14de9..15fb815 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -43,8 +43,7 @@ jobs: # --- Step 2: Check PR Branch --- - name: Checkout PR branch and check for contributor status - # Only run if contributor wasn't found on the base branch - if: steps.check_contributor_base.outputs.on_base != 'true' + # Always check PR branch to detect if contributor removed themselves uses: actions/checkout@v5 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -52,16 +51,20 @@ jobs: - name: Determine if contributor exists in PR branch id: check_contributor_pr - # Only run if contributor wasn't found on the base branch - if: steps.check_contributor_base.outputs.on_base != 'true' + # Always check PR branch to detect if contributor removed themselves run: | AUTHOR="${{ github.event.pull_request.user.login }}" - if grep -q "^$AUTHOR" CONTRIBUTORS; then - echo "signed=true" >> $GITHUB_OUTPUT - echo "✅ $AUTHOR has updated their CLA signature in CONTRIBUTORS file." + if [ -f "CONTRIBUTORS" ]; then + if grep -q "^$AUTHOR" CONTRIBUTORS; then + echo "on_pr=true" >> $GITHUB_OUTPUT + echo "✅ $AUTHOR is in the CONTRIBUTORS file on PR branch." + else + echo "on_pr=false" >> $GITHUB_OUTPUT + echo "⚠️ $AUTHOR is not in the CONTRIBUTORS file on PR branch." + fi else - echo "signed=false" >> $GITHUB_OUTPUT - echo "❌ $AUTHOR has not signed the CLA." + echo "on_pr=undefined" >> $GITHUB_OUTPUT + echo "🔴 CONTRIBUTORS file does not exist on PR branch." fi # --- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) --- @@ -74,14 +77,16 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const signedOnBase = '${{ steps.check_contributor_base.outputs.on_base }}' === 'true'; - // Default signed status is false if the second check wasn't run - const signedOnPr = '${{ steps.check_contributor_pr.outputs.signed }}' === 'true'; - const cla_met = signedOnBase || signedOnPr; + const signedOnPr = '${{ steps.check_contributor_pr.outputs.on_pr }}' === 'true'; const issue_number = context.issue.number; const owner = context.repo.owner; const repo = context.repo.repo; const author = context.payload.pull_request.user.login; + // Check if contributor was on base but removed themselves from PR + const removedFromPr = signedOnBase && !signedOnPr; + const cla_met = signedOnBase && signedOnPr; + // Helper function to create or update a label with a specific color async function ensureLabel(name, color, description) { try { @@ -98,7 +103,28 @@ jobs: const COLOR_SIGNED = '0052cc'; // Blue const COLOR_REQUIRED = 'b60205'; // Red - console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr})`); + console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Removed: ${removedFromPr})`); + + // Handle case where contributor removed themselves from CONTRIBUTORS file + if (removedFromPr) { + await ensureLabel('cla-required', COLOR_REQUIRED, 'CLA signature is required for this PR.'); + console.log('⚠️ Contributor was in base branch but removed from PR branch.'); + + // Ensure labels are correct + await Promise.allSettled([ + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-signed' }), + github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-required'] }) + ]); + + // Post warning comment + const commentBody = `⚠️ Hello @${author}!\n\nYour CLA signature was found on the base branch, but you appear to have removed yourself from the _CONTRIBUTORS_ file in this PR.\n\nPlease ensure your entry remains in the _CONTRIBUTORS_ file. If you have already signed the CLA, you should not remove your details from the file.`; + + await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); + + // Fail the GitHub Action run + console.error("⚠️ Contributor removed themselves from CONTRIBUTORS file."); + process.exit(1); + } if (cla_met) { await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.'); @@ -106,12 +132,17 @@ jobs: // Use Promise.allSettled for robust label management await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), + github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) + ]); + + } else if (!signedOnBase && signedOnPr) { + // New contributor signing CLA for the first time + await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.'); + console.log('✅ New contributor has signed the CLA in PR branch.'); + await Promise.allSettled([ + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), + github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) ]); - if ( signedOnBase === false ) { - await Promise.allSettled([ - github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) - ]); - } } else { await ensureLabel('cla-required', COLOR_REQUIRED, 'CLA signature is required for this PR.'); From e78dd860a4d37a6f67830d18dbad2a09f23d2575 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:31:04 +0000 Subject: [PATCH 003/165] refactor: update step name for contributor check in CLA workflow --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 15fb815..e61b1e7 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -23,7 +23,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.base.sha }} - - name: Determine if contributor exists in base + - name: Determine if contributor exists in base (new) id: check_contributor_base run: | AUTHOR="${{ github.event.pull_request.user.login }}" From fc10ca36df6eab4c7f9484c2a9280b70db172a9b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:51:48 +0000 Subject: [PATCH 004/165] refactor: update ref input handling for CLA verification --- .github/workflows/cla-check.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index e61b1e7..0759974 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -7,6 +7,11 @@ on: required: false type: string default: 'ubuntu-24.04' + ref: + description: 'The git ref (branch, tag, or commit SHA) to check out for CLA verification' + required: false + type: string + default: '' permissions: contents: read @@ -21,7 +26,7 @@ jobs: uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ github.event.pull_request.base.sha }} + ref: ${{ inputs.ref != '' && inputs.ref || github.event.pull_request.base.sha }} - name: Determine if contributor exists in base (new) id: check_contributor_base From 2bd6f129321ab1287d01a69abf85e049c82dda58 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 22:58:19 +0000 Subject: [PATCH 005/165] refactor: add functionality to delete old CLA-related comments --- .github/workflows/cla-check.yaml | 40 ++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 0759974..60d49b0 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -108,6 +108,37 @@ jobs: const COLOR_SIGNED = '0052cc'; // Blue const COLOR_REQUIRED = 'b60205'; // Red + // Helper function to delete old CLA-related comments from this bot + async function deleteOldClaComments() { + try { + const comments = await github.rest.issues.listComments({ + owner, + repo, + issue_number + }); + + // Filter comments from the bot that contain CLA-related content + const botComments = comments.data.filter(comment => + comment.user.type === 'Bot' && + (comment.body.includes('CLA') || + comment.body.includes('CONTRIBUTORS') || + comment.body.includes('Contributor Licence Agreement')) + ); + + // Delete all old CLA comments + for (const comment of botComments) { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: comment.id + }); + console.log(`Deleted old CLA comment #${comment.id}`); + } + } catch (error) { + console.log('Error deleting old comments:', error.message); + } + } + console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Removed: ${removedFromPr})`); // Handle case where contributor removed themselves from CONTRIBUTORS file @@ -121,6 +152,9 @@ jobs: github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-required'] }) ]); + // Delete old CLA comments before posting new one + await deleteOldClaComments(); + // Post warning comment const commentBody = `⚠️ Hello @${author}!\n\nYour CLA signature was found on the base branch, but you appear to have removed yourself from the _CONTRIBUTORS_ file in this PR.\n\nPlease ensure your entry remains in the _CONTRIBUTORS_ file. If you have already signed the CLA, you should not remove your details from the file.`; @@ -149,6 +183,9 @@ jobs: github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) ]); + // Delete old CLA comments since CLA is now signed + await deleteOldClaComments(); + } else { await ensureLabel('cla-required', COLOR_REQUIRED, 'CLA signature is required for this PR.'); console.log('❌ CLA condition NOT met. Adding required label and ensuring signed label is absent.'); @@ -159,6 +196,9 @@ jobs: github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-required'] }) ]); + // Delete old CLA comments before posting new one + await deleteOldClaComments(); + // Post CLA comment const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/simulation-systems/blob/github_wps/Momentum-CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, real name, organisation, email, and date) to the _CONTRIBUTORS_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; From ff15ce5cbd8d1a2b5ab9a9680ba6269ecf90a9da Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:06:04 +0000 Subject: [PATCH 006/165] refactor: improve filtering of CLA-related comments from GitHub Actions bot --- .github/workflows/cla-check.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 60d49b0..8b4920e 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -117,14 +117,17 @@ jobs: issue_number }); - // Filter comments from the bot that contain CLA-related content + // Filter comments from GitHub Actions bot that contain CLA-related content + // GitHub Actions bot username is 'github-actions[bot]' const botComments = comments.data.filter(comment => - comment.user.type === 'Bot' && + (comment.user.login === 'github-actions[bot]' || comment.user.type === 'Bot') && (comment.body.includes('CLA') || comment.body.includes('CONTRIBUTORS') || comment.body.includes('Contributor Licence Agreement')) ); + console.log(`Found ${botComments.length} old CLA comment(s) to delete`); + // Delete all old CLA comments for (const comment of botComments) { await github.rest.issues.deleteComment({ @@ -132,12 +135,8 @@ jobs: repo, comment_id: comment.id }); - console.log(`Deleted old CLA comment #${comment.id}`); + console.log(`Deleted old CLA comment #${comment.id} from ${comment.user.login}`); } - } catch (error) { - console.log('Error deleting old comments:', error.message); - } - } console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Removed: ${removedFromPr})`); From 1181bdbab832c97b8dce9cf1396552f2482a18a5 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:35:51 +0000 Subject: [PATCH 007/165] refactor: remove unused ref input and simplify checkout ref handling --- .github/workflows/cla-check.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 8b4920e..483813a 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -7,11 +7,6 @@ on: required: false type: string default: 'ubuntu-24.04' - ref: - description: 'The git ref (branch, tag, or commit SHA) to check out for CLA verification' - required: false - type: string - default: '' permissions: contents: read @@ -26,7 +21,7 @@ jobs: uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ inputs.ref != '' && inputs.ref || github.event.pull_request.base.sha }} + ref: ${{ github.ref }} - name: Determine if contributor exists in base (new) id: check_contributor_base From e2f37e77b1e2bfa134a88ea71d53423cf6374c64 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:40:30 +0000 Subject: [PATCH 008/165] refactor: add error handling for deleting old CLA comments --- .github/workflows/cla-check.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 483813a..2131469 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -132,6 +132,10 @@ jobs: }); console.log(`Deleted old CLA comment #${comment.id} from ${comment.user.login}`); } + } catch (error) { + console.log('Error deleting old comments:', error.message); + } + } console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Removed: ${removedFromPr})`); From 48dd50303bc01f8b48bf48635cbb81dce9e70e5b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:43:34 +0000 Subject: [PATCH 009/165] refactor: enhance error handling for label creation in CLA workflow --- .github/workflows/cla-check.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 2131469..085f85f 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -94,8 +94,12 @@ jobs: console.log(`Updated label: ${name} with color ${color}`); } catch (error) { // If update fails (label doesn't exist), create it - await github.rest.issues.createLabel({ owner, repo, name, color, description }); - console.log(`Created new label: ${name} with color ${color}`); + try { + await github.rest.issues.createLabel({ owner, repo, name, color, description }); + console.log(`Created new label: ${name} with color ${color}`); + } catch (createError) { + console.log(`Error with label ${name}:`, createError.message); + } } } From 4e23060e79997a38338492209962eb648523a051 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Dec 2025 23:55:20 +0000 Subject: [PATCH 010/165] refactor: update CONTRIBUTORS file references to CONTRIBUTORS.md in CLA workflow --- .github/workflows/cla-check.yaml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 085f85f..3c0d211 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -27,8 +27,8 @@ jobs: id: check_contributor_base run: | AUTHOR="${{ github.event.pull_request.user.login }}" - if [ -f "CONTRIBUTORS" ]; then - if grep -q "^$AUTHOR" CONTRIBUTORS; then + if [ -f "CONTRIBUTORS.md" ]; then + if grep -q "| $AUTHOR |" CONTRIBUTORS.md || grep -q "| $AUTHOR " CONTRIBUTORS.md; then echo "on_base=true" >> $GITHUB_OUTPUT echo "🎉 $AUTHOR has already signed the CLA on base branch." else @@ -36,9 +36,9 @@ jobs: echo "⚠️ $AUTHOR not on base. Proceeding to check PR branch." fi else - # If CONTRIBUTORS file doesn't exist, we must check PR branch + # If CONTRIBUTORS.md file doesn't exist, we must check PR branch echo "on_base=undefined" >> $GITHUB_OUTPUT - echo "🔴 CONTRIBUTORS file does not exist on base. Proceeding to check PR branch." + echo "🔴 CONTRIBUTORS.md file does not exist on base. Proceeding to check PR branch." fi # --- Step 2: Check PR Branch --- @@ -54,17 +54,17 @@ jobs: # Always check PR branch to detect if contributor removed themselves run: | AUTHOR="${{ github.event.pull_request.user.login }}" - if [ -f "CONTRIBUTORS" ]; then - if grep -q "^$AUTHOR" CONTRIBUTORS; then + if [ -f "CONTRIBUTORS.md" ]; then + if grep -q "| $AUTHOR |" CONTRIBUTORS.md || grep -q "| $AUTHOR " CONTRIBUTORS.md; then echo "on_pr=true" >> $GITHUB_OUTPUT - echo "✅ $AUTHOR is in the CONTRIBUTORS file on PR branch." + echo "✅ $AUTHOR is in the CONTRIBUTORS.md file on PR branch." else echo "on_pr=false" >> $GITHUB_OUTPUT - echo "⚠️ $AUTHOR is not in the CONTRIBUTORS file on PR branch." + echo "⚠️ $AUTHOR is not in the CONTRIBUTORS.md file on PR branch." fi else echo "on_pr=undefined" >> $GITHUB_OUTPUT - echo "🔴 CONTRIBUTORS file does not exist on PR branch." + echo "🔴 CONTRIBUTORS.md file does not exist on PR branch." fi # --- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) --- @@ -158,7 +158,7 @@ jobs: await deleteOldClaComments(); // Post warning comment - const commentBody = `⚠️ Hello @${author}!\n\nYour CLA signature was found on the base branch, but you appear to have removed yourself from the _CONTRIBUTORS_ file in this PR.\n\nPlease ensure your entry remains in the _CONTRIBUTORS_ file. If you have already signed the CLA, you should not remove your details from the file.`; + const commentBody = `⚠️ Hello @${author}!\n\nYour CLA signature was found on the base branch, but you appear to have removed yourself from the _CONTRIBUTORS.md_ file in this PR.\n\nPlease ensure your entry remains in the _CONTRIBUTORS.md_ file. If you have already signed the CLA, you should not remove your details from the file.`; await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); @@ -202,11 +202,11 @@ jobs: await deleteOldClaComments(); // Post CLA comment - const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/simulation-systems/blob/github_wps/Momentum-CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, real name, organisation, email, and date) to the _CONTRIBUTORS_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; + const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/simulation-systems/blob/github_wps/Momentum-CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, real name, organisation, email, and date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); // Fail the GitHub Action run - console.error("⚠️ Please add yourself to the CONTRIBUTORS file to sign the CLA."); + console.error("⚠️ Please add yourself to the CONTRIBUTORS.md file to sign the CLA."); process.exit(1); } From 548cddfd7795928c6cd9d9aef8618275786d6924 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:20:10 +0000 Subject: [PATCH 011/165] refactor: update CLA comment link to point to the correct repository location --- .github/workflows/cla-check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 3c0d211..e757f49 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -1,4 +1,4 @@ -name: CLA +name: CLA Checker on: workflow_call: inputs: @@ -202,7 +202,7 @@ jobs: await deleteOldClaComments(); // Post CLA comment - const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/simulation-systems/blob/github_wps/Momentum-CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, real name, organisation, email, and date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; + const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/Momentum/blob/main/CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, real name, organisation, email, and date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); From dcf2c978f5af68e60876a67b22c3486f5877e063 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Dec 2025 00:47:27 +0000 Subject: [PATCH 012/165] Update post message --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index e757f49..2fb3e65 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -202,7 +202,7 @@ jobs: await deleteOldClaComments(); // Post CLA comment - const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/Momentum/blob/main/CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, real name, organisation, email, and date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; + const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/Momentum/blob/main/CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, Real Name, Affiliation, and Date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); From a455d679330865f87f8ba8ce0e11096c38f82064 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:03:44 +0000 Subject: [PATCH 013/165] Handle the scenario where a contributor has already signed the CLA in the base branch --- .github/workflows/cla-check.yaml | 42 ++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 2fb3e65..a4d1caf 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -28,7 +28,7 @@ jobs: run: | AUTHOR="${{ github.event.pull_request.user.login }}" if [ -f "CONTRIBUTORS.md" ]; then - if grep -q "| $AUTHOR |" CONTRIBUTORS.md || grep -q "| $AUTHOR " CONTRIBUTORS.md; then + if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then echo "on_base=true" >> $GITHUB_OUTPUT echo "🎉 $AUTHOR has already signed the CLA on base branch." else @@ -44,7 +44,7 @@ jobs: # --- Step 2: Check PR Branch --- - name: Checkout PR branch and check for contributor status # Always check PR branch to detect if contributor removed themselves - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.head.sha }} @@ -55,7 +55,7 @@ jobs: run: | AUTHOR="${{ github.event.pull_request.user.login }}" if [ -f "CONTRIBUTORS.md" ]; then - if grep -q "| $AUTHOR |" CONTRIBUTORS.md || grep -q "| $AUTHOR " CONTRIBUTORS.md; then + if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then echo "on_pr=true" >> $GITHUB_OUTPUT echo "✅ $AUTHOR is in the CONTRIBUTORS.md file on PR branch." else @@ -67,6 +67,22 @@ jobs: echo "🔴 CONTRIBUTORS.md file does not exist on PR branch." fi + # --- Check if CONTRIBUTORS.md was modified in this PR --- + - name: Check if CONTRIBUTORS.md was modified in PR + id: check_contributors_modified + run: | + # Get the list of changed files in the PR + git fetch origin ${{ github.event.pull_request.base.ref }} + CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD) + + if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then + echo "modified=true" >> $GITHUB_OUTPUT + echo "📝 CONTRIBUTORS.md file was modified in this PR." + else + echo "modified=false" >> $GITHUB_OUTPUT + echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." + fi + # --- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) --- - name: Manage CLA Status, Labels, and Comments uses: actions/github-script@v8 @@ -78,14 +94,16 @@ jobs: script: | const signedOnBase = '${{ steps.check_contributor_base.outputs.on_base }}' === 'true'; const signedOnPr = '${{ steps.check_contributor_pr.outputs.on_pr }}' === 'true'; + const contributorsModified = '${{ steps.check_contributors_modified.outputs.modified }}' === 'true'; const issue_number = context.issue.number; const owner = context.repo.owner; const repo = context.repo.repo; const author = context.payload.pull_request.user.login; // Check if contributor was on base but removed themselves from PR - const removedFromPr = signedOnBase && !signedOnPr; - const cla_met = signedOnBase && signedOnPr; + const removedFromPr = signedOnBase && !signedOnPr && contributorsModified; + // CLA is met if: (1) signed on both base and PR, OR (2) signed on base and didn't modify CONTRIBUTORS.md + const cla_met = (signedOnBase && signedOnPr) || (signedOnBase && !contributorsModified); // Helper function to create or update a label with a specific color async function ensureLabel(name, color, description) { @@ -141,7 +159,7 @@ jobs: } } - console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Removed: ${removedFromPr})`); + console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Modified: ${contributorsModified}, Removed: ${removedFromPr})`); // Handle case where contributor removed themselves from CONTRIBUTORS file if (removedFromPr) { @@ -169,13 +187,23 @@ jobs: if (cla_met) { await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.'); - console.log('✅ CLA condition met. Removing required label and adding signed label.'); + + // Different messages based on scenario + if (signedOnBase && !contributorsModified) { + console.log('✅ CLA already signed on base branch, and CONTRIBUTORS.md not modified in PR.'); + } else { + console.log('✅ CLA condition met. Removing required label and adding signed label.'); + } + // Use Promise.allSettled for robust label management await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) ]); + // Delete old CLA comments since CLA is satisfied + await deleteOldClaComments(); + } else if (!signedOnBase && signedOnPr) { // New contributor signing CLA for the first time await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.'); From 8e563d540019ba0dd3bfbade46fff11ea5172638 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:39:18 +0000 Subject: [PATCH 014/165] Empty Commit From 0eeacb5f4755b8b98f819dff278cc768384ca567 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Dec 2025 21:54:13 +0000 Subject: [PATCH 015/165] Refactor CLA workflow to use base SHA for accurate comparison --- .github/workflows/cla-check.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index a4d1caf..769cba8 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -67,13 +67,18 @@ jobs: echo "🔴 CONTRIBUTORS.md file does not exist on PR branch." fi - # --- Check if CONTRIBUTORS.md was modified in this PR --- + # -- Check if CONTRIBUTORS.md was modified in this PR - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified run: | - # Get the list of changed files in the PR + # Fetch the base branch git fetch origin ${{ github.event.pull_request.base.ref }} - CHANGED_FILES=$(git diff --name-only origin/${{ github.event.pull_request.base.ref }}...HEAD) + + # Use the base SHA directly for comparison (works with forks) + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + + CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA) if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then echo "modified=true" >> $GITHUB_OUTPUT @@ -83,7 +88,7 @@ jobs: echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." fi - # --- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) --- + # -- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) - name: Manage CLA Status, Labels, and Comments uses: actions/github-script@v8 # Using 'always()' here so this step runs regardless of previous From 49f528b6a6fe7000d8771b6a6faf6e1760af149b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Dec 2025 22:33:39 +0000 Subject: [PATCH 016/165] Add copyright notice to CLA Checker workflow file --- .github/workflows/cla-check.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 769cba8..6a26e11 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -1,3 +1,8 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ name: CLA Checker on: workflow_call: From 61639ace025ca630ad61ae999a67c7193a3ad66d Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 17 Dec 2025 08:54:44 +0000 Subject: [PATCH 017/165] remove labelling when cla already signed on base (#46) Co-authored-by: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> --- .github/workflows/cla-check.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index cf55824..1b24712 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -53,7 +53,6 @@ jobs: uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ github.event.pull_request.head.sha }} - name: Determine if contributor exists in PR branch id: check_contributor_pr @@ -82,9 +81,8 @@ jobs: # Use the base SHA directly for comparison (works with forks) BASE_SHA="${{ github.event.pull_request.base.sha }}" - HEAD_SHA="${{ github.event.pull_request.head.sha }}" - CHANGED_FILES=$(git diff --name-only $BASE_SHA..$HEAD_SHA) + CHANGED_FILES=$(git diff --name-only $BASE_SHA) if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then echo "modified=true" >> $GITHUB_OUTPUT @@ -197,7 +195,6 @@ jobs: } if (cla_met) { - await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.'); // Different messages based on scenario if (signedOnBase && !contributorsModified) { From 61964fe34c4f091f3ef45a94b52988368f043c48 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 17 Dec 2025 10:54:36 +0000 Subject: [PATCH 018/165] Remove label (#47) --- .github/workflows/cla-check.yaml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 1b24712..32ead2e 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -47,16 +47,27 @@ jobs: echo "🔴 CONTRIBUTORS.md file does not exist on base. Proceeding to check PR branch." fi - # --- Step 2: Check PR Branch --- + # --- Step 2: Check PR Branch - not merged --- + # Use to determine if developer has added themselves in the PR + - name: Checkout PR branch and check for contributor status + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ github.event.pull_request.head.sha }} + path: clean_pr_branch + + # --- Check Merged PR Branch + # Use to determine if developer has altered the PR - name: Checkout PR branch and check for contributor status - # Always check PR branch to detect if contributor removed themselves uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} + path: merged_branch - name: Determine if contributor exists in PR branch id: check_contributor_pr # Always check PR branch to detect if contributor removed themselves + working-directory: ${GITHUB_WORKSPACE}/clean_pr_branch run: | AUTHOR="${{ github.event.pull_request.user.login }}" if [ -f "CONTRIBUTORS.md" ]; then @@ -75,6 +86,7 @@ jobs: # -- Check if CONTRIBUTORS.md was modified in this PR - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified + working-directory: ${GITHUB_WORKSPACE}/merged_branch run: | # Fetch the base branch git fetch origin ${{ github.event.pull_request.base.ref }} From dc0cdf9b187a9cdb4f8e99cfe2a63c845da399a3 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:05:37 +0000 Subject: [PATCH 019/165] Remove label (#48) --- .github/workflows/cla-check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 32ead2e..d4d0352 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -67,7 +67,7 @@ jobs: - name: Determine if contributor exists in PR branch id: check_contributor_pr # Always check PR branch to detect if contributor removed themselves - working-directory: ${GITHUB_WORKSPACE}/clean_pr_branch + working-directory: ./clean_pr_branch run: | AUTHOR="${{ github.event.pull_request.user.login }}" if [ -f "CONTRIBUTORS.md" ]; then @@ -86,7 +86,7 @@ jobs: # -- Check if CONTRIBUTORS.md was modified in this PR - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified - working-directory: ${GITHUB_WORKSPACE}/merged_branch + working-directory: ./merged_branch run: | # Fetch the base branch git fetch origin ${{ github.event.pull_request.base.ref }} From ba7228b298939d6ec238bc4e6f64f1cba9176746 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 18 Dec 2025 11:22:21 +0000 Subject: [PATCH 020/165] Remove label (#49) --- .github/workflows/cla-check.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index d4d0352..41028c6 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -218,7 +218,6 @@ jobs: // Use Promise.allSettled for robust label management await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), - github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) ]); // Delete old CLA comments since CLA is satisfied From 641b29d1552ef7063b76ef75434029dcccb699b0 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Fri, 19 Dec 2025 08:26:44 +0000 Subject: [PATCH 021/165] update cla action (#50) --- .github/workflows/cla-check.yaml | 84 ++++++++++++++++---------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 41028c6..52cf6b1 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -22,15 +22,16 @@ jobs: check-cla: runs-on: ${{ inputs.runner }} steps: - # --- Step 1: Check Base Branch --- - - name: Checkout base branch and check for contributor status + - name: Checkout Base Branch uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.ref }} + path: base_branch - name: Determine if contributor exists in base (new) id: check_contributor_base + working-directory: ./base_branch run: | AUTHOR="${{ github.event.pull_request.user.login }}" if [ -f "CONTRIBUTORS.md" ]; then @@ -47,27 +48,16 @@ jobs: echo "🔴 CONTRIBUTORS.md file does not exist on base. Proceeding to check PR branch." fi - # --- Step 2: Check PR Branch - not merged --- - # Use to determine if developer has added themselves in the PR - - name: Checkout PR branch and check for contributor status + - name: Checkout PR Branch uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.event.pull_request.head.sha }} - path: clean_pr_branch - - # --- Check Merged PR Branch - # Use to determine if developer has altered the PR - - name: Checkout PR branch and check for contributor status - uses: actions/checkout@v6 - with: - token: ${{ secrets.GITHUB_TOKEN }} - path: merged_branch + path: pr_branch - name: Determine if contributor exists in PR branch id: check_contributor_pr - # Always check PR branch to detect if contributor removed themselves - working-directory: ./clean_pr_branch + working-directory: pr_branch run: | AUTHOR="${{ github.event.pull_request.user.login }}" if [ -f "CONTRIBUTORS.md" ]; then @@ -83,12 +73,18 @@ jobs: echo "🔴 CONTRIBUTORS.md file does not exist on PR branch." fi + - name: Checkout Merge Branch + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ref: "refs/pull/${{ github.event.number }}/merge" + path: merge_branch + # -- Check if CONTRIBUTORS.md was modified in this PR - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified - working-directory: ./merged_branch + working-directory: ./merge_branch run: | - # Fetch the base branch git fetch origin ${{ github.event.pull_request.base.ref }} # Use the base SHA directly for comparison (works with forks) @@ -121,30 +117,36 @@ jobs: const repo = context.repo.repo; const author = context.payload.pull_request.user.login; - // Check if contributor was on base but removed themselves from PR - const removedFromPr = signedOnBase && !signedOnPr && contributorsModified; - // CLA is met if: (1) signed on both base and PR, OR (2) signed on base and didn't modify CONTRIBUTORS.md - const cla_met = (signedOnBase && signedOnPr) || (signedOnBase && !contributorsModified); + // Check if contributor was on base but modified the file + // This may be valid but will require an admin to ok it + const invalidModify = signedOnBase && contributorsModified; - // Helper function to create or update a label with a specific color - async function ensureLabel(name, color, description) { + // CLA is met if: + // * (1) Didn't Invalidly Modify the PR and + // * (2) signed on both base and PR, OR + // * (3) signed on base and didn't modify CONTRIBUTORS.md + const cla_met = !invalidModify && ((signedOnBase && signedOnPr) || (signedOnBase && !contributorsModified)); + + // Helper function to create or update a label with a specific COLOUR + async function ensureLabel(name, COLOUR, description) { try { - await github.rest.issues.updateLabel({ owner, repo, name, color, description }); - console.log(`Updated label: ${name} with color ${color}`); + await github.rest.issues.updateLabel({ owner, repo, name, COLOUR, description }); + console.log(`Updated label: ${name} with COLOUR ${COLOUR}`); } catch (error) { // If update fails (label doesn't exist), create it try { - await github.rest.issues.createLabel({ owner, repo, name, color, description }); - console.log(`Created new label: ${name} with color ${color}`); + await github.rest.issues.createLabel({ owner, repo, name, COLOUR, description }); + console.log(`Created new label: ${name} with COLOUR ${COLOUR}`); } catch (createError) { console.log(`Error with label ${name}:`, createError.message); } } } - // Define desired colors and descriptions for consistency - const COLOR_SIGNED = '0052cc'; // Blue - const COLOR_REQUIRED = 'b60205'; // Red + // Define desired COLOURs and descriptions for consistency + const COLOUR_SIGNED = '0052cc'; // Blue + const COLOUR_REQUIRED = 'b60205'; // Red + const COLOUR_MODIFIED = 'F56F27'; // Orange // Helper function to delete old CLA-related comments from this bot async function deleteOldClaComments() { @@ -180,29 +182,29 @@ jobs: } } - console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Modified: ${contributorsModified}, Removed: ${removedFromPr})`); + console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Modified: ${contributorsModified}, Invalid Modify: ${invalidModify})`); - // Handle case where contributor removed themselves from CONTRIBUTORS file - if (removedFromPr) { - await ensureLabel('cla-required', COLOR_REQUIRED, 'CLA signature is required for this PR.'); - console.log('⚠️ Contributor was in base branch but removed from PR branch.'); + // Handle case where contributor modified the file when already on it + if (invalidModify) { + await ensureLabel('cla-modified', COLOUR_MODIFIED, 'CLA signature has been modified.'); + console.log('⚠️ Contributor was in base branch but has modified the file.'); // Ensure labels are correct await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-signed' }), - github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-required'] }) + github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-modified'] }) ]); // Delete old CLA comments before posting new one await deleteOldClaComments(); // Post warning comment - const commentBody = `⚠️ Hello @${author}!\n\nYour CLA signature was found on the base branch, but you appear to have removed yourself from the _CONTRIBUTORS.md_ file in this PR.\n\nPlease ensure your entry remains in the _CONTRIBUTORS.md_ file. If you have already signed the CLA, you should not remove your details from the file.`; + const commentBody = `⚠️ Hello @${author}!\n\nYour CLA signature was found on the base branch, but you appear to have modified the _CONTRIBUTORS.md_ file in this PR.\n\nPlease do not edit the _CONTRIBUTORS.md_ file. If you have already signed the CLA, revert changes to the file and your signature will be picked up.`; await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); // Fail the GitHub Action run - console.error("⚠️ Contributor removed themselves from CONTRIBUTORS file."); + console.error("⚠️ Contributor edited the CONTRIBUTORS file when already on base."); process.exit(1); } @@ -225,7 +227,7 @@ jobs: } else if (!signedOnBase && signedOnPr) { // New contributor signing CLA for the first time - await ensureLabel('cla-signed', COLOR_SIGNED, 'This contributor has signed the CLA.'); + await ensureLabel('cla-signed', COLOUR_SIGNED, 'This contributor has signed the CLA.'); console.log('✅ New contributor has signed the CLA in PR branch.'); await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), @@ -236,7 +238,7 @@ jobs: await deleteOldClaComments(); } else { - await ensureLabel('cla-required', COLOR_REQUIRED, 'CLA signature is required for this PR.'); + await ensureLabel('cla-required', COLOUR_REQUIRED, 'CLA signature is required for this PR.'); console.log('❌ CLA condition NOT met. Adding required label and ensuring signed label is absent.'); // Ensure labels are correct From ac9a69a592ac9e185236e0a2bc4da369d83def3f Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:42:45 +0000 Subject: [PATCH 022/165] Project edit action (#53) --- .github/workflows/track-review-project.yaml | 232 ++++++++++++++++++++ track-review-project/README.md | 34 +++ 2 files changed, 266 insertions(+) create mode 100644 .github/workflows/track-review-project.yaml create mode 100644 track-review-project/README.md diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml new file mode 100644 index 0000000..bdf8c9b --- /dev/null +++ b/.github/workflows/track-review-project.yaml @@ -0,0 +1,232 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + +# This workflow runs whenever a pull request in the repository is marked as "ready for review". +name: Track Review Project +on: + workflow_call: + inputs: + runner: + description: 'The runner to use for the job' + required: false + type: string + default: 'ubuntu-24.04' + project_secret: + description: 'The name of the secret in the repository with access to projects' + required: false + type: string + default: 'PROJECT_ACTION_PAT' + +jobs: + track_project: + # Read reviewers from PR Summary + runs-on: ${{ inputs.runner }} + timeout-minutes: 2 + + steps: + # Add the PR Author as the Assignee if no assignees + - name: Assign Author + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + AUTHOR: ${{ github.event.pull_request.user.login }} + run: | + assignees=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "assignees" | jq '.assignees | length') + if [[ "$assignees" -eq 0 ]]; then + gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" + fi + + - name: Read Reviewers + env: + PR_BODY: ${{ github.event.pull_request.body }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + echo "Running on PR #$PR_NUMBER in Repository $REPO" + + SCITECH_REVIEWER=$(echo "$PR_BODY" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true + CODE_REVIEWER=$(echo "$PR_BODY" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true + + echo "Expected SciTech Reviewer: $SCITECH_REVIEWER" + echo "Expected Code Reviewer: $CODE_REVIEWER" + + echo "SCITECH_REVIEWER=$SCITECH_REVIEWER" >> $GITHUB_ENV + echo "CODE_REVIEWER=$CODE_REVIEWER" >> $GITHUB_ENV + + # Get Reviews by the SR and CR + - name: Get reviews and determine state + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + reviews=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "reviews,reviewRequests") + sr_requested=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviewRequests[] | select(.login == $sr) ] | length') + cr_requested=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviewRequests[] | select(.login == $cr) ] | length') + + # If there is no SR set and the CR review has been requested, assume this doesn't need an SR + if [[ -z "$SCITECH_REVIEWER" ]] && [[ "$cr_requested" -gt 0 ]]; then + echo "No SR set and CR requested so assuming this is trivial" + else + sr_reviews=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviews[] | select(.author.login == $sr) ]') + sr_approved=$(echo "$sr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') + if [[ "$sr_approved" -eq 0 ]]; then + sr_rejected=$(echo "$sr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') + if [[ "$sr_rejected" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then + required_state="Changes Requested" + else + required_state="SciTech Review" + fi + # Correct state set + echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + exit 0 + fi + fi + + cr_reviews=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviews[] | select(.author.login == $cr) ]') + cr_approved=$(echo "$cr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') + cr_rejected=$(echo "$cr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') + if [[ "$cr_approved" -gt 0 ]]; then + required_state="Approved" + else + if [[ "$cr_rejected" -gt 0 ]] && [[ "$cr_requested" -eq 0 ]]; then + required_state="Changes Requested" + else + required_state="Code Review" + fi + fi + + echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + if [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_rejected" -eq 0 ]]; then + echo "CR_REQUEST_REQUIRED=true" >> $GITHUB_ENV + fi + + # If CR hasn't been requested and state is CR then add them as a reviewer + - name: Request CR review + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + if [[ -z "$CR_REQUEST_REQUIRED" ]]; then + CR_REQUEST_REQUIRED=false + fi + if [[ "$CR_REQUEST_REQUIRED" = true ]] && [[ "$REQUIRED_STATE" == "Code Review" ]]; then + gh pr edit -R "$REPO" "$PR_NUMBER" --add-reviewer "$CODE_REVIEWER" + fi + + # Get Project IDs for project and relevant fields + # Project Number hard coded to Simulation Systems Review Tracker + - name: Get project id's + env: + GH_TOKEN: secrets.${{ inputs.project_secret }} + ORGANIZATION: MetOffice + PROJECT_NUMBER: 376 + run: | + gh api graphql -f query=' + query($org: String!, $number: Int!) { + organization(login: $org){ + projectV2(number: $number) { + id + fields(first:20) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json + + echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV + echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV + echo 'STATUS_OPTION_ID='$(jq -r --arg status "$REQUIRED_STATE" '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name==$status) |.id' project_data.json) >> $GITHUB_ENV + echo 'SCITECH_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "SciTech Review") | .id' project_data.json) >> $GITHUB_ENV + echo 'CODE_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Code Review") | .id' project_data.json) >> $GITHUB_ENV + + # Add the PR to the Project - get item id for this PR + - name: Add PR to project + env: + GH_TOKEN: secrets.${{ inputs.project_secret }} + PR_ID: ${{ github.event.pull_request.node_id }} + run: | + item_id="$( gh api graphql -f query=' + mutation($project:ID!, $pr:ID!) { + addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) { + item { + id + } + } + }' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')" + + # Stores the ID of the created item as an environment variable. + echo 'ITEM_ID='$item_id >> $GITHUB_ENV + + # Sets Status, CR and SR Fields based on Output from previous steps + - name: Set fields + env: + GH_TOKEN: secrets.${{ inputs.project_secret }} + run: | + gh api graphql -f query=' + mutation ( + $project: ID! + $item: ID! + $status_field: ID! + $status_value: String! + $cr_field: ID! + $cr_value: String! + $sr_field: ID! + $sr_value: String! + ) { + set_status: updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $status_field + value: { + singleSelectOptionId: $status_value + } + }) { + projectV2Item { + id + } + } + set_code_review: updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $cr_field + value: { + text: $cr_value + } + }) { + projectV2Item { + id + } + } + set_scitech_review: updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $sr_field + value: { + text: $sr_value + } + }) { + projectV2Item { + id + } + } + }' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=$STATUS_OPTION_ID -f cr_field=$CODE_REVIEW_ID -f cr_value="$CODE_REVIEWER" -f sr_field=$SCITECH_REVIEW_ID -f sr_value="$SCITECH_REVIEWER" --silent diff --git a/track-review-project/README.md b/track-review-project/README.md new file mode 100644 index 0000000..ac058ac --- /dev/null +++ b/track-review-project/README.md @@ -0,0 +1,34 @@ +# Track Review Project Action + +This action modifies the Simulation Systems Review Tracker project entries in +pull requests. It will automatically fill the reviewer fields based on the +entries in the pull request template. It will change the state based on reviews +requested and performed. If a CR hasn't been requested, it will do so when +moving to the CR state. It will also add the PR author as an assignee if none +exist. + +It requires a PAT with permissions to write to projects to be added as a secret +to the repository - the default name of this secret is `PROJECT_ACTION_PAT` but +this can be overridden with the `project_pat` input. Project edits made using +this token will be shown as performed by the owner of the token. + +## Usage + +```yaml +name: Track Review Project + +on: + pull_request: + types: ["opened", "synchronize", "reopened", "edited", "review_requested", "review_request_removed"] + pull_request_review: + pull_request_review_comment: + workflow_dispatch: + +jobs: + track_review_project: + uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main + # Optional inputs (with default values) + with: + runner: "ubuntu-22.04" + project_secret: "PROJECT_ACTION_PAT" +``` From 322090a9986587a916162cd1beb6348802692ab5 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:59:40 +0000 Subject: [PATCH 023/165] add permissions to develop branch action --- .github/workflows/track-review-project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index bdf8c9b..8e57c42 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -22,6 +22,7 @@ on: jobs: track_project: + permissions: write-all # Read reviewers from PR Summary runs-on: ${{ inputs.runner }} timeout-minutes: 2 From b57ad638c16b1f675baacebb8cf9eb7b1fcf1f07 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:07:11 +0000 Subject: [PATCH 024/165] update permissions syntax --- .github/workflows/track-review-project.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 8e57c42..84bcc21 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -20,9 +20,10 @@ on: type: string default: 'PROJECT_ACTION_PAT' +permissions: write-all + jobs: track_project: - permissions: write-all # Read reviewers from PR Summary runs-on: ${{ inputs.runner }} timeout-minutes: 2 From 6132c4b09cd070453932e71500dbf6c2325c4efd Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:25:56 +0000 Subject: [PATCH 025/165] try read-all --- .github/workflows/track-review-project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 84bcc21..2976511 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -20,7 +20,7 @@ on: type: string default: 'PROJECT_ACTION_PAT' -permissions: write-all +permissions: read-all jobs: track_project: From 7dcbe758fd57e3a48f59a4a1bb0d6eca95f3ab49 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:29:16 +0000 Subject: [PATCH 026/165] revert to write --- .github/workflows/track-review-project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 2976511..84bcc21 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -20,7 +20,7 @@ on: type: string default: 'PROJECT_ACTION_PAT' -permissions: read-all +permissions: write-all jobs: track_project: From 05cadd461ff4b472bb8d2191649e7bf74188a337 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:31:22 +0000 Subject: [PATCH 027/165] test without assigning --- .github/workflows/track-review-project.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 84bcc21..1ed5352 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -38,9 +38,9 @@ jobs: AUTHOR: ${{ github.event.pull_request.user.login }} run: | assignees=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "assignees" | jq '.assignees | length') - if [[ "$assignees" -eq 0 ]]; then - gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" - fi + # if [[ "$assignees" -eq 0 ]]; then + # gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" + # fi - name: Read Reviewers env: From c412c3257230afea15ba8f522d70db1bee9a4da2 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:34:28 +0000 Subject: [PATCH 028/165] update secret --- .github/workflows/track-review-project.yaml | 11 +++-------- track-review-project/README.md | 5 ++--- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 1ed5352..510eb83 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -14,11 +14,6 @@ on: required: false type: string default: 'ubuntu-24.04' - project_secret: - description: 'The name of the secret in the repository with access to projects' - required: false - type: string - default: 'PROJECT_ACTION_PAT' permissions: write-all @@ -126,7 +121,7 @@ jobs: # Project Number hard coded to Simulation Systems Review Tracker - name: Get project id's env: - GH_TOKEN: secrets.${{ inputs.project_secret }} + GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} ORGANIZATION: MetOffice PROJECT_NUMBER: 376 run: | @@ -164,7 +159,7 @@ jobs: # Add the PR to the Project - get item id for this PR - name: Add PR to project env: - GH_TOKEN: secrets.${{ inputs.project_secret }} + GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} PR_ID: ${{ github.event.pull_request.node_id }} run: | item_id="$( gh api graphql -f query=' @@ -182,7 +177,7 @@ jobs: # Sets Status, CR and SR Fields based on Output from previous steps - name: Set fields env: - GH_TOKEN: secrets.${{ inputs.project_secret }} + GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} run: | gh api graphql -f query=' mutation ( diff --git a/track-review-project/README.md b/track-review-project/README.md index ac058ac..38e0877 100644 --- a/track-review-project/README.md +++ b/track-review-project/README.md @@ -8,8 +8,7 @@ moving to the CR state. It will also add the PR author as an assignee if none exist. It requires a PAT with permissions to write to projects to be added as a secret -to the repository - the default name of this secret is `PROJECT_ACTION_PAT` but -this can be overridden with the `project_pat` input. Project edits made using +to the repository with the name 'PROJECT_ACTION_PAT'. Project edits made using this token will be shown as performed by the owner of the token. ## Usage @@ -30,5 +29,5 @@ jobs: # Optional inputs (with default values) with: runner: "ubuntu-22.04" - project_secret: "PROJECT_ACTION_PAT" + project_secret: "" ``` From 3830a9f5a6312c0ebeda6eb77c28eb06a200655e Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:43:47 +0000 Subject: [PATCH 029/165] reenable assignee --- .github/workflows/track-review-project.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 510eb83..c851a5c 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -33,9 +33,9 @@ jobs: AUTHOR: ${{ github.event.pull_request.user.login }} run: | assignees=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "assignees" | jq '.assignees | length') - # if [[ "$assignees" -eq 0 ]]; then - # gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" - # fi + if [[ "$assignees" -eq 0 ]]; then + gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" + fi - name: Read Reviewers env: From 401087e343a01819eff389d56bed83f8a0f59ba2 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:09:09 +0000 Subject: [PATCH 030/165] update to Yashs version --- .github/workflows/track-review-project.yaml | 234 +++++++------------- 1 file changed, 78 insertions(+), 156 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index c851a5c..3ce5238 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -4,7 +4,8 @@ # under which the code may be used. # ------------------------------------------------------------------------------ -# This workflow runs whenever a pull request in the repository is marked as "ready for review". +# This workflow runs whenever a pull request in the repository is marked as +# "Ready for review" (i.e., a non-Draft PR). name: Track Review Project on: workflow_call: @@ -14,216 +15,137 @@ on: required: false type: string default: 'ubuntu-24.04' + project_org: + description: 'The organization that owns the project' + required: false + type: string + default: 'MetOffice' + project_number: + description: 'The project number' + required: false + type: number + default: 376 # Simulation Systems Review Tracker + secrets: + PROJECT_ACTION_PAT: + required: true -permissions: write-all +permissions: + contents: read + pull-requests: write jobs: track_project: - # Read reviewers from PR Summary runs-on: ${{ inputs.runner }} - timeout-minutes: 2 + timeout-minutes: 5 + env: + REPO: ${{ github.repository }} + PR_NUM: ${{ github.event.pull_request.number }} + PR_ID: ${{ github.event.pull_request.node_id }} steps: - # Add the PR Author as the Assignee if no assignees - - name: Assign Author + - name: Assign Author if no assignees env: - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} - AUTHOR: ${{ github.event.pull_request.user.login }} run: | - assignees=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "assignees" | jq '.assignees | length') - if [[ "$assignees" -eq 0 ]]; then - gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" + pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json assignees,author) + if [[ $(echo "$pr_data" | jq '.assignees | length') -eq 0 ]]; then + author=$(echo "$pr_data" | jq -r '.author.login') + gh pr edit -R "$REPO" "$PR_NUM" --add-assignee "$author" fi - - name: Read Reviewers + - name: Extract reviewers and determine state env: - PR_BODY: ${{ github.event.pull_request.body }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} run: | - echo "Running on PR #$PR_NUMBER in Repository $REPO" + pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json body,reviews,reviewRequests) - SCITECH_REVIEWER=$(echo "$PR_BODY" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true - CODE_REVIEWER=$(echo "$PR_BODY" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true + # Extract reviewers from PR body + pr_body=$(echo "$pr_data" | jq -r '.body') + scitech_reviewer=$(echo "$pr_body" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) + code_reviewer=$(echo "$pr_body" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) - echo "Expected SciTech Reviewer: $SCITECH_REVIEWER" - echo "Expected Code Reviewer: $CODE_REVIEWER" + echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" + echo "Expected Code Reviewer: ${code_reviewer:-None}" + echo "SCITECH_REVIEWER=$scitech_reviewer" >> $GITHUB_ENV + echo "CODE_REVIEWER=$code_reviewer" >> $GITHUB_ENV - echo "SCITECH_REVIEWER=$SCITECH_REVIEWER" >> $GITHUB_ENV - echo "CODE_REVIEWER=$CODE_REVIEWER" >> $GITHUB_ENV + # Determine review state + reviews=$(echo "$pr_data" | jq -c '{reviews, reviewRequests}') + sr_requested=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviewRequests[] | select(.login == $sr)] | length') + cr_requested=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviewRequests[] | select(.login == $cr)] | length') - # Get Reviews by the SR and CR - - name: Get reviews and determine state - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} - run: | - reviews=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "reviews,reviewRequests") - sr_requested=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviewRequests[] | select(.login == $sr) ] | length') - cr_requested=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviewRequests[] | select(.login == $cr) ] | length') - - # If there is no SR set and the CR review has been requested, assume this doesn't need an SR - if [[ -z "$SCITECH_REVIEWER" ]] && [[ "$cr_requested" -gt 0 ]]; then + # Determine required state + if [[ -z "$scitech_reviewer" ]] && [[ "$cr_requested" -gt 0 ]]; then echo "No SR set and CR requested so assuming this is trivial" else - sr_reviews=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviews[] | select(.author.login == $sr) ]') - sr_approved=$(echo "$sr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') + sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') if [[ "$sr_approved" -eq 0 ]]; then - sr_rejected=$(echo "$sr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') - if [[ "$sr_rejected" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then - required_state="Changes Requested" - else - required_state="SciTech Review" - fi - # Correct state set + sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + required_state=$([[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested" || echo "SciTech Review") echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV exit 0 fi fi - cr_reviews=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviews[] | select(.author.login == $cr) ]') - cr_approved=$(echo "$cr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') - cr_rejected=$(echo "$cr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') + cr_approved=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and .state == "APPROVED")] | length') + cr_changes=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + if [[ "$cr_approved" -gt 0 ]]; then required_state="Approved" + elif [[ "$cr_changes" -gt 0 ]] && [[ "$cr_requested" -eq 0 ]]; then + required_state="Changes Requested" else - if [[ "$cr_rejected" -gt 0 ]] && [[ "$cr_requested" -eq 0 ]]; then - required_state="Changes Requested" - else - required_state="Code Review" - fi + required_state="Code Review" + [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_changes" -eq 0 ]] && echo "CR_REQUEST_REQUIRED=true" >> $GITHUB_ENV fi echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV - if [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_rejected" -eq 0 ]]; then - echo "CR_REQUEST_REQUIRED=true" >> $GITHUB_ENV - fi - # If CR hasn't been requested and state is CR then add them as a reviewer - - name: Request CR review + - name: Request code review, if needed + if: env.CR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'Code Review' && env.CODE_REVIEWER != '' env: - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} run: | - if [[ -z "$CR_REQUEST_REQUIRED" ]]; then - CR_REQUEST_REQUIRED=false - fi - if [[ "$CR_REQUEST_REQUIRED" = true ]] && [[ "$REQUIRED_STATE" == "Code Review" ]]; then - gh pr edit -R "$REPO" "$PR_NUMBER" --add-reviewer "$CODE_REVIEWER" - fi + gh pr edit -R "$REPO" "$PR_NUM" --add-reviewer "$CODE_REVIEWER" - # Get Project IDs for project and relevant fields - # Project Number hard coded to Simulation Systems Review Tracker - - name: Get project id's + - name: Update project tracking env: GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} - ORGANIZATION: MetOffice - PROJECT_NUMBER: 376 run: | - gh api graphql -f query=' + # Get project data + project_data=$(gh api graphql -f query=' query($org: String!, $number: Int!) { organization(login: $org){ projectV2(number: $number) { id fields(first:20) { nodes { - ... on ProjectV2Field { - id - name - } - ... on ProjectV2SingleSelectField { - id - name - options { - id - name - } - } + ... on ProjectV2Field { id name } + ... on ProjectV2SingleSelectField { id name options { id name } } } } } } - }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json + }' -f org=${{ inputs.project_org }} -F number=${{ inputs.project_number }}) - echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV - echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV - echo 'STATUS_OPTION_ID='$(jq -r --arg status "$REQUIRED_STATE" '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name==$status) |.id' project_data.json) >> $GITHUB_ENV - echo 'SCITECH_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "SciTech Review") | .id' project_data.json) >> $GITHUB_ENV - echo 'CODE_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Code Review") | .id' project_data.json) >> $GITHUB_ENV + project_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.id') + status_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id') + status_option_id=$(echo "$project_data" | jq -r --arg status "$REQUIRED_STATE" '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name==$status) | .id') + scitech_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "SciTech Review") | .id') + code_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "Code Review") | .id') - # Add the PR to the Project - get item id for this PR - - name: Add PR to project - env: - GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} - PR_ID: ${{ github.event.pull_request.node_id }} - run: | - item_id="$( gh api graphql -f query=' + # Add PR to project and update fields + item_id=$(gh api graphql -f query=' mutation($project:ID!, $pr:ID!) { addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) { - item { - id - } + item { id } } - }' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')" - - # Stores the ID of the created item as an environment variable. - echo 'ITEM_ID='$item_id >> $GITHUB_ENV + }' -f project="$project_id" -f pr="$PR_ID" --jq '.data.addProjectV2ItemById.item.id') - # Sets Status, CR and SR Fields based on Output from previous steps - - name: Set fields - env: - GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} - run: | + # Update all fields in single mutation gh api graphql -f query=' - mutation ( - $project: ID! - $item: ID! - $status_field: ID! - $status_value: String! - $cr_field: ID! - $cr_value: String! - $sr_field: ID! - $sr_value: String! - ) { - set_status: updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $status_field - value: { - singleSelectOptionId: $status_value - } - }) { - projectV2Item { - id - } - } - set_code_review: updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $cr_field - value: { - text: $cr_value - } - }) { - projectV2Item { - id - } - } - set_scitech_review: updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $sr_field - value: { - text: $sr_value - } - }) { - projectV2Item { - id - } - } - }' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=$STATUS_OPTION_ID -f cr_field=$CODE_REVIEW_ID -f cr_value="$CODE_REVIEWER" -f sr_field=$SCITECH_REVIEW_ID -f sr_value="$SCITECH_REVIEWER" --silent + mutation($project: ID!, $item: ID!, $status_field: ID!, $status_value: String!, $cr_field: ID!, $cr_value: String!, $sr_field: ID!, $sr_value: String!) { + set_status: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $status_field, value: {singleSelectOptionId: $status_value}}) { projectV2Item { id } } + set_code_review: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $cr_field, value: {text: $cr_value}}) { projectV2Item { id } } + set_scitech_review: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $sr_field, value: {text: $sr_value}}) { projectV2Item { id } } + }' -f project="$project_id" -f item="$item_id" -f status_field="$status_field_id" -f status_value="$status_option_id" -f cr_field="$code_field_id" -f cr_value="$CODE_REVIEWER" -f sr_field="$scitech_field_id" -f sr_value="$SCITECH_REVIEWER" --silent From eccbe422d442ac7694a99ad9dd1dcdd2e5f2f2c7 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:29:37 +0000 Subject: [PATCH 031/165] update secrets --- .github/workflows/track-review-project.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 3ce5238..a5081c9 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -26,8 +26,7 @@ on: type: number default: 376 # Simulation Systems Review Tracker secrets: - PROJECT_ACTION_PAT: - required: true + PROJECT_ACTION_PAT: ${{ secrets.PROJECT_ACTION_PAT }} permissions: contents: read From d8ee4bb76538081e2cb685f8531ead1b33f6b44f Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 08:34:49 +0000 Subject: [PATCH 032/165] update secrets --- .github/workflows/track-review-project.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index a5081c9..2fb2ba8 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -25,8 +25,6 @@ on: required: false type: number default: 376 # Simulation Systems Review Tracker - secrets: - PROJECT_ACTION_PAT: ${{ secrets.PROJECT_ACTION_PAT }} permissions: contents: read From 15227f82c19c2c781362acdabc8ceec396ef7680 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:52:27 +0000 Subject: [PATCH 033/165] update secrets --- .github/workflows/track-review-project.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 2fb2ba8..6930a22 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -72,13 +72,14 @@ jobs: cr_requested=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviewRequests[] | select(.login == $cr)] | length') # Determine required state + sr_approved=0 if [[ -z "$scitech_reviewer" ]] && [[ "$cr_requested" -gt 0 ]]; then echo "No SR set and CR requested so assuming this is trivial" else sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') if [[ "$sr_approved" -eq 0 ]]; then sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') - required_state=$([[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested" || echo "SciTech Review") + required_state=$([[ "$sr_requested" -eq 0 ]] && [[ "sr_changes" -eq 0 ]] && echo "In Progress" || [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested" || echo "SciTech Review") echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV exit 0 fi @@ -89,6 +90,8 @@ jobs: if [[ "$cr_approved" -gt 0 ]]; then required_state="Approved" + elif [[ "cr_changes" -eq 0 ]] && [[ "$cr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]]; then + required_state="In Progress" elif [[ "$cr_changes" -gt 0 ]] && [[ "$cr_requested" -eq 0 ]]; then required_state="Changes Requested" else From 53f5f40bab5e0c62aa8464c0b5d39991c0e13b87 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:02:08 +0000 Subject: [PATCH 034/165] debug --- .github/workflows/track-review-project.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 6930a22..5b83443 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -79,7 +79,9 @@ jobs: sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') if [[ "$sr_approved" -eq 0 ]]; then sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + echo "HERE 1" required_state=$([[ "$sr_requested" -eq 0 ]] && [[ "sr_changes" -eq 0 ]] && echo "In Progress" || [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested" || echo "SciTech Review") + echo "HERE 2" echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV exit 0 fi From 84ab60d18ae35ab75fde9a2d2b4712a171c4e08a Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:04:41 +0000 Subject: [PATCH 035/165] debug --- .github/workflows/track-review-project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 5b83443..bbc8acc 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -54,6 +54,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + set -x pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json body,reviews,reviewRequests) # Extract reviewers from PR body From 7222d473e6e7e9814e18ffecc79fb895caa51dc2 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:07:03 +0000 Subject: [PATCH 036/165] debug --- .github/workflows/track-review-project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index bbc8acc..4306dac 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -81,7 +81,7 @@ jobs: if [[ "$sr_approved" -eq 0 ]]; then sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') echo "HERE 1" - required_state=$([[ "$sr_requested" -eq 0 ]] && [[ "sr_changes" -eq 0 ]] && echo "In Progress" || [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested" || echo "SciTech Review") + required_state=$([[ "$sr_requested" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "In Progress" || [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested" || echo "SciTech Review") echo "HERE 2" echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV exit 0 From 57146df9cfa3fdebb3bc17c5387d53c2d73210eb Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:16:06 +0000 Subject: [PATCH 037/165] add brackets --- .github/workflows/track-review-project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 4306dac..dbc1cfe 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -81,7 +81,7 @@ jobs: if [[ "$sr_approved" -eq 0 ]]; then sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') echo "HERE 1" - required_state=$([[ "$sr_requested" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "In Progress" || [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested" || echo "SciTech Review") + required_state=$(([[ "$sr_requested" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "In Progress") || ([[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested") || echo "SciTech Review") echo "HERE 2" echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV exit 0 From 46931a4e7611e7b329d8a7743075c273cb463d06 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:17:41 +0000 Subject: [PATCH 038/165] remove debug --- .github/workflows/track-review-project.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index dbc1cfe..dac8731 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -54,7 +54,6 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - set -x pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json body,reviews,reviewRequests) # Extract reviewers from PR body From d7a9da2822921b8fceec388be5cd1301347fd91f Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:26:35 +0000 Subject: [PATCH 039/165] remove debug --- .github/workflows/track-review-project.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index dac8731..43112d3 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -79,9 +79,13 @@ jobs: sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') if [[ "$sr_approved" -eq 0 ]]; then sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') - echo "HERE 1" - required_state=$(([[ "$sr_requested" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "In Progress") || ([[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]] && echo "Changes Requested") || echo "SciTech Review") - echo "HERE 2" + if [[ "$sr_requested" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]]; then + required_state="In Progress" + elif [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then + required_state="Changes Requested" + else + required_state="SciTech Review" + fi echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV exit 0 fi From f3d6a6e04331eae08c1e020a213975de6c6c821e Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:52:40 +0000 Subject: [PATCH 040/165] auto add SR --- .github/workflows/track-review-project.yaml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 43112d3..3320ca4 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -72,13 +72,15 @@ jobs: cr_requested=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviewRequests[] | select(.login == $cr)] | length') # Determine required state - sr_approved=0 + sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') + sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + + [[ "$sr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "SR_REQUEST_REQUIRED=true" >> $GITHUB_ENV + if [[ -z "$scitech_reviewer" ]] && [[ "$cr_requested" -gt 0 ]]; then echo "No SR set and CR requested so assuming this is trivial" else - sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') if [[ "$sr_approved" -eq 0 ]]; then - sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') if [[ "$sr_requested" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]]; then required_state="In Progress" elif [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then @@ -107,6 +109,13 @@ jobs: echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + - name: Request SciTech review, if needed + if: env.SR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'SciTech Review' && env.SCITECH_REVIEWER != '' + env: + GH_TOKEN: ${{ github.token }} + run: | + gh pr edit -R "$REPO" "$PR_NUM" --add-reviewer "$SCITECH_REVIEWER" + - name: Request code review, if needed if: env.CR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'Code Review' && env.CODE_REVIEWER != '' env: From 2b682516a481923b3421c6e82dd3384ba172c95b Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 10:59:32 +0000 Subject: [PATCH 041/165] auto add sr --- .github/workflows/track-review-project.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 3320ca4..a046649 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -75,13 +75,13 @@ jobs: sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') - [[ "$sr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "SR_REQUEST_REQUIRED=true" >> $GITHUB_ENV + [[ ! -z "$scitech_reviewer" ]] && [[ "$sr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "SR_REQUEST_REQUIRED=true" >> $GITHUB_ENV if [[ -z "$scitech_reviewer" ]] && [[ "$cr_requested" -gt 0 ]]; then echo "No SR set and CR requested so assuming this is trivial" else if [[ "$sr_approved" -eq 0 ]]; then - if [[ "$sr_requested" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]]; then + if [[ -z "$scitech_reviewer" ]] && [[ "$sr_changes" -eq 0 ]]; then required_state="In Progress" elif [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then required_state="Changes Requested" From bc0ca124ca2e99186a2dc1765ba6b294287a8f16 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:03:01 +0000 Subject: [PATCH 042/165] make case insensitive --- .github/workflows/track-review-project.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index a046649..9977868 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -58,8 +58,8 @@ jobs: # Extract reviewers from PR body pr_body=$(echo "$pr_data" | jq -r '.body') - scitech_reviewer=$(echo "$pr_body" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) - code_reviewer=$(echo "$pr_body" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) + scitech_reviewer=$(echo "$pr_body" | grep -oiP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) + code_reviewer=$(echo "$pr_body" | grep -oiP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" echo "Expected Code Reviewer: ${code_reviewer:-None}" From 95da5efb65d2e8eb0a57bbfe76d16b2ece98414c Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 13:57:12 +0000 Subject: [PATCH 043/165] testing workflow_run --- .github/workflows/track-review-project.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 9977868..d0f1c15 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -36,14 +36,17 @@ jobs: timeout-minutes: 5 env: REPO: ${{ github.repository }} - PR_NUM: ${{ github.event.pull_request.number }} - PR_ID: ${{ github.event.pull_request.node_id }} steps: + # If running based on the github + - name: Checkout repo + uses: actions/checkout@v6 - name: Assign Author if no assignees env: GH_TOKEN: ${{ github.token }} run: | + echo "PR #: ${{ github.event.workflow_run.pull_requests[0].number }}" + echo "PR: ${{ github.event.workflow_run.pull_requests[0] }}" pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json assignees,author) if [[ $(echo "$pr_data" | jq '.assignees | length') -eq 0 ]]; then author=$(echo "$pr_data" | jq -r '.author.login') From ef6d34b6c13dffb88975fc94ebcc9f9030df4546 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:10:59 +0000 Subject: [PATCH 044/165] update --- .github/workflows/track-review-project.yaml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index d0f1c15..e04bb78 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -41,12 +41,28 @@ jobs: # If running based on the github - name: Checkout repo uses: actions/checkout@v6 + + - name: Determine PR Number + env: + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + FORK_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} + GH_TOKEN: ${{ github.token }} + run: + echo $OWNER + echo $REPO + echo $HEAD_BRANCH + echo $FORK_OWNER + + PR_JSON=$(gh api \ + -X GET "repos/$OWNER/$REPO/pulls?state=all&head=${FORK_OWNER}:${HEAD_BRANCH}") + PR_NUMBER=$(jq -r '.[0].number // empty' <<<"$PR_JSON") + - name: Assign Author if no assignees env: GH_TOKEN: ${{ github.token }} run: | - echo "PR #: ${{ github.event.workflow_run.pull_requests[0].number }}" - echo "PR: ${{ github.event.workflow_run.pull_requests[0] }}" pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json assignees,author) if [[ $(echo "$pr_data" | jq '.assignees | length') -eq 0 ]]; then author=$(echo "$pr_data" | jq -r '.author.login') From 27e53c2d59a510e945231b7d74f34bad0dcb9eaf Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:13:12 +0000 Subject: [PATCH 045/165] update --- .github/workflows/track-review-project.yaml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index e04bb78..d1b85a8 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -49,15 +49,11 @@ jobs: HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} FORK_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} GH_TOKEN: ${{ github.token }} - run: - echo $OWNER - echo $REPO - echo $HEAD_BRANCH - echo $FORK_OWNER - + run: | PR_JSON=$(gh api \ -X GET "repos/$OWNER/$REPO/pulls?state=all&head=${FORK_OWNER}:${HEAD_BRANCH}") PR_NUMBER=$(jq -r '.[0].number // empty' <<<"$PR_JSON") + echo "PR_NUM=$PR_NUMBER" >> $GITHUB_ENV - name: Assign Author if no assignees env: From fc4e94fed620b71592e8c83d19f6bf3b44bfe410 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:20:52 +0000 Subject: [PATCH 046/165] update --- .github/workflows/track-review-project.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index d1b85a8..aa13a7e 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -42,7 +42,7 @@ jobs: - name: Checkout repo uses: actions/checkout@v6 - - name: Determine PR Number + - name: Determine PR Data env: OWNER: ${{ github.repository_owner }} REPO: ${{ github.event.repository.name }} @@ -50,10 +50,11 @@ jobs: FORK_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} GH_TOKEN: ${{ github.token }} run: | - PR_JSON=$(gh api \ - -X GET "repos/$OWNER/$REPO/pulls?state=all&head=${FORK_OWNER}:${HEAD_BRANCH}") - PR_NUMBER=$(jq -r '.[0].number // empty' <<<"$PR_JSON") - echo "PR_NUM=$PR_NUMBER" >> $GITHUB_ENV + PR_JSON=$(gh api -X GET "repos/$OWNER/$REPO/pulls?state=all&head=${FORK_OWNER}:${HEAD_BRANCH}") + pr_num=$(jq -r '.[0].number // empty' <<<"$PR_JSON") + pr_id=$(jq -r '.[0].node_id // empty' <<<"$PR_JSON") + echo "PR_NUM=$pr_num" >> $GITHUB_ENV + echo "PR_ID=$pr_id" >> $GITHUB_ENV - name: Assign Author if no assignees env: From ccbc0031ae6980599705f684c26098bbe36beb99 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:41:20 +0000 Subject: [PATCH 047/165] debug --- .github/workflows/track-review-project.yaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index aa13a7e..e3ff962 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -38,14 +38,14 @@ jobs: REPO: ${{ github.repository }} steps: - # If running based on the github + # Required as running from the workflow_run trigger - name: Checkout repo uses: actions/checkout@v6 + # This workflow is run from the workflow_run trigger so needs to identify the PR info itself - name: Determine PR Data env: OWNER: ${{ github.repository_owner }} - REPO: ${{ github.event.repository.name }} HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} FORK_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} GH_TOKEN: ${{ github.token }} @@ -53,6 +53,9 @@ jobs: PR_JSON=$(gh api -X GET "repos/$OWNER/$REPO/pulls?state=all&head=${FORK_OWNER}:${HEAD_BRANCH}") pr_num=$(jq -r '.[0].number // empty' <<<"$PR_JSON") pr_id=$(jq -r '.[0].node_id // empty' <<<"$PR_JSON") + echo $PR_JSON + echo $pr_num + echo $pr_id echo "PR_NUM=$pr_num" >> $GITHUB_ENV echo "PR_ID=$pr_id" >> $GITHUB_ENV From 0b2d6310b4aef285b5db3183d425b0576ec9e2b4 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 14:55:31 +0000 Subject: [PATCH 048/165] update trigger --- .github/workflows/track-review-project.yaml | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index e3ff962..96916e9 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -36,29 +36,14 @@ jobs: timeout-minutes: 5 env: REPO: ${{ github.repository }} + PR_NUM: ${{ github.event.workflow_run.outputs.pr_num }} + PR_ID: ${{ github.event.workflow_run.outputs.pr_id }} steps: # Required as running from the workflow_run trigger - name: Checkout repo uses: actions/checkout@v6 - # This workflow is run from the workflow_run trigger so needs to identify the PR info itself - - name: Determine PR Data - env: - OWNER: ${{ github.repository_owner }} - HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} - FORK_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} - GH_TOKEN: ${{ github.token }} - run: | - PR_JSON=$(gh api -X GET "repos/$OWNER/$REPO/pulls?state=all&head=${FORK_OWNER}:${HEAD_BRANCH}") - pr_num=$(jq -r '.[0].number // empty' <<<"$PR_JSON") - pr_id=$(jq -r '.[0].node_id // empty' <<<"$PR_JSON") - echo $PR_JSON - echo $pr_num - echo $pr_id - echo "PR_NUM=$pr_num" >> $GITHUB_ENV - echo "PR_ID=$pr_id" >> $GITHUB_ENV - - name: Assign Author if no assignees env: GH_TOKEN: ${{ github.token }} From abb8dd2b61f6acb5f0e3ec443725b509101e346b Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:09:54 +0000 Subject: [PATCH 049/165] add log --- .github/workflows/track-review-project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 96916e9..728ec25 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -48,6 +48,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | + echo "NUM: ${{ github.event.workflow_run.outputs.pr_num }}" pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json assignees,author) if [[ $(echo "$pr_data" | jq '.assignees | length') -eq 0 ]]; then author=$(echo "$pr_data" | jq -r '.author.login') From 43bc9eaa880fef1325cba06edfa2e1f793b3fe37 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:20:45 +0000 Subject: [PATCH 050/165] add log --- .github/workflows/track-review-project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 728ec25..994dc9f 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -48,7 +48,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - echo "NUM: ${{ github.event.workflow_run.outputs.pr_num }}" + echo "NUM: ${{ github.event.workflow_run.jobs.trigger_project_workflow.outputs.pr_num }}" pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json assignees,author) if [[ $(echo "$pr_data" | jq '.assignees | length') -eq 0 ]]; then author=$(echo "$pr_data" | jq -r '.author.login') From f7dd63bccba13bfa7a938ef187b51115a8584e92 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:12:20 +0000 Subject: [PATCH 051/165] switch to artifact --- .github/workflows/track-review-project.yaml | 32 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 994dc9f..63aa129 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -36,19 +36,45 @@ jobs: timeout-minutes: 5 env: REPO: ${{ github.repository }} - PR_NUM: ${{ github.event.workflow_run.outputs.pr_num }} - PR_ID: ${{ github.event.workflow_run.outputs.pr_id }} steps: # Required as running from the workflow_run trigger - name: Checkout repo uses: actions/checkout@v6 + - name: Download PR data artifact + uses: actions/github-script@v6 + with: + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }) + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "context.json" + })[0] + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }) + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/context.zip`, Buffer.from(download.data)); + + - name: Extract PR Data + run: | + unzip context.zip + pr_num=$(jq -r '.pr_num' context.json) + pr_id=$(jq -r '.pr_id' context.json) + echo "PR_NUM=$pr_num" >> $GITHUB_ENV + echo "PR_ID=$pr_id" >> $GITHUB_ENV + - name: Assign Author if no assignees env: GH_TOKEN: ${{ github.token }} run: | - echo "NUM: ${{ github.event.workflow_run.jobs.trigger_project_workflow.outputs.pr_num }}" pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json assignees,author) if [[ $(echo "$pr_data" | jq '.assignees | length') -eq 0 ]]; then author=$(echo "$pr_data" | jq -r '.author.login') From 6fce635110e8e5e31486049bafd62f211378a72f Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:16:42 +0000 Subject: [PATCH 052/165] add token --- .github/workflows/track-review-project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 63aa129..edb3059 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -45,6 +45,7 @@ jobs: - name: Download PR data artifact uses: actions/github-script@v6 with: + github-token: ${{ secrets.GITHUB_TOKEN }} script: | let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, From c972630e00e2975fcce580b921ce9881d69c6c1c Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:21:27 +0000 Subject: [PATCH 053/165] add token --- .github/workflows/track-review-project.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index edb3059..224c5bf 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -43,7 +43,7 @@ jobs: uses: actions/checkout@v6 - name: Download PR data artifact - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 2009ac2cbc3fa0eeb3b2101a5a59ea2065337fc0 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:24:40 +0000 Subject: [PATCH 054/165] add token --- .github/workflows/track-review-project.yaml | 29 ++++++++++++--------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 224c5bf..dbb1b8a 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -48,21 +48,26 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ - owner: context.repo.owner, - repo: context.repo.repo, - run_id: context.payload.workflow_run.id, - }) + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { return artifact.name == "context.json" - })[0] + })[0]; let download = await github.rest.actions.downloadArtifact({ - owner: context.repo.owner, - repo: context.repo.repo, - artifact_id: matchArtifact.id, - archive_format: 'zip', - }) - let fs = require('fs'); - fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/context.zip`, Buffer.from(download.data)); + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + const fs = require('fs'); + const path = require('path'); + const temp = '${{ runner.temp }}/artifacts'; + if (!fs.existsSync(temp)){ + fs.mkdirSync(temp); + } + fs.writeFileSync(path.join(temp, 'context.zip'), Buffer.from(download.data)); - name: Extract PR Data run: | From 753af3bd262a08736e85746fb4154135b45d7f74 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:30:43 +0000 Subject: [PATCH 055/165] add token --- .github/workflows/track-review-project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index dbb1b8a..3f4dae4 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -28,6 +28,7 @@ on: permissions: contents: read + artifact-metadata: read pull-requests: write jobs: From 7101e2c02552233cdb59aed1e0a9b62f42401fb8 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:31:24 +0000 Subject: [PATCH 056/165] permissions --- .github/workflows/track-review-project.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 3f4dae4..1c94638 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -27,9 +27,7 @@ on: default: 376 # Simulation Systems Review Tracker permissions: - contents: read - artifact-metadata: read - pull-requests: write + write-all jobs: track_project: From 11e55a2613c2efc983886bd44df85dbf561c775b Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:33:03 +0000 Subject: [PATCH 057/165] extract location --- .github/workflows/track-review-project.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 1c94638..56216bd 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -51,22 +51,20 @@ jobs: repo: context.repo.repo, run_id: context.payload.workflow_run.id, }); + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { return artifact.name == "context.json" })[0]; + let download = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, artifact_id: matchArtifact.id, archive_format: 'zip', }); - const fs = require('fs'); - const path = require('path'); - const temp = '${{ runner.temp }}/artifacts'; - if (!fs.existsSync(temp)){ - fs.mkdirSync(temp); - } - fs.writeFileSync(path.join(temp, 'context.zip'), Buffer.from(download.data)); + + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/context.zip`, Buffer.from(download.data)); - name: Extract PR Data run: | From 2f9f83fecb528ec5b37b44a270d55c5722aca253 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:33:53 +0000 Subject: [PATCH 058/165] extract location --- .github/workflows/track-review-project.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 56216bd..f9111db 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -69,6 +69,7 @@ jobs: - name: Extract PR Data run: | unzip context.zip + less context.json pr_num=$(jq -r '.pr_num' context.json) pr_id=$(jq -r '.pr_id' context.json) echo "PR_NUM=$pr_num" >> $GITHUB_ENV From 8a3c57c56b6326477d17966317ad9870744bd1c2 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:01:17 +0000 Subject: [PATCH 059/165] make trigger reusable --- .github/workflows/track-review-project.yaml | 1 - .../workflows/trigger-project-workflow.yaml | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/trigger-project-workflow.yaml diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index f9111db..56216bd 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -69,7 +69,6 @@ jobs: - name: Extract PR Data run: | unzip context.zip - less context.json pr_num=$(jq -r '.pr_num' context.json) pr_id=$(jq -r '.pr_id' context.json) echo "PR_NUM=$pr_num" >> $GITHUB_ENV diff --git a/.github/workflows/trigger-project-workflow.yaml b/.github/workflows/trigger-project-workflow.yaml new file mode 100644 index 0000000..6fa7e72 --- /dev/null +++ b/.github/workflows/trigger-project-workflow.yaml @@ -0,0 +1,34 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + +# This workflow acts as a triggering workflow (via on: workflow_run) for the +# track-review-project workflow. It uploads an artifact containing PR data for +# the latter +name: Trigger Review Project + +on: + workflow_call: + +permissions: + contents: read + pull-requests: write + +jobs: + trigger_project: + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + PR_NUM: ${{ github.event.pull_request.number }} + PR_ID: ${{ github.event.pull_request.node_id }} + steps: + - name: Triggering Workflow + run: | + echo "{\"pr_num\": $PR_NUM, \"pr_id\": \"$PR_ID\"}" >> context.json + - uses: actions/upload-artifact@v6 + with: + name: context.json + path: context.json + retention-days: 1 From 94a1d6dacb05d1a34134b5cb311f7a82b64faafb Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:06:27 +0000 Subject: [PATCH 060/165] update permissions --- .github/workflows/track-review-project.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 56216bd..24c143e 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -27,7 +27,9 @@ on: default: 376 # Simulation Systems Review Tracker permissions: - write-all + actions: read + contents: read + pull-requests: write jobs: track_project: From b5c7540e0ee6032b5eaa3023f91581df061cc2d5 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 19:56:51 +0000 Subject: [PATCH 061/165] update cla action to allow for conflicts --- .github/workflows/cla-check.yaml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 52cf6b1..6b76973 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -74,7 +74,9 @@ jobs: fi - name: Checkout Merge Branch + id: checkout_merge uses: actions/checkout@v6 + continue-on-error: true with: token: ${{ secrets.GITHUB_TOKEN }} ref: "refs/pull/${{ github.event.number }}/merge" @@ -85,20 +87,24 @@ jobs: id: check_contributors_modified working-directory: ./merge_branch run: | - git fetch origin ${{ github.event.pull_request.base.ref }} + if [[ ${{ steps.checkout_merge.outcome }} == "failure" ]]; then + echo "::warn::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." + echo "modified=false" >> $GITHUB_OUTPUT + else + git fetch origin ${{ github.event.pull_request.base.ref }} - # Use the base SHA directly for comparison (works with forks) - BASE_SHA="${{ github.event.pull_request.base.sha }}" + # Use the base SHA directly for comparison (works with forks) + BASE_SHA="${{ github.event.pull_request.base.sha }}" - CHANGED_FILES=$(git diff --name-only $BASE_SHA) + CHANGED_FILES=$(git diff --name-only $BASE_SHA) - if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then - echo "modified=true" >> $GITHUB_OUTPUT - echo "📝 CONTRIBUTORS.md file was modified in this PR." - else - echo "modified=false" >> $GITHUB_OUTPUT - echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." - fi + if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then + echo "modified=true" >> $GITHUB_OUTPUT + echo "📝 CONTRIBUTORS.md file was modified in this PR." + else + echo "modified=false" >> $GITHUB_OUTPUT + echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." + fi # -- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) - name: Manage CLA Status, Labels, and Comments From 53a2096e4894266f67b55bccf1ce5f1820e0cd23 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:02:09 +0000 Subject: [PATCH 062/165] syntax --- .github/workflows/cla-check.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 6b76973..6255d67 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -105,6 +105,7 @@ jobs: echo "modified=false" >> $GITHUB_OUTPUT echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." fi + fi # -- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) - name: Manage CLA Status, Labels, and Comments From a93a805dc743900c119152a5f35f82cef7dc17d4 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:09:35 +0000 Subject: [PATCH 063/165] syntax --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 6255d67..9990885 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -88,7 +88,7 @@ jobs: working-directory: ./merge_branch run: | if [[ ${{ steps.checkout_merge.outcome }} == "failure" ]]; then - echo "::warn::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." + echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else git fetch origin ${{ github.event.pull_request.base.ref }} From 1af96cc12f6b1a8d89dfaf133cea11b8ec9ac47f Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:13:02 +0000 Subject: [PATCH 064/165] test exists approach --- .github/workflows/cla-check.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 9990885..47205e2 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -73,6 +73,13 @@ jobs: echo "🔴 CONTRIBUTORS.md file does not exist on PR branch." fi + - name: check ref exists + run: | + ref="refs/pull/${{ github.event.number }}/merge" + git rev-parse --verify $ref + exists=$? + echo $exists + - name: Checkout Merge Branch id: checkout_merge uses: actions/checkout@v6 From 17efe2d220a58634f1519fd37818e1670d69ae76 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:16:28 +0000 Subject: [PATCH 065/165] test exists approach --- .github/workflows/cla-check.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 47205e2..861b4d8 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -74,6 +74,7 @@ jobs: fi - name: check ref exists + working-directory: pr_branch run: | ref="refs/pull/${{ github.event.number }}/merge" git rev-parse --verify $ref From a1f8cf1f24d65525ba1889fe88ddd178e393fe99 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:18:42 +0000 Subject: [PATCH 066/165] test exists approach --- .github/workflows/cla-check.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 861b4d8..444536f 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -77,9 +77,9 @@ jobs: working-directory: pr_branch run: | ref="refs/pull/${{ github.event.number }}/merge" - git rev-parse --verify $ref - exists=$? - echo $exists + ls .git/refs + ls .git/refs/pull + ls .git/refs/pull/110 - name: Checkout Merge Branch id: checkout_merge From 62493c4805b2dbb6074c9c3abdaa4059ad2ea416 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:24:51 +0000 Subject: [PATCH 067/165] test exists approach --- .github/workflows/cla-check.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 444536f..ef8506d 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -74,12 +74,13 @@ jobs: fi - name: check ref exists - working-directory: pr_branch + working-directory: ./base_branch + shell: bash run: | ref="refs/pull/${{ github.event.number }}/merge" - ls .git/refs - ls .git/refs/pull - ls .git/refs/pull/110 + git remote -v + git ls-remote origin $ref + git ls-remote origin refs/pull/110/merge - name: Checkout Merge Branch id: checkout_merge From 53731f92bbb6d9895448f386d43ad9e4e2e87048 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:25:04 +0000 Subject: [PATCH 068/165] test exists approach --- .github/workflows/cla-check.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index ef8506d..1e5ab47 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -80,7 +80,9 @@ jobs: ref="refs/pull/${{ github.event.number }}/merge" git remote -v git ls-remote origin $ref + echo $? git ls-remote origin refs/pull/110/merge + echo $? - name: Checkout Merge Branch id: checkout_merge From e3df2d49f3d7306aa89b16cafeac77308c0663cc Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:28:08 +0000 Subject: [PATCH 069/165] test exists approach --- .github/workflows/cla-check.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 1e5ab47..656f9f7 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -79,10 +79,10 @@ jobs: run: | ref="refs/pull/${{ github.event.number }}/merge" git remote -v - git ls-remote origin $ref - echo $? - git ls-remote origin refs/pull/110/merge - echo $? + hash1=$(git ls-remote origin $ref) + echo $hash1 + hash2=$(git ls-remote origin refs/pull/110/head) + echo $hash2 - name: Checkout Merge Branch id: checkout_merge From cfc5a913659c561915882e66c3f47ede10f4f2b2 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:33:52 +0000 Subject: [PATCH 070/165] use checking of ref --- .github/workflows/cla-check.yaml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 656f9f7..caa6ecd 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -73,18 +73,21 @@ jobs: echo "🔴 CONTRIBUTORS.md file does not exist on PR branch." fi - - name: check ref exists + - name: Check Merge Ref Exists + id: check_merge_ref working-directory: ./base_branch shell: bash run: | ref="refs/pull/${{ github.event.number }}/merge" - git remote -v - hash1=$(git ls-remote origin $ref) - echo $hash1 - hash2=$(git ls-remote origin refs/pull/110/head) - echo $hash2 + hash=$(git ls-remote origin $ref) + if [[ -z $hash ]]; then + echo "merge_ref_defined=false" >> $GITHUB_OUTPUT + else + echo "merge_ref_defined=true" >> $GITHUB_OUTPUT + fi - name: Checkout Merge Branch + if: ${{ steps.check_merge_ref.merge_ref_defined == 'true' }} id: checkout_merge uses: actions/checkout@v6 continue-on-error: true @@ -98,7 +101,7 @@ jobs: id: check_contributors_modified working-directory: ./merge_branch run: | - if [[ ${{ steps.checkout_merge.outcome }} == "failure" ]]; then + if [[ ${{ steps.check_merge_ref.merge_ref_defined }} == "true" ]]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else From 2b50b4e1ee2798219717018abb5fb062c17efa55 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:36:46 +0000 Subject: [PATCH 071/165] syntax --- .github/workflows/cla-check.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index caa6ecd..f14baa7 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -81,13 +81,13 @@ jobs: ref="refs/pull/${{ github.event.number }}/merge" hash=$(git ls-remote origin $ref) if [[ -z $hash ]]; then - echo "merge_ref_defined=false" >> $GITHUB_OUTPUT + echo "merge_ref_defined=false" >> $GITHUB_ENV else - echo "merge_ref_defined=true" >> $GITHUB_OUTPUT + echo "merge_ref_defined=true" >> $GITHUB_ENV fi - name: Checkout Merge Branch - if: ${{ steps.check_merge_ref.merge_ref_defined == 'true' }} + if: ${{ env.merge_ref_defined == 'true' }} id: checkout_merge uses: actions/checkout@v6 continue-on-error: true @@ -101,7 +101,7 @@ jobs: id: check_contributors_modified working-directory: ./merge_branch run: | - if [[ ${{ steps.check_merge_ref.merge_ref_defined }} == "true" ]]; then + if [[ ${{ env.merge_ref_defined }} == "true" ]]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else From 79a1ad6a178a1b70ea5a1cc919ca2d307354c778 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:38:58 +0000 Subject: [PATCH 072/165] syntax --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index f14baa7..664d4b4 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -101,7 +101,7 @@ jobs: id: check_contributors_modified working-directory: ./merge_branch run: | - if [[ ${{ env.merge_ref_defined }} == "true" ]]; then + if [[ $merge_ref_defined == "true" ]]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else From 066db691b34d6e707a45a3c5abc41771401f7f3d Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:39:10 +0000 Subject: [PATCH 073/165] syntax --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 664d4b4..0841a67 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -87,7 +87,7 @@ jobs: fi - name: Checkout Merge Branch - if: ${{ env.merge_ref_defined == 'true' }} + if: env.merge_ref_defined == 'true' id: checkout_merge uses: actions/checkout@v6 continue-on-error: true From e13050cf5a63cf94a5268ab42308c2a43b49f128 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:40:10 +0000 Subject: [PATCH 074/165] syntax --- .github/workflows/cla-check.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 0841a67..669c558 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -82,6 +82,7 @@ jobs: hash=$(git ls-remote origin $ref) if [[ -z $hash ]]; then echo "merge_ref_defined=false" >> $GITHUB_ENV + mkdir merge_branch else echo "merge_ref_defined=true" >> $GITHUB_ENV fi From 24d627288d65c2558a2a57c8ca4c82d04fb5ecd6 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:41:13 +0000 Subject: [PATCH 075/165] syntax --- .github/workflows/cla-check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 669c558..5de0929 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -82,7 +82,6 @@ jobs: hash=$(git ls-remote origin $ref) if [[ -z $hash ]]; then echo "merge_ref_defined=false" >> $GITHUB_ENV - mkdir merge_branch else echo "merge_ref_defined=true" >> $GITHUB_ENV fi @@ -100,12 +99,13 @@ jobs: # -- Check if CONTRIBUTORS.md was modified in this PR - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified - working-directory: ./merge_branch run: | if [[ $merge_ref_defined == "true" ]]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else + cd merge_branch + git fetch origin ${{ github.event.pull_request.base.ref }} # Use the base SHA directly for comparison (works with forks) From 847707aa4c81ed8423c60b8f03823e0bad3c2f65 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:42:50 +0000 Subject: [PATCH 076/165] syntax --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 5de0929..633f3a3 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -100,7 +100,7 @@ jobs: - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified run: | - if [[ $merge_ref_defined == "true" ]]; then + if [[ "$merge_ref_defined" == "true" ]]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else From 61d2d219ee8e18e4186826306d6530dd2b7caf01 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:43:38 +0000 Subject: [PATCH 077/165] debug --- .github/workflows/cla-check.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 633f3a3..ec045fd 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -104,6 +104,7 @@ jobs: echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else + echo "HERE" cd merge_branch git fetch origin ${{ github.event.pull_request.base.ref }} From 2fe9565dfecfb5c21b68fa2c3310e3f6a49fe9f1 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:44:17 +0000 Subject: [PATCH 078/165] debug --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index ec045fd..b1a0085 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -100,7 +100,7 @@ jobs: - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified run: | - if [[ "$merge_ref_defined" == "true" ]]; then + if [[ "$merge_ref_defined" == "false" ]]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else From 94d5d09ea17138e66a9ac4a49366ec5321d21b7b Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Mon, 12 Jan 2026 20:45:46 +0000 Subject: [PATCH 079/165] remove debug --- .github/workflows/cla-check.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index b1a0085..24da274 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -104,7 +104,6 @@ jobs: echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else - echo "HERE" cd merge_branch git fetch origin ${{ github.event.pull_request.base.ref }} From 428b9ec44efacf43a68fbc97b2b559f17cfad9df Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:36:50 +0000 Subject: [PATCH 080/165] simplify logic --- .github/workflows/cla-check.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 24da274..5d5043a 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -144,10 +144,9 @@ jobs: const invalidModify = signedOnBase && contributorsModified; // CLA is met if: - // * (1) Didn't Invalidly Modify the PR and - // * (2) signed on both base and PR, OR - // * (3) signed on base and didn't modify CONTRIBUTORS.md - const cla_met = !invalidModify && ((signedOnBase && signedOnPr) || (signedOnBase && !contributorsModified)); + // * Didn't Invalidly Modify the CONTRIBUTORS and + // * signed on either base or PR + const cla_met = !invalidModify && (signedOnBase || signedOnPr); // Helper function to create or update a label with a specific COLOUR async function ensureLabel(name, COLOUR, description) { From 7761d23fbd51fbf39f9bf288f7d9268f612fd5c7 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 15 Jan 2026 16:29:24 +0000 Subject: [PATCH 081/165] Update .github/workflows/cla-check.yaml Co-authored-by: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 5d5043a..345fa12 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -29,7 +29,7 @@ jobs: ref: ${{ github.ref }} path: base_branch - - name: Determine if contributor exists in base (new) + - name: Determine if contributor exists in base id: check_contributor_base working-directory: ./base_branch run: | From 8e7dfdc509aae569f51d544ce4b4c47fdd4b2f4b Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Fri, 16 Jan 2026 07:58:38 +0000 Subject: [PATCH 082/165] make lowercase --- .github/workflows/cla-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 5d5043a..5035035 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -167,7 +167,7 @@ jobs: // Define desired COLOURs and descriptions for consistency const COLOUR_SIGNED = '0052cc'; // Blue const COLOUR_REQUIRED = 'b60205'; // Red - const COLOUR_MODIFIED = 'F56F27'; // Orange + const COLOUR_MODIFIED = 'f56f27'; // Orange // Helper function to delete old CLA-related comments from this bot async function deleteOldClaComments() { From 678d62ae9cf8c340d6fee41b176de879dd475d1d Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Fri, 16 Jan 2026 08:05:49 +0000 Subject: [PATCH 083/165] update label calls --- .github/workflows/cla-check.yaml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index c15c290..312d195 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -151,12 +151,24 @@ jobs: // Helper function to create or update a label with a specific COLOUR async function ensureLabel(name, COLOUR, description) { try { - await github.rest.issues.updateLabel({ owner, repo, name, COLOUR, description }); + await github.rest.issues.updateLabel({ + owner: owner, + repo: repo, + name: name, + description: description, + color: COLOUR + }); console.log(`Updated label: ${name} with COLOUR ${COLOUR}`); } catch (error) { // If update fails (label doesn't exist), create it try { - await github.rest.issues.createLabel({ owner, repo, name, COLOUR, description }); + await github.rest.issues.createLabel({ + owner: owner, + repo: repo, + name: name, + description: description, + color: COLOUR + }); console.log(`Created new label: ${name} with COLOUR ${COLOUR}`); } catch (createError) { console.log(`Error with label ${name}:`, createError.message); @@ -207,7 +219,7 @@ jobs: // Handle case where contributor modified the file when already on it if (invalidModify) { - await ensureLabel('cla-modified', COLOUR_MODIFIED, 'CLA signature has been modified.'); + await ensureLabel('cla-modified', COLOUR_MODIFIED, 'The CLA has been modified as part of this PR - added by GA'); console.log('⚠️ Contributor was in base branch but has modified the file.'); // Ensure labels are correct @@ -248,7 +260,7 @@ jobs: } else if (!signedOnBase && signedOnPr) { // New contributor signing CLA for the first time - await ensureLabel('cla-signed', COLOUR_SIGNED, 'This contributor has signed the CLA.'); + await ensureLabel('cla-signed', COLOUR_SIGNED, 'The CLA has been signed as part of this PR - added by GA'); console.log('✅ New contributor has signed the CLA in PR branch.'); await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), @@ -259,7 +271,7 @@ jobs: await deleteOldClaComments(); } else { - await ensureLabel('cla-required', COLOUR_REQUIRED, 'CLA signature is required for this PR.'); + await ensureLabel('cla-required', COLOUR_REQUIRED, 'The CLA has not yet been signed by the author of this PR - added by GA'); console.log('❌ CLA condition NOT met. Adding required label and ensuring signed label is absent.'); // Ensure labels are correct From 11be8c2612a5ccc88dd0ef82dde683c5c10384ef Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 11 Feb 2026 10:23:39 +0000 Subject: [PATCH 084/165] Merge main --- .github/workflows/cla-check.yaml | 19 +++++++---------- .github/workflows/track-review-project.yaml | 23 +++++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 312d195..c8b78e9 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -108,10 +108,7 @@ jobs: git fetch origin ${{ github.event.pull_request.base.ref }} - # Use the base SHA directly for comparison (works with forks) - BASE_SHA="${{ github.event.pull_request.base.sha }}" - - CHANGED_FILES=$(git diff --name-only $BASE_SHA) + CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF) if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then echo "modified=true" >> $GITHUB_OUTPUT @@ -143,11 +140,6 @@ jobs: // This may be valid but will require an admin to ok it const invalidModify = signedOnBase && contributorsModified; - // CLA is met if: - // * Didn't Invalidly Modify the CONTRIBUTORS and - // * signed on either base or PR - const cla_met = !invalidModify && (signedOnBase || signedOnPr); - // Helper function to create or update a label with a specific COLOUR async function ensureLabel(name, COLOUR, description) { try { @@ -215,7 +207,7 @@ jobs: } } - console.log(`CLA Met: ${cla_met} (Base: ${signedOnBase}, PR: ${signedOnPr}, Modified: ${contributorsModified}, Invalid Modify: ${invalidModify})`); + console.log(`Base: ${signedOnBase}, PR: ${signedOnPr}, Modified: ${contributorsModified}, Invalid Modify: ${invalidModify}`); // Handle case where contributor modified the file when already on it if (invalidModify) { @@ -225,6 +217,7 @@ jobs: // Ensure labels are correct await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-signed' }), + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-modified'] }) ]); @@ -241,7 +234,7 @@ jobs: process.exit(1); } - if (cla_met) { + if (signedOnBase) { // Different messages based on scenario if (signedOnBase && !contributorsModified) { @@ -258,12 +251,13 @@ jobs: // Delete old CLA comments since CLA is satisfied await deleteOldClaComments(); - } else if (!signedOnBase && signedOnPr) { + } else if (signedOnPr) { // New contributor signing CLA for the first time await ensureLabel('cla-signed', COLOUR_SIGNED, 'The CLA has been signed as part of this PR - added by GA'); console.log('✅ New contributor has signed the CLA in PR branch.'); await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-modified' }), github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) ]); @@ -277,6 +271,7 @@ jobs: // Ensure labels are correct await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-signed' }), + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-modified' }), github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-required'] }) ]); diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index ff91537..998d168 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -70,13 +70,11 @@ jobs: - name: Extract PR Data run: | - unzip context.zip - pr_num=$(jq -r '.pr_num' context.json) - pr_id=$(jq -r '.pr_id' context.json) - echo "PR_NUM=$pr_num" >> $GITHUB_ENV - echo "PR_ID=$pr_id" >> $GITHUB_ENV + unzip -q context.zip + jq -r '"PR_NUM=\(.pr_num)\nPR_ID=\(.pr_id)"' context.json >> $GITHUB_ENV - name: Assign Author if no assignees + continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | @@ -111,10 +109,13 @@ jobs: sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + cr_approved=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and .state == "APPROVED")] | length') + cr_changes=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + [[ ! -z "$scitech_reviewer" ]] && [[ "$sr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "SR_REQUEST_REQUIRED=true" >> $GITHUB_ENV - if [[ -z "$scitech_reviewer" ]] && [[ "$cr_requested" -gt 0 ]]; then - echo "No SR set and CR requested so assuming this is trivial" + if [[ -z "$scitech_reviewer" ]] && [[ "$cr_requested" -gt 0 || "$cr_approved" -gt 0 || "$cr_changes" -gt 0 ]]; then + echo "No SR set and CR in progress so assuming this is trivial" else if [[ "$sr_approved" -eq 0 ]]; then if [[ -z "$scitech_reviewer" ]] && [[ "$sr_changes" -eq 0 ]]; then @@ -129,8 +130,7 @@ jobs: fi fi - cr_approved=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and .state == "APPROVED")] | length') - cr_changes=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + # SR complete or trivial PR so move to code review states if [[ "$cr_approved" -gt 0 ]]; then required_state="Approved" @@ -147,13 +147,14 @@ jobs: - name: Request SciTech review, if needed if: env.SR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'SciTech Review' && env.SCITECH_REVIEWER != '' + continue-on-error: true env: GH_TOKEN: ${{ github.token }} - run: | - gh pr edit -R "$REPO" "$PR_NUM" --add-reviewer "$SCITECH_REVIEWER" + run: gh pr edit -R "$REPO" "$PR_NUM" --add-reviewer "$SCITECH_REVIEWER" - name: Request code review, if needed if: env.CR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'Code Review' && env.CODE_REVIEWER != '' + continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | From 54f30f5a5319994b3c0f81a33ceb36590cf5b90c Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:13:37 +0000 Subject: [PATCH 085/165] Enhance cla-check workflow --- .github/workflows/cla-check.yaml | 68 ++++++++++++++---- cla-check/README.md | 119 +++++++++++++++++++++++++++++-- 2 files changed, 168 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index c8b78e9..526abb8 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -1,9 +1,8 @@ -# ------------------------------------------------------------------------------ -# (c) Crown copyright Met Office. All rights reserved. +# ----------------------------------------------------------------------------- +# (C) Crown copyright Met Office. All rights reserved. # The file LICENCE, distributed with this code, contains details of the terms # under which the code may be used. -# ------------------------------------------------------------------------------ - +# ----------------------------------------------------------------------------- name: CLA Checker on: workflow_call: @@ -13,6 +12,11 @@ on: required: false type: string default: 'ubuntu-24.04' + cla-url: + description: 'URL to the CLA document' + required: false + type: string + default: 'https://github.com/MetOffice/Momentum/blob/main/CLA.md' permissions: contents: read @@ -29,13 +33,29 @@ jobs: ref: ${{ github.ref }} path: base_branch + - name: Validate Author Username + id: validate_author + run: | + AUTHOR="${{ github.event.pull_request.user.login }}" + # GitHub usernames: alphanumeric + hyphen, 1-39 chars, cannot start/end with hyphen + # Cannot contain consecutive hyphens + if [[ ! "$AUTHOR" =~ ^[a-zA-Z0-9]([a-zA-Z0-9]|-[a-zA-Z0-9]){0,37}[a-zA-Z0-9]?$ ]]; then + echo "::error::Invalid GitHub username format detected: $AUTHOR" + echo "This may indicate a security issue or data integrity problem." + exit 1 + fi + echo "✅ Username validation passed for: $AUTHOR" + echo "validated_author=$AUTHOR" >> "$GITHUB_OUTPUT" + - name: Determine if contributor exists in base id: check_contributor_base working-directory: ./base_branch run: | - AUTHOR="${{ github.event.pull_request.user.login }}" + # AUTHOR="${{ github.event.pull_request.user.login }}" + AUTHOR="${{ steps.validate_author.outputs.validated_author }}" if [ -f "CONTRIBUTORS.md" ]; then - if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then + # if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then + if grep -qP "^\s*\|\s*\Q$AUTHOR\E\s*\|" CONTRIBUTORS.md; then echo "on_base=true" >> $GITHUB_OUTPUT echo "🎉 $AUTHOR has already signed the CLA on base branch." else @@ -59,9 +79,11 @@ jobs: id: check_contributor_pr working-directory: pr_branch run: | - AUTHOR="${{ github.event.pull_request.user.login }}" + # AUTHOR="${{ github.event.pull_request.user.login }}" + AUTHOR="${{ steps.validate_author.outputs.validated_author }}" if [ -f "CONTRIBUTORS.md" ]; then - if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then + # if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then + if grep -qP "^\s*\|\s*\Q$AUTHOR\E\s*\|" CONTRIBUTORS.md; then echo "on_pr=true" >> $GITHUB_OUTPUT echo "✅ $AUTHOR is in the CONTRIBUTORS.md file on PR branch." else @@ -96,7 +118,6 @@ jobs: ref: "refs/pull/${{ github.event.number }}/merge" path: merge_branch - # -- Check if CONTRIBUTORS.md was modified in this PR - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified run: | @@ -104,13 +125,14 @@ jobs: echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> $GITHUB_OUTPUT else - cd merge_branch + cd merge_branch || exit 1 - git fetch origin ${{ github.event.pull_request.base.ref }} + git fetch origin "${{ github.event.pull_request.base.ref }}" - CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF) + # Only check for exact CONTRIBUTORS.md file (not subdirectories) + CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF -- CONTRIBUTORS.md) - if echo "$CHANGED_FILES" | grep -q "^CONTRIBUTORS.md$"; then + if [ -n "$CHANGED_FILES" ]; then echo "modified=true" >> $GITHUB_OUTPUT echo "📝 CONTRIBUTORS.md file was modified in this PR." else @@ -134,7 +156,25 @@ jobs: const issue_number = context.issue.number; const owner = context.repo.owner; const repo = context.repo.repo; + const author = context.payload.pull_request.user.login; + // Validate author (GitHub usernames are alphanumeric + hyphens) + if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(author)) { + console.error(`::error::Invalid username format: ${author}`); + process.exit(1); + } + + const claUrl = '${{ inputs.cla-url }}'; + // Validate CLA URL + try { + const url = new URL(claUrl); + if (url.protocol !== 'https:') { + throw new Error('CLA URL must use HTTPS protocol'); + } + } catch (error) { + console.error(`Invalid CLA URL: ${error.message}`); + throw error; + } // Check if contributor was on base but modified the file // This may be valid but will require an admin to ok it @@ -279,7 +319,7 @@ jobs: await deleteOldClaComments(); // Post CLA comment - const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](https://github.com/MetOffice/Momentum/blob/main/CLA.md).\n\nTo agree to the CLA, please add your details (**GitHub username**, Real Name, Affiliation, and Date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; + const commentBody = `Hello @${author}! 👋\n\nThank you for your contribution. Since this is your first time contributing to this repository, we ask that you sign our Contributor Licence Agreement (CLA).\n\n📄 [You can read the CLA here](${claUrl}).\n\nTo agree to the CLA, please add your details (**GitHub username**, Real Name, Affiliation, and Date) to the _CONTRIBUTORS.md_ file (create one, if required) in the development branch for this PR. After signing the CLA, you won't need to do this again for future PRs.`; await github.rest.issues.createComment({ owner, repo, issue_number, body: commentBody }); diff --git a/cla-check/README.md b/cla-check/README.md index 75de9ec..6646eef 100644 --- a/cla-check/README.md +++ b/cla-check/README.md @@ -1,20 +1,129 @@ -# CLA Check +# CLA Check Workflow -A reusable action to test that a contributor has agreed to a CLA by adding their -username to the CONTRIBUTORS file in that repository. +A reusable GitHub Actions workflow that verifies contributors have agreed to the +Contributor License Agreement (CLA) by checking their presence in the +`CONTRIBUTORS.md` file. + +## Features + +- ✅ Automatically checks if PR authors have signed the CLA +- 🏷️ Manages PR labels (`cla-signed`, `cla-required`, `cla-modified`) +- 💬 Posts helpful comments guiding contributors through the CLA process +- 🔄 Validates both base and PR branches for existing signatures +- 🛡️ Prevents unauthorized modifications to the `CONTRIBUTORS.md` file ## Usage +### Basic Usage + +Add this workflow to your repository by creating +`.github/workflows/cla-check.yml`: + ```yaml name: CLA Check on: pull_request_target: + types: [opened, synchronize, reopened] jobs: - cla_check: + cla-check: + uses: MetOffice/growss/.github/workflows/cla-check.yaml@main +``` + +### With Custom Configuration + +```yaml +name: CLA Check + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +jobs: + cla-check: uses: MetOffice/growss/.github/workflows/cla-check.yaml@main - # Optional with: + # URL to your CLA document (any HTTPS URL) + cla-url: "https://example.com/legal/cla" + # Or use a documentation platform + # cla-url: "https://docs.example.com/contributing/cla.html" + # Or GitLab + # cla-url: "https://gitlab.com/yourorg/yourrepo/-/blob/main/CLA.md" + # GitHub runner to use runner: "ubuntu-24.04" ``` + +## Input Parameters + +| Parameter | Description | Required | Default | +| --------- | --------------------------------------------- | -------- | -------------------------------------------------------- | +| `cla-url` | URL to the CLA document (any valid HTTPS URL) | No | `https://github.com/MetOffice/Momentum/blob/main/CLA.md` | +| `runner` | GitHub Actions runner to use for the job | No | `ubuntu-24.04` | + +**Note:** The `cla-url` can point to any publicly accessible web page containing +your CLA terms (e.g., GitHub, GitLab, Confluence, your organization's website, +etc.). It must be an HTTPS URL. + +## How It Works + +1. **Check Base Branch**: Verifies if the contributor has already signed the CLA + on the base branch +2. **Check PR Branch**: If not found on base, checks if the contributor added + themselves in the current PR +3. **Label Management**: Automatically adds/removes appropriate labels based on + CLA status +4. **Comment Control**: Posts helpful guidance for new contributors or warnings + for policy violations + +## Labels + +The workflow manages the following labels: + +- **🔵 cla-signed**: Author has signed the CLA in this PR (new contributor) +- **🔴 cla-required**: Author needs to sign the CLA before the PR can be merged +- **🟠 cla-modified**: Author modified CONTRIBUTORS.md when already signed + (requires admin review) + +## For Contributors + +To sign the CLA for your first contribution: + +1. Read the CLA document (link provided in the automated comment) +2. Add your details to the `CONTRIBUTORS.md` file in this PR: + + ```markdown + | GitHub Username | Real Name | Affiliation | Date | + | --------------- | --------- | ----------- | ---------- | + | username | Your Name | Your Org | YYYY-MM-DD | + ``` + +3. Commit and push the changes +4. The workflow will automatically verify your signature + +## Permissions Required + +This workflow requires the following permissions: + +```yaml +permissions: + contents: read # To checkout repository code + pull-requests: write # To add labels and post comments +``` + +## Security Considerations + +- Uses `pull_request_target` event for security when working with PRs from forks +- Only requires `read` access to repository contents +- Validates contributor identity through GitHub username matching +- **Uses fixed-string matching** to prevent regex injection attacks +- **Validates CLA URL format** - must be a valid HTTPS URL to a public web page + (not HTTP or file://) +- **Validates username format** to prevent malicious input +- **Never checks out untrusted code** - only inspects CONTRIBUTORS.md file +- **Scoped file checking** - only monitors the exact CONTRIBUTORS.md file + +## Licence + +© Crown copyright Met Office. All rights reserved. See the LICENCE file for +terms under which this code may be used. From f4ba020f08cd4534b239dc705069706eee0eaf8e Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:53:08 +0000 Subject: [PATCH 086/165] Refactor CLA label management to remove all related labels when signed and file not modified --- .github/workflows/cla-check.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 526abb8..b435e9e 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -283,9 +283,11 @@ jobs: console.log('✅ CLA condition met. Removing required label and adding signed label.'); } - // Use Promise.allSettled for robust label management + // Remove all CLA-related labels when signed on base and file not modified await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-modified' }), + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-signed' }) ]); // Delete old CLA comments since CLA is satisfied @@ -297,8 +299,7 @@ jobs: console.log('✅ New contributor has signed the CLA in PR branch.'); await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), - github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-modified' }), - github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-modified' }) ]); // Delete old CLA comments since CLA is now signed From 6e2433fc7ec49161d5c7789b1434526a3b47773c Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:29:41 +0000 Subject: [PATCH 087/165] Tidy up and address reviewer comments --- .github/workflows/cla-check.yaml | 10 ++++------ cla-check/README.md | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index b435e9e..cac5370 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -37,8 +37,8 @@ jobs: id: validate_author run: | AUTHOR="${{ github.event.pull_request.user.login }}" - # GitHub usernames: alphanumeric + hyphen, 1-39 chars, cannot start/end with hyphen - # Cannot contain consecutive hyphens + # GitHub usernames: alphanumeric + hyphen, 1-39 chars, + # cannot start/end with hyphen, cannot contain consecutive hyphens if [[ ! "$AUTHOR" =~ ^[a-zA-Z0-9]([a-zA-Z0-9]|-[a-zA-Z0-9]){0,37}[a-zA-Z0-9]?$ ]]; then echo "::error::Invalid GitHub username format detected: $AUTHOR" echo "This may indicate a security issue or data integrity problem." @@ -51,10 +51,8 @@ jobs: id: check_contributor_base working-directory: ./base_branch run: | - # AUTHOR="${{ github.event.pull_request.user.login }}" AUTHOR="${{ steps.validate_author.outputs.validated_author }}" if [ -f "CONTRIBUTORS.md" ]; then - # if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then if grep -qP "^\s*\|\s*\Q$AUTHOR\E\s*\|" CONTRIBUTORS.md; then echo "on_base=true" >> $GITHUB_OUTPUT echo "🎉 $AUTHOR has already signed the CLA on base branch." @@ -79,7 +77,6 @@ jobs: id: check_contributor_pr working-directory: pr_branch run: | - # AUTHOR="${{ github.event.pull_request.user.login }}" AUTHOR="${{ steps.validate_author.outputs.validated_author }}" if [ -f "CONTRIBUTORS.md" ]; then # if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then @@ -299,7 +296,8 @@ jobs: console.log('✅ New contributor has signed the CLA in PR branch.'); await Promise.allSettled([ github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-required' }), - github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-modified' }) + github.rest.issues.removeLabel({ owner, repo, issue_number, name: 'cla-modified' }), + github.rest.issues.addLabels({ owner, repo, issue_number, labels: ['cla-signed'] }) ]); // Delete old CLA comments since CLA is now signed diff --git a/cla-check/README.md b/cla-check/README.md index 6646eef..948bd30 100644 --- a/cla-check/README.md +++ b/cla-check/README.md @@ -10,7 +10,7 @@ Contributor License Agreement (CLA) by checking their presence in the - 🏷️ Manages PR labels (`cla-signed`, `cla-required`, `cla-modified`) - 💬 Posts helpful comments guiding contributors through the CLA process - 🔄 Validates both base and PR branches for existing signatures -- 🛡️ Prevents unauthorized modifications to the `CONTRIBUTORS.md` file +- 🛡️ Prevents unauthorised modifications to the `CONTRIBUTORS.md` file ## Usage From c556df468574c03353bfc22507a4aa5e6b736e91 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:56:26 +0000 Subject: [PATCH 088/165] Fix fortran linter workflow --- .github/workflows/fortran-lint.yaml | 11 +++++++---- fortran-lint/README.md | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fortran-lint.yaml b/.github/workflows/fortran-lint.yaml index a50559a..20f25ba 100644 --- a/.github/workflows/fortran-lint.yaml +++ b/.github/workflows/fortran-lint.yaml @@ -43,8 +43,11 @@ jobs: python-version: ${{ env.UV_PYTHON }} enable-cache: false - - name: Install fortitude-lint - run: uv pip install fortitude-lint==0.7.3 - - name: Run Fortitude Linter - run: uv run fortitude check --respect-gitignore --show-fixes --file-extensions=f90,F90,x90,X90,t90,T90,pf . + run: | + uvx --with fortitude-lint==0.7.3 fortitude check \ + --respect-gitignore \ + --show-fixes \ + --statistics \ + --file-extensions=f90,F90,x90,X90,t90,T90,pf,PF\ + . diff --git a/fortran-lint/README.md b/fortran-lint/README.md index 6d144f9..f307ce3 100644 --- a/fortran-lint/README.md +++ b/fortran-lint/README.md @@ -30,7 +30,7 @@ as a YAML file in your `.github/workflows` directory containing the follwing: ```yaml steps: - name: Lint Fortran - uses: MetOffice/growss/.github/workflows/fortan-lint.yaml@main + uses: MetOffice/growss/.github/workflows/fortran-lint.yaml@main with: runner: The runner to use for the job (ubuntu-latest) From f82c7e0ce8b34b821d51b97ce6c379206d790155 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:59:41 +0000 Subject: [PATCH 089/165] use correct flag to install fortitude-lint --- .github/workflows/fortran-lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/fortran-lint.yaml b/.github/workflows/fortran-lint.yaml index 20f25ba..dc50082 100644 --- a/.github/workflows/fortran-lint.yaml +++ b/.github/workflows/fortran-lint.yaml @@ -45,7 +45,7 @@ jobs: - name: Run Fortitude Linter run: | - uvx --with fortitude-lint==0.7.3 fortitude check \ + uvx --from fortitude-lint==0.7.3 fortitude check \ --respect-gitignore \ --show-fixes \ --statistics \ From d72c3d039890af1e5f6c1ab17dacb2a2a013d71b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:03:58 +0000 Subject: [PATCH 090/165] Refactor fortran-lint workflow --- .github/workflows/fortran-lint.yaml | 55 +++++++++-- fortran-lint/README.md | 142 +++++++++++++++++++++------- 2 files changed, 157 insertions(+), 40 deletions(-) diff --git a/.github/workflows/fortran-lint.yaml b/.github/workflows/fortran-lint.yaml index dc50082..fdf1a54 100644 --- a/.github/workflows/fortran-lint.yaml +++ b/.github/workflows/fortran-lint.yaml @@ -23,6 +23,46 @@ on: required: false type: string default: "3.14" + fortitude-version: + description: "The Fortitude linter version to use" + required: false + type: string + default: "0.7.3" + file-extensions: + description: "Comma-separated list of Fortran file extensions to check" + required: false + type: string + default: "f90,F90,x90,X90,t90,T90,pf,PF" + respect-gitignore: + description: "Whether to respect .gitignore files" + required: false + type: boolean + default: true + show-fixes: + description: "Whether to show suggested fixes" + required: false + type: boolean + default: true + show-statistics: + description: "Whether to show statistics" + required: false + type: boolean + default: true + source-path: + description: "Path to source code directory to lint" + required: false + type: string + default: "." + fail-on-error: + description: "Whether to fail the workflow on linting errors" + required: false + type: boolean + default: true + enable-cache: + description: "Whether to enable UV cache" + required: false + type: boolean + default: false jobs: fortran-linting: @@ -41,13 +81,14 @@ jobs: uses: astral-sh/setup-uv@v7 with: python-version: ${{ env.UV_PYTHON }} - enable-cache: false + enable-cache: ${{ inputs.enable-cache }} - name: Run Fortitude Linter run: | - uvx --from fortitude-lint==0.7.3 fortitude check \ - --respect-gitignore \ - --show-fixes \ - --statistics \ - --file-extensions=f90,F90,x90,X90,t90,T90,pf,PF\ - . + uvx --from fortitude-lint==${{ inputs.fortitude-version }} fortitude check \ + ${{ inputs.respect-gitignore && '--respect-gitignore' || '' }} \ + ${{ inputs.show-fixes && '--show-fixes' || '' }} \ + ${{ inputs.show-statistics && '--statistics' || '' }} \ + --file-extensions=${{ inputs.file-extensions }} \ + ${{ inputs.source-path }} + continue-on-error: ${{ !inputs.fail-on-error }} diff --git a/fortran-lint/README.md b/fortran-lint/README.md index f307ce3..c4c0c24 100644 --- a/fortran-lint/README.md +++ b/fortran-lint/README.md @@ -1,49 +1,125 @@ -# Fortran Linting workflow (using Fortitude) +# Fortran Lint Workflow -This reusable workflow serves as a a generic workflow to lint fortran code using -the fortitude linter. +A reusable GitHub Actions workflow that performs Fortran code linting using the +Fortitude linter. + +## Features + +- 🔍 Configurable Fortran file extensions +- 🎯 Customizable linter version +- 📊 Optional statistics and fix suggestions +- ⚙️ Flexible execution options +- 🔄 Version-controlled Python environment ## Usage -By default this workflow will check all the rules included in fortitude linter. -However, should you wish to check only a certain set of rules in your local -repository you can do this by implementing a `fortitude.toml` file in the -top-level/root directory of your local repository, defining the rules which -should be checked when running this reusable workflow from a caller workflow. -The following toml file is an example of what your `fortitude.toml` file could -look like: - -### Example fortitude configuration - -```toml -[check] -select = ["S", "T"] -ignore = ["S001", "S051"] -line-length = 132 +### Basic Usage + +```yaml +name: Lint Fortran Code +on: + pull_request: + paths: + - "**.f90" + - "**.F90" + push: + branches: + - main + +jobs: + fortran-lint: + uses: MetOffice/growss/.github/workflows/fortran-lint.yaml@main +``` + +### Advanced Usage + +```yaml +name: Custom Fortran Lint +on: + pull_request: + workflow_dispatch: + +jobs: + fortran-lint: + uses: MetOffice/growss/.github/workflows/fortran-lint.yaml@main + with: + # Runner configuration + runner: ubuntu-latest + timeout: 5 + + # Python and tooling versions + python-version: "3.12" + fortitude-version: "0.7.3" + + # Linting options + file-extensions: "f90,F90,pf,PF" + source-path: "./src" + respect-gitignore: true + show-fixes: true + show-statistics: true + + # Workflow behavior + fail-on-error: true + enable-cache: false ``` -In order to use this reusable workflow you should implement a calling workflow -as a YAML file in your `.github/workflows` directory containing the follwing: +## Input Parameters + +| Parameter | Description | Required | Default | Type | +| ------------------- | --------------------------------------- | -------- | ------------------------------- | ------- | +| `runner` | The runner to use for the job | No | `ubuntu-latest` | string | +| `timeout` | Maximum time in minutes the job can run | No | `10` | number | +| `python-version` | Python version to use | No | `3.14` | string | +| `fortitude-version` | Fortitude linter version | No | `0.7.3` | string | +| `file-extensions` | Comma-separated Fortran file extensions | No | `f90,F90,x90,X90,t90,T90,pf,PF` | string | +| `respect-gitignore` | Respect .gitignore files | No | `true` | boolean | +| `show-fixes` | Show suggested fixes | No | `true` | boolean | +| `show-statistics` | Show linting statistics | No | `true` | boolean | +| `source-path` | Path to source code directory | No | `.` | string | +| `fail-on-error` | Fail workflow on linting errors | No | `true` | boolean | +| `enable-cache` | Enable UV cache | No | `false` | boolean | -### Example usage in caller workflow +## Examples + +### Lint Specific Directory ```yaml -steps: - - name: Lint Fortran +jobs: + lint-src: uses: MetOffice/growss/.github/workflows/fortran-lint.yaml@main + with: + source-path: "./src/fortran" + file-extensions: "f90,F90" +``` + +### Warning-Only Mode +```yaml +jobs: + lint-warning: + uses: MetOffice/growss/.github/workflows/fortran-lint.yaml@main with: - runner: The runner to use for the job (ubuntu-latest) - timeout: The maximum time in minutes the job can run for (10) - python-version: The Python version to use (3.14) + fail-on-error: false + show-fixes: true ``` -Where runner,timeout and python-version are all appropriately defined inputs to -the workflow as defined in `MetOffice/growss/.github/workflows/fortan-lint.yaml` +### Custom Extensions and Performance + +```yaml +jobs: + lint-custom: + uses: MetOffice/growss/.github/workflows/fortran-lint.yaml@main + with: + file-extensions: "f90,F90,for,FOR" + timeout: 20 + enable-cache: true +``` + +## About Fortitude + +[Fortitude](https://github.com/PlasmaFAIR/fortitude) is a modern Fortran linter +that helps maintain code quality and consistency. -### Further details +## License -Fortitude is a fortran linter developed and maintained by PlasmaFAIR. For -further details on how to configure your `fortitude.toml` file and how to -interpret error messages given by this reusable workflow please refer to the -[fortitude doumentation](https://fortitude.readthedocs.io/en/latest/) +© Crown copyright Met Office. See LICENCE file for details. From 3c23a139bc43332d15bb4909b82d2f6983d87dc2 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:07:35 +0000 Subject: [PATCH 091/165] Add link to workflow file from README --- fortran-lint/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fortran-lint/README.md b/fortran-lint/README.md index c4c0c24..5df3b23 100644 --- a/fortran-lint/README.md +++ b/fortran-lint/README.md @@ -1,7 +1,8 @@ # Fortran Lint Workflow -A reusable GitHub Actions workflow that performs Fortran code linting using the -Fortitude linter. +A reusable GitHub Actions [workflow](../.github/workflows/fortran-lint.yaml) +that performs Fortran code linting using the +[Fortitude](https://github.com/PlasmaFAIR/fortitude) linter. ## Features From 417f7ccb038a0cd7cd9e22b7b6d2652258e6dabe Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 20 Feb 2026 21:44:01 +0000 Subject: [PATCH 092/165] Explain sections --- fortran-lint/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/fortran-lint/README.md b/fortran-lint/README.md index 5df3b23..4ac6037 100644 --- a/fortran-lint/README.md +++ b/fortran-lint/README.md @@ -16,6 +16,17 @@ that performs Fortran code linting using the ### Basic Usage +This section provides instructions on how to use the fortran-lint tool in its +most common scenarios. The basic workflow includes running the linter on Fortran +source files to check for style violations, code quality issues, and potential +errors. + +The `paths` filter ensures the workflow only runs when Fortran files (with +`.f90` or `.F90` extensions) are modified in pull requests, avoiding unnecessary +checks when other file types are changed. If paths filters are omitted, then +default `file-extensions` are used instead (see +[Input Parameters](#input-parameters)). + ```yaml name: Lint Fortran Code on: @@ -34,6 +45,12 @@ jobs: ### Advanced Usage +This section covers advanced configuration options and usage patterns for the +Fortran linting tool. It provides detailed examples and explanations for users +who need to customize the linting behavior beyond the basic setup, including +custom rule configurations, integration with CI/CD pipelines, and handling +complex project structures. + ```yaml name: Custom Fortran Lint on: From 2f74857b015283ab3c361abe7d3e4ee33ed7d15a Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 6 Mar 2026 08:49:07 +0000 Subject: [PATCH 093/165] Bring develop up-to-date with main (#72) Co-authored-by: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Co-authored-by: Jenny Hickson <61183013+jennyhickson@users.noreply.github.com> --- .github/pull_request_template.md | 31 +++++++++++++ .../workflows/call-track-review-project.yaml | 18 ++++++++ .../call-trigger-project-workflow.yaml | 12 ++++++ .github/workflows/check-cr-approved.yaml | 2 +- .github/workflows/track-review-project.yaml | 4 +- .github/workflows/validate_symlinks.yaml | 43 +++++++++++++++++++ validate_symlinks/README.md | 23 ++++++++++ 7 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/call-track-review-project.yaml create mode 100644 .github/workflows/call-trigger-project-workflow.yaml create mode 100644 .github/workflows/validate_symlinks.yaml create mode 100644 validate_symlinks/README.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..41944d9 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +# PR Summary + +Code Reviewer: + + + + + +## Code Quality Checklist + +(_Some checks are automatically carried out via the CI pipeline_) + +- [ ] I have performed a self-review of my own code +- [ ] My code follows the project's style guidelines +- [ ] The modified workflow's README has been updated, if required +- [ ] The changes have been sufficiently tested (please describe) + +## AI Assistance and Attribution + +- [ ] Some of the content of this change has been produced with the assistance + of _Generative AI tool name_ (e.g., Met Office Github Copilot Enterprise, + Github Copilot Personal, ChatGPT GPT-4, etc) and I have followed the + [Simulation Systems AI policy](https://metoffice.github.io/simulation-systems/FurtherDetails/ai.html) + (including attribution labels) + + + +# Code Review + +- [ ] The changes are approriate and testing has been sufficient diff --git a/.github/workflows/call-track-review-project.yaml b/.github/workflows/call-track-review-project.yaml new file mode 100644 index 0000000..eeab646 --- /dev/null +++ b/.github/workflows/call-track-review-project.yaml @@ -0,0 +1,18 @@ +name: Track Review Project + +on: + workflow_run: + workflows: [Trigger Review Project] + types: + - completed + workflow_dispatch: + +jobs: + track_review_project: + uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main + secrets: inherit + # Optional inputs (with default values) + with: + runner: "ubuntu-22.04" + project_org: "MetOffice" + project_number: 376 diff --git a/.github/workflows/call-trigger-project-workflow.yaml b/.github/workflows/call-trigger-project-workflow.yaml new file mode 100644 index 0000000..f22c433 --- /dev/null +++ b/.github/workflows/call-trigger-project-workflow.yaml @@ -0,0 +1,12 @@ +name: Trigger Review Project + +on: + pull_request_target: + types: ["opened", "synchronize", "reopened", "edited", "review_requested", "review_request_removed", "closed"] + pull_request_review: + pull_request_review_comment: + +jobs: + trigger_project_workflow: + uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main + secrets: inherit diff --git a/.github/workflows/check-cr-approved.yaml b/.github/workflows/check-cr-approved.yaml index 2e72821..1ad5afc 100644 --- a/.github/workflows/check-cr-approved.yaml +++ b/.github/workflows/check-cr-approved.yaml @@ -27,7 +27,7 @@ jobs: echo "Running on PR #$PR_NUMBER in Repository $REPO" # -- 1. Extract the assigned Code Reviewer from the PR body - CODE_REVIEWER=$(echo "$PR_BODY" | grep -oP "Code Reviewer:\s?@\K([\w\-]{1,39})\b") || true + CODE_REVIEWER=$(echo "$PR_BODY" | perl -00 -ne 's///gs; print if(s/.*Code Reviewer:\s*@([\w\-]{1,39})\b.*/\1\n/s);') || true if [[ -z "$CODE_REVIEWER" ]]; then echo "::error::No 'Code Reviewer: @' found in the PR body." diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 4d9d116..af7891d 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -92,8 +92,8 @@ jobs: # Extract reviewers from PR body pr_body=$(echo "$pr_data" | jq -r '.body') - scitech_reviewer=$(echo "$pr_body" | grep -oP "Sci/Tech Reviewer:\s?@\K([\w\-]{1,39})\b" || true) - code_reviewer=$(echo "$pr_body" | grep -oP "Code Reviewer:\s?@\K([\w\-]{1,39})\b" || true) + scitech_reviewer=$(echo "$pr_body" | perl -00 -ne 's///gs; print if(s/.*Sci\/Tech Reviewer:\s*@([\w\-]{1,39})\b.*/\1\n/s);') || true + code_reviewer=$(echo "$pr_body" | perl -00 -ne 's///gs; print if(s/.*Code Reviewer:\s*@([\w\-]{1,39})\b.*/\1\n/s);') || true echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" echo "Expected Code Reviewer: ${code_reviewer:-None}" diff --git a/.github/workflows/validate_symlinks.yaml b/.github/workflows/validate_symlinks.yaml new file mode 100644 index 0000000..d668cef --- /dev/null +++ b/.github/workflows/validate_symlinks.yaml @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + +name: Validate Symlinks + +on: + workflow_call: + inputs: + runner: + description: 'The runner to use for the job' + required: false + type: string + default: 'ubuntu-24.04' + timeout: + description: 'The time limit for the workflow, default 5 minutes' + required: false + type: number + default: 5 + +permissions: read-all + +jobs: + validate_symlinks: + runs-on: ${{ inputs.runner }} + timeout-minutes: ${{ inputs.timeout }} + + steps: + - name: Checkout Branch + uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Find broken symlinks + run: | + broken_links=$(find . -path '.git' -prune -o -xtype l -printf '%p -> %l\n' || true) + if [ -n "$broken_links" ]; then + echo "::error::Broken symlinks have been identified:" + printf "%s\n" "$broken_links" + exit 1 + fi + echo "::notice::No broken symlinks identified." diff --git a/validate_symlinks/README.md b/validate_symlinks/README.md new file mode 100644 index 0000000..a2586c7 --- /dev/null +++ b/validate_symlinks/README.md @@ -0,0 +1,23 @@ +# Validate Symlinks + +This workflow will look for broken symlinks in the calling repository, raising +an error and listing details if these exist. + +## Usage + +```yaml +name: Validate Symlinks + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +jobs: + validate_symlinks: + uses: MetOffice/growss/.github/workflows/validate_symlinks.yaml@main + # Optional non default inputs + with: + runner: "ubuntu-latest" + timeout: 10 +``` From 9c332ba89ed3e4ddb8e4bab868bdcfd59020bc47 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:03:47 +0000 Subject: [PATCH 094/165] Enhance Sphinx workflow with pyproject.toml support and improved dependency resolution --- .github/workflows/sphinx-docs.yaml | 244 ++++++++++++++++++++--------- sphinx-docs/README.md | 196 ++++++++++++++++++----- 2 files changed, 329 insertions(+), 111 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index f459480..033b734 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -56,6 +56,28 @@ on: required: false type: string default: '_build' + use-pyproject: + description: > + Override automatic dependency resolution and force the use of + pyproject.toml instead of conf.py. The workflow will search for + pyproject.toml in docs-dir, source-dir, then repo root (in that + order). Takes precedence over conf.py when set to true. + required: false + type: boolean + default: false + extras: + description: > + Optional dependency group to install from pyproject.toml when + use-pyproject is true or when falling back to pyproject.toml. + Corresponds to a key under [project.optional-dependencies] or a + [dependency-groups] group (PEP 735). e.g. "docs" installs the + extras defined under [project.optional-dependencies] docs = [...] + via "uv sync --extra docs", or a dependency group via + "uv sync --group docs". + Leave empty to run plain "uv sync". + required: false + type: string + default: '' outputs: pages-url: description: 'URL of the deployed GitHub Pages site' @@ -75,6 +97,15 @@ jobs: env: PYTHON_VRSN: ${{ inputs.python-version }} VENV_PATH: ${{ inputs.venv-path }} + # Deploy only when: caller opted in AND we are on the upstream main branch + # (not a fork's main, not a PR, not any other branch) + DO_DEPLOY: >- + ${{ + inputs.deploy + && github.ref == 'refs/heads/main' + && github.repository == github.event.repository.full_name + && github.event_name != 'pull_request' + }} steps: - name: Checkout repository @@ -85,25 +116,86 @@ jobs: with: python-version: ${{ env.PYTHON_VRSN }} - - name: Detect pyproject.toml location - id: detect-pyproject + - name: Detect dependency source + id: detect-deps run: | - if [ -f "pyproject.toml" ]; then - echo "found=true" >> "$GITHUB_OUTPUT" - echo "location=root" >> "$GITHUB_OUTPUT" + # When use-pyproject is true, skip conf.py entirely and go straight + # to pyproject.toml resolution + if [ "${{ inputs.use-pyproject }}" = "true" ]; then + if [ -f "${{ inputs.docs-dir }}/pyproject.toml" ]; then + echo "source=pyproject-docs" >> "$GITHUB_OUTPUT" + elif [ -f "${{ inputs.source-dir }}/pyproject.toml" ]; then + echo "source=pyproject-source" >> "$GITHUB_OUTPUT" + elif [ -f "pyproject.toml" ]; then + echo "source=pyproject-root" >> "$GITHUB_OUTPUT" + else + echo "::error::use-pyproject is true but no pyproject.toml found in docs-dir, source-dir, or repo root." + exit 1 + fi + + # Default auto-detection: conf.py has highest priority + elif [ -f "${{ inputs.source-dir }}/conf.py" ]; then + echo "source=conf" >> "$GITHUB_OUTPUT" elif [ -f "${{ inputs.docs-dir }}/pyproject.toml" ]; then - echo "found=true" >> "$GITHUB_OUTPUT" - echo "location=docs" >> "$GITHUB_OUTPUT" + echo "source=pyproject-docs" >> "$GITHUB_OUTPUT" elif [ -f "${{ inputs.source-dir }}/pyproject.toml" ]; then - echo "found=true" >> "$GITHUB_OUTPUT" - echo "location=source" >> "$GITHUB_OUTPUT" + echo "source=pyproject-source" >> "$GITHUB_OUTPUT" + elif [ -f "pyproject.toml" ]; then + echo "source=pyproject-root" >> "$GITHUB_OUTPUT" else - echo "found=false" >> "$GITHUB_OUTPUT" - echo "location=none" >> "$GITHUB_OUTPUT" + echo "::error::No conf.py or pyproject.toml found. Cannot resolve dependencies." + exit 1 + fi + + - name: Resolve uv sync flags + id: uv-flags + if: ${{ steps.detect-deps.outputs.source != 'conf' }} + run: | + # Determine whether `extras` refers to an optional-dependency extra + # or a dependency-group, then set the appropriate uv sync flag. + EXTRAS="${{ inputs.extras }}" + if [ -z "$EXTRAS" ]; then + echo "flags=" >> "$GITHUB_OUTPUT" + exit 0 fi + # Locate the relevant pyproject.toml + case "${{ steps.detect-deps.outputs.source }}" in + pyproject-docs) PYPROJECT="${{ inputs.docs-dir }}/pyproject.toml" ;; + pyproject-source) PYPROJECT="${{ inputs.source-dir }}/pyproject.toml" ;; + *) PYPROJECT="pyproject.toml" ;; + esac + + # Inspect pyproject.toml to determine the correct flag + FLAGS=$(python3 - <> "$GITHUB_OUTPUT" + - name: Cache uv venv - if: ${{ steps.detect-pyproject.outputs.found == 'true' }} + if: ${{ steps.detect-deps.outputs.source != 'conf' }} uses: actions/cache@v5 with: path: ${{ env.VENV_PATH }} @@ -111,32 +203,12 @@ jobs: restore-keys: | ${{ runner.os }}-${{ env.PYTHON_VRSN }}- - - name: Install dependencies via uv sync (root pyproject.toml) - if: ${{ steps.detect-pyproject.outputs.location == 'root' }} - run: uv sync - - - name: Install dependencies via uv sync (docs pyproject.toml) - if: ${{ steps.detect-pyproject.outputs.location == 'docs' }} - working-directory: ${{ inputs.docs-dir }} - run: uv sync - - - name: Install dependencies via uv sync (source pyproject.toml) - if: ${{ steps.detect-pyproject.outputs.location == 'source' }} - working-directory: ${{ inputs.source-dir }} - run: uv sync - - - name: Extract Sphinx extensions from conf.py - if: ${{ steps.detect-pyproject.outputs.found == 'false' }} - id: extract-extensions + - name: Install dependencies from conf.py (extensions + html_theme) + if: ${{ steps.detect-deps.outputs.source == 'conf' }} run: | CONF_FILE="${{ inputs.source-dir }}/conf.py" - if [ ! -f "$CONF_FILE" ]; then - echo "Error: conf.py not found at $CONF_FILE" >&2 - exit 1 - fi - # Extract extensions list from conf.py and map to PyPI packages - EXTENSIONS=$(python3 - <<'EOF' - import ast, sys + PACKAGES=$(python3 - <<'EOF' + import ast conf_path = "${{ inputs.source-dir }}/conf.py" with open(conf_path) as f: @@ -144,80 +216,108 @@ jobs: tree = ast.parse(source) extensions = [] + html_theme = None + for node in ast.walk(tree): if isinstance(node, ast.Assign): for target in node.targets: - if isinstance(target, ast.Name) and target.id == "extensions": + if not isinstance(target, ast.Name): + continue + if target.id == "extensions": if isinstance(node.value, (ast.List, ast.Tuple)): for elt in node.value.elts: if isinstance(elt, ast.Constant): extensions.append(elt.value) + if target.id == "html_theme": + if isinstance(node.value, ast.Constant): + html_theme = node.value.value - # Map known Sphinx extensions to their PyPI package names ext_to_pkg = { - "sphinx.ext.autodoc": None, - "sphinx.ext.autosummary": None, - "sphinx.ext.autosectionlabel": None, - "sphinx.ext.viewcode": None, - "sphinx.ext.napoleon": None, - "sphinx.ext.intersphinx": None, - "sphinx.ext.todo": None, - "sphinx.ext.coverage": None, - "sphinx.ext.ifconfig": None, - "sphinx.ext.githubpages": None, - "sphinx.ext.mathjax": None, - "sphinx.ext.imgmath": None, - "sphinx.ext.doctest": None, - "sphinx.ext.duration": None, - "sphinx.ext.graphviz": None, - "sphinx.ext.inheritance_diagram": None, - "myst_parser": "myst-parser", - "myst_nb": "myst-nb", - "nbsphinx": "nbsphinx", + # -- sphinxcontrib third-party (not bundled with Sphinx) "sphinxcontrib.bibtex": "sphinxcontrib-bibtex", "sphinxcontrib.httpdomain": "sphinxcontrib-httpdomain", "sphinxcontrib.plantuml": "sphinxcontrib-plantuml", "sphinxcontrib.mermaid": "sphinxcontrib-mermaid", "sphinxcontrib.napoleon": "sphinxcontrib-napoleon", + "sphinxcontrib.rsvgconverter": "sphinxcontrib-rsvgconverter", + # -- MyST / Jupyter + "myst_parser": "myst-parser", + "myst_nb": "myst-nb", + "nbsphinx": "nbsphinx", + # -- Themes (also registered as extensions in some setups) "sphinx_rtd_theme": "sphinx-rtd-theme", "sphinx_book_theme": "sphinx-book-theme", "pydata_sphinx_theme": "pydata-sphinx-theme", "furo": "furo", + # -- Autodoc / API "sphinx_autodoc_typehints": "sphinx-autodoc-typehints", + "autoapi.extension": "sphinx-autoapi", + "sphinx_autoapi": "sphinx-autoapi", + "breathe": "breathe", + "exhale": "exhale", + # -- UI / UX "sphinx_copybutton": "sphinx-copybutton", "sphinx_design": "sphinx-design", "sphinx_tabs": "sphinx-tabs", "sphinx_panels": "sphinx-panels", "sphinx_togglebutton": "sphinx-togglebutton", - "autoapi.extension": "sphinx-autoapi", - "sphinx_autoapi": "sphinx-autoapi", - "breathe": "breathe", - "exhale": "exhale", + # -- CLI / args "sphinx_click": "sphinx-click", "sphinxarg.ext": "sphinx-argparse", "sphinx_argparse_cli": "sphinx-argparse-cli", + # -- Misc "sphinx_sitemap": "sphinx-sitemap", - "sphinxcontrib.rsvgconverter": "sphinxcontrib-rsvgconverter", + } + + # Bundled Sphinx themes — no install needed + _BUILTIN_THEMES = { + "alabaster", "classic", "nature", "bizstyle", + "pyramid", "scrolls", "agogo", "traditional", "haiku", + } + + theme_to_pkg = { + "sphinx_rtd_theme": "sphinx-rtd-theme", + "sphinx_book_theme": "sphinx-book-theme", + "pydata_sphinx_theme": "pydata-sphinx-theme", + "furo": "furo", } pkgs = {"sphinx", "sphinx-lint"} + for ext in extensions: + if ext.startswith("sphinx.ext."): + continue pkg = ext_to_pkg.get(ext) if pkg is not None: pkgs.add(pkg) - elif not ext.startswith("sphinx."): - # Unknown third-party extension: use ext root as package guess + elif ext not in ext_to_pkg: pkg_guess = ext.split(".")[0].replace("_", "-") pkgs.add(pkg_guess) + if html_theme is not None: + if html_theme not in _BUILTIN_THEMES: + theme_pkg = theme_to_pkg.get(html_theme) + pkgs.add(theme_pkg if theme_pkg is not None + else html_theme.replace("_", "-")) + print(" ".join(sorted(pkgs))) EOF ) - echo "packages=$EXTENSIONS" >> "$GITHUB_OUTPUT" + uv pip install --system $PACKAGES + + - name: Install dependencies via uv sync (docs pyproject.toml) + if: ${{ steps.detect-deps.outputs.source == 'pyproject-docs' }} + working-directory: ${{ inputs.docs-dir }} + run: uv sync ${{ steps.uv-flags.outputs.flags }} - - name: Install dependencies from conf.py extensions (no pyproject.toml) - if: ${{ steps.detect-pyproject.outputs.found == 'false' }} - run: uv pip install --system ${{ steps.extract-extensions.outputs.packages }} + - name: Install dependencies via uv sync (source pyproject.toml) + if: ${{ steps.detect-deps.outputs.source == 'pyproject-source' }} + working-directory: ${{ inputs.source-dir }} + run: uv sync ${{ steps.uv-flags.outputs.flags }} + + - name: Install dependencies via uv sync (root pyproject.toml) + if: ${{ steps.detect-deps.outputs.source == 'pyproject-root' }} + run: uv sync ${{ steps.uv-flags.outputs.flags }} - name: Lint Sphinx docs run: uv run sphinx-lint ${{ inputs.source-dir }} @@ -247,16 +347,16 @@ jobs: "${{ inputs.docs-dir }}/${{ inputs.build-dir }}/html" - name: Minimize uv cache - if: ${{ steps.detect-pyproject.outputs.found == 'true' }} + if: ${{ steps.detect-deps.outputs.source != 'conf' }} run: uv cache prune --ci # -- Deploy to GitHub Pages only on push to upstream main - name: Setup GitHub Pages - if: ${{ inputs.deploy }} + if: ${{ env.DO_DEPLOY == 'true' }} uses: actions/configure-pages@v5 - name: Upload artifact to GitHub Pages - if: ${{ inputs.deploy }} + if: ${{ env.DO_DEPLOY == 'true' }} uses: actions/upload-pages-artifact@v4 with: name: github-pages @@ -265,5 +365,5 @@ jobs: - name: Deploy to GitHub Pages id: deployment - if: ${{ inputs.deploy }} + if: ${{ env.DO_DEPLOY == 'true' }} uses: actions/deploy-pages@v4 diff --git a/sphinx-docs/README.md b/sphinx-docs/README.md index af2b528..86abe0e 100644 --- a/sphinx-docs/README.md +++ b/sphinx-docs/README.md @@ -1,19 +1,24 @@ # Build Sphinx Documentation and Deploy to GitHub Pages -A reusable GitHub Actions workflow that builds Sphinx documentation and -optionally deploys it to GitHub Pages. +A reusable GitHub Actions [workflow](../.github/workflows/sphinx-docs.yaml) that +builds [Sphinx documentation](https://www.sphinx-doc.org/) and optionally +deploys it to GitHub Pages. ## Features -- Supports repositories with or without a `pyproject.toml` -- Auto-detects `pyproject.toml` at the repo root, docs directory, or Sphinx - source directory -- When no `pyproject.toml` is present, parses `conf.py` to resolve and install - required Sphinx extension packages automatically -- Lints documentation source with `sphinx-lint` +- Auto-detects dependencies from `conf.py` (highest priority), falling back to + `pyproject.toml` in the docs tree, then the repo root +- Supports forcing `pyproject.toml` resolution via `use-pyproject: true`, + bypassing `conf.py` entirely — useful when dependencies are already declared + under `[project.optional-dependencies]` or `[dependency-groups]` +- When resolving from `conf.py`, detects packages from both `extensions` and + `html_theme` +- Lints documentation source with + [`sphinx-lint`](https://github.com/sphinx-contrib/sphinx-lint) - Uses `make html` if a `Makefile` is present in the docs directory, otherwise falls back to `sphinx-build` -- Optionally deploys the built HTML to GitHub Pages +- Deploys to GitHub Pages **only on pushes to the upstream `main` branch** — + never from forks or pull requests ## Supported Directory Layouts @@ -21,12 +26,12 @@ optionally deploys it to GitHub Pages. ```text repo/ -├── pyproject.toml # optional +├── pyproject.toml # optional — used if no conf.py found, or forced via use-pyproject ├── src/ └── docs/ ├── Makefile # optional └── source/ - ├── conf.py + ├── conf.py # highest priority for dependency resolution └── index.rst ``` @@ -42,10 +47,10 @@ jobs: ```text repo/ -├── pyproject.toml # optional +├── pyproject.toml # optional — used if no conf.py found, or forced via use-pyproject └── docs/ ├── Makefile # optional - ├── conf.py + ├── conf.py # highest priority for dependency resolution └── index.rst ``` @@ -61,16 +66,18 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| ----------------- | --------------------------------------------------------- | -------- | -------------- | -| `python-version` | Python version to use | No | `3.12` | -| `venv-path` | Virtual environment path | No | `.venv` | -| `runner` | GitHub Actions runner | No | `ubuntu-24.04` | -| `timeout-minutes` | Job timeout in minutes | No | `5` | -| `deploy` | Whether to deploy to GitHub Pages | No | `true` | -| `docs-dir` | Path to the docs directory where `Makefile` lives | No | `docs` | -| `source-dir` | Path to the Sphinx source directory where `conf.py` lives | No | `docs/source` | -| `build-dir` | Sphinx build output directory (relative to `docs-dir`) | No | `_build` | +| Input | Description | Required | Default | +| ----------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | -------------- | +| `python-version` | Python version to use | No | `3.12` | +| `venv-path` | Virtual environment path | No | `.venv` | +| `runner` | GitHub Actions runner | No | `ubuntu-24.04` | +| `timeout-minutes` | Job timeout in minutes | No | `5` | +| `deploy` | Whether to deploy to GitHub Pages | No | `true` | +| `docs-dir` | Path to the docs directory where `Makefile` lives | No | `docs` | +| `source-dir` | Path to the Sphinx source directory where `conf.py` lives | No | `docs/source` | +| `build-dir` | Sphinx build output directory (relative to `docs-dir`) | No | `_build` | +| `use-pyproject` | Force `pyproject.toml` resolution, skipping `conf.py` entirely | No | `false` | +| `extras` | Optional dependency extra or group name to pass to `uv sync` (see [Dependency Resolution](#dependency-resolution)) | No | `''` | ## Outputs @@ -91,7 +98,7 @@ permissions: ## Usage Examples -### Minimal — deploy on every push to `main` +### Minimal — build and deploy on push to `main` ```yaml name: Docs @@ -135,26 +142,91 @@ jobs: docs-dir: docs source-dir: docs/source build-dir: _build - deploy: ${{ github.ref_name == 'main' && github.event_name == 'push' }} permissions: contents: read pages: write id-token: write ``` -### Without a `pyproject.toml` +> **Note:** You do not need to pass `deploy: ${{ github.ref_name == 'main' }}` +> yourself. The workflow enforces deployment guards internally — it will only +> deploy when **all** of the following are true: +> +> - `deploy` input is `true` (the default) +> - The ref is exactly `refs/heads/main` +> - The run is on the **upstream** repository, not a fork +> - The event is **not** a `pull_request` + +### Force `pyproject.toml` with optional dependencies + +Use this when Sphinx dependencies are declared under +`[project.optional-dependencies]` in your `pyproject.toml`: + +```toml +# pyproject.toml +[project.optional-dependencies] +docs = [ + "sphinx", + "furo", + "myst-parser", + "sphinx-copybutton", +] +``` + +```yaml +jobs: + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + with: + use-pyproject: true + extras: docs # → uv sync --extra docs + permissions: + contents: read + pages: write + id-token: write +``` -No additional configuration is required. The workflow will parse `conf.py` -automatically to detect and install the required Sphinx extension packages. +### Force `pyproject.toml` with a dependency group (PEP 735) + +Use this when dependencies are declared under `[dependency-groups]`: + +```toml +# pyproject.toml +[dependency-groups] +docs = [ + "sphinx", + "furo", + "myst-parser", + "sphinx-copybutton", +] +``` ```yaml jobs: docs: uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main with: - docs-dir: docs - source-dir: docs/source - deploy: false # build only, no deployment + use-pyproject: true + extras: docs # → uv sync --group docs + permissions: + contents: read + pages: write + id-token: write +``` + +The workflow automatically inspects `pyproject.toml` to determine whether +`extras` refers to an optional-dependency extra (`--extra`) or a dependency +group (`--group`) and sets the correct `uv sync` flag. If the name is not found +in either section, a warning is emitted and plain `uv sync` is run. + +### Build only, no deployment + +```yaml +jobs: + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + with: + deploy: false permissions: contents: read pages: write @@ -181,14 +253,60 @@ jobs: ## Dependency Resolution -The workflow resolves dependencies in the following order of precedence: - -1. **`pyproject.toml` at repo root** — `uv sync` is run from the repo root -2. **`pyproject.toml` in `docs-dir`** — `uv sync` is run from `docs-dir` -3. **`pyproject.toml` in `source-dir`** — `uv sync` is run from `source-dir` -4. **No `pyproject.toml`** — `conf.py` is parsed to extract the `extensions` - list; known extensions are mapped to their PyPI package names and installed - via `uv pip install`. `sphinx` and `sphinx-lint` are always included. +Dependencies are resolved in the following order of precedence: + +| Priority | Condition | Action | +| -------- | -------------------------------------------------------- | --------------------------------------------------------------------------------- | +| — | `use-pyproject: true` + `pyproject.toml` in `docs-dir` | Run `uv sync [flags]` from `docs-dir`; skip `conf.py` | +| — | `use-pyproject: true` + `pyproject.toml` in `source-dir` | Run `uv sync [flags]` from `source-dir`; skip `conf.py` | +| — | `use-pyproject: true` + `pyproject.toml` at root | Run `uv sync [flags]` from repo root; skip `conf.py` | +| — | `use-pyproject: true` + no `pyproject.toml` found | **Fails** with an error | +| 1 | `conf.py` found in `source-dir` (default) | Parse `extensions` and `html_theme`; install mapped packages via `uv pip install` | +| 2 | `pyproject.toml` found in `docs-dir` | Run `uv sync [flags]` from `docs-dir` | +| 3 | `pyproject.toml` found in `source-dir` | Run `uv sync [flags]` from `source-dir` | +| 4 | `pyproject.toml` found at repo root | Run `uv sync [flags]` from repo root | +| — | None of the above | **Fails** with an error | + +### `extras` and `uv sync` flags + +When `extras` is set and a `pyproject.toml` is used, the workflow inspects the +file to determine the correct `uv sync` flag: + +| Section in `pyproject.toml` | `uv sync` flag emitted | +| --------------------------------- | ------------------------------------ | +| `[project.optional-dependencies]` | `--extra ` | +| `[dependency-groups]` (PEP 735) | `--group ` | +| Not found in either | _(plain `uv sync`, warning emitted)_ | + +### `conf.py` parsing + +When `conf.py` is used as the dependency source, the workflow extracts packages +from two variables: + +- **`extensions`** — each entry is looked up in a built-in mapping of known + Sphinx extension module names to PyPI package names. Unknown third-party + extensions not in the mapping are included using a best-effort guess derived + from the module root (e.g. `my_ext.sub` → `my-ext`). Built-in `sphinx.ext.*` + extensions are always skipped as they are bundled with Sphinx. +- **`html_theme`** — the theme name is looked up in a built-in mapping of known + theme names to PyPI package names. Built-in Sphinx themes (e.g. `alabaster`, + `classic`) are skipped. Unknown themes are included using a best-effort guess + (e.g. `my_theme` → `my-theme`). + +`sphinx` and `sphinx-lint` are always installed regardless of `conf.py` content. + +### Deployment guards + +The `deploy` input enables deployment intent, but the workflow enforces the +following additional conditions internally to prevent accidental deployments +from forks or pull requests: + +| Condition | Purpose | +| -------------------------------------------------------- | ------------------------------------------------------- | +| `deploy` input is `true` | Caller opt-in | +| `github.ref == 'refs/heads/main'` | Exact ref match — rules out all other branches and tags | +| `github.repository == github.event.repository.full_name` | Upstream repo only — excludes forks | +| `github.event_name != 'pull_request'` | Belt-and-braces guard against PR events | ## Licence From 59a3c6f9a35867c8c347c6ca259ef718429aba95 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:21:21 +0000 Subject: [PATCH 095/165] Fix dependency installation in Sphinx workflow to use Python version from environment --- .github/workflows/sphinx-docs.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 033b734..e6e0438 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -205,8 +205,9 @@ jobs: - name: Install dependencies from conf.py (extensions + html_theme) if: ${{ steps.detect-deps.outputs.source == 'conf' }} + env: + UV_PYTHON: ${{ env.PYTHON_VRSN }} run: | - CONF_FILE="${{ inputs.source-dir }}/conf.py" PACKAGES=$(python3 - <<'EOF' import ast @@ -303,7 +304,7 @@ jobs: print(" ".join(sorted(pkgs))) EOF ) - uv pip install --system $PACKAGES + uv pip install $PACKAGES - name: Install dependencies via uv sync (docs pyproject.toml) if: ${{ steps.detect-deps.outputs.source == 'pyproject-docs' }} From eeee122dfd1f248f718c8c442663395f27dae8f0 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:26:45 +0000 Subject: [PATCH 096/165] Refactor dependency installation in Sphinx workflow to use uv-managed virtual environment --- .github/workflows/sphinx-docs.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index e6e0438..610d492 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -205,8 +205,6 @@ jobs: - name: Install dependencies from conf.py (extensions + html_theme) if: ${{ steps.detect-deps.outputs.source == 'conf' }} - env: - UV_PYTHON: ${{ env.PYTHON_VRSN }} run: | PACKAGES=$(python3 - <<'EOF' import ast @@ -303,8 +301,9 @@ jobs: print(" ".join(sorted(pkgs))) EOF - ) - uv pip install $PACKAGES + # Create a venv using the uv-managed Python and install into it. + uv venv ${{ env.VENV_PATH }} + uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES - name: Install dependencies via uv sync (docs pyproject.toml) if: ${{ steps.detect-deps.outputs.source == 'pyproject-docs' }} From c354ea8ac85db5528bc29ba1d230a7bd37ce65e9 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:30:31 +0000 Subject: [PATCH 097/165] Fix syntax error in Sphinx workflow by closing print statement --- .github/workflows/sphinx-docs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 610d492..20a0a6d 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -301,6 +301,8 @@ jobs: print(" ".join(sorted(pkgs))) EOF + ) + # Create a venv using the uv-managed Python and install into it. uv venv ${{ env.VENV_PATH }} uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES From e065c0d701190fa05662fa7c8cdf909fd9795d3a Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:46:53 +0000 Subject: [PATCH 098/165] Update Sphinx workflow to use virtual environment for linting and building docs --- .github/workflows/sphinx-docs.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 20a0a6d..a11eea7 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -322,7 +322,7 @@ jobs: run: uv sync ${{ steps.uv-flags.outputs.flags }} - name: Lint Sphinx docs - run: uv run sphinx-lint ${{ inputs.source-dir }} + run: ${{ env.VENV_PATH }}/bin/sphinx-lint ${{ inputs.source-dir }} - name: Detect Makefile in docs directory id: detect-makefile @@ -336,12 +336,12 @@ jobs: - name: Build HTML docs (make) if: ${{ steps.detect-makefile.outputs.found == 'true' }} working-directory: ${{ inputs.docs-dir }} - run: uv run make clean html BUILDDIR="${{ inputs.build-dir }}" + run: ${{ env.VENV_PATH }}/bin/make clean html BUILDDIR="${{ inputs.build-dir }}" - name: Build HTML docs (sphinx-build) if: ${{ steps.detect-makefile.outputs.found == 'false' }} run: | - uv run sphinx-build \ + ${{ env.VENV_PATH }}/bin/sphinx-build \ --builder html \ --fresh-env \ --write-all \ From 9fc5fb22829dc9ca052e4295cfd70e021b7069f4 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 17 Mar 2026 23:51:35 +0000 Subject: [PATCH 099/165] Refactor Sphinx workflow to streamline dependency installation and use uv for running commands --- .github/workflows/sphinx-docs.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index a11eea7..25170e4 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -205,6 +205,8 @@ jobs: - name: Install dependencies from conf.py (extensions + html_theme) if: ${{ steps.detect-deps.outputs.source == 'conf' }} + env: + UV_PYTHON: ${{ env.PYTHON_VRSN }} run: | PACKAGES=$(python3 - <<'EOF' import ast @@ -302,10 +304,7 @@ jobs: print(" ".join(sorted(pkgs))) EOF ) - - # Create a venv using the uv-managed Python and install into it. - uv venv ${{ env.VENV_PATH }} - uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES + uv pip install $PACKAGES - name: Install dependencies via uv sync (docs pyproject.toml) if: ${{ steps.detect-deps.outputs.source == 'pyproject-docs' }} @@ -336,7 +335,7 @@ jobs: - name: Build HTML docs (make) if: ${{ steps.detect-makefile.outputs.found == 'true' }} working-directory: ${{ inputs.docs-dir }} - run: ${{ env.VENV_PATH }}/bin/make clean html BUILDDIR="${{ inputs.build-dir }}" + run: uv run make clean html BUILDDIR="${{ inputs.build-dir }}" - name: Build HTML docs (sphinx-build) if: ${{ steps.detect-makefile.outputs.found == 'false' }} From 95eab0bd5ecb5f2a5695cd3ed0cd1cc66eeacd07 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 18 Mar 2026 00:09:46 +0000 Subject: [PATCH 100/165] Enhance Sphinx workflow to create a virtual environment with uv and install dependencies, ensuring compatibility with managed Python --- .github/workflows/sphinx-docs.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 25170e4..c0af1e1 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -304,7 +304,12 @@ jobs: print(" ".join(sorted(pkgs))) EOF ) - uv pip install $PACKAGES + + # Create a venv using the uv-managed Python and install into it. + # This avoids --system which requires a system Python installation + # and is incompatible with the managed Python provided by setup-uv. + uv venv ${{ env.VENV_PATH }} + uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES - name: Install dependencies via uv sync (docs pyproject.toml) if: ${{ steps.detect-deps.outputs.source == 'pyproject-docs' }} @@ -321,7 +326,7 @@ jobs: run: uv sync ${{ steps.uv-flags.outputs.flags }} - name: Lint Sphinx docs - run: ${{ env.VENV_PATH }}/bin/sphinx-lint ${{ inputs.source-dir }} + run: uv run --no-project sphinx-lint ${{ inputs.source-dir }} - name: Detect Makefile in docs directory id: detect-makefile @@ -335,7 +340,7 @@ jobs: - name: Build HTML docs (make) if: ${{ steps.detect-makefile.outputs.found == 'true' }} working-directory: ${{ inputs.docs-dir }} - run: uv run make clean html BUILDDIR="${{ inputs.build-dir }}" + run: uv run --no-project make clean html BUILDDIR="${{ inputs.build-dir }}" - name: Build HTML docs (sphinx-build) if: ${{ steps.detect-makefile.outputs.found == 'false' }} From 8add976f914892547480109ba4200c7c12a6f6f1 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:09:38 +0000 Subject: [PATCH 101/165] Refactor Sphinx workflow to streamline dependency installation and improve handling of pyproject.toml files --- .github/workflows/sphinx-docs.yaml | 58 +++++++++++++++++++----------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index c0af1e1..de1a468 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -298,54 +298,70 @@ jobs: if html_theme is not None: if html_theme not in _BUILTIN_THEMES: theme_pkg = theme_to_pkg.get(html_theme) - pkgs.add(theme_pkg if theme_pkg is not None - else html_theme.replace("_", "-")) + pkgs.add(theme_pkg if theme_pkg is not None else html_theme.replace("_", "-")) print(" ".join(sorted(pkgs))) EOF ) + uv pip install --project . $PACKAGES - # Create a venv using the uv-managed Python and install into it. - # This avoids --system which requires a system Python installation - # and is incompatible with the managed Python provided by setup-uv. - uv venv ${{ env.VENV_PATH }} - uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES + - name: Install dependencies via uv sync (pyproject.toml) + if: ${{ steps.detect-deps.outputs.source != 'conf' }} + run: | + case "${{ steps.detect-deps.outputs.source }}" in + pyproject-docs) + TARGET_FILE="${{ inputs.docs-dir }}/pyproject.toml" + ;; + pyproject-source) + TARGET_FILE="${{ inputs.source-dir }}/pyproject.toml" + ;; + pyproject-root) + TARGET_FILE="pyproject.toml" + ;; + *) + echo "::error::Unexpected dependency source: '${{ steps.detect-deps.outputs.source }}'." + exit 1 + ;; + esac - - name: Install dependencies via uv sync (docs pyproject.toml) - if: ${{ steps.detect-deps.outputs.source == 'pyproject-docs' }} - working-directory: ${{ inputs.docs-dir }} - run: uv sync ${{ steps.uv-flags.outputs.flags }} + FLAGS="${{ steps.uv-flags.outputs.flags }}" + if [ -n "$FLAGS" ]; then + echo "::notice::Installing dependencies from '$TARGET_FILE' with: uv sync $FLAGS" + else + echo "::notice::Installing dependencies from '$TARGET_FILE' with: uv sync" + fi - - name: Install dependencies via uv sync (source pyproject.toml) - if: ${{ steps.detect-deps.outputs.source == 'pyproject-source' }} - working-directory: ${{ inputs.source-dir }} - run: uv sync ${{ steps.uv-flags.outputs.flags }} + # --project points uv at the correct pyproject.toml regardless of + # its location; --venv-dir / UV_PROJECT_ENVIRONMENT pins the venv to + # VENV_PATH at the repo root so it is always predictable. + UV_PROJECT_ENVIRONMENT="${{ env.VENV_PATH }}" uv sync --project "$TARGET_FILE" $FLAGS - - name: Install dependencies via uv sync (root pyproject.toml) - if: ${{ steps.detect-deps.outputs.source == 'pyproject-root' }} - run: uv sync ${{ steps.uv-flags.outputs.flags }} + - name: Add venv to PATH + run: echo "${{ github.workspace }}/${{ env.VENV_PATH }}/bin" >> "$GITHUB_PATH" - name: Lint Sphinx docs - run: uv run --no-project sphinx-lint ${{ inputs.source-dir }} + run: sphinx-lint ${{ inputs.source-dir }} - name: Detect Makefile in docs directory id: detect-makefile run: | if [ -f "${{ inputs.docs-dir }}/Makefile" ]; then echo "found=true" >> "$GITHUB_OUTPUT" + echo "::notice::Makefile detected in '${{ inputs.docs-dir }}'. Will use 'make html' to build docs." else echo "found=false" >> "$GITHUB_OUTPUT" + echo "::notice::No Makefile detected in '${{ inputs.docs-dir }}'. Will use 'sphinx-build' to build docs." fi - name: Build HTML docs (make) if: ${{ steps.detect-makefile.outputs.found == 'true' }} working-directory: ${{ inputs.docs-dir }} - run: uv run --no-project make clean html BUILDDIR="${{ inputs.build-dir }}" + run: make clean html BUILDDIR="${{ inputs.build-dir }}" - name: Build HTML docs (sphinx-build) if: ${{ steps.detect-makefile.outputs.found == 'false' }} run: | - ${{ env.VENV_PATH }}/bin/sphinx-build \ + sphinx-build \ --builder html \ --fresh-env \ --write-all \ From ee5ceacb37888fd5f148aa18370c5c770ee06fe9 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:16:35 +0000 Subject: [PATCH 102/165] Add notices for dependency source detection and resolved uv sync flags in Sphinx workflow --- .github/workflows/sphinx-docs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index de1a468..cb594ff 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -146,6 +146,7 @@ jobs: echo "::error::No conf.py or pyproject.toml found. Cannot resolve dependencies." exit 1 fi + echo "::notice::Dependency source detected: '${{ steps.detect-deps.outputs.source }}'." - name: Resolve uv sync flags id: uv-flags @@ -193,6 +194,7 @@ jobs: EOF ) echo "flags=$FLAGS" >> "$GITHUB_OUTPUT" + echo "::notice::Resolved uv sync flags: '$FLAGS' for extras group '$EXTRAS'." - name: Cache uv venv if: ${{ steps.detect-deps.outputs.source != 'conf' }} From fbf80a4c7752c0b9abeaa19046731371215f3e8a Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:25:15 +0000 Subject: [PATCH 103/165] Add step to create virtual environment and update dependency installation commands in Sphinx workflow --- .github/workflows/sphinx-docs.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index cb594ff..14907b5 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -205,10 +205,11 @@ jobs: restore-keys: | ${{ runner.os }}-${{ env.PYTHON_VRSN }}- + - name: Create virtual environment + run: uv venv --python ${{ env.PYTHON_VRSN }} ${{ env.VENV_PATH }} + - name: Install dependencies from conf.py (extensions + html_theme) if: ${{ steps.detect-deps.outputs.source == 'conf' }} - env: - UV_PYTHON: ${{ env.PYTHON_VRSN }} run: | PACKAGES=$(python3 - <<'EOF' import ast @@ -305,7 +306,7 @@ jobs: print(" ".join(sorted(pkgs))) EOF ) - uv pip install --project . $PACKAGES + uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES - name: Install dependencies via uv sync (pyproject.toml) if: ${{ steps.detect-deps.outputs.source != 'conf' }} From aaf3cc862884da720db3e66a44b29dab128c77d4 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:38:49 +0000 Subject: [PATCH 104/165] Allow existing virtual environment during creation in Sphinx workflow --- .github/workflows/sphinx-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 14907b5..fa7a69f 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -206,7 +206,7 @@ jobs: ${{ runner.os }}-${{ env.PYTHON_VRSN }}- - name: Create virtual environment - run: uv venv --python ${{ env.PYTHON_VRSN }} ${{ env.VENV_PATH }} + run: uv venv --python ${{ env.PYTHON_VRSN }} --allow-existing ${{ env.VENV_PATH }} - name: Install dependencies from conf.py (extensions + html_theme) if: ${{ steps.detect-deps.outputs.source == 'conf' }} From 8290febe4a68a6bbae09b92e2754d399c1e775c8 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:50:12 +0000 Subject: [PATCH 105/165] Refactor dependency source detection in Sphinx workflow to improve clarity and error handling --- .github/workflows/sphinx-docs.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index fa7a69f..3be29de 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -123,30 +123,31 @@ jobs: # to pyproject.toml resolution if [ "${{ inputs.use-pyproject }}" = "true" ]; then if [ -f "${{ inputs.docs-dir }}/pyproject.toml" ]; then - echo "source=pyproject-docs" >> "$GITHUB_OUTPUT" + SOURCE="pyproject-docs" elif [ -f "${{ inputs.source-dir }}/pyproject.toml" ]; then - echo "source=pyproject-source" >> "$GITHUB_OUTPUT" + SOURCE="pyproject-source" elif [ -f "pyproject.toml" ]; then - echo "source=pyproject-root" >> "$GITHUB_OUTPUT" + SOURCE="pyproject-root" else echo "::error::use-pyproject is true but no pyproject.toml found in docs-dir, source-dir, or repo root." exit 1 fi - # Default auto-detection: conf.py has highest priority elif [ -f "${{ inputs.source-dir }}/conf.py" ]; then - echo "source=conf" >> "$GITHUB_OUTPUT" + SOURCE="conf" elif [ -f "${{ inputs.docs-dir }}/pyproject.toml" ]; then - echo "source=pyproject-docs" >> "$GITHUB_OUTPUT" + SOURCE="pyproject-docs" elif [ -f "${{ inputs.source-dir }}/pyproject.toml" ]; then - echo "source=pyproject-source" >> "$GITHUB_OUTPUT" + SOURCE="pyproject-source" elif [ -f "pyproject.toml" ]; then - echo "source=pyproject-root" >> "$GITHUB_OUTPUT" + SOURCE="pyproject-root" else echo "::error::No conf.py or pyproject.toml found. Cannot resolve dependencies." exit 1 fi - echo "::notice::Dependency source detected: '${{ steps.detect-deps.outputs.source }}'." + + echo "source=$SOURCE" >> "$GITHUB_OUTPUT" + echo "::notice::Dependency source detected: '$SOURCE'." - name: Resolve uv sync flags id: uv-flags From a66f52c0d0e07a444be316fa9a13c60f50883429 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:21:19 +0000 Subject: [PATCH 106/165] Add job summary step --- .github/workflows/sphinx-docs.yaml | 70 ++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 3be29de..2c441d5 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -147,7 +147,7 @@ jobs: fi echo "source=$SOURCE" >> "$GITHUB_OUTPUT" - echo "::notice::Dependency source detected: '$SOURCE'." + echo "Dependency source detected: '$SOURCE'." - name: Resolve uv sync flags id: uv-flags @@ -351,10 +351,10 @@ jobs: run: | if [ -f "${{ inputs.docs-dir }}/Makefile" ]; then echo "found=true" >> "$GITHUB_OUTPUT" - echo "::notice::Makefile detected in '${{ inputs.docs-dir }}'. Will use 'make html' to build docs." + echo "Makefile detected in '${{ inputs.docs-dir }}'. Will use 'make html' to build docs." else echo "found=false" >> "$GITHUB_OUTPUT" - echo "::notice::No Makefile detected in '${{ inputs.docs-dir }}'. Will use 'sphinx-build' to build docs." + echo "No Makefile detected in '${{ inputs.docs-dir }}'. Will use 'sphinx-build' to build docs." fi - name: Build HTML docs (make) @@ -393,3 +393,67 @@ jobs: id: deployment if: ${{ env.DO_DEPLOY == 'true' }} uses: actions/deploy-pages@v4 + + - name: Job summary + if: always() + run: | + STATUS="${{ job.status }}" + DEP_SOURCE="${{ steps.detect-deps.outputs.source }}" + MAKEFILE="${{ steps.detect-makefile.outputs.found }}" + PAGES_URL="${{ steps.deployment.outputs.page_url }}" + + # Status icon + case "$STATUS" in + success) ICON="✅" ;; + failure) ICON="❌" ;; + cancelled) ICON="⚠️" ;; + *) ICON="❓" ;; + esac + + # Dependency source label + case "$DEP_SOURCE" in + conf) DEP_LABEL="conf.py (extensions + html_theme)" ;; + pyproject-docs) DEP_LABEL="pyproject.toml (${{ inputs.docs-dir }})" ;; + pyproject-source) DEP_LABEL="pyproject.toml (${{ inputs.source-dir }})" ;; + pyproject-root) DEP_LABEL="pyproject.toml (repo root)" ;; + *) DEP_LABEL="unknown" ;; + esac + + # Build method label + if [ "$MAKEFILE" = "true" ]; then + BUILD_METHOD="make html" + else + BUILD_METHOD="sphinx-build" + fi + + # uv sync flags (empty when using conf.py) + FLAGS="${{ steps.uv-flags.outputs.flags }}" + if [ -z "$FLAGS" ]; then + FLAGS="_(none)_" + fi + + cat >> "$GITHUB_STEP_SUMMARY" <> "$GITHUB_STEP_SUMMARY" < Date: Wed, 18 Mar 2026 14:19:02 +0000 Subject: [PATCH 107/165] Text style in log summary --- .github/workflows/sphinx-docs.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 2c441d5..0759321 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -330,9 +330,9 @@ jobs: FLAGS="${{ steps.uv-flags.outputs.flags }}" if [ -n "$FLAGS" ]; then - echo "::notice::Installing dependencies from '$TARGET_FILE' with: uv sync $FLAGS" + echo "Installing dependencies from '$TARGET_FILE' with: uv sync $FLAGS" else - echo "::notice::Installing dependencies from '$TARGET_FILE' with: uv sync" + echo "Installing dependencies from '$TARGET_FILE' with: uv sync" fi # --project points uv at the correct pyproject.toml regardless of @@ -412,10 +412,10 @@ jobs: # Dependency source label case "$DEP_SOURCE" in - conf) DEP_LABEL="conf.py (extensions + html_theme)" ;; - pyproject-docs) DEP_LABEL="pyproject.toml (${{ inputs.docs-dir }})" ;; - pyproject-source) DEP_LABEL="pyproject.toml (${{ inputs.source-dir }})" ;; - pyproject-root) DEP_LABEL="pyproject.toml (repo root)" ;; + conf) DEP_LABEL="\`conf.py\` (extensions + html_theme)" ;; + pyproject-docs) DEP_LABEL="\`pyproject.toml\` (${{ inputs.docs-dir }})" ;; + pyproject-source) DEP_LABEL="\`pyproject.toml\` (${{ inputs.source-dir }})" ;; + pyproject-root) DEP_LABEL="\`pyproject.toml\` (repo root)" ;; *) DEP_LABEL="unknown" ;; esac From d64af417ff7f605bc0c81996278529d677632883 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:57:57 +0000 Subject: [PATCH 108/165] Set default options for sphinx-build based on review --- .github/workflows/sphinx-docs.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 0759321..18feb62 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -29,6 +29,11 @@ on: required: false type: number default: 5 + sphinx-options: + description: 'Additional options to pass to sphinx-build when no Makefile is found' + required: false + type: string + default: '--jobs auto --write-all --fresh-env --nitpicky --fail-on-warnings --keep-going' deploy: description: 'Whether to deploy to GitHub Pages' required: false @@ -360,15 +365,17 @@ jobs: - name: Build HTML docs (make) if: ${{ steps.detect-makefile.outputs.found == 'true' }} working-directory: ${{ inputs.docs-dir }} - run: make clean html BUILDDIR="${{ inputs.build-dir }}" + run: | + make clean html \ + SPHINXOPTS="${{ inputs.sphinx-options }}" \ + BUILDDIR="${{ inputs.build-dir }}" - name: Build HTML docs (sphinx-build) if: ${{ steps.detect-makefile.outputs.found == 'false' }} run: | sphinx-build \ --builder html \ - --fresh-env \ - --write-all \ + ${{ inputs.sphinx-options }} \ "${{ inputs.source-dir }}" \ "${{ inputs.docs-dir }}/${{ inputs.build-dir }}/html" From ef6d552e8d4bda75eed4e557be23b58f23c99879 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:09:30 +0000 Subject: [PATCH 109/165] Update README --- .github/workflows/sphinx-docs.yaml | 2 +- sphinx-docs/README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 18feb62..a52e517 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -30,7 +30,7 @@ on: type: number default: 5 sphinx-options: - description: 'Additional options to pass to sphinx-build when no Makefile is found' + description: 'Additional options to pass to sphinx-build or make (via SPHINXOPTS)' required: false type: string default: '--jobs auto --write-all --fresh-env --nitpicky --fail-on-warnings --keep-going' diff --git a/sphinx-docs/README.md b/sphinx-docs/README.md index 86abe0e..d4b29bf 100644 --- a/sphinx-docs/README.md +++ b/sphinx-docs/README.md @@ -72,6 +72,7 @@ jobs: | `venv-path` | Virtual environment path | No | `.venv` | | `runner` | GitHub Actions runner | No | `ubuntu-24.04` | | `timeout-minutes` | Job timeout in minutes | No | `5` | +| `sphinx-options` | Additional options passed to `sphinx-build` or `make` via `SPHINXOPTS` | No | `--jobs auto --write-all --fresh-env --nitpicky --fail-on-warnings --keep-going` | | `deploy` | Whether to deploy to GitHub Pages | No | `true` | | `docs-dir` | Path to the docs directory where `Makefile` lives | No | `docs` | | `source-dir` | Path to the Sphinx source directory where `conf.py` lives | No | `docs/source` | From dcb50e9290b2fe5fc37e609b4816c3dce5bcd8ba Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:37:21 +0000 Subject: [PATCH 110/165] Ability to show full traceback on exception, when debug logging is on --- .github/workflows/sphinx-docs.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index a52e517..5af0098 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -102,6 +102,7 @@ jobs: env: PYTHON_VRSN: ${{ inputs.python-version }} VENV_PATH: ${{ inputs.venv-path }} + SPHINX_DEBUG: ${{ runner.debug == '1' && '-v --show-traceback' || '' }} # Deploy only when: caller opted in AND we are on the upstream main branch # (not a fork's main, not a PR, not any other branch) DO_DEPLOY: >- @@ -367,7 +368,7 @@ jobs: working-directory: ${{ inputs.docs-dir }} run: | make clean html \ - SPHINXOPTS="${{ inputs.sphinx-options }}" \ + SPHINXOPTS="${{ inputs.sphinx-options }} ${{ env.SPHINX_DEBUG }}" \ BUILDDIR="${{ inputs.build-dir }}" - name: Build HTML docs (sphinx-build) @@ -376,6 +377,7 @@ jobs: sphinx-build \ --builder html \ ${{ inputs.sphinx-options }} \ + ${{ env.SPHINX_DEBUG }} \ "${{ inputs.source-dir }}" \ "${{ inputs.docs-dir }}/${{ inputs.build-dir }}/html" From b814690e477a516fa5046b6f5947d4a3679c8a85 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 23 Mar 2026 22:48:14 +0000 Subject: [PATCH 111/165] UPDATE README --- .github/workflows/sphinx-docs.yaml | 2 +- sphinx-docs/README.md | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 5af0098..1f3497b 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -28,7 +28,7 @@ on: description: 'Job timeout in minutes' required: false type: number - default: 5 + default: 10 sphinx-options: description: 'Additional options to pass to sphinx-build or make (via SPHINXOPTS)' required: false diff --git a/sphinx-docs/README.md b/sphinx-docs/README.md index d4b29bf..aaa68e6 100644 --- a/sphinx-docs/README.md +++ b/sphinx-docs/README.md @@ -17,6 +17,9 @@ deploys it to GitHub Pages. [`sphinx-lint`](https://github.com/sphinx-contrib/sphinx-lint) - Uses `make html` if a `Makefile` is present in the docs directory, otherwise falls back to `sphinx-build` +- Automatically appends `-v --show-traceback` to Sphinx invocations when the + [GitHub Actions runner debug mode](https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging) + is active (`ACTIONS_RUNNER_DEBUG=true`) - Deploys to GitHub Pages **only on pushes to the upstream `main` branch** — never from forks or pull requests @@ -71,12 +74,12 @@ jobs: | `python-version` | Python version to use | No | `3.12` | | `venv-path` | Virtual environment path | No | `.venv` | | `runner` | GitHub Actions runner | No | `ubuntu-24.04` | -| `timeout-minutes` | Job timeout in minutes | No | `5` | +| `timeout-minutes` | Job timeout in minutes | No | `10` | | `sphinx-options` | Additional options passed to `sphinx-build` or `make` via `SPHINXOPTS` | No | `--jobs auto --write-all --fresh-env --nitpicky --fail-on-warnings --keep-going` | | `deploy` | Whether to deploy to GitHub Pages | No | `true` | | `docs-dir` | Path to the docs directory where `Makefile` lives | No | `docs` | | `source-dir` | Path to the Sphinx source directory where `conf.py` lives | No | `docs/source` | -| `build-dir` | Sphinx build output directory (relative to `docs-dir`) | No | `_build` | +| `build-dir` | Sphinx build output directory — relative to `docs-dir` when using `make`, or relative to repo root when using `sphinx-build` | No | `_build` | | `use-pyproject` | Force `pyproject.toml` resolution, skipping `conf.py` entirely | No | `false` | | `extras` | Optional dependency extra or group name to pass to `uv sync` (see [Dependency Resolution](#dependency-resolution)) | No | `''` | From 7ad7214984660dcc30b07e13f9176601f5526997 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:02:45 +0000 Subject: [PATCH 112/165] Fix runner context --- .github/workflows/sphinx-docs.yaml | 6 +++++- sphinx-docs/README.md | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 1f3497b..97303528 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -102,7 +102,7 @@ jobs: env: PYTHON_VRSN: ${{ inputs.python-version }} VENV_PATH: ${{ inputs.venv-path }} - SPHINX_DEBUG: ${{ runner.debug == '1' && '-v --show-traceback' || '' }} + SPHINX_DEBUG: '' # Deploy only when: caller opted in AND we are on the upstream main branch # (not a fork's main, not a PR, not any other branch) DO_DEPLOY: >- @@ -114,6 +114,10 @@ jobs: }} steps: + - name: Set Sphinx Debug Options + if: ${{ runner.debug == '1' }} + run: echo "SPHINX_DEBUG=-v --show-traceback" >> "$GITHUB_ENV" + - name: Checkout repository uses: actions/checkout@v6 diff --git a/sphinx-docs/README.md b/sphinx-docs/README.md index aaa68e6..bc156ef 100644 --- a/sphinx-docs/README.md +++ b/sphinx-docs/README.md @@ -79,7 +79,7 @@ jobs: | `deploy` | Whether to deploy to GitHub Pages | No | `true` | | `docs-dir` | Path to the docs directory where `Makefile` lives | No | `docs` | | `source-dir` | Path to the Sphinx source directory where `conf.py` lives | No | `docs/source` | -| `build-dir` | Sphinx build output directory — relative to `docs-dir` when using `make`, or relative to repo root when using `sphinx-build` | No | `_build` | +| `build-dir` | Sphinx build output directory (relative to `docs-dir`) | No | `_build` | | `use-pyproject` | Force `pyproject.toml` resolution, skipping `conf.py` entirely | No | `false` | | `extras` | Optional dependency extra or group name to pass to `uv sync` (see [Dependency Resolution](#dependency-resolution)) | No | `''` | From 14c792897d1d385d3f931611638b947ade2676c8 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 23 Mar 2026 23:06:15 +0000 Subject: [PATCH 113/165] Fix typo --- .github/workflows/sphinx-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 97303528..9b2a3cd 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -33,7 +33,7 @@ on: description: 'Additional options to pass to sphinx-build or make (via SPHINXOPTS)' required: false type: string - default: '--jobs auto --write-all --fresh-env --nitpicky --fail-on-warnings --keep-going' + default: '--jobs auto --write-all --fresh-env --nitpicky --fail-on-warning --keep-going' deploy: description: 'Whether to deploy to GitHub Pages' required: false From df5b0f9a00da858b4b753653ae79f55b3e6958a0 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 30 Mar 2026 16:47:51 +0100 Subject: [PATCH 114/165] Rename variable for clarity --- .github/workflows/sphinx-docs.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 9b2a3cd..10a2967 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -360,15 +360,15 @@ jobs: id: detect-makefile run: | if [ -f "${{ inputs.docs-dir }}/Makefile" ]; then - echo "found=true" >> "$GITHUB_OUTPUT" + echo "found_makefile=true" >> "$GITHUB_OUTPUT" echo "Makefile detected in '${{ inputs.docs-dir }}'. Will use 'make html' to build docs." else - echo "found=false" >> "$GITHUB_OUTPUT" + echo "found_makefile=false" >> "$GITHUB_OUTPUT" echo "No Makefile detected in '${{ inputs.docs-dir }}'. Will use 'sphinx-build' to build docs." fi - name: Build HTML docs (make) - if: ${{ steps.detect-makefile.outputs.found == 'true' }} + if: steps.detect-makefile.outputs.found_makefile working-directory: ${{ inputs.docs-dir }} run: | make clean html \ @@ -376,7 +376,7 @@ jobs: BUILDDIR="${{ inputs.build-dir }}" - name: Build HTML docs (sphinx-build) - if: ${{ steps.detect-makefile.outputs.found == 'false' }} + if: steps.detect-makefile.outputs.found_makefile == 'false' run: | sphinx-build \ --builder html \ From 77b0e37169828b6724d78fd774bd43e3f6fd9f18 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 30 Mar 2026 23:56:38 +0100 Subject: [PATCH 115/165] Use yq to parse toml, and make the python package resolution more concise --- .github/workflows/sphinx-docs.yaml | 165 +++++++---------------------- 1 file changed, 41 insertions(+), 124 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 10a2967..c9b8f2b 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -161,15 +161,13 @@ jobs: - name: Resolve uv sync flags id: uv-flags - if: ${{ steps.detect-deps.outputs.source != 'conf' }} + if: steps.detect-deps.outputs.source != 'conf' run: | # Determine whether `extras` refers to an optional-dependency extra # or a dependency-group, then set the appropriate uv sync flag. + EXTRAS="${{ inputs.extras }}" - if [ -z "$EXTRAS" ]; then - echo "flags=" >> "$GITHUB_OUTPUT" - exit 0 - fi + [ -z "$EXTRAS" ] && { echo "flags=" >> "$GITHUB_OUTPUT"; exit 0; } # Locate the relevant pyproject.toml case "${{ steps.detect-deps.outputs.source }}" in @@ -178,37 +176,21 @@ jobs: *) PYPROJECT="pyproject.toml" ;; esac - # Inspect pyproject.toml to determine the correct flag - FLAGS=$(python3 - <> "$GITHUB_OUTPUT" echo "::notice::Resolved uv sync flags: '$FLAGS' for extras group '$EXTRAS'." - name: Cache uv venv - if: ${{ steps.detect-deps.outputs.source != 'conf' }} + if: steps.detect-deps.outputs.source != 'conf' uses: actions/cache@v5 with: path: ${{ env.VENV_PATH }} @@ -220,99 +202,34 @@ jobs: run: uv venv --python ${{ env.PYTHON_VRSN }} --allow-existing ${{ env.VENV_PATH }} - name: Install dependencies from conf.py (extensions + html_theme) - if: ${{ steps.detect-deps.outputs.source == 'conf' }} + if: steps.detect-deps.outputs.source == 'conf' run: | PACKAGES=$(python3 - <<'EOF' import ast - conf_path = "${{ inputs.source-dir }}/conf.py" - with open(conf_path) as f: - source = f.read() - - tree = ast.parse(source) - extensions = [] - html_theme = None - - for node in ast.walk(tree): - if isinstance(node, ast.Assign): - for target in node.targets: - if not isinstance(target, ast.Name): - continue - if target.id == "extensions": - if isinstance(node.value, (ast.List, ast.Tuple)): - for elt in node.value.elts: - if isinstance(elt, ast.Constant): - extensions.append(elt.value) - if target.id == "html_theme": - if isinstance(node.value, ast.Constant): - html_theme = node.value.value - - ext_to_pkg = { - # -- sphinxcontrib third-party (not bundled with Sphinx) - "sphinxcontrib.bibtex": "sphinxcontrib-bibtex", - "sphinxcontrib.httpdomain": "sphinxcontrib-httpdomain", - "sphinxcontrib.plantuml": "sphinxcontrib-plantuml", - "sphinxcontrib.mermaid": "sphinxcontrib-mermaid", - "sphinxcontrib.napoleon": "sphinxcontrib-napoleon", - "sphinxcontrib.rsvgconverter": "sphinxcontrib-rsvgconverter", - # -- MyST / Jupyter - "myst_parser": "myst-parser", - "myst_nb": "myst-nb", - "nbsphinx": "nbsphinx", - # -- Themes (also registered as extensions in some setups) - "sphinx_rtd_theme": "sphinx-rtd-theme", - "sphinx_book_theme": "sphinx-book-theme", - "pydata_sphinx_theme": "pydata-sphinx-theme", - "furo": "furo", - # -- Autodoc / API - "sphinx_autodoc_typehints": "sphinx-autodoc-typehints", - "autoapi.extension": "sphinx-autoapi", - "sphinx_autoapi": "sphinx-autoapi", - "breathe": "breathe", - "exhale": "exhale", - # -- UI / UX - "sphinx_copybutton": "sphinx-copybutton", - "sphinx_design": "sphinx-design", - "sphinx_tabs": "sphinx-tabs", - "sphinx_panels": "sphinx-panels", - "sphinx_togglebutton": "sphinx-togglebutton", - # -- CLI / args - "sphinx_click": "sphinx-click", - "sphinxarg.ext": "sphinx-argparse", - "sphinx_argparse_cli": "sphinx-argparse-cli", - # -- Misc - "sphinx_sitemap": "sphinx-sitemap", - } - - # Bundled Sphinx themes — no install needed - _BUILTIN_THEMES = { - "alabaster", "classic", "nature", "bizstyle", - "pyramid", "scrolls", "agogo", "traditional", "haiku", - } - - theme_to_pkg = { - "sphinx_rtd_theme": "sphinx-rtd-theme", - "sphinx_book_theme": "sphinx-book-theme", - "pydata_sphinx_theme": "pydata-sphinx-theme", - "furo": "furo", - } + with open("${{ inputs.source-dir }}/conf.py") as f: + tree = ast.parse(f.read()) + + exts, theme = [], None + for n in [n for n in ast.walk(tree) if isinstance(n, ast.Assign)]: + ids = [t.id for t in n.targets if isinstance(t, ast.Name)] + if "extensions" in ids and isinstance(n.value, (ast.List, ast.Tuple)): + exts = [e.value for e in n.value.elts if isinstance(e, ast.Constant)] + if "html_theme" in ids and isinstance(n.value, ast.Constant): + theme = n.value.value + + # Map only extensions for which package name cannot be guessed correctly from the extension name or theme name + MAP = {"autoapi.extension": "sphinx-autoapi", "sphinxarg.ext": "sphinx-argparse", "sphinxgallery": "sphinx-gallery",} + BUILTIN = {"alabaster", "classic", "nature", "bizstyle", "pyramid", "scrolls", "agogo", "traditional", "haiku"} pkgs = {"sphinx", "sphinx-lint"} - for ext in extensions: - if ext.startswith("sphinx.ext."): - continue - pkg = ext_to_pkg.get(ext) - if pkg is not None: - pkgs.add(pkg) - elif ext not in ext_to_pkg: - pkg_guess = ext.split(".")[0].replace("_", "-") - pkgs.add(pkg_guess) - - if html_theme is not None: - if html_theme not in _BUILTIN_THEMES: - theme_pkg = theme_to_pkg.get(html_theme) - pkgs.add(theme_pkg if theme_pkg is not None else html_theme.replace("_", "-")) + for e in [x for x in exts if not x.startswith("sphinx.ext.")]: + # Try the map first, then fall back to standard naming convention + pkgs.add(MAP.get(e, e.replace("_", "-").replace(".", "-"))) + + if theme and theme not in BUILTIN: + pkgs.add(MAP.get(theme, theme.replace("_", "-"))) print(" ".join(sorted(pkgs))) EOF @@ -320,7 +237,7 @@ jobs: uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES - name: Install dependencies via uv sync (pyproject.toml) - if: ${{ steps.detect-deps.outputs.source != 'conf' }} + if: steps.detect-deps.outputs.source != 'conf' run: | case "${{ steps.detect-deps.outputs.source }}" in pyproject-docs) @@ -368,7 +285,7 @@ jobs: fi - name: Build HTML docs (make) - if: steps.detect-makefile.outputs.found_makefile + if: steps.detect-makefile.outputs.found_makefile == 'true' working-directory: ${{ inputs.docs-dir }} run: | make clean html \ @@ -386,16 +303,16 @@ jobs: "${{ inputs.docs-dir }}/${{ inputs.build-dir }}/html" - name: Minimize uv cache - if: ${{ steps.detect-deps.outputs.source != 'conf' }} + if: steps.detect-deps.outputs.source != 'conf' run: uv cache prune --ci # -- Deploy to GitHub Pages only on push to upstream main - name: Setup GitHub Pages - if: ${{ env.DO_DEPLOY == 'true' }} + if: env.DO_DEPLOY == 'true' uses: actions/configure-pages@v5 - name: Upload artifact to GitHub Pages - if: ${{ env.DO_DEPLOY == 'true' }} + if: env.DO_DEPLOY == 'true' uses: actions/upload-pages-artifact@v4 with: name: github-pages @@ -404,7 +321,7 @@ jobs: - name: Deploy to GitHub Pages id: deployment - if: ${{ env.DO_DEPLOY == 'true' }} + if: env.DO_DEPLOY == 'true' uses: actions/deploy-pages@v4 - name: Job summary From 1f5eaffc509fac74941515a6d7f59763ebc8fe9e Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:25:52 +0100 Subject: [PATCH 116/165] Refactor yq command to check for optional dependencies and improve warning message --- .github/workflows/sphinx-docs.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index c9b8f2b..f006abb 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -177,13 +177,10 @@ jobs: esac # Use yq to check for the extra/group and set the flag - FLAGS=$(yq " - if .project.optional-dependencies | has(\"$EXTRAS\") then \"--extra $EXTRAS\" - elif .dependency-groups | has(\"$EXTRAS\") then \"--group $EXTRAS\" - else \"\" end" "$PYPROJECT") + FLAGS=$(yq "if .project.optional-dependencies.[\"$EXTRAS\"] then \"--extra $EXTRAS\" elif .dependency-groups.[\"$EXTRAS\"] then \"--group $EXTRAS\" else \"\" end" "$PYPROJECT") if [ -z "$FLAGS" ]; then - echo "::warning::'$EXTRAS' not found in pyproject.toml. Running plain uv sync." + echo "::warning::'$EXTRAS' not found in $PYPROJECT. Running plain uv sync." fi echo "flags=$FLAGS" >> "$GITHUB_OUTPUT" From 93de45940d074d31c4a239212ea868f5ea7f261d Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:30:29 +0100 Subject: [PATCH 117/165] Refactor yq command to improve flag resolution --- .github/workflows/sphinx-docs.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index f006abb..cc593d6 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -177,14 +177,14 @@ jobs: esac # Use yq to check for the extra/group and set the flag - FLAGS=$(yq "if .project.optional-dependencies.[\"$EXTRAS\"] then \"--extra $EXTRAS\" elif .dependency-groups.[\"$EXTRAS\"] then \"--group $EXTRAS\" else \"\" end" "$PYPROJECT") - - if [ -z "$FLAGS" ]; then - echo "::warning::'$EXTRAS' not found in $PYPROJECT. Running plain uv sync." - fi + FLAGS=$(yq " + (select(.project.optional-dependencies.[\"$EXTRAS\"]) | \"--extra $EXTRAS\") // + (select(.dependency-groups.[\"$EXTRAS\"]) | \"--group $EXTRAS\") // + \"\" + " "$PYPROJECT") echo "flags=$FLAGS" >> "$GITHUB_OUTPUT" - echo "::notice::Resolved uv sync flags: '$FLAGS' for extras group '$EXTRAS'." + [ -z "$FLAGS" ] && echo "::warning::'$EXTRAS' not found in $PYPROJECT." || echo "::notice::Resolved flags: '$FLAGS'" - name: Cache uv venv if: steps.detect-deps.outputs.source != 'conf' From ed6099ca5985b098205b47afdef55c482009cd18 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 10:42:40 +0100 Subject: [PATCH 118/165] Avoid code injection via template expansion --- .github/workflows/sphinx-docs.yaml | 130 ++++++++++++++++------------- sphinx-docs/README.md | 7 ++ 2 files changed, 80 insertions(+), 57 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index cc593d6..113dc40 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -90,8 +90,8 @@ on: permissions: contents: read - pages: write - id-token: write + pages: write # Required for deploying to GitHub Pages + id-token: write # Required for authentication in uv sync when using pyproject.toml jobs: build-and-deploy: @@ -103,6 +103,11 @@ jobs: PYTHON_VRSN: ${{ inputs.python-version }} VENV_PATH: ${{ inputs.venv-path }} SPHINX_DEBUG: '' + USE_PYPROJECT: ${{ inputs.use-pyproject }} + DOCS_DIR: ${{ inputs.docs-dir }} + SOURCE_DIR: ${{ inputs.source-dir }} + BUILD_DIR: ${{ inputs.build-dir }} + SPHINX_OPTIONS: ${{ inputs.sphinx-options }} # Deploy only when: caller opted in AND we are on the upstream main branch # (not a fork's main, not a PR, not any other branch) DO_DEPLOY: >- @@ -119,10 +124,12 @@ jobs: run: echo "SPHINX_DEBUG=-v --show-traceback" >> "$GITHUB_ENV" - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Setup uv with Python ${{ env.PYTHON_VRSN }} - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ env.PYTHON_VRSN }} @@ -131,10 +138,10 @@ jobs: run: | # When use-pyproject is true, skip conf.py entirely and go straight # to pyproject.toml resolution - if [ "${{ inputs.use-pyproject }}" = "true" ]; then - if [ -f "${{ inputs.docs-dir }}/pyproject.toml" ]; then + if [ "${USE_PYPROJECT}" = "true" ]; then + if [ -f "${DOCS_DIR}/pyproject.toml" ]; then SOURCE="pyproject-docs" - elif [ -f "${{ inputs.source-dir }}/pyproject.toml" ]; then + elif [ -f "${SOURCE_DIR}/pyproject.toml" ]; then SOURCE="pyproject-source" elif [ -f "pyproject.toml" ]; then SOURCE="pyproject-root" @@ -143,11 +150,11 @@ jobs: exit 1 fi # Default auto-detection: conf.py has highest priority - elif [ -f "${{ inputs.source-dir }}/conf.py" ]; then + elif [ -f "${SOURCE_DIR}/conf.py" ]; then SOURCE="conf" - elif [ -f "${{ inputs.docs-dir }}/pyproject.toml" ]; then + elif [ -f "${DOCS_DIR}/pyproject.toml" ]; then SOURCE="pyproject-docs" - elif [ -f "${{ inputs.source-dir }}/pyproject.toml" ]; then + elif [ -f "${SOURCE_DIR}/pyproject.toml" ]; then SOURCE="pyproject-source" elif [ -f "pyproject.toml" ]; then SOURCE="pyproject-root" @@ -162,17 +169,20 @@ jobs: - name: Resolve uv sync flags id: uv-flags if: steps.detect-deps.outputs.source != 'conf' + env: + DETECTED_SOURCE: ${{ steps.detect-deps.outputs.source }} + EXTRA_DEPS: ${{ inputs.extras }} run: | # Determine whether `extras` refers to an optional-dependency extra # or a dependency-group, then set the appropriate uv sync flag. - EXTRAS="${{ inputs.extras }}" + EXTRAS="$EXTRA_DEPS" [ -z "$EXTRAS" ] && { echo "flags=" >> "$GITHUB_OUTPUT"; exit 0; } # Locate the relevant pyproject.toml - case "${{ steps.detect-deps.outputs.source }}" in - pyproject-docs) PYPROJECT="${{ inputs.docs-dir }}/pyproject.toml" ;; - pyproject-source) PYPROJECT="${{ inputs.source-dir }}/pyproject.toml" ;; + case "${DETECTED_SOURCE}" in + pyproject-docs) PYPROJECT="${DOCS_DIR}/pyproject.toml" ;; + pyproject-source) PYPROJECT="${SOURCE_DIR}/pyproject.toml" ;; *) PYPROJECT="pyproject.toml" ;; esac @@ -188,7 +198,7 @@ jobs: - name: Cache uv venv if: steps.detect-deps.outputs.source != 'conf' - uses: actions/cache@v5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.VENV_PATH }} key: ${{ runner.os }}-${{ env.PYTHON_VRSN }}-${{ hashFiles('pyproject.toml', format('{0}/pyproject.toml', inputs.docs-dir), format('{0}/pyproject.toml', inputs.source-dir)) }} @@ -196,15 +206,18 @@ jobs: ${{ runner.os }}-${{ env.PYTHON_VRSN }}- - name: Create virtual environment - run: uv venv --python ${{ env.PYTHON_VRSN }} --allow-existing ${{ env.VENV_PATH }} + run: uv venv --python ${PYTHON_VRSN} --allow-existing ${VENV_PATH} - name: Install dependencies from conf.py (extensions + html_theme) if: steps.detect-deps.outputs.source == 'conf' run: | PACKAGES=$(python3 - <<'EOF' - import ast + import ast, os + + source_dir = os.environ["SOURCE_DIR"] + conf_path = os.path.join(source_dir, "conf.py") - with open("${{ inputs.source-dir }}/conf.py") as f: + with open(conf_path) as f: tree = ast.parse(f.read()) exts, theme = [], None @@ -231,30 +244,32 @@ jobs: print(" ".join(sorted(pkgs))) EOF ) - uv pip install --python ${{ env.VENV_PATH }}/bin/python $PACKAGES + uv pip install --python ${VENV_PATH}/bin/python $PACKAGES - name: Install dependencies via uv sync (pyproject.toml) if: steps.detect-deps.outputs.source != 'conf' + env: + DETECTED_SOURCE: ${{ steps.detect-deps.outputs.source }} + UV_FLAGS: ${{ steps.uv-flags.outputs.flags }} run: | - case "${{ steps.detect-deps.outputs.source }}" in + case "${DETECTED_SOURCE}" in pyproject-docs) - TARGET_FILE="${{ inputs.docs-dir }}/pyproject.toml" + TARGET_FILE="${DOCS_DIR}/pyproject.toml" ;; pyproject-source) - TARGET_FILE="${{ inputs.source-dir }}/pyproject.toml" + TARGET_FILE="${SOURCE_DIR}/pyproject.toml" ;; pyproject-root) TARGET_FILE="pyproject.toml" ;; *) - echo "::error::Unexpected dependency source: '${{ steps.detect-deps.outputs.source }}'." + echo "::error::Unexpected dependency source: '${DETECTED_SOURCE}'." exit 1 ;; esac - FLAGS="${{ steps.uv-flags.outputs.flags }}" - if [ -n "$FLAGS" ]; then - echo "Installing dependencies from '$TARGET_FILE' with: uv sync $FLAGS" + if [ -n "$UV_FLAGS" ]; then + echo "Installing dependencies from '$TARGET_FILE' with: uv sync $UV_FLAGS" else echo "Installing dependencies from '$TARGET_FILE' with: uv sync" fi @@ -262,23 +277,23 @@ jobs: # --project points uv at the correct pyproject.toml regardless of # its location; --venv-dir / UV_PROJECT_ENVIRONMENT pins the venv to # VENV_PATH at the repo root so it is always predictable. - UV_PROJECT_ENVIRONMENT="${{ env.VENV_PATH }}" uv sync --project "$TARGET_FILE" $FLAGS + UV_PROJECT_ENVIRONMENT="${VENV_PATH}" uv sync --project "$TARGET_FILE" $UV_FLAGS - name: Add venv to PATH - run: echo "${{ github.workspace }}/${{ env.VENV_PATH }}/bin" >> "$GITHUB_PATH" + run: echo "${GITHUB_WORKSPACE}/${VENV_PATH}/bin" >> "$GITHUB_PATH" - name: Lint Sphinx docs - run: sphinx-lint ${{ inputs.source-dir }} + run: sphinx-lint ${SOURCE_DIR} - name: Detect Makefile in docs directory id: detect-makefile run: | - if [ -f "${{ inputs.docs-dir }}/Makefile" ]; then + if [ -f "${DOCS_DIR}/Makefile" ]; then echo "found_makefile=true" >> "$GITHUB_OUTPUT" - echo "Makefile detected in '${{ inputs.docs-dir }}'. Will use 'make html' to build docs." + echo "Makefile detected in '${DOCS_DIR}'. Will use 'make html' to build docs." else echo "found_makefile=false" >> "$GITHUB_OUTPUT" - echo "No Makefile detected in '${{ inputs.docs-dir }}'. Will use 'sphinx-build' to build docs." + echo "No Makefile detected in '${DOCS_DIR}'. Will use 'sphinx-build' to build docs." fi - name: Build HTML docs (make) @@ -286,18 +301,18 @@ jobs: working-directory: ${{ inputs.docs-dir }} run: | make clean html \ - SPHINXOPTS="${{ inputs.sphinx-options }} ${{ env.SPHINX_DEBUG }}" \ - BUILDDIR="${{ inputs.build-dir }}" + SPHINXOPTS="${SPHINX_OPTIONS} ${SPHINX_DEBUG}" \ + BUILDDIR="${BUILD_DIR}" - name: Build HTML docs (sphinx-build) if: steps.detect-makefile.outputs.found_makefile == 'false' run: | sphinx-build \ --builder html \ - ${{ inputs.sphinx-options }} \ - ${{ env.SPHINX_DEBUG }} \ - "${{ inputs.source-dir }}" \ - "${{ inputs.docs-dir }}/${{ inputs.build-dir }}/html" + ${SPHINX_OPTIONS} \ + ${SPHINX_DEBUG} \ + "${SOURCE_DIR}" \ + "${DOCS_DIR}/${BUILD_DIR}/html" - name: Minimize uv cache if: steps.detect-deps.outputs.source != 'conf' @@ -306,11 +321,11 @@ jobs: # -- Deploy to GitHub Pages only on push to upstream main - name: Setup GitHub Pages if: env.DO_DEPLOY == 'true' - uses: actions/configure-pages@v5 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload artifact to GitHub Pages if: env.DO_DEPLOY == 'true' - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: name: github-pages path: ${{ inputs.docs-dir }}/${{ inputs.build-dir }}/html @@ -319,16 +334,17 @@ jobs: - name: Deploy to GitHub Pages id: deployment if: env.DO_DEPLOY == 'true' - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 - name: Job summary if: always() + env: + JOB_STATUS: ${{ job.status }} + DEP_SOURCE: ${{ steps.detect-deps.outputs.source }} + MAKEFILE_FOUND: ${{ steps.detect-makefile.outputs.found }} + PAGES_URL: ${{ steps.deployment.outputs.page_url }} + UV_FLAGS: ${{ steps.uv-flags.outputs.flags }} run: | - STATUS="${{ job.status }}" - DEP_SOURCE="${{ steps.detect-deps.outputs.source }}" - MAKEFILE="${{ steps.detect-makefile.outputs.found }}" - PAGES_URL="${{ steps.deployment.outputs.page_url }}" - # Status icon case "$STATUS" in success) ICON="✅" ;; @@ -340,21 +356,21 @@ jobs: # Dependency source label case "$DEP_SOURCE" in conf) DEP_LABEL="\`conf.py\` (extensions + html_theme)" ;; - pyproject-docs) DEP_LABEL="\`pyproject.toml\` (${{ inputs.docs-dir }})" ;; - pyproject-source) DEP_LABEL="\`pyproject.toml\` (${{ inputs.source-dir }})" ;; + pyproject-docs) DEP_LABEL="\`pyproject.toml\` (${SOURCE_DIR})" ;; + pyproject-source) DEP_LABEL="\`pyproject.toml\` (${SOURCE_DIR})" ;; pyproject-root) DEP_LABEL="\`pyproject.toml\` (repo root)" ;; *) DEP_LABEL="unknown" ;; esac # Build method label - if [ "$MAKEFILE" = "true" ]; then + if [ "$MAKEFILE_FOUND" = "true" ]; then BUILD_METHOD="make html" else BUILD_METHOD="sphinx-build" fi # uv sync flags (empty when using conf.py) - FLAGS="${{ steps.uv-flags.outputs.flags }}" + FLAGS="${UV_FLAGS}" if [ -z "$FLAGS" ]; then FLAGS="_(none)_" fi @@ -364,16 +380,16 @@ jobs: | | | |---|---| - | **Python version** | ${{ env.PYTHON_VRSN }} | - | **Runner** | ${{ inputs.runner }} | + | **Python version** | ${PYTHON_VRSN }} | + | **Runner** | ${RUNNER} | | **Dependency source** | $DEP_LABEL | | **uv sync flags** | $FLAGS | | **Build method** | $BUILD_METHOD | - | **Docs directory** | \`${{ inputs.docs-dir }}\` | - | **Source directory** | \`${{ inputs.source-dir }}\` | - | **Build directory** | \`${{ inputs.build-dir }}\` | - | **Deploy enabled** | ${{ inputs.deploy }} | - | **Deployed** | ${{ env.DO_DEPLOY }} | + | **Docs directory** | `${DOCS_DIR}` | + | **Source directory** | `${SOURCE_DIR}` | + | **Build directory** | `${BUILD_DIR}` | + | **Deploy enabled** | ${DEPLOY} | + | **Deployed** | ${DO_DEPLOY} | EOF diff --git a/sphinx-docs/README.md b/sphinx-docs/README.md index bc156ef..9d7f558 100644 --- a/sphinx-docs/README.md +++ b/sphinx-docs/README.md @@ -23,6 +23,13 @@ deploys it to GitHub Pages. - Deploys to GitHub Pages **only on pushes to the upstream `main` branch** — never from forks or pull requests +## Platform Support + +The workflow is compatible against `ubuntu-*` and `macos-*` GitHub Actions runners, +where [`yq`](https://github.com/mikefarah/yq) and `bash` are pre-installed. + +Windows runners (`windows-*`) are **not natively supported**. + ## Supported Directory Layouts ### Typical layout (default) From f239e886d803bedc828a347664c3add3dce33f08 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 10:44:55 +0100 Subject: [PATCH 119/165] Fix yamllint error --- .github/workflows/sphinx-docs.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 113dc40..015bc70 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -124,12 +124,12 @@ jobs: run: echo "SPHINX_DEBUG=-v --show-traceback" >> "$GITHUB_ENV" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Setup uv with Python ${{ env.PYTHON_VRSN }} - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: python-version: ${{ env.PYTHON_VRSN }} @@ -198,7 +198,7 @@ jobs: - name: Cache uv venv if: steps.detect-deps.outputs.source != 'conf' - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.VENV_PATH }} key: ${{ runner.os }}-${{ env.PYTHON_VRSN }}-${{ hashFiles('pyproject.toml', format('{0}/pyproject.toml', inputs.docs-dir), format('{0}/pyproject.toml', inputs.source-dir)) }} @@ -321,11 +321,11 @@ jobs: # -- Deploy to GitHub Pages only on push to upstream main - name: Setup GitHub Pages if: env.DO_DEPLOY == 'true' - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload artifact to GitHub Pages if: env.DO_DEPLOY == 'true' - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: name: github-pages path: ${{ inputs.docs-dir }}/${{ inputs.build-dir }}/html @@ -334,7 +334,7 @@ jobs: - name: Deploy to GitHub Pages id: deployment if: env.DO_DEPLOY == 'true' - uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 - name: Job summary if: always() From b612d59a3ac30a5b55d96be2e2d5c36684f0b2f6 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 10:46:57 +0100 Subject: [PATCH 120/165] Reformat README --- sphinx-docs/README.md | 179 +++++++++++++++++++++--------------------- 1 file changed, 90 insertions(+), 89 deletions(-) diff --git a/sphinx-docs/README.md b/sphinx-docs/README.md index 9d7f558..a7637a0 100644 --- a/sphinx-docs/README.md +++ b/sphinx-docs/README.md @@ -25,8 +25,9 @@ deploys it to GitHub Pages. ## Platform Support -The workflow is compatible against `ubuntu-*` and `macos-*` GitHub Actions runners, -where [`yq`](https://github.com/mikefarah/yq) and `bash` are pre-installed. +The workflow is compatible against `ubuntu-*` and `macos-*` GitHub Actions +runners, where [`yq`](https://github.com/mikefarah/yq) and `bash` are +pre-installed. Windows runners (`windows-*`) are **not natively supported**. @@ -49,8 +50,8 @@ Use with defaults — no input overrides required: ```yaml jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main ``` ### Flat docs layout @@ -68,27 +69,27 @@ Override `source-dir` to point to `docs`: ```yaml jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main - with: - source-dir: docs + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + with: + source-dir: docs ``` ## Inputs -| Input | Description | Required | Default | -| ----------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | -------------- | -| `python-version` | Python version to use | No | `3.12` | -| `venv-path` | Virtual environment path | No | `.venv` | -| `runner` | GitHub Actions runner | No | `ubuntu-24.04` | -| `timeout-minutes` | Job timeout in minutes | No | `10` | +| Input | Description | Required | Default | +| ----------------- | ------------------------------------------------------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------- | +| `python-version` | Python version to use | No | `3.12` | +| `venv-path` | Virtual environment path | No | `.venv` | +| `runner` | GitHub Actions runner | No | `ubuntu-24.04` | +| `timeout-minutes` | Job timeout in minutes | No | `10` | | `sphinx-options` | Additional options passed to `sphinx-build` or `make` via `SPHINXOPTS` | No | `--jobs auto --write-all --fresh-env --nitpicky --fail-on-warnings --keep-going` | -| `deploy` | Whether to deploy to GitHub Pages | No | `true` | -| `docs-dir` | Path to the docs directory where `Makefile` lives | No | `docs` | -| `source-dir` | Path to the Sphinx source directory where `conf.py` lives | No | `docs/source` | -| `build-dir` | Sphinx build output directory (relative to `docs-dir`) | No | `_build` | -| `use-pyproject` | Force `pyproject.toml` resolution, skipping `conf.py` entirely | No | `false` | -| `extras` | Optional dependency extra or group name to pass to `uv sync` (see [Dependency Resolution](#dependency-resolution)) | No | `''` | +| `deploy` | Whether to deploy to GitHub Pages | No | `true` | +| `docs-dir` | Path to the docs directory where `Makefile` lives | No | `docs` | +| `source-dir` | Path to the Sphinx source directory where `conf.py` lives | No | `docs/source` | +| `build-dir` | Sphinx build output directory (relative to `docs-dir`) | No | `_build` | +| `use-pyproject` | Force `pyproject.toml` resolution, skipping `conf.py` entirely | No | `false` | +| `extras` | Optional dependency extra or group name to pass to `uv sync` (see [Dependency Resolution](#dependency-resolution)) | No | `''` | ## Outputs @@ -102,9 +103,9 @@ The calling workflow must grant the following permissions: ```yaml permissions: - contents: read - pages: write - id-token: write + contents: read + pages: write + id-token: write ``` ## Usage Examples @@ -115,16 +116,16 @@ permissions: name: Docs on: - push: - branches: [main] + push: + branches: [main] jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main - permissions: - contents: read - pages: write - id-token: write + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + permissions: + contents: read + pages: write + id-token: write ``` ### Full — build on PRs, deploy only when merged to `main` @@ -133,30 +134,30 @@ jobs: name: Docs on: - push: - branches: [main] - pull_request: - types: [opened, reopened, synchronize] - workflow_dispatch: + push: + branches: [main] + pull_request: + types: [opened, reopened, synchronize] + workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main - with: - python-version: "3.12" - runner: ubuntu-24.04 - timeout-minutes: 10 - docs-dir: docs - source-dir: docs/source - build-dir: _build - permissions: - contents: read - pages: write - id-token: write + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + with: + python-version: "3.12" + runner: ubuntu-24.04 + timeout-minutes: 10 + docs-dir: docs + source-dir: docs/source + build-dir: _build + permissions: + contents: read + pages: write + id-token: write ``` > **Note:** You do not need to pass `deploy: ${{ github.ref_name == 'main' }}` @@ -186,15 +187,15 @@ docs = [ ```yaml jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main - with: - use-pyproject: true - extras: docs # → uv sync --extra docs - permissions: - contents: read - pages: write - id-token: write + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + with: + use-pyproject: true + extras: docs # → uv sync --extra docs + permissions: + contents: read + pages: write + id-token: write ``` ### Force `pyproject.toml` with a dependency group (PEP 735) @@ -214,15 +215,15 @@ docs = [ ```yaml jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main - with: - use-pyproject: true - extras: docs # → uv sync --group docs - permissions: - contents: read - pages: write - id-token: write + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + with: + use-pyproject: true + extras: docs # → uv sync --group docs + permissions: + contents: read + pages: write + id-token: write ``` The workflow automatically inspects `pyproject.toml` to determine whether @@ -234,32 +235,32 @@ in either section, a warning is emitted and plain `uv sync` is run. ```yaml jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main - with: - deploy: false - permissions: - contents: read - pages: write - id-token: write + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + with: + deploy: false + permissions: + contents: read + pages: write + id-token: write ``` ### Use the deployed URL in a downstream job ```yaml jobs: - docs: - uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main - permissions: - contents: read - pages: write - id-token: write - - notify: - needs: docs - runs-on: ubuntu-24.04 - steps: - - run: echo "Docs published at ${{ needs.docs.outputs.pages-url }}" + docs: + uses: MetOffice/growss/.github/workflows/sphinx-docs.yaml@main + permissions: + contents: read + pages: write + id-token: write + + notify: + needs: docs + runs-on: ubuntu-24.04 + steps: + - run: echo "Docs published at ${{ needs.docs.outputs.pages-url }}" ``` ## Dependency Resolution From bf8c1ad46c7f5b4ed1b35d2aa54380e461a87fe1 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 10:59:59 +0100 Subject: [PATCH 121/165] Fix syntax error in Sphinx Docs workflow table --- .github/workflows/sphinx-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 015bc70..01478fb 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -380,7 +380,7 @@ jobs: | | | |---|---| - | **Python version** | ${PYTHON_VRSN }} | + | **Python version** | ${PYTHON_VRSN} | | **Runner** | ${RUNNER} | | **Dependency source** | $DEP_LABEL | | **uv sync flags** | $FLAGS | From e7733cea3f2a1bee0c0e07b910f43cd5b0575e73 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 11:08:01 +0100 Subject: [PATCH 122/165] Refactor environment variable handling in Sphinx Docs workflow Summary report --- .github/workflows/sphinx-docs.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 01478fb..aacc3ec 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -344,9 +344,15 @@ jobs: MAKEFILE_FOUND: ${{ steps.detect-makefile.outputs.found }} PAGES_URL: ${{ steps.deployment.outputs.page_url }} UV_FLAGS: ${{ steps.uv-flags.outputs.flags }} + PYTHON_VRSN: ${{ env.PYTHON_VRSN }} + DOCS_DIR: ${{ env.DOCS_DIR }} + SOURCE_DIR: ${{ env.SOURCE_DIR }} + BUILD_DIR: ${{ env.BUILD_DIR }} + DEPLOY: ${{ env.DEPLOY }} + DO_DEPLOY: ${{ env.DO_DEPLOY }} run: | # Status icon - case "$STATUS" in + case "$JOB_STATUS" in success) ICON="✅" ;; failure) ICON="❌" ;; cancelled) ICON="⚠️" ;; @@ -385,9 +391,9 @@ jobs: | **Dependency source** | $DEP_LABEL | | **uv sync flags** | $FLAGS | | **Build method** | $BUILD_METHOD | - | **Docs directory** | `${DOCS_DIR}` | - | **Source directory** | `${SOURCE_DIR}` | - | **Build directory** | `${BUILD_DIR}` | + | **Docs directory** | \`${DOCS_DIR}\` | + | **Source directory** | \`${SOURCE_DIR}\` | + | **Build directory** | \`${BUILD_DIR}\` | | **Deploy enabled** | ${DEPLOY} | | **Deployed** | ${DO_DEPLOY} | From 6192c4c1d5e0ba336693d0cb880c6db7cf61f523 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 11:11:15 +0100 Subject: [PATCH 123/165] Add RUNNER environment variable to job summary step in Sphinx Docs workflow --- .github/workflows/sphinx-docs.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index aacc3ec..2ae1eab 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -339,6 +339,7 @@ jobs: - name: Job summary if: always() env: + RUNNER: ${{ inputs.runner }} JOB_STATUS: ${{ job.status }} DEP_SOURCE: ${{ steps.detect-deps.outputs.source }} MAKEFILE_FOUND: ${{ steps.detect-makefile.outputs.found }} From 5cbd034d0eea83d429bb56d7739bbb5f5e9728cf Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 11:14:07 +0100 Subject: [PATCH 124/165] Update deployment environment variable to use inputs in Sphinx Docs workflow --- .github/workflows/sphinx-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 2ae1eab..dc79c2c 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -349,7 +349,7 @@ jobs: DOCS_DIR: ${{ env.DOCS_DIR }} SOURCE_DIR: ${{ env.SOURCE_DIR }} BUILD_DIR: ${{ env.BUILD_DIR }} - DEPLOY: ${{ env.DEPLOY }} + DEPLOY: ${{ inputs.deploy }} DO_DEPLOY: ${{ env.DO_DEPLOY }} run: | # Status icon From 86d9b7e13fc741467c680033e3c6fd33393f879f Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 11:14:53 +0100 Subject: [PATCH 125/165] Update job status variable in Sphinx Docs workflow summary --- .github/workflows/sphinx-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index dc79c2c..40f2719 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -383,7 +383,7 @@ jobs: fi cat >> "$GITHUB_STEP_SUMMARY" < Date: Fri, 22 May 2026 11:20:08 +0100 Subject: [PATCH 126/165] Format uv sync flags and deployment variables in Sphinx Docs workflow summary --- .github/workflows/sphinx-docs.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 40f2719..1f7d804 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -390,13 +390,13 @@ jobs: | **Python version** | ${PYTHON_VRSN} | | **Runner** | ${RUNNER} | | **Dependency source** | $DEP_LABEL | - | **uv sync flags** | $FLAGS | - | **Build method** | $BUILD_METHOD | + | **uv sync flags** | \`${FLAGS}\` | + | **Build method** | \`${BUILD_METHOD}\` | | **Docs directory** | \`${DOCS_DIR}\` | | **Source directory** | \`${SOURCE_DIR}\` | | **Build directory** | \`${BUILD_DIR}\` | - | **Deploy enabled** | ${DEPLOY} | - | **Deployed** | ${DO_DEPLOY} | + | **Deploy enabled** | \`${DEPLOY}\` | + | **Deployed** | \`${DO_DEPLOY}\` | EOF From 99fc14cbbd91c6c26d709f108868b4710241d02a Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 22 May 2026 11:21:47 +0100 Subject: [PATCH 127/165] Update Sphinx Docs workflow summary to include property-value table heading --- .github/workflows/sphinx-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 1f7d804..7409492 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -385,7 +385,7 @@ jobs: cat >> "$GITHUB_STEP_SUMMARY" < Date: Fri, 22 May 2026 11:30:46 +0100 Subject: [PATCH 128/165] Clarify Windows runner support in Sphinx Docs README --- sphinx-docs/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx-docs/README.md b/sphinx-docs/README.md index a7637a0..f20170d 100644 --- a/sphinx-docs/README.md +++ b/sphinx-docs/README.md @@ -25,11 +25,12 @@ deploys it to GitHub Pages. ## Platform Support -The workflow is compatible against `ubuntu-*` and `macos-*` GitHub Actions +The workflow is compatible with `ubuntu-*` and `macos-*` GitHub Actions runners, where [`yq`](https://github.com/mikefarah/yq) and `bash` are pre-installed. -Windows runners (`windows-*`) are **not natively supported**. +> [!WARNING] +> This workflow is **not natively supported** on Windows runners (`windows-*`). ## Supported Directory Layouts From 11b4bfc68b9fae0efec16088c119c531b4c1bbda Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 5 Jun 2026 15:59:13 +0100 Subject: [PATCH 129/165] Pin third party actions --- .github/dependabot.yml | 45 +++++++++++++++++++ .github/workflows/build-sphinx-docs.yaml | 8 ++-- .github/workflows/cla-check.yaml | 37 ++++++++++----- .github/workflows/deploy-sphinx-docs.yaml | 4 +- .github/workflows/fortran-lint.yaml | 19 +++++--- .github/workflows/track-review-project.yaml | 10 +++-- .../workflows/trigger-project-workflow.yaml | 2 +- .github/workflows/umdp3_fixer.yaml | 8 ++-- .github/workflows/validate.yaml | 8 ++-- .github/workflows/validate_symlinks.yaml | 3 +- 10 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..124e29f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,45 @@ +# ============================================================================== +# Dependabot Configuration +# ============================================================================== +# This configuration automates security updates for third-party GitHub Actions. +# To protect our downstream consumers, all actions must be pinned to a +# 40-character commit SHA rather than a mutable version tag. +# +# Custom rulesets in place: +# 1. Monthly Schedule: Checks for updates once a month to minimize noise. +# 2. Major Version Lock: Automated major updates (e.g., v9 to v10) are blocked +# to prevent unexpected breaking code changes. +# 3. PR Grouping: All discovered minor/patch updates are bundled into a single +# monthly Pull Request instead of flooding the repository notifications. +# +# How to manually trigger a Major Update: +# To safely upgrade an action to its next major version baseline: +# 1. Open the workflow file. +# 2. Leave the old SHA exactly as it is, but manually change the trailing +# comment tag to the new target baseline version (e.g., update the text +# from "# v4.0.0" to "# v5.0.0"). +# 3. Push your change. Dependabot will instantly recognize the target update +# and generate a new PR containing the correct, matching v5 commit hash. +# ============================================================================== + +version: 2 +updates: + # Enable automatic tracking for GitHub Actions dependencies + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + cooldown: + default-days: 7 + + # Ignore rules to block major version jumps + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] + + groups: + # Group all action updates into a single PR to reduce noise + # Rule applies across every single third-party action used in all workflows + github-actions-dependencies: + patterns: + - "*" diff --git a/.github/workflows/build-sphinx-docs.yaml b/.github/workflows/build-sphinx-docs.yaml index c50fcb8..fb06f94 100644 --- a/.github/workflows/build-sphinx-docs.yaml +++ b/.github/workflows/build-sphinx-docs.yaml @@ -52,10 +52,12 @@ jobs: BUILD_DIR: ${{ inputs.build-directory }} steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Set up Python 3.12 - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.12' cache: 'pip' @@ -80,7 +82,7 @@ jobs: - name: Upload artifact to GitHub Pages if: ${{ (github.event_name == 'push' || github.event_name == 'merge_group') && github.ref_name == 'main'}} - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: name: github-pages path: "$BUILD_DIR/html" diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index cac5370..52acfff 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -27,16 +27,19 @@ jobs: runs-on: ${{ inputs.runner }} steps: - name: Checkout Base Branch - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: + persist-credentials: false token: ${{ secrets.GITHUB_TOKEN }} ref: ${{ github.ref }} path: base_branch - name: Validate Author Username id: validate_author + env: + PR_AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} run: | - AUTHOR="${{ github.event.pull_request.user.login }}" + AUTHOR="$PR_AUTHOR_LOGIN" # GitHub usernames: alphanumeric + hyphen, 1-39 chars, # cannot start/end with hyphen, cannot contain consecutive hyphens if [[ ! "$AUTHOR" =~ ^[a-zA-Z0-9]([a-zA-Z0-9]|-[a-zA-Z0-9]){0,37}[a-zA-Z0-9]?$ ]]; then @@ -50,8 +53,10 @@ jobs: - name: Determine if contributor exists in base id: check_contributor_base working-directory: ./base_branch + env: + VALIDATED_AUTHOR: ${{ steps.validate_author.outputs.validated_author }} run: | - AUTHOR="${{ steps.validate_author.outputs.validated_author }}" + AUTHOR="$VALIDATED_AUTHOR" if [ -f "CONTRIBUTORS.md" ]; then if grep -qP "^\s*\|\s*\Q$AUTHOR\E\s*\|" CONTRIBUTORS.md; then echo "on_base=true" >> $GITHUB_OUTPUT @@ -67,17 +72,19 @@ jobs: fi - name: Checkout PR Branch - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} path: pr_branch - name: Determine if contributor exists in PR branch id: check_contributor_pr working-directory: pr_branch + env: + AUTHOR: ${{ steps.validate_author.outputs.validated_author }} run: | - AUTHOR="${{ steps.validate_author.outputs.validated_author }}" if [ -f "CONTRIBUTORS.md" ]; then # if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then if grep -qP "^\s*\|\s*\Q$AUTHOR\E\s*\|" CONTRIBUTORS.md; then @@ -108,10 +115,11 @@ jobs: - name: Checkout Merge Branch if: env.merge_ref_defined == 'true' id: checkout_merge - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 continue-on-error: true with: token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false ref: "refs/pull/${{ github.event.number }}/merge" path: merge_branch @@ -124,7 +132,7 @@ jobs: else cd merge_branch || exit 1 - git fetch origin "${{ github.event.pull_request.base.ref }}" + git fetch origin "$GITHUB_BASE_REF" # Only check for exact CONTRIBUTORS.md file (not subdirectories) CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF -- CONTRIBUTORS.md) @@ -140,16 +148,21 @@ jobs: # -- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) - name: Manage CLA Status, Labels, and Comments - uses: actions/github-script@v8 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 # Using 'always()' here so this step runs regardless of previous # success/failure to manage labels correctly if: always() + env: + SIGNED_ON_BASE: ${{ steps.check_contributor_base.outputs.on_base }} + SIGNED_ON_PR: ${{ steps.check_contributor_pr.outputs.on_pr }} + CONTRIBUTORS_MODIFIED: ${{ steps.check_contributors_modified.outputs.modified }} + CLA_URL: ${{ inputs.cla-url }} with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const signedOnBase = '${{ steps.check_contributor_base.outputs.on_base }}' === 'true'; - const signedOnPr = '${{ steps.check_contributor_pr.outputs.on_pr }}' === 'true'; - const contributorsModified = '${{ steps.check_contributors_modified.outputs.modified }}' === 'true'; + const signedOnBase = process.env.SIGNED_ON_BASE === 'true'; + const signedOnPr = process.env.SIGNED_ON_PR === 'true'; + const contributorsModified = process.env.CONTRIBUTORS_MODIFIED === 'true'; const issue_number = context.issue.number; const owner = context.repo.owner; const repo = context.repo.repo; @@ -161,7 +174,7 @@ jobs: process.exit(1); } - const claUrl = '${{ inputs.cla-url }}'; + const claUrl = process.env.CLA_URL; // Validate CLA URL try { const url = new URL(claUrl); diff --git a/.github/workflows/deploy-sphinx-docs.yaml b/.github/workflows/deploy-sphinx-docs.yaml index 0e8071e..9303178 100644 --- a/.github/workflows/deploy-sphinx-docs.yaml +++ b/.github/workflows/deploy-sphinx-docs.yaml @@ -31,7 +31,7 @@ jobs: steps: - name: Configure GitHub Pages id: configure - uses: actions/configure-pages@v5 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d #v6.0.0 - name: Deploy GitHub Pages id: deploy - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/fortran-lint.yaml b/.github/workflows/fortran-lint.yaml index 35f3e05..4216228 100644 --- a/.github/workflows/fortran-lint.yaml +++ b/.github/workflows/fortran-lint.yaml @@ -79,21 +79,28 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Setup uv and Python ${{ env.UV_PYTHON }} - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0 with: python-version: ${{ env.UV_PYTHON }} enable-cache: ${{ inputs.enable-cache }} - name: Run Fortitude Linter + env: + FORTITUDE_VERSION: ${{ inputs.fortitude-version }} + FILE_EXTENSIONS: ${{ inputs.file-extensions }} + CONFIG_PATH: ${{ inputs.config-path }} + SOURCE_PATH: ${{ inputs.source-path }} run: | - uvx --from fortitude-lint==${{ inputs.fortitude-version }} fortitude check \ + uvx --from "fortitude-lint==$FORTITUDE_VERSION" fortitude check \ ${{ inputs.respect-gitignore && '--respect-gitignore' || '' }} \ ${{ inputs.show-fixes && '--show-fixes' || '' }} \ ${{ inputs.show-statistics && '--statistics' || '' }} \ - --file-extensions=${{ inputs.file-extensions }} \ - ${{ inputs.config-path && format('--config-file={0}', inputs.config-path) || '' }} \ - ${{ inputs.source-path }} + "--file-extensions=$FILE_EXTENSIONS" \ + ${CONFIG_PATH:+"--config-file=$CONFIG_PATH"} \ + "$SOURCE_PATH" continue-on-error: ${{ !inputs.fail-on-error }} diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index a743eed..cc77199 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -41,10 +41,12 @@ jobs: steps: # Required as running from the workflow_run trigger - name: Checkout repo - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Download PR data artifact - uses: actions/github-script@v8 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -187,6 +189,8 @@ jobs: - name: Update project tracking env: GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} + PROJECT_ORG: ${{ inputs.project_org }} + PROJECT_NUMBER: ${{ inputs.project_number }} run: | # Get project data project_data=$(gh api graphql -f query=' @@ -202,7 +206,7 @@ jobs: } } } - }' -f org=${{ inputs.project_org }} -F number=${{ inputs.project_number }}) + }' -f org="$PROJECT_ORG" -F number="$PROJECT_NUMBER") project_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.id') status_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id') diff --git a/.github/workflows/trigger-project-workflow.yaml b/.github/workflows/trigger-project-workflow.yaml index 6fa7e72..cc9362b 100644 --- a/.github/workflows/trigger-project-workflow.yaml +++ b/.github/workflows/trigger-project-workflow.yaml @@ -27,7 +27,7 @@ jobs: - name: Triggering Workflow run: | echo "{\"pr_num\": $PR_NUM, \"pr_id\": \"$PR_ID\"}" >> context.json - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: context.json path: context.json diff --git a/.github/workflows/umdp3_fixer.yaml b/.github/workflows/umdp3_fixer.yaml index 8e63e2e..d7298e9 100644 --- a/.github/workflows/umdp3_fixer.yaml +++ b/.github/workflows/umdp3_fixer.yaml @@ -29,18 +29,20 @@ jobs: steps: - name: Checkout Branch - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: path: pr_branch token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false - name: Checkout SimSys_Scripts - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v 6.0.3 with: repository: MetOffice/SimSys_Scripts sparse-checkout: umdp3_fixer path: SimSys_Scripts + persist-credentials: false - name: Set up Python 3.14 - uses: actions/setup-python@v6 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.14 - name: Run UMDP3 Fixer diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index c1e170a..a28f7f9 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -30,16 +30,18 @@ jobs: UV_PYTHON: "3.14" steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Setup uv - uses: astral-sh/setup-uv@v7 + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 with: python-version: ${{ env.UV_PYTHON }} enable-cache: true - name: Restore uv cache - uses: actions/cache@v4 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ env.UV_CACHE_DIR }} key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }} diff --git a/.github/workflows/validate_symlinks.yaml b/.github/workflows/validate_symlinks.yaml index d668cef..12d8c75 100644 --- a/.github/workflows/validate_symlinks.yaml +++ b/.github/workflows/validate_symlinks.yaml @@ -29,9 +29,10 @@ jobs: steps: - name: Checkout Branch - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false - name: Find broken symlinks run: | broken_links=$(find . -path '.git' -prune -o -xtype l -printf '%p -> %l\n' || true) From 3b64e3b366bed745022f8ec71d632f1a73be00f4 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Fri, 5 Jun 2026 16:11:08 +0100 Subject: [PATCH 130/165] space after comment --- .github/workflows/deploy-sphinx-docs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy-sphinx-docs.yaml b/.github/workflows/deploy-sphinx-docs.yaml index 9303178..5b576b4 100644 --- a/.github/workflows/deploy-sphinx-docs.yaml +++ b/.github/workflows/deploy-sphinx-docs.yaml @@ -31,7 +31,7 @@ jobs: steps: - name: Configure GitHub Pages id: configure - uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d #v6.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Deploy GitHub Pages id: deploy uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 From 437d782347beae85f30bc63bc1cae0e50ef730cb Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:02:21 +0100 Subject: [PATCH 131/165] Add depandabot and zizmor configuration --- .github/{dependabot.yml => dependabot.yaml} | 8 ++++++- .github/pull_request_template.md | 17 +++++--------- .../workflows/call-track-review-project.yaml | 23 ++++++++++++++++++- .../call-trigger-project-workflow.yaml | 10 ++++++++ .github/workflows/fortran-lint.yaml | 16 +++++++++---- .../workflows/trigger-project-workflow.yaml | 7 +++--- .github/zizmor.yaml | 23 +++++++++++++++++++ 7 files changed, 84 insertions(+), 20 deletions(-) rename .github/{dependabot.yml => dependabot.yaml} (84%) create mode 100644 .github/zizmor.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yaml similarity index 84% rename from .github/dependabot.yml rename to .github/dependabot.yaml index 124e29f..66fb87b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yaml @@ -1,6 +1,12 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ +# # ============================================================================== # Dependabot Configuration -# ============================================================================== +# # This configuration automates security updates for third-party GitHub Actions. # To protect our downstream consumers, all actions must be pinned to a # 40-character commit SHA rather than a mutable version tag. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 41944d9..273f835 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,10 +4,9 @@ Code Reviewer: - + -## Code Quality Checklist +## :clipboard: Code Quality Checklist (_Some checks are automatically carried out via the CI pipeline_) @@ -16,16 +15,12 @@ Code Reviewer: - [ ] The modified workflow's README has been updated, if required - [ ] The changes have been sufficiently tested (please describe) -## AI Assistance and Attribution +## :robot: AI Assistance and Attribution -- [ ] Some of the content of this change has been produced with the assistance - of _Generative AI tool name_ (e.g., Met Office Github Copilot Enterprise, - Github Copilot Personal, ChatGPT GPT-4, etc) and I have followed the - [Simulation Systems AI policy](https://metoffice.github.io/simulation-systems/FurtherDetails/ai.html) - (including attribution labels) +- [ ] Some of the content of this change has been produced with the assistance of _Generative AI tool name_ (e.g., Met Office Github Copilot Enterprise, Github Copilot Personal, ChatGPT GPT-4, etc) and I have followed the [Simulation Systems AI policy](https://metoffice.github.io/simulation-systems/FurtherDetails/ai.html) (including attribution labels) -# Code Review +## :eyes: Code Review -- [ ] The changes are approriate and testing has been sufficient +- [ ] The changes are appropriate and testing has been sufficient diff --git a/.github/workflows/call-track-review-project.yaml b/.github/workflows/call-track-review-project.yaml index eeab646..957735b 100644 --- a/.github/workflows/call-track-review-project.yaml +++ b/.github/workflows/call-track-review-project.yaml @@ -1,3 +1,9 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + name: Track Review Project on: @@ -7,12 +13,27 @@ on: - completed workflow_dispatch: +# Best Practice: Explicitly strip all top-level default permissions +permissions: {} + jobs: track_review_project: + if: >- + github.event_name == 'workflow_dispatch' || + ( + github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'push' && + github.event.workflow_run.head_branch == 'main' && + github.event.workflow_run.conclusion == 'success' + ) + # Granular permissions are safely locked directly to this specific execution context + permissions: + actions: read # Required to view the status of the upstream triggered workflow run + repository-projects: write # Required to add or mutate tasks inside GitHub Project 376 uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main secrets: inherit # Optional inputs (with default values) with: - runner: "ubuntu-22.04" + runner: "ubuntu-24.04" project_org: "MetOffice" project_number: 376 diff --git a/.github/workflows/call-trigger-project-workflow.yaml b/.github/workflows/call-trigger-project-workflow.yaml index f22c433..2cf9b0c 100644 --- a/.github/workflows/call-trigger-project-workflow.yaml +++ b/.github/workflows/call-trigger-project-workflow.yaml @@ -1,3 +1,9 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + name: Trigger Review Project on: @@ -6,7 +12,11 @@ on: pull_request_review: pull_request_review_comment: +permissions: {} + jobs: trigger_project_workflow: + permissions: + contents: read # Required for checking out code or referencing internal workflow actions uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main secrets: inherit diff --git a/.github/workflows/fortran-lint.yaml b/.github/workflows/fortran-lint.yaml index 4216228..859a130 100644 --- a/.github/workflows/fortran-lint.yaml +++ b/.github/workflows/fortran-lint.yaml @@ -73,7 +73,8 @@ jobs: name: Fortran runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout }} - + permissions: + contents: read # Required to check out code and scan files using fortitude linter env: UV_PYTHON: ${{ inputs.python-version }} @@ -95,11 +96,18 @@ jobs: FILE_EXTENSIONS: ${{ inputs.file-extensions }} CONFIG_PATH: ${{ inputs.config-path }} SOURCE_PATH: ${{ inputs.source-path }} + RESPECT_GITIGNORE: ${{ inputs.respect-gitignore }} + SHOW_FIXES: ${{ inputs.show-fixes }} + SHOW_STATISTICS: ${{ inputs.show-statistics }} run: | + ARGS=() + + [ "$RESPECT_GITIGNORE" = "true" ] && ARGS+=("--respect-gitignore") + [ "$SHOW_FIXES" = "true" ] && ARGS+=("--show-fixes") + [ "$SHOW_STATISTICS" = "true" ] && ARGS+=("--statistics") + uvx --from "fortitude-lint==$FORTITUDE_VERSION" fortitude check \ - ${{ inputs.respect-gitignore && '--respect-gitignore' || '' }} \ - ${{ inputs.show-fixes && '--show-fixes' || '' }} \ - ${{ inputs.show-statistics && '--statistics' || '' }} \ + "${ARGS[@]}" \ "--file-extensions=$FILE_EXTENSIONS" \ ${CONFIG_PATH:+"--config-file=$CONFIG_PATH"} \ "$SOURCE_PATH" diff --git a/.github/workflows/trigger-project-workflow.yaml b/.github/workflows/trigger-project-workflow.yaml index cc9362b..59662d2 100644 --- a/.github/workflows/trigger-project-workflow.yaml +++ b/.github/workflows/trigger-project-workflow.yaml @@ -12,14 +12,15 @@ name: Trigger Review Project on: workflow_call: -permissions: - contents: read - pull-requests: write +permissions: {} jobs: trigger_project: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read # Required to evaluate metadata from the triggering GitHub event context + pull-requests: write # Required to modify or comment on pull requests within this pipeline process env: PR_NUM: ${{ github.event.pull_request.number }} PR_ID: ${{ github.event.pull_request.node_id }} diff --git a/.github/zizmor.yaml b/.github/zizmor.yaml new file mode 100644 index 0000000..750c124 --- /dev/null +++ b/.github/zizmor.yaml @@ -0,0 +1,23 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + +# Rules for Zizmor GitHub workflow linter. This file is used to configure which +# rules to ignore for specific workflow files, allowing for exceptions to be +# made where necessary while still enforcing best practices across the codebase. +rules: + unpinned-uses: + ignore: + - "call-track-review-project.yaml" + - "call-trigger-project-workflow.yaml" + - "label-pr.yaml" + dangerous-triggers: + ignore: + - "call-track-review-project.yaml" + - "call-trigger-project-workflow.yaml" + secrets-inherit: + ignore: + - "call-track-review-project.yaml" + - "call-trigger-project-workflow.yaml" From e05254d8d5822fb3367aec2c920c374b0b892e2b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:12:01 +0100 Subject: [PATCH 132/165] Fix yamllint --- .github/workflows/call-track-review-project.yaml | 4 ++-- .github/workflows/call-trigger-project-workflow.yaml | 2 +- .yamllint | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/call-track-review-project.yaml b/.github/workflows/call-track-review-project.yaml index 957735b..79f3f1f 100644 --- a/.github/workflows/call-track-review-project.yaml +++ b/.github/workflows/call-track-review-project.yaml @@ -28,8 +28,8 @@ jobs: ) # Granular permissions are safely locked directly to this specific execution context permissions: - actions: read # Required to view the status of the upstream triggered workflow run - repository-projects: write # Required to add or mutate tasks inside GitHub Project 376 + actions: read # Required to view the status of the upstream triggered workflow run + repository-projects: write # Required to add or mutate tasks inside GitHub Project 376 uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main secrets: inherit # Optional inputs (with default values) diff --git a/.github/workflows/call-trigger-project-workflow.yaml b/.github/workflows/call-trigger-project-workflow.yaml index 2cf9b0c..a031fcd 100644 --- a/.github/workflows/call-trigger-project-workflow.yaml +++ b/.github/workflows/call-trigger-project-workflow.yaml @@ -17,6 +17,6 @@ permissions: {} jobs: trigger_project_workflow: permissions: - contents: read # Required for checking out code or referencing internal workflow actions + contents: read # Required for checking out code or referencing internal workflow actions uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main secrets: inherit diff --git a/.yamllint b/.yamllint index d11dac5..262da28 100644 --- a/.yamllint +++ b/.yamllint @@ -1,11 +1,15 @@ --- extends: default -ignore: | - .venv/ - .svn/ +ignore: + - ".venv/" + - ".svn/" rules: document-start: disable line-length: disable truthy: disable + comments: + min-spaces-from-content: 1 + comments-indentation: disable + From a9577c569cc4ce2f45bd8655c9922c11dceaf20a Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:14:15 +0100 Subject: [PATCH 133/165] Fix yamllint config --- .yamllint | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.yamllint b/.yamllint index 262da28..8ff33f7 100644 --- a/.yamllint +++ b/.yamllint @@ -11,5 +11,5 @@ rules: truthy: disable comments: min-spaces-from-content: 1 - comments-indentation: disable + comments-indentation: disable From c18be21e704843bb251ed5bca58f5c4f2d587972 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:15:44 +0100 Subject: [PATCH 134/165] Fix yamllint too many blank lines --- .yamllint | 1 - 1 file changed, 1 deletion(-) diff --git a/.yamllint b/.yamllint index 8ff33f7..e07b8bf 100644 --- a/.yamllint +++ b/.yamllint @@ -12,4 +12,3 @@ rules: comments: min-spaces-from-content: 1 comments-indentation: disable - From ae02afa26c1a9ff970389697f88aa72220f19745 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:14:38 +0100 Subject: [PATCH 135/165] Use gh api to query merge ref metadata --- .github/workflows/cla-check.yaml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 52acfff..15d2c25 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -103,14 +103,25 @@ jobs: id: check_merge_ref working-directory: ./base_branch shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - ref="refs/pull/${{ github.event.number }}/merge" - hash=$(git ls-remote origin $ref) - if [[ -z $hash ]]; then - echo "merge_ref_defined=false" >> $GITHUB_ENV - else + # Use gh api to query pull request metadata directly + # without network git ls-remote authentication + mergeable=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.number }} --jq ".mergeable" 2>/dev/null || echo "undefined") + if [ "$mergeable" != "undefined" ]; then echo "merge_ref_defined=true" >> $GITHUB_ENV + else + echo "merge_ref_defined=false" >> $GITHUB_ENV fi + # run: | + # ref="refs/pull/${{ github.event.number }}/merge" + # hash=$(git ls-remote origin $ref) + # if [[ -z $hash ]]; then + # echo "merge_ref_defined=false" >> $GITHUB_ENV + # else + # echo "merge_ref_defined=true" >> $GITHUB_ENV + # fi - name: Checkout Merge Branch if: env.merge_ref_defined == 'true' From fde7019300227ddae9d68ee212ed6d358d0e6e5b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:29:59 +0100 Subject: [PATCH 136/165] Call gh files api directly to check modifcation status --- .github/workflows/cla-check.yaml | 44 ++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 15d2c25..c890846 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -136,26 +136,48 @@ jobs: - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified + shell: bash + env: + # Safely inject the scoped token into the native GitHub CLI tool + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if [[ "$merge_ref_defined" == "false" ]]; then + if [ "$merge_ref_defined" = "false" ]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." - echo "modified=false" >> $GITHUB_OUTPUT + echo "modified=false" >> "$GITHUB_OUTPUT" else - cd merge_branch || exit 1 + # Call GitHub Files API directly to check PR modifications without + # running local network git fetches + # Filtering precisely for the CONTRIBUTORS.md file signature + has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) - git fetch origin "$GITHUB_BASE_REF" - - # Only check for exact CONTRIBUTORS.md file (not subdirectories) - CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF -- CONTRIBUTORS.md) - - if [ -n "$CHANGED_FILES" ]; then - echo "modified=true" >> $GITHUB_OUTPUT + if [ -n "$has_modification" ]; then + echo "modified=true" >> "$GITHUB_OUTPUT" echo "📝 CONTRIBUTORS.md file was modified in this PR." else - echo "modified=false" >> $GITHUB_OUTPUT + echo "modified=false" >> "$GITHUB_OUTPUT" echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." fi fi + # run: | + # if [[ "$merge_ref_defined" == "false" ]]; then + # echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." + # echo "modified=false" >> $GITHUB_OUTPUT + # else + # cd merge_branch || exit 1 + + # git fetch origin "$GITHUB_BASE_REF" + + # # Only check for exact CONTRIBUTORS.md file (not subdirectories) + # CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF -- CONTRIBUTORS.md) + + # if [ -n "$CHANGED_FILES" ]; then + # echo "modified=true" >> $GITHUB_OUTPUT + # echo "📝 CONTRIBUTORS.md file was modified in this PR." + # else + # echo "modified=false" >> $GITHUB_OUTPUT + # echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." + # fi + # fi # -- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) - name: Manage CLA Status, Labels, and Comments From f30059d079d00d49e3ff2baaea583866eff3c288 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:05:37 +0100 Subject: [PATCH 137/165] Check CONTRIBUTOR content changes --- .github/workflows/cla-check.yaml | 44 +++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index c890846..8b1242d 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -140,24 +140,54 @@ jobs: env: # Safely inject the scoped token into the native GitHub CLI tool GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | if [ "$merge_ref_defined" = "false" ]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> "$GITHUB_OUTPUT" else - # Call GitHub Files API directly to check PR modifications without - # running local network git fetches - # Filtering precisely for the CONTRIBUTORS.md file signature - has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) + # 1. Fetch raw content from base using the safe $BASE_REF environment variable + gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ + --jq '.content' | base64 -d > base_contributors.md 2>/dev/null || touch base_contributors.md - if [ -n "$has_modification" ]; then + # 2. Fetch raw content from head using the safe $HEAD_REPO and $HEAD_REF environment variables + gh api "repos/$HEAD_REPO/contents/CONTRIBUTORS.md?ref=$HEAD_REF" \ + --jq '.content' | base64 -d > pr_contributors.md 2>/dev/null || touch pr_contributors.md + + # 3. Use git diff in no-index mode to evaluate the text files while ignoring whitespace changes + # -w ignores all whitespaces, --quiet returns exit status 1 if meaningful changes exist + if ! git diff --no-index -w --quiet base_contributors.md pr_contributors.md; then echo "modified=true" >> "$GITHUB_OUTPUT" - echo "📝 CONTRIBUTORS.md file was modified in this PR." + echo "📝 CONTRIBUTORS.md file content was modified in this PR (ignoring whitespaces)." else echo "modified=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." + echo "ℹ️ CONTRIBUTORS.md file content was NOT modified in this PR (ignoring whitespaces)." fi + + # Clean up temporary verification files to maintain a stateless workspace + rm -f base_contributors.md pr_contributors.md fi + # run: | + # if [ "$merge_ref_defined" = "false" ]; then + # echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." + # echo "modified=false" >> "$GITHUB_OUTPUT" + # else + # # Call GitHub Files API directly to check PR modifications without + # # running local network git fetches + # # Filtering precisely for the CONTRIBUTORS.md file signature + # has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) + + # if [ -n "$has_modification" ]; then + # echo "modified=true" >> "$GITHUB_OUTPUT" + # echo "📝 CONTRIBUTORS.md file was modified in this PR." + # else + # echo "modified=false" >> "$GITHUB_OUTPUT" + # echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." + # fi + # fi + # run: | # if [[ "$merge_ref_defined" == "false" ]]; then # echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." From 6a419f41d27dc752cac575d744131ac835b75103 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:17:20 +0100 Subject: [PATCH 138/165] Check CONTRIBUTOR content changes 2 --- .github/workflows/cla-check.yaml | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 8b1242d..55be5ef 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -138,36 +138,23 @@ jobs: id: check_contributors_modified shell: bash env: - # Safely inject the scoped token into the native GitHub CLI tool + # This token is safely scoped to job-level permissions GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BASE_REF: ${{ github.event.pull_request.base.ref }} - HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} - HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | if [ "$merge_ref_defined" = "false" ]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> "$GITHUB_OUTPUT" else - # 1. Fetch raw content from base using the safe $BASE_REF environment variable - gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ - --jq '.content' | base64 -d > base_contributors.md 2>/dev/null || touch base_contributors.md + # Leverages only clean system integers and repo constraints to completely bypass template-injection + changed_files=$(gh pr diff "${{ github.event.number }}" -R "${{ github.repository }}" -w --name-only 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) - # 2. Fetch raw content from head using the safe $HEAD_REPO and $HEAD_REF environment variables - gh api "repos/$HEAD_REPO/contents/CONTRIBUTORS.md?ref=$HEAD_REF" \ - --jq '.content' | base64 -d > pr_contributors.md 2>/dev/null || touch pr_contributors.md - - # 3. Use git diff in no-index mode to evaluate the text files while ignoring whitespace changes - # -w ignores all whitespaces, --quiet returns exit status 1 if meaningful changes exist - if ! git diff --no-index -w --quiet base_contributors.md pr_contributors.md; then + if [ -n "$changed_files" ]; then echo "modified=true" >> "$GITHUB_OUTPUT" - echo "📝 CONTRIBUTORS.md file content was modified in this PR (ignoring whitespaces)." + echo "📝 CONTRIBUTORS.md content was substantively modified (ignoring whitespaces)." else echo "modified=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ CONTRIBUTORS.md file content was NOT modified in this PR (ignoring whitespaces)." + echo "ℹ️ CONTRIBUTORS.md content was NOT substantively modified (ignoring whitespaces)." fi - - # Clean up temporary verification files to maintain a stateless workspace - rm -f base_contributors.md pr_contributors.md fi # run: | # if [ "$merge_ref_defined" = "false" ]; then From e4549deb849b74bc89679de92409f2efe47e3c26 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:27:41 +0100 Subject: [PATCH 139/165] Check CONTRIBUTOR content changes 3 --- .github/workflows/cla-check.yaml | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 55be5ef..7dd80d9 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -140,21 +140,41 @@ jobs: env: # This token is safely scoped to job-level permissions GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_REF: ${{ github.event.pull_request.base.ref }} run: | if [ "$merge_ref_defined" = "false" ]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> "$GITHUB_OUTPUT" else - # Leverages only clean system integers and repo constraints to completely bypass template-injection - changed_files=$(gh pr diff "${{ github.event.number }}" -R "${{ github.repository }}" -w --name-only 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) + # 1. Fetch the base reference from the live repository API securely using the environment variable + gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ + --jq '.content' | base64 -d > base_contributors.md 2>/dev/null || touch base_contributors.md + + # 2. Fetch the incoming pull request's active patched variant file data + gh pr diff "${{ github.event.number }}" -R "${{ github.repository }}" > pr_patch.diff 2>/dev/null || true + + # Create a target file copy to isolate the changes + cp base_contributors.md pr_contributors.md + + # Apply the patch file changes specifically for CONTRIBUTORS.md if present in the diff + if grep -q "b/CONTRIBUTORS.md" pr_patch.diff; then + # Isolate the file's patch segment and apply it to our temporary file + git apply --include="CONTRIBUTORS.md" pr_patch.diff 2>/dev/null || true + # Update our tracking copy with the patched file contents + [ -f CONTRIBUTORS.md ] && mv CONTRIBUTORS.md pr_contributors.md + fi - if [ -n "$changed_files" ]; then + # Perform a strict content-only comparison while safely ignoring all whitespace adjustments + if ! git diff --no-index -w --quiet base_contributors.md pr_contributors.md; then echo "modified=true" >> "$GITHUB_OUTPUT" - echo "📝 CONTRIBUTORS.md content was substantively modified (ignoring whitespaces)." + echo "📝 CONTRIBUTORS.md content was substantives modified." else echo "modified=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ CONTRIBUTORS.md content was NOT substantively modified (ignoring whitespaces)." + echo "ℹ️ CONTRIBUTORS.md content was NOT substantives modified (whitespace only or unchanged)." fi + + # Clean up disk workspace to prevent cache pollution leaks + rm -f base_contributors.md pr_contributors.md pr_patch.diff CONTRIBUTORS.md fi # run: | # if [ "$merge_ref_defined" = "false" ]; then From 53d3c7e9f6271ed5a0a5ce822c03ef9584482f1e Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:32:10 +0100 Subject: [PATCH 140/165] Revert --- .github/workflows/cla-check.yaml | 50 +++++--------------------------- 1 file changed, 7 insertions(+), 43 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 7dd80d9..855d2d8 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -140,60 +140,24 @@ jobs: env: # This token is safely scoped to job-level permissions GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BASE_REF: ${{ github.event.pull_request.base.ref }} run: | if [ "$merge_ref_defined" = "false" ]; then echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." echo "modified=false" >> "$GITHUB_OUTPUT" else - # 1. Fetch the base reference from the live repository API securely using the environment variable - gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ - --jq '.content' | base64 -d > base_contributors.md 2>/dev/null || touch base_contributors.md - - # 2. Fetch the incoming pull request's active patched variant file data - gh pr diff "${{ github.event.number }}" -R "${{ github.repository }}" > pr_patch.diff 2>/dev/null || true - - # Create a target file copy to isolate the changes - cp base_contributors.md pr_contributors.md - - # Apply the patch file changes specifically for CONTRIBUTORS.md if present in the diff - if grep -q "b/CONTRIBUTORS.md" pr_patch.diff; then - # Isolate the file's patch segment and apply it to our temporary file - git apply --include="CONTRIBUTORS.md" pr_patch.diff 2>/dev/null || true - # Update our tracking copy with the patched file contents - [ -f CONTRIBUTORS.md ] && mv CONTRIBUTORS.md pr_contributors.md - fi + # Call GitHub Files API directly to check PR modifications without + # running local network git fetches + # Filtering precisely for the CONTRIBUTORS.md file signature + has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) - # Perform a strict content-only comparison while safely ignoring all whitespace adjustments - if ! git diff --no-index -w --quiet base_contributors.md pr_contributors.md; then + if [ -n "$has_modification" ]; then echo "modified=true" >> "$GITHUB_OUTPUT" - echo "📝 CONTRIBUTORS.md content was substantives modified." + echo "📝 CONTRIBUTORS.md file was modified in this PR." else echo "modified=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ CONTRIBUTORS.md content was NOT substantives modified (whitespace only or unchanged)." + echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." fi - - # Clean up disk workspace to prevent cache pollution leaks - rm -f base_contributors.md pr_contributors.md pr_patch.diff CONTRIBUTORS.md fi - # run: | - # if [ "$merge_ref_defined" = "false" ]; then - # echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." - # echo "modified=false" >> "$GITHUB_OUTPUT" - # else - # # Call GitHub Files API directly to check PR modifications without - # # running local network git fetches - # # Filtering precisely for the CONTRIBUTORS.md file signature - # has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) - - # if [ -n "$has_modification" ]; then - # echo "modified=true" >> "$GITHUB_OUTPUT" - # echo "📝 CONTRIBUTORS.md file was modified in this PR." - # else - # echo "modified=false" >> "$GITHUB_OUTPUT" - # echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." - # fi - # fi # run: | # if [[ "$merge_ref_defined" == "false" ]]; then From 2e4d833eccd36e406187244e58a0ff39436a8cc0 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:36:32 +0100 Subject: [PATCH 141/165] Update pull request template to standardize checklist headings --- .github/pull_request_template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 273f835..e9f957e 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ Code Reviewer: -## :clipboard: Code Quality Checklist +## :white_check_mark: Code Quality Checklist (_Some checks are automatically carried out via the CI pipeline_) @@ -21,6 +21,6 @@ Code Reviewer: -## :eyes: Code Review +## :computer: Code Review - [ ] The changes are appropriate and testing has been sufficient From bfd31d8d774b3ccf3cfbf3894a37863381cbe3c4 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Mon, 8 Jun 2026 23:50:44 +0100 Subject: [PATCH 142/165] Refactor CONTRIBUTORS.md modification check in CLA workflow to improve handling of merge conflicts and streamline file comparison --- .github/workflows/cla-check.yaml | 91 ++++++++++++++++++-------------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 855d2d8..e539c59 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -114,14 +114,6 @@ jobs: else echo "merge_ref_defined=false" >> $GITHUB_ENV fi - # run: | - # ref="refs/pull/${{ github.event.number }}/merge" - # hash=$(git ls-remote origin $ref) - # if [[ -z $hash ]]; then - # echo "merge_ref_defined=false" >> $GITHUB_ENV - # else - # echo "merge_ref_defined=true" >> $GITHUB_ENV - # fi - name: Checkout Merge Branch if: env.merge_ref_defined == 'true' @@ -140,47 +132,66 @@ jobs: env: # This token is safely scoped to job-level permissions GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BASE_REF: ${{ github.event.pull_request.base.ref }} + HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | if [ "$merge_ref_defined" = "false" ]; then - echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." + echo "::warning::Merge conflicts detected. The CONTRIBUTORS.md modification check was skipped, but the workflow has been allowed to pass. Please resolve conflicts." echo "modified=false" >> "$GITHUB_OUTPUT" else - # Call GitHub Files API directly to check PR modifications without - # running local network git fetches - # Filtering precisely for the CONTRIBUTORS.md file signature - has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) - - if [ -n "$has_modification" ]; then + # 1. Fetch raw CONTRIBUTORS.md from the base branch path + gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ + --jq '.content' | base64 -d > base_raw.txt 2>/dev/null || touch base_raw.txt + + # 2. Fetch raw CONTRIBUTORS.md from the incoming PR head branch path + gh api "repos/$HEAD_REPO/contents/CONTRIBUTORS.md?ref=$HEAD_REF" \ + --jq '.content' | base64 -d > pr_raw.txt 2>/dev/null || touch pr_raw.txt + + # 3. Clean and normalise files completely using pure shell primitives + # This strips all spaces, tabs, newlines, and carriage returns, squashing the file into a single string + tr -d '[:space:]' < base_raw.txt > base_clean.txt 2>/dev/null || touch base_clean.txt + tr -d '[:space:]' < pr_raw.txt > pr_clean.txt 2>/dev/null || touch pr_clean.txt + + # 4. Perform a direct text comparison on the squashed data + # cmp returns exit code 0 if identical, or 1 if any content characters differ + if ! cmp -s base_clean.txt pr_clean.txt; then echo "modified=true" >> "$GITHUB_OUTPUT" - echo "📝 CONTRIBUTORS.md file was modified in this PR." + echo "📝 CONTRIBUTORS.md content was substantively modified (additions or deletions detected)." else echo "modified=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." + echo "ℹ️ CONTRIBUTORS.md content was NOT substantively modified (whitespace only or unchanged)." fi - fi - - # run: | - # if [[ "$merge_ref_defined" == "false" ]]; then - # echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." - # echo "modified=false" >> $GITHUB_OUTPUT - # else - # cd merge_branch || exit 1 - - # git fetch origin "$GITHUB_BASE_REF" - # # Only check for exact CONTRIBUTORS.md file (not subdirectories) - # CHANGED_FILES=$(git diff --name-only origin/$GITHUB_BASE_REF -- CONTRIBUTORS.md) - - # if [ -n "$CHANGED_FILES" ]; then - # echo "modified=true" >> $GITHUB_OUTPUT - # echo "📝 CONTRIBUTORS.md file was modified in this PR." - # else - # echo "modified=false" >> $GITHUB_OUTPUT - # echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." - # fi - # fi - - # -- Step 3: Manage PR Labels, Comments, and Final Status (Consolidated) + # Clean up the localised workspace to keep the step stateless + rm -f base_raw.txt pr_raw.txt base_clean.txt pr_clean.txt + fi + # - name: Check if CONTRIBUTORS.md was modified in PR + # id: check_contributors_modified + # shell: bash + # env: + # # This token is safely scoped to job-level permissions + # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # run: | + # if [ "$merge_ref_defined" = "false" ]; then + # echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." + # echo "modified=false" >> "$GITHUB_OUTPUT" + # else + # # Call GitHub Files API directly to check PR modifications without + # # running local network git fetches + # # Filtering precisely for the CONTRIBUTORS.md file signature + # has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) + + # if [ -n "$has_modification" ]; then + # echo "modified=true" >> "$GITHUB_OUTPUT" + # echo "📝 CONTRIBUTORS.md file was modified in this PR." + # else + # echo "modified=false" >> "$GITHUB_OUTPUT" + # echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." + # fi + # fi + + # -- Manage PR Labels, Comments, and Final Status (Consolidated) - name: Manage CLA Status, Labels, and Comments uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 # Using 'always()' here so this step runs regardless of previous From ce36a2be73d31cee60967b2796588d56a86445b2 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:09:08 +0100 Subject: [PATCH 143/165] Enhance CONTRIBUTORS.md check in CLA workflow to improve handling of private forks and streamline file comparison --- .github/workflows/cla-check.yaml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index e539c59..d87ca9e 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -133,28 +133,27 @@ jobs: # This token is safely scoped to job-level permissions GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BASE_REF: ${{ github.event.pull_request.base.ref }} - HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} - HEAD_REF: ${{ github.event.pull_request.head.ref }} + # Use the safe PR number context to completely avoid the private fork 404 + PR_NUMBER: ${{ github.event.number }} run: | if [ "$merge_ref_defined" = "false" ]; then echo "::warning::Merge conflicts detected. The CONTRIBUTORS.md modification check was skipped, but the workflow has been allowed to pass. Please resolve conflicts." echo "modified=false" >> "$GITHUB_OUTPUT" else - # 1. Fetch raw CONTRIBUTORS.md from the base branch path + # 1. Fetch raw CONTRIBUTORS.md from the base branch path securely gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ --jq '.content' | base64 -d > base_raw.txt 2>/dev/null || touch base_raw.txt - # 2. Fetch raw CONTRIBUTORS.md from the incoming PR head branch path - gh api "repos/$HEAD_REPO/contents/CONTRIBUTORS.md?ref=$HEAD_REF" \ + # 2. Fetch CONTRIBUTORS.md from the repository using the unified PR head reference. + # This completely bypasses the private fork 404 authentication barrier. + gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=refs/pull/$PR_NUMBER/head" \ --jq '.content' | base64 -d > pr_raw.txt 2>/dev/null || touch pr_raw.txt - # 3. Clean and normalise files completely using pure shell primitives - # This strips all spaces, tabs, newlines, and carriage returns, squashing the file into a single string + # 3. Clean and normalise files using pure shell primitives tr -d '[:space:]' < base_raw.txt > base_clean.txt 2>/dev/null || touch base_clean.txt tr -d '[:space:]' < pr_raw.txt > pr_clean.txt 2>/dev/null || touch pr_clean.txt # 4. Perform a direct text comparison on the squashed data - # cmp returns exit code 0 if identical, or 1 if any content characters differ if ! cmp -s base_clean.txt pr_clean.txt; then echo "modified=true" >> "$GITHUB_OUTPUT" echo "📝 CONTRIBUTORS.md content was substantively modified (additions or deletions detected)." @@ -163,7 +162,7 @@ jobs: echo "ℹ️ CONTRIBUTORS.md content was NOT substantively modified (whitespace only or unchanged)." fi - # Clean up the localised workspace to keep the step stateless + # Clean up the localised workspace rm -f base_raw.txt pr_raw.txt base_clean.txt pr_clean.txt fi # - name: Check if CONTRIBUTORS.md was modified in PR From 52765d3b665274ef4c6565d445970a37a14a12c5 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:52:44 +0100 Subject: [PATCH 144/165] Refactor permissions scope in Sphinx Docs workflow --- .github/workflows/sphinx-docs.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 7409492..1d76543 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -88,17 +88,19 @@ on: description: 'URL of the deployed GitHub Pages site' value: ${{ jobs.build-and-deploy.outputs.pages-url }} -permissions: - contents: read - pages: write # Required for deploying to GitHub Pages - id-token: write # Required for authentication in uv sync when using pyproject.toml +permissions: {} jobs: build-and-deploy: + name: Build and Deploy runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout-minutes }} outputs: pages-url: ${{ steps.deployment.outputs.page_url }} + permissions: + contents: read + pages: write # Required for deploying to GitHub Pages + id-token: write # Required for authentication in uv sync when using pyproject.toml env: PYTHON_VRSN: ${{ inputs.python-version }} VENV_PATH: ${{ inputs.venv-path }} From 838b2d9f102a2eb6f2370bb87753d341864573f9 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:04:27 +0100 Subject: [PATCH 145/165] Fix code injection via template expansion --- .github/workflows/cla-check.yaml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index d87ca9e..9bcef3d 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -18,12 +18,14 @@ on: type: string default: 'https://github.com/MetOffice/Momentum/blob/main/CLA.md' -permissions: - contents: read - pull-requests: write # Required to add labels and comments +permissions: {} jobs: check-cla: + name: Manage CLA + permissions: + contents: read + pull-requests: write # Required to add labels and comments runs-on: ${{ inputs.runner }} steps: - name: Checkout Base Branch @@ -105,10 +107,12 @@ jobs: shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.number }} run: | # Use gh api to query pull request metadata directly # without network git ls-remote authentication - mergeable=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.number }} --jq ".mergeable" 2>/dev/null || echo "undefined") + mergeable=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq ".mergeable" 2>/dev/null || echo "undefined") if [ "$mergeable" != "undefined" ]; then echo "merge_ref_defined=true" >> $GITHUB_ENV else @@ -132,6 +136,7 @@ jobs: env: # This token is safely scoped to job-level permissions GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPOSITORY: ${{ github.repository }} BASE_REF: ${{ github.event.pull_request.base.ref }} # Use the safe PR number context to completely avoid the private fork 404 PR_NUMBER: ${{ github.event.number }} @@ -141,12 +146,12 @@ jobs: echo "modified=false" >> "$GITHUB_OUTPUT" else # 1. Fetch raw CONTRIBUTORS.md from the base branch path securely - gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ + gh api "repos/$REPOSITORY/contents/CONTRIBUTORS.md?ref=$BASE_REF" \ --jq '.content' | base64 -d > base_raw.txt 2>/dev/null || touch base_raw.txt # 2. Fetch CONTRIBUTORS.md from the repository using the unified PR head reference. # This completely bypasses the private fork 404 authentication barrier. - gh api "repos/${{ github.repository }}/contents/CONTRIBUTORS.md?ref=refs/pull/$PR_NUMBER/head" \ + gh api "repos/$REPOSITORY/contents/CONTRIBUTORS.md?ref=refs/pull/$PR_NUMBER/head" \ --jq '.content' | base64 -d > pr_raw.txt 2>/dev/null || touch pr_raw.txt # 3. Clean and normalise files using pure shell primitives From ccc1b257ccac1c569f45d880c6701ae3641a4b0c Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:08:30 +0100 Subject: [PATCH 146/165] Refactor permissions structure in track-review-project workflow --- .github/workflows/track-review-project.yaml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index cc77199..84a2fa7 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -26,15 +26,17 @@ on: type: number default: 376 # Simulation Systems Review Tracker -permissions: - actions: read - contents: read - pull-requests: write +permissions: {} jobs: track_project: + name: Track Review Project runs-on: ${{ inputs.runner }} timeout-minutes: 5 + permissions: + actions: read # Required to query the artifact lists + contents: read # Required to check out code or parse files safely + pull-requests: write # Required to assign authors or append metadata notes directly on the pull request target env: REPO: ${{ github.repository }} From 6b34550a25f1c20086db75ca49de03a60c75fdf8 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:18:31 +0100 Subject: [PATCH 147/165] Refactor label management --- .github/workflows/label-pr.yaml | 6 ++++++ labeler/action.yaml | 23 ++++++++++++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/label-pr.yaml b/.github/workflows/label-pr.yaml index 111565a..24bf4bf 100644 --- a/.github/workflows/label-pr.yaml +++ b/.github/workflows/label-pr.yaml @@ -9,10 +9,16 @@ name: Label PR on: workflow_call: +permissions: {} + jobs: label: + name: Label runs-on: ubuntu-latest if: ${{ github.event.pull_request.head.repo.fork }} + permissions: + contents: read # Required for checking out code or referencing internal workflow actions + pull-requests: write # Required for the composite action to add or remove labels on the PR steps: - name: Add label `contributor` if: ${{ github.event.action == 'opened' }} diff --git a/labeler/action.yaml b/labeler/action.yaml index 9d367e4..6c46d26 100644 --- a/labeler/action.yaml +++ b/labeler/action.yaml @@ -36,9 +36,12 @@ runs: steps: - name: Validate inputs shell: python + env: + ACTION: ${{ inputs.action }} run: | + import os action_opts = ["remove", "add"] - action = "${{ inputs.action }}" + action = os.environ["ACTION"] if action not in action_opts: raise ValueError(f"Action has to be one of {action_opts}.") @@ -47,23 +50,29 @@ runs: shell: bash run: | set -e - if gh issue --repo "${{ inputs.repository }}" view "${{ inputs.issue }}" > /dev/null 2>&1; then - gh issue --repo "${{ inputs.repository }}" edit "${{ inputs.issue }}" --remove-label "${{ inputs.label }}" + if gh issue --repo "$REPOSITORY" view "$ISSUE" > /dev/null 2>&1; then + gh issue --repo "$REPOSITORY" edit "$ISSUE" --remove-label "$LABEL" else - gh pr --repo "${{ inputs.repository }}" edit "${{ inputs.issue }}" --remove-label "${{ inputs.label }}" + gh pr --repo "$REPOSITORY" edit "$ISSUE" --remove-label "$LABEL" fi env: GH_TOKEN: ${{ inputs.token }} + REPOSITORY: ${{ inputs.repository }} + ISSUE: ${{ inputs.issue }} + LABEL: ${{ inputs.label }} - name: Add label `${{ inputs.label }}` to issue ${{ inputs.issue }} if: ${{ inputs.action == 'add' }} shell: bash run: | set -e - if gh issue --repo "${{ inputs.repository }}" view "${{ inputs.issue }}" > /dev/null 2>&1; then - gh issue --repo "${{ inputs.repository }}" edit "${{ inputs.issue }}" --add-label "${{ inputs.label }}" + if gh issue --repo "$REPOSITORY" view "$ISSUE" > /dev/null 2>&1; then + gh issue --repo "$REPOSITORY" edit "$ISSUE" --add-label "$LABEL" else - gh pr --repo "${{ inputs.repository }}" edit "${{ inputs.issue }}" --add-label "${{ inputs.label }}" + gh pr --repo "$REPOSITORY" edit "$ISSUE" --add-label "$LABEL" fi env: GH_TOKEN: ${{ inputs.token }} + REPOSITORY: ${{ inputs.repository }} + ISSUE: ${{ inputs.issue }} + LABEL: ${{ inputs.label }} From 2b2a7906c4faf96242e166cf428d77a511bcf519 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:06:09 +0100 Subject: [PATCH 148/165] zizmor pedantic compliance --- .github/workflows/build-sphinx-docs.yaml | 7 +++++++ .github/workflows/check-cr-approved.yaml | 11 ++++++++++- .github/workflows/cla-check.yaml | 4 ++-- .github/workflows/deploy-sphinx-docs.yaml | 11 ++++++++--- .github/workflows/fortran-lint.yaml | 4 ++++ .github/workflows/trigger-project-workflow.yaml | 1 + .github/workflows/umdp3_fixer.yaml | 7 ++++++- .github/workflows/validate.yaml | 5 +++-- .github/workflows/validate_symlinks.yaml | 4 +++- 9 files changed, 44 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build-sphinx-docs.yaml b/.github/workflows/build-sphinx-docs.yaml index fb06f94..90972c5 100644 --- a/.github/workflows/build-sphinx-docs.yaml +++ b/.github/workflows/build-sphinx-docs.yaml @@ -40,11 +40,18 @@ on: type: number default: 5 +permissions: + contents: read + jobs: build-sphinx-docs: name: Build Sphinx Docs runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout }} + permissions: + contents: read # Required to read repository file configurations + pages: write # Required to deploy to GitHub Pages + env: DOCS_DIR: ${{ inputs.docs-directory }} REQS_FILE: ${{ inputs.requirements }} diff --git a/.github/workflows/check-cr-approved.yaml b/.github/workflows/check-cr-approved.yaml index 1ad5afc..833be05 100644 --- a/.github/workflows/check-cr-approved.yaml +++ b/.github/workflows/check-cr-approved.yaml @@ -9,10 +9,13 @@ name: Check CR Approval on: workflow_call: -permissions: read-all +permissions: + contents: read # Required to evaluate metadata from the triggering GitHub event context + pull-requests: read # Required to check the PR reviews and approval status jobs: cr_check: + name: CR Approval runs-on: ubuntu-24.04 timeout-minutes: 2 @@ -24,6 +27,12 @@ jobs: REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} run: | + # Ensure context variables are present, don't run from a non-PR event + if [[ -z "$PR_NUMBER" ]]; then + echo "::error::This workflow must be triggered by a pull_request event." + exit 1 + fi + echo "Running on PR #$PR_NUMBER in Repository $REPO" # -- 1. Extract the assigned Code Reviewer from the PR body diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 9bcef3d..0e5ddca 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -21,8 +21,8 @@ on: permissions: {} jobs: - check-cla: - name: Manage CLA + manage-cla: + name: check-cla permissions: contents: read pull-requests: write # Required to add labels and comments diff --git a/.github/workflows/deploy-sphinx-docs.yaml b/.github/workflows/deploy-sphinx-docs.yaml index 5b576b4..4a062f4 100644 --- a/.github/workflows/deploy-sphinx-docs.yaml +++ b/.github/workflows/deploy-sphinx-docs.yaml @@ -19,15 +19,20 @@ on: required: false type: number default: 5 + +permissions: + contents: read + jobs: deploy-sphinx-docs: + name: Deploy Sphinx Docs runs-on: ${{ inputs.runner }} environment: name: github-pages permissions: - contents: read - pages: write - id-token: write + contents: read # Required to read repository file configurations + pages: write # Required to deploy to GitHub Pages + id-token: write # Required to authenticate with GitHub Pages steps: - name: Configure GitHub Pages id: configure diff --git a/.github/workflows/fortran-lint.yaml b/.github/workflows/fortran-lint.yaml index 859a130..b4b8c8e 100644 --- a/.github/workflows/fortran-lint.yaml +++ b/.github/workflows/fortran-lint.yaml @@ -68,6 +68,10 @@ on: type: boolean default: false +# Global baseline configuration: prevents default read/write fallback permissions +permissions: + contents: read # Required to check out code and scan files using fortitude linter + jobs: fortran-linting: name: Fortran diff --git a/.github/workflows/trigger-project-workflow.yaml b/.github/workflows/trigger-project-workflow.yaml index 59662d2..180e84f 100644 --- a/.github/workflows/trigger-project-workflow.yaml +++ b/.github/workflows/trigger-project-workflow.yaml @@ -16,6 +16,7 @@ permissions: {} jobs: trigger_project: + name: Trigger Project runs-on: ubuntu-latest timeout-minutes: 5 permissions: diff --git a/.github/workflows/umdp3_fixer.yaml b/.github/workflows/umdp3_fixer.yaml index d7298e9..beacc91 100644 --- a/.github/workflows/umdp3_fixer.yaml +++ b/.github/workflows/umdp3_fixer.yaml @@ -20,10 +20,12 @@ on: type: number default: 10 -permissions: read-all +permissions: + contents: read jobs: umdp3_fixer: + name: UMDP3 Fixer runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout }} @@ -34,6 +36,7 @@ jobs: path: pr_branch token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false + - name: Checkout SimSys_Scripts uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v 6.0.3 with: @@ -41,10 +44,12 @@ jobs: sparse-checkout: umdp3_fixer path: SimSys_Scripts persist-credentials: false + - name: Set up Python 3.14 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: 3.14 + - name: Run UMDP3 Fixer working-directory: ./SimSys_Scripts/umdp3_fixer run: | diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index a28f7f9..8749807 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -16,10 +16,10 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} -permissions: read-all +permissions: + contents: read jobs: - validate: name: QC runs-on: ubuntu-24.04 @@ -58,6 +58,7 @@ jobs: - name: Python Lint if: always() run: uv run ruff check --respect-gitignore . + - name: Python Format Check if: always() run: uv run ruff format --check --diff -s --respect-gitignore . diff --git a/.github/workflows/validate_symlinks.yaml b/.github/workflows/validate_symlinks.yaml index 12d8c75..4c9ba90 100644 --- a/.github/workflows/validate_symlinks.yaml +++ b/.github/workflows/validate_symlinks.yaml @@ -20,10 +20,12 @@ on: type: number default: 5 -permissions: read-all +permissions: + contents: read jobs: validate_symlinks: + name: Validate Symlinks runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout }} From 916299fbef397ee7777ffc33a12db53a4132c54b Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:16:16 +0100 Subject: [PATCH 149/165] Update linked README files --- cla-check/README.md | 17 ++++++++++++++--- track-review-project/README.md | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/cla-check/README.md b/cla-check/README.md index 948bd30..d53c6f2 100644 --- a/cla-check/README.md +++ b/cla-check/README.md @@ -103,12 +103,14 @@ To sign the CLA for your first contribution: ## Permissions Required -This workflow requires the following permissions: +Permissions are managed internally at the job level. The reusable workflow sets +`permissions: {}` at the workflow level and grants only the minimum required +permissions directly to the job: ```yaml permissions: - contents: read # To checkout repository code - pull-requests: write # To add labels and post comments + contents: read # To checkout repository code + pull-requests: write # To add labels and post comments ``` ## Security Considerations @@ -122,6 +124,15 @@ permissions: - **Validates username format** to prevent malicious input - **Never checks out untrusted code** - only inspects CONTRIBUTORS.md file - **Scoped file checking** - only monitors the exact CONTRIBUTORS.md file +- **Template injection prevention** - all `github.*` and `inputs.*` context + values are passed via `env:` vars and read through `process.env` / shell + variables rather than being interpolated directly into scripts +- **API-based merge-ref detection** - uses `gh api` to query PR mergeability + instead of `git ls-remote`, avoiding unauthenticated git network calls from + forks +- **GitHub Contents API for CONTRIBUTORS.md comparison** - fetches and compares + file content via the GitHub API with base64 decoding and whitespace + normalisation, avoiding authenticated git fetches from private fork contexts ## Licence diff --git a/track-review-project/README.md b/track-review-project/README.md index ab41186..27de645 100644 --- a/track-review-project/README.md +++ b/track-review-project/README.md @@ -34,7 +34,7 @@ jobs: secrets: inherit # Optional inputs (with default values) with: - runner: "ubuntu-22.04" + runner: "ubuntu-24.04" project_org: "MetOffice" project_number: 376 From acdad39676b25cfeb5ca0d3a64f3bc17b87e6cfc Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:27:32 +0100 Subject: [PATCH 150/165] Fix formatting in README.md for permissions section --- cla-check/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cla-check/README.md b/cla-check/README.md index d53c6f2..19e400e 100644 --- a/cla-check/README.md +++ b/cla-check/README.md @@ -109,7 +109,7 @@ permissions directly to the job: ```yaml permissions: - contents: read # To checkout repository code + contents: read # To checkout repository code pull-requests: write # To add labels and post comments ``` From 07d5a66f130f83acc74ed031d65206b4ffc7e94f Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 9 Jun 2026 23:45:51 +0100 Subject: [PATCH 151/165] Enhance workflow permissions --- .github/workflows/track-review-project.yaml | 9 +++++++-- .github/workflows/trigger-project-workflow.yaml | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 84a2fa7..c5850f2 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -25,8 +25,13 @@ on: required: false type: number default: 376 # Simulation Systems Review Tracker + secrets: + PROJECT_ACTION_PAT: + description: 'The PAT used to authorize changes to Organization Project Board 376' + required: true -permissions: {} +permissions: + contents: read # Required to check out code or parse files safely jobs: track_project: @@ -59,7 +64,7 @@ jobs: }); let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { - return artifact.name == "context.json" + return artifact.name == "context" })[0]; let download = await github.rest.actions.downloadArtifact({ diff --git a/.github/workflows/trigger-project-workflow.yaml b/.github/workflows/trigger-project-workflow.yaml index 180e84f..9a5ae13 100644 --- a/.github/workflows/trigger-project-workflow.yaml +++ b/.github/workflows/trigger-project-workflow.yaml @@ -12,7 +12,8 @@ name: Trigger Review Project on: workflow_call: -permissions: {} +permissions: + contents: read # Required to evaluate metadata from the triggering GitHub event context jobs: trigger_project: @@ -31,6 +32,6 @@ jobs: echo "{\"pr_num\": $PR_NUM, \"pr_id\": \"$PR_ID\"}" >> context.json - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: context.json + name: context path: context.json retention-days: 1 From 3dc3aff16515d528c170cbbd6a8a8309b7e43103 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:34:37 +0100 Subject: [PATCH 152/165] Refactor PR reviewer extraction and project metadata handling for improved safety and clarity --- .github/workflows/track-review-project.yaml | 45 +++++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index c5850f2..580a678 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -101,9 +101,16 @@ jobs: # Extract reviewers from PR body pr_body=$(echo "$pr_data" | jq -r '.body') - sr_expected=$( (echo $pr_body | grep -iq "Sci/Tech Review" && echo "True") || echo "False") - scitech_reviewer=$(echo "$pr_body" | perl -00 -ne 's///gs; print if(s/.*Sci\/Tech Reviewer:\s*@([\w\-]{1,39})\b.*/\1\n/s);') || true - code_reviewer=$(echo "$pr_body" | perl -00 -ne 's///gs; print if(s/.*Code Reviewer:\s*@([\w\-]{1,39})\b.*/\1\n/s);') || true + + # Using safe Here-strings to preserve multi-line content + if grep -iq "Sci/Tech Review" <<< "$pr_body"; then + sr_expected="True" + else + sr_expected="False" + fi + # Pass string via Here-strings to prevent line skipping or shell expression leaks + scitech_reviewer=$(perl -00 -ne 's///gs; print if(s/.*Sci\/Tech Reviewer:\s*@([\w\-]{1,39})\b.*/\1\n/s);' <<< "$pr_body") || true + code_reviewer=$(perl -00 -ne 's///gs; print if(s/.*Code Reviewer:\s*@([\w\-]{1,39})\b.*/\1\n/s);' <<< "$pr_body") || true echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" echo "Expected Code Reviewer: ${code_reviewer:-None}" @@ -199,7 +206,7 @@ jobs: PROJECT_ORG: ${{ inputs.project_org }} PROJECT_NUMBER: ${{ inputs.project_number }} run: | - # Get project data + # Get project metadata project_data=$(gh api graphql -f query=' query($org: String!, $number: Int!) { organization(login: $org){ @@ -215,13 +222,22 @@ jobs: } }' -f org="$PROJECT_ORG" -F number="$PROJECT_NUMBER") - project_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.id') + project_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.id // empty') + if [ -z "$project_id" ]; then + echo "::error::Could not resolve Project ID. Check PAT permissions or Project Number." + exit 1 + fi + status_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id') status_option_id=$(echo "$project_data" | jq -r --arg status "$REQUIRED_STATE" '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name==$status) | .id') scitech_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "SciTech Review") | .id') code_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "Code Review") | .id') - # Add PR to project and update fields + # Map reviewers safely to prevent raw GraphQL empty text payload crashes + final_cr_val="${CODE_REVIEWER:-None}" + final_sr_val="${SCITECH_REVIEWER:-None}" + + # Securely add PR link element to board layout item_id=$(gh api graphql -f query=' mutation($project:ID!, $pr:ID!) { addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) { @@ -229,10 +245,23 @@ jobs: } }' -f project="$project_id" -f pr="$PR_ID" --jq '.data.addProjectV2ItemById.item.id') - # Update all fields in single mutation + if [[ -z "$item_id" ]]; then + echo "::warning::Could not resolve Project Board Item ID. PR may already be tracked." + exit 0 + fi + + # Commit all updates via single atomic multi-mutation to prevent partial update states on the board gh api graphql -f query=' mutation($project: ID!, $item: ID!, $status_field: ID!, $status_value: String!, $cr_field: ID!, $cr_value: String!, $sr_field: ID!, $sr_value: String!) { set_status: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $status_field, value: {singleSelectOptionId: $status_value}}) { projectV2Item { id } } set_code_review: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $cr_field, value: {text: $cr_value}}) { projectV2Item { id } } set_scitech_review: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $sr_field, value: {text: $sr_value}}) { projectV2Item { id } } - }' -f project="$project_id" -f item="$item_id" -f status_field="$status_field_id" -f status_value="$status_option_id" -f cr_field="$code_field_id" -f cr_value="$CODE_REVIEWER" -f sr_field="$scitech_field_id" -f sr_value="$SCITECH_REVIEWER" --silent + }' \ + -f project="$project_id" \ + -f item="$item_id" \ + -f status_field="$status_field_id" \ + -f status_value="$status_option_id" \ + -f cr_field="$code_field_id" \ + -f cr_value="$final_cr_val" \ + -f sr_field="$scitech_field_id" \ + -f sr_value="$final_sr_val" --silent From 499779a3aa213658d325685f04c588e9d02e2459 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:56:46 +0100 Subject: [PATCH 153/165] Update README.md to clarify project entry handling and enhance permissions for workflows --- track-review-project/README.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/track-review-project/README.md b/track-review-project/README.md index 27de645..f958e4b 100644 --- a/track-review-project/README.md +++ b/track-review-project/README.md @@ -1,7 +1,7 @@ # Track Review Project Action -This action modifies the Simulation Systems Review Tracker project entries in -pull requests. It will automatically fill the reviewer fields based on the +This action modifies the Simulation Systems Review Tracker project (376) entries +in pull requests. It will automatically fill the reviewer fields based on the entries in the pull request template. It will change the state based on reviews requested and performed. If a CR hasn't been requested, it will do so when moving to the CR state. It will also add the PR author as an assignee if none @@ -28,10 +28,18 @@ on: - completed workflow_dispatch: +permissions: + contents: read + jobs: track_review_project: + permissions: + actions: read + contents: read + pull-requests: write uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main - secrets: inherit + secrets: + PROJECT_ACTION_PAT: ${{ secrets.PROJECT_ACTION_PAT }} # Optional inputs (with default values) with: runner: "ubuntu-24.04" @@ -49,8 +57,16 @@ on: pull_request_review: pull_request_review_comment: +permissions: + contents: read + jobs: trigger_project_workflow: - uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main - secrets: inherit + # Only run on upstream repository + if: github.repository == 'MetOffice/' + permissions: + actions: read # Required to evaluate the event trigger metadata safely + contents: read # Required to check out code or parse files safely + pull-requests: write # Required for the downstream engine to add comments or update metadata on the PR + uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main ``` From a1c1b0fb42f54a8fda60ecdb770dada0f3264e82 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:43:31 +0100 Subject: [PATCH 154/165] Update workflows and dependencies for improved validation and linting - Set 'requirements' in build-sphinx-docs.yaml to optional - Update output redirection syntax in cla-check.yaml, sphinx-docs.yaml, and track-review-project.yaml for consistency - Add Actionlint, Zizmor, and Markdown Lint steps to validate workflows in validate.yaml - Change shell from Python to Bash in labeler action for input validation - Update pyproject.toml to include new dependencies and configure Markdown Lint settings - Improve formatting and clarity in README.md --- .github/workflows/build-sphinx-docs.yaml | 2 +- .github/workflows/cla-check.yaml | 16 ++++++------- .github/workflows/sphinx-docs.yaml | 4 ++-- .github/workflows/track-review-project.yaml | 26 ++++++++++++--------- .github/workflows/validate.yaml | 13 +++++++++++ README.md | 17 +++++++++----- labeler/action.yaml | 11 ++++----- pyproject.toml | 14 +++++++++++ 8 files changed, 69 insertions(+), 34 deletions(-) diff --git a/.github/workflows/build-sphinx-docs.yaml b/.github/workflows/build-sphinx-docs.yaml index 90972c5..f20ddd9 100644 --- a/.github/workflows/build-sphinx-docs.yaml +++ b/.github/workflows/build-sphinx-docs.yaml @@ -16,7 +16,7 @@ on: default: 'documentation' requirements: description: 'Path to the requirements file to install dependencies from' - required: true + required: false type: string default: 'requirements.txt' sphinx-opts: diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 0e5ddca..c89831a 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -61,15 +61,15 @@ jobs: AUTHOR="$VALIDATED_AUTHOR" if [ -f "CONTRIBUTORS.md" ]; then if grep -qP "^\s*\|\s*\Q$AUTHOR\E\s*\|" CONTRIBUTORS.md; then - echo "on_base=true" >> $GITHUB_OUTPUT + echo "on_base=true" >> "$GITHUB_OUTPUT" echo "🎉 $AUTHOR has already signed the CLA on base branch." else - echo "on_base=false" >> $GITHUB_OUTPUT + echo "on_base=false" >> "$GITHUB_OUTPUT" echo "⚠️ $AUTHOR not on base. Proceeding to check PR branch." fi else # If CONTRIBUTORS.md file doesn't exist, we must check PR branch - echo "on_base=undefined" >> $GITHUB_OUTPUT + echo "on_base=undefined" >> "$GITHUB_OUTPUT" echo "🔴 CONTRIBUTORS.md file does not exist on base. Proceeding to check PR branch." fi @@ -90,14 +90,14 @@ jobs: if [ -f "CONTRIBUTORS.md" ]; then # if grep -qE "\|\s*$AUTHOR\s*\|" CONTRIBUTORS.md; then if grep -qP "^\s*\|\s*\Q$AUTHOR\E\s*\|" CONTRIBUTORS.md; then - echo "on_pr=true" >> $GITHUB_OUTPUT + echo "on_pr=true" >> "$GITHUB_OUTPUT" echo "✅ $AUTHOR is in the CONTRIBUTORS.md file on PR branch." else - echo "on_pr=false" >> $GITHUB_OUTPUT + echo "on_pr=false" >> "$GITHUB_OUTPUT" echo "⚠️ $AUTHOR is not in the CONTRIBUTORS.md file on PR branch." fi else - echo "on_pr=undefined" >> $GITHUB_OUTPUT + echo "on_pr=undefined" >> "$GITHUB_OUTPUT" echo "🔴 CONTRIBUTORS.md file does not exist on PR branch." fi @@ -114,9 +114,9 @@ jobs: # without network git ls-remote authentication mergeable=$(gh api "repos/$REPOSITORY/pulls/$PR_NUMBER" --jq ".mergeable" 2>/dev/null || echo "undefined") if [ "$mergeable" != "undefined" ]; then - echo "merge_ref_defined=true" >> $GITHUB_ENV + echo "merge_ref_defined=true" >> "$GITHUB_ENV" else - echo "merge_ref_defined=false" >> $GITHUB_ENV + echo "merge_ref_defined=false" >> "$GITHUB_ENV" fi - name: Checkout Merge Branch diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 1d76543..5fc94e1 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -208,7 +208,7 @@ jobs: ${{ runner.os }}-${{ env.PYTHON_VRSN }}- - name: Create virtual environment - run: uv venv --python ${PYTHON_VRSN} --allow-existing ${VENV_PATH} + run: uv venv --python "${PYTHON_VRSN}" --allow-existing "${VENV_PATH}" - name: Install dependencies from conf.py (extensions + html_theme) if: steps.detect-deps.outputs.source == 'conf' @@ -285,7 +285,7 @@ jobs: run: echo "${GITHUB_WORKSPACE}/${VENV_PATH}/bin" >> "$GITHUB_PATH" - name: Lint Sphinx docs - run: sphinx-lint ${SOURCE_DIR} + run: sphinx-lint "${SOURCE_DIR}" - name: Detect Makefile in docs directory id: detect-makefile diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 580a678..3d1b394 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -80,7 +80,7 @@ jobs: - name: Extract PR Data run: | unzip -q context.zip - jq -r '"PR_NUM=\(.pr_num)\nPR_ID=\(.pr_id)"' context.json >> $GITHUB_ENV + jq -r '"PR_NUM=\(.pr_num)\nPR_ID=\(.pr_id)"' context.json >> "$GITHUB_ENV" - name: Assign Author if no assignees continue-on-error: true @@ -114,17 +114,21 @@ jobs: echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" echo "Expected Code Reviewer: ${code_reviewer:-None}" - echo "SCITECH_REVIEWER=$scitech_reviewer" >> $GITHUB_ENV - echo "CODE_REVIEWER=$code_reviewer" >> $GITHUB_ENV + { + echo "SCITECH_REVIEWER=$scitech_reviewer" + echo "CODE_REVIEWER=$code_reviewer" + } >> "$GITHUB_ENV" # If the PR is in Draft, then don't request reviews and set state as "In Progress" isDraft=$(echo "$pr_data" | jq -r '.isDraft') if [[ "$isDraft" == "true" ]]; then echo "PR is still in draft. Not requesting reviews or moving from In Progress" required_state="In Progress" - echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV - echo "SR_REQUEST_REQUIRED=false" >> $GITHUB_ENV - echo "CR_REQUEST_REQUIRED=false" >> $GITHUB_ENV + { + echo "REQUIRED_STATE=$required_state" + echo "SR_REQUEST_REQUIRED=false" + echo "CR_REQUEST_REQUIRED=false" + } >> "$GITHUB_ENV" exit 0 fi @@ -133,7 +137,7 @@ jobs: state=$(echo "$pr_data" | jq -r '.state') if [[ "$state" != "OPEN" ]]; then required_state="Done" - echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + echo "REQUIRED_STATE=$required_state" >> "$GITHUB_ENV" exit 0 fi @@ -149,7 +153,7 @@ jobs: cr_approved=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and .state == "APPROVED")] | length') cr_changes=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') - [[ "$sr_expected" == "True" ]] && [[ ! -z "$scitech_reviewer" ]] && [[ "$sr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "SR_REQUEST_REQUIRED=true" >> $GITHUB_ENV + [[ "$sr_expected" == "True" ]] && [[ ! -z "$scitech_reviewer" ]] && [[ "$sr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "SR_REQUEST_REQUIRED=true" >> "$GITHUB_ENV" if [[ "$sr_expected" == "False" ]]; then echo "Not expecting a SciTech Review for this repository" @@ -165,13 +169,13 @@ jobs: else required_state="SciTech Review" fi - echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + echo "REQUIRED_STATE=$required_state" >> "$GITHUB_ENV" exit 0 fi # SR complete or trivial PR so move to code review states - [[ "$sr_approved" -gt 0 ]] && [[ ! -z "$code_reviewer" ]] && [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_changes" -eq 0 ]] && echo "CR_REQUEST_REQUIRED=true" >> $GITHUB_ENV + [[ "$sr_approved" -gt 0 ]] && [[ ! -z "$code_reviewer" ]] && [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_changes" -eq 0 ]] && echo "CR_REQUEST_REQUIRED=true" >> "$GITHUB_ENV" if [[ -z "$code_reviewer" ]] && [[ "$cr_changes" -eq 0 ]]; then required_state="In Progress" @@ -183,7 +187,7 @@ jobs: required_state="Code Review" fi - echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + echo "REQUIRED_STATE=$required_state" >> "$GITHUB_ENV" - name: Request SciTech review, if needed if: env.SR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'SciTech Review' && env.SCITECH_REVIEWER != '' diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 8749807..849759f 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -81,5 +81,18 @@ jobs: && echo "All checks passed!" continue-on-error: true + - name: Actionlint + if: always() + run: uv run actionlint -shellcheck "-S warning" .github/workflows/*.yaml + + - name: Zizmor (GitHub Actions security) + if: always() + run: uv run zizmor .github/workflows/ + + - name: Markdown Lint + if: always() + run: | + uv run rumdl check . + - name: Minimize uv cache run: uv cache prune --ci diff --git a/README.md b/README.md index 243bb9a..5ade82b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GRoWSS - GitHub Reusable Workflows for Simulation Systems :recycle: +# :recycle: GRoWSS - GitHub Reusable Workflows for Simulation Systems [![CI](https://github.com/MetOffice/growss/actions/workflows/validate.yaml/badge.svg?branch=main)](https://github.com/MetOffice/growss/actions/workflows/validate.yaml) @@ -15,13 +15,18 @@ checking your files before opening a pull request. For example, you can run ## Troubleshooting -This section intends to help users and developers quickly identify and resolve common issues which may be encountered while using the growss repository. If an issue which isn't listed here is found please notify us via discussions on the simulation systems [Q&A](https://github.com/MetOffice/simulation-systems/discussions/categories/q-a) channel. +This section intends to help users and developers quickly identify and resolve +common issues which may be encountered while using the growss repository. If an +issue which isn't listed here is found please notify us via discussions on the +simulation systems +[Q&A](https://github.com/MetOffice/simulation-systems/discussions/categories/q-a) +channel. ### YAML file being called from growss is not found -1. Navigate to the **Settings** of the repository initiating the growss workflow. +1. Navigate to the **Settings** of the repository initiating the growss + workflow. 2. Select **Actions** > **General** from the left-hand sidebar. 3. Scroll to the **Workflow permissions** section. -4. Select **Read and write permissions** to allow the workflow to access other repositories within the MetOffice organisation. - - +4. Select **Read and write permissions** to allow the workflow to access other + repositories within the MetOffice organisation. diff --git a/labeler/action.yaml b/labeler/action.yaml index 6c46d26..9d8dbe9 100644 --- a/labeler/action.yaml +++ b/labeler/action.yaml @@ -35,15 +35,14 @@ runs: using: composite steps: - name: Validate inputs - shell: python + shell: bash env: ACTION: ${{ inputs.action }} run: | - import os - action_opts = ["remove", "add"] - action = os.environ["ACTION"] - if action not in action_opts: - raise ValueError(f"Action has to be one of {action_opts}.") + if [[ "$ACTION" != "remove" && "$ACTION" != "add" ]]; then + echo "::error::Action has to be one of ['remove', 'add']." + exit 1 + fi - name: Remove label `${{ inputs.label }}` from issue ${{ inputs.issue }} if: ${{ inputs.action == 'remove' }} diff --git a/pyproject.toml b/pyproject.toml index 640237b..2a1f361 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,21 @@ description = "GitHub Reusable Workflows for Simulation Systems" readme = "README.md" requires-python = ">=3.12.9" dependencies = [ + "actionlint-py==1.7.12.24", "ruff==0.14.4", "shellcheck-py==0.11.0.1", "yamllint==1.37.1", + "rumdl==0.2.10", + "zizmor==1.25.2", ] + +[tool.rumdl] +respect-gitignore = true +exclude = [ + "pull_request_template.md", +] +[tool.rumdl.MD013] +line-length = 80 # Keeps normal paragraph text restricted to 80 chars +code-blocks = false # Disables the 80 char limit strictly for fenced code blocks +tables = false # Fixed: Disables the 80 char limit strictly for tables +headings = false From ba8edcb07068f77a3e57fb106cf0815a077d8c36 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:35:59 +0100 Subject: [PATCH 155/165] Enhance shell script detection in validation workflow with improved messaging and error handling --- .github/workflows/validate.yaml | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 849759f..6b12f96 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -67,18 +67,29 @@ jobs: if: always() shell: bash run: | + # Identify potential shell scripts by looking for files with typical + # shell script extensions (.sh, .bash, .etc) or no extension, while excluding + # files in version control directories and virtual environments. mapfile -d '' potential_files < <( find . -type f \( -name "*.*sh" -o ! -name "*.*" \) \ -not -path "*.git/*" -not -path "*.venv/*" -print0 ) if [ ${#potential_files[@]} -eq 0 ]; then - echo "No shell scripts to check." + echo "::info::No files found with matching pattern to inspect." exit 0 fi - printf "%s\0" "${potential_files[@]}" \ - | xargs -0 file | grep "shell script" | cut -d: -f1 \ - | xargs -r uv run shellcheck -S warning \ - && echo "All checks passed!" + + # Extract shell scripts into an array, safely handling grep exit codes + mapfile -t shell_files < <(printf "%s\0" "${potential_files[@]}" \ + | xargs -0 file | grep "shell script" | cut -d: -f1 || true) + + if [ ${#shell_files[@]} -eq 0 ]; then + echo "::info::No shell scripts detected." + exit 0 + fi + + # Run shellcheck on discoverd files, treating warnings as errors + uv run shellcheck -S warning "${shell_files[@]}" && echo "::info::All checks passed!" continue-on-error: true - name: Actionlint From c97548ba70e1cb081dad3ed10152fd6066273523 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:41:01 +0100 Subject: [PATCH 156/165] Improve shell script detection messaging in validation workflow --- .github/workflows/validate.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 6b12f96..d3ddff9 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -75,7 +75,7 @@ jobs: -not -path "*.git/*" -not -path "*.venv/*" -print0 ) if [ ${#potential_files[@]} -eq 0 ]; then - echo "::info::No files found with matching pattern to inspect." + echo "::notice::No files found with matching pattern to inspect." exit 0 fi @@ -84,12 +84,12 @@ jobs: | xargs -0 file | grep "shell script" | cut -d: -f1 || true) if [ ${#shell_files[@]} -eq 0 ]; then - echo "::info::No shell scripts detected." + echo "::notice::No shell scripts detected." exit 0 fi # Run shellcheck on discoverd files, treating warnings as errors - uv run shellcheck -S warning "${shell_files[@]}" && echo "::info::All checks passed!" + uv run shellcheck -S warning "${shell_files[@]}" && echo "✅ All checks passed!" continue-on-error: true - name: Actionlint From 5749f74c46af0803c16a84fc8398ecf6c640600d Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:17:14 +0100 Subject: [PATCH 157/165] Empty Commit From 83fdb518d9ab2b0048700e45727d954f25277149 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:50:27 +0100 Subject: [PATCH 158/165] Update workflows to use actions/checkout@v7.0.0 and adjust permissions in YAML files --- .github/workflows/build-sphinx-docs.yaml | 2 +- .github/workflows/call-track-review-project.yaml | 4 ++-- .github/workflows/call-trigger-project-workflow.yaml | 3 ++- .github/workflows/cla-check.yaml | 6 +++--- .github/workflows/fortran-lint.yaml | 2 +- .github/workflows/sphinx-docs.yaml | 2 +- .github/workflows/track-review-project.yaml | 2 +- .github/workflows/umdp3_fixer.yaml | 4 ++-- .github/workflows/validate.yaml | 2 +- .github/workflows/validate_symlinks.yaml | 2 +- pyproject.toml | 2 +- 11 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build-sphinx-docs.yaml b/.github/workflows/build-sphinx-docs.yaml index f20ddd9..09e7bfc 100644 --- a/.github/workflows/build-sphinx-docs.yaml +++ b/.github/workflows/build-sphinx-docs.yaml @@ -59,7 +59,7 @@ jobs: BUILD_DIR: ${{ inputs.build-directory }} steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/call-track-review-project.yaml b/.github/workflows/call-track-review-project.yaml index 79f3f1f..dc34dab 100644 --- a/.github/workflows/call-track-review-project.yaml +++ b/.github/workflows/call-track-review-project.yaml @@ -23,7 +23,6 @@ jobs: ( github.event_name == 'workflow_run' && github.event.workflow_run.event == 'push' && - github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.conclusion == 'success' ) # Granular permissions are safely locked directly to this specific execution context @@ -31,7 +30,8 @@ jobs: actions: read # Required to view the status of the upstream triggered workflow run repository-projects: write # Required to add or mutate tasks inside GitHub Project 376 uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main - secrets: inherit + secrets: + PROJECT_ACTION_PAT: ${{ secrets.PROJECT_ACTION_PAT }} # Optional inputs (with default values) with: runner: "ubuntu-24.04" diff --git a/.github/workflows/call-trigger-project-workflow.yaml b/.github/workflows/call-trigger-project-workflow.yaml index a031fcd..3f46cde 100644 --- a/.github/workflows/call-trigger-project-workflow.yaml +++ b/.github/workflows/call-trigger-project-workflow.yaml @@ -18,5 +18,6 @@ jobs: trigger_project_workflow: permissions: contents: read # Required for checking out code or referencing internal workflow actions + pull-requests: write # Required to modify or comment on pull requests within this pipeline process uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main - secrets: inherit + # secrets: inherit diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index c89831a..79c02b4 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -29,7 +29,7 @@ jobs: runs-on: ${{ inputs.runner }} steps: - name: Checkout Base Branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false token: ${{ secrets.GITHUB_TOKEN }} @@ -74,7 +74,7 @@ jobs: fi - name: Checkout PR Branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false @@ -122,7 +122,7 @@ jobs: - name: Checkout Merge Branch if: env.merge_ref_defined == 'true' id: checkout_merge - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 continue-on-error: true with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/fortran-lint.yaml b/.github/workflows/fortran-lint.yaml index b4b8c8e..008e4ad 100644 --- a/.github/workflows/fortran-lint.yaml +++ b/.github/workflows/fortran-lint.yaml @@ -84,7 +84,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 5fc94e1..59d5fb2 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -126,7 +126,7 @@ jobs: run: echo "SPHINX_DEBUG=-v --show-traceback" >> "$GITHUB_ENV" - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 3d1b394..fe0a316 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -48,7 +48,7 @@ jobs: steps: # Required as running from the workflow_run trigger - name: Checkout repo - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/umdp3_fixer.yaml b/.github/workflows/umdp3_fixer.yaml index beacc91..85414e9 100644 --- a/.github/workflows/umdp3_fixer.yaml +++ b/.github/workflows/umdp3_fixer.yaml @@ -31,14 +31,14 @@ jobs: steps: - name: Checkout Branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: path: pr_branch token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false - name: Checkout SimSys_Scripts - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v 6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: repository: MetOffice/SimSys_Scripts sparse-checkout: umdp3_fixer diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index d3ddff9..53838cd 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -30,7 +30,7 @@ jobs: UV_PYTHON: "3.14" steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false diff --git a/.github/workflows/validate_symlinks.yaml b/.github/workflows/validate_symlinks.yaml index 4c9ba90..c478a5b 100644 --- a/.github/workflows/validate_symlinks.yaml +++ b/.github/workflows/validate_symlinks.yaml @@ -31,7 +31,7 @@ jobs: steps: - name: Checkout Branch - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} persist-credentials: false diff --git a/pyproject.toml b/pyproject.toml index 2a1f361..e5ec9b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ [tool.rumdl] respect-gitignore = true exclude = [ - "pull_request_template.md", + ".github/pull_request_template.md", ] [tool.rumdl.MD013] line-length = 80 # Keeps normal paragraph text restricted to 80 chars From 4e0a9f075d183fe6f3f25e0e77854e7c6dc9498d Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:12:35 +0100 Subject: [PATCH 159/165] Allow unsafe PR checkout for branches from forks in CLA workflow --- .github/workflows/cla-check.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 79c02b4..55ac8df 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -80,6 +80,7 @@ jobs: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} path: pr_branch + allow-unsafe-pr-checkout: true # allow checking out PR head refs from forks - name: Determine if contributor exists in PR branch id: check_contributor_pr @@ -129,6 +130,7 @@ jobs: persist-credentials: false ref: "refs/pull/${{ github.event.number }}/merge" path: merge_branch + allow-unsafe-pr-checkout: true # allow checking out PR head refs from forks - name: Check if CONTRIBUTORS.md was modified in PR id: check_contributors_modified From e190ca0a32bf06484e7760ea3b3785d1c439b3a7 Mon Sep 17 00:00:00 2001 From: Yaswant Pradhan <2984440+yaswant@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:39:56 +0100 Subject: [PATCH 160/165] Remove commented-out code for CONTRIBUTORS.md modification check in CLA workflow --- .../call-trigger-project-workflow.yaml | 1 - .github/workflows/cla-check.yaml | 24 ------------------- 2 files changed, 25 deletions(-) diff --git a/.github/workflows/call-trigger-project-workflow.yaml b/.github/workflows/call-trigger-project-workflow.yaml index 3f46cde..54b7ea1 100644 --- a/.github/workflows/call-trigger-project-workflow.yaml +++ b/.github/workflows/call-trigger-project-workflow.yaml @@ -20,4 +20,3 @@ jobs: contents: read # Required for checking out code or referencing internal workflow actions pull-requests: write # Required to modify or comment on pull requests within this pipeline process uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main - # secrets: inherit diff --git a/.github/workflows/cla-check.yaml b/.github/workflows/cla-check.yaml index 55ac8df..68493c6 100644 --- a/.github/workflows/cla-check.yaml +++ b/.github/workflows/cla-check.yaml @@ -172,30 +172,6 @@ jobs: # Clean up the localised workspace rm -f base_raw.txt pr_raw.txt base_clean.txt pr_clean.txt fi - # - name: Check if CONTRIBUTORS.md was modified in PR - # id: check_contributors_modified - # shell: bash - # env: - # # This token is safely scoped to job-level permissions - # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # run: | - # if [ "$merge_ref_defined" = "false" ]; then - # echo "::warning::Merge conflicts mean the check for modification of a signed CONTRIBUTORS.md file was not completed. Allowing this to pass while conflict exists." - # echo "modified=false" >> "$GITHUB_OUTPUT" - # else - # # Call GitHub Files API directly to check PR modifications without - # # running local network git fetches - # # Filtering precisely for the CONTRIBUTORS.md file signature - # has_modification=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.number }}/files" --paginate --jq '.[].filename' 2>/dev/null | grep -E '^CONTRIBUTORS\.md$' || true) - - # if [ -n "$has_modification" ]; then - # echo "modified=true" >> "$GITHUB_OUTPUT" - # echo "📝 CONTRIBUTORS.md file was modified in this PR." - # else - # echo "modified=false" >> "$GITHUB_OUTPUT" - # echo "ℹ️ CONTRIBUTORS.md file was NOT modified in this PR." - # fi - # fi # -- Manage PR Labels, Comments, and Final Status (Consolidated) - name: Manage CLA Status, Labels, and Comments From 898b698fb7f011aeef6528554aaafad3369ecb34 Mon Sep 17 00:00:00 2001 From: yaswant <2984440+yaswant@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:55:02 +0100 Subject: [PATCH 161/165] Update setup-uv action to v8.3.2 and enable caching options --- .github/workflows/sphinx-docs.yaml | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 59d5fb2..08a94d6 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -131,9 +131,11 @@ jobs: persist-credentials: false - name: Setup uv with Python ${{ env.PYTHON_VRSN }} - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 with: python-version: ${{ env.PYTHON_VRSN }} + enable-cache: true + ignore-nothing-to-cache: true - name: Detect dependency source id: detect-deps @@ -198,14 +200,14 @@ jobs: echo "flags=$FLAGS" >> "$GITHUB_OUTPUT" [ -z "$FLAGS" ] && echo "::warning::'$EXTRAS' not found in $PYPROJECT." || echo "::notice::Resolved flags: '$FLAGS'" - - name: Cache uv venv - if: steps.detect-deps.outputs.source != 'conf' - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: ${{ env.VENV_PATH }} - key: ${{ runner.os }}-${{ env.PYTHON_VRSN }}-${{ hashFiles('pyproject.toml', format('{0}/pyproject.toml', inputs.docs-dir), format('{0}/pyproject.toml', inputs.source-dir)) }} - restore-keys: | - ${{ runner.os }}-${{ env.PYTHON_VRSN }}- + # - name: Cache uv venv + # if: steps.detect-deps.outputs.source != 'conf' + # uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + # with: + # path: ${{ env.VENV_PATH }} + # key: ${{ runner.os }}-${{ env.PYTHON_VRSN }}-${{ hashFiles('pyproject.toml', format('{0}/pyproject.toml', inputs.docs-dir), format('{0}/pyproject.toml', inputs.source-dir)) }} + # restore-keys: | + # ${{ runner.os }}-${{ env.PYTHON_VRSN }}- - name: Create virtual environment run: uv venv --python "${PYTHON_VRSN}" --allow-existing "${VENV_PATH}" @@ -316,9 +318,9 @@ jobs: "${SOURCE_DIR}" \ "${DOCS_DIR}/${BUILD_DIR}/html" - - name: Minimize uv cache - if: steps.detect-deps.outputs.source != 'conf' - run: uv cache prune --ci + # - name: Minimize uv cache + # if: steps.detect-deps.outputs.source != 'conf' + # run: uv cache prune --ci # -- Deploy to GitHub Pages only on push to upstream main - name: Setup GitHub Pages From d9ca7dd6e9f69d0f29af0b77717d74ebaf47d933 Mon Sep 17 00:00:00 2001 From: yaswant <2984440+yaswant@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:29:13 +0100 Subject: [PATCH 162/165] Enhance caching in Sphinx Docs workflow by adding cache for pyproject.toml and conf.py, and update virtual environment caching logic --- .github/workflows/sphinx-docs.yaml | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/sphinx-docs.yaml b/.github/workflows/sphinx-docs.yaml index 08a94d6..c3ed2f5 100644 --- a/.github/workflows/sphinx-docs.yaml +++ b/.github/workflows/sphinx-docs.yaml @@ -135,6 +135,9 @@ jobs: with: python-version: ${{ env.PYTHON_VRSN }} enable-cache: true + cache-dependency-glob: | + **/pyproject.toml + ${{ env.SOURCE_DIR }}/conf.py ignore-nothing-to-cache: true - name: Detect dependency source @@ -200,14 +203,14 @@ jobs: echo "flags=$FLAGS" >> "$GITHUB_OUTPUT" [ -z "$FLAGS" ] && echo "::warning::'$EXTRAS' not found in $PYPROJECT." || echo "::notice::Resolved flags: '$FLAGS'" - # - name: Cache uv venv - # if: steps.detect-deps.outputs.source != 'conf' - # uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - # with: - # path: ${{ env.VENV_PATH }} - # key: ${{ runner.os }}-${{ env.PYTHON_VRSN }}-${{ hashFiles('pyproject.toml', format('{0}/pyproject.toml', inputs.docs-dir), format('{0}/pyproject.toml', inputs.source-dir)) }} - # restore-keys: | - # ${{ runner.os }}-${{ env.PYTHON_VRSN }}- + - name: Cache virtual environment + id: cache-venv + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ${{ env.VENV_PATH }} + key: ${{ runner.os }}-${{ env.PYTHON_VRSN }}-${{ hashFiles('pyproject.toml', format('{0}/pyproject.toml', inputs.docs-dir), format('{0}/pyproject.toml', inputs.source-dir), format('{0}/conf.py', inputs.source-dir)) }} + restore-keys: | + ${{ runner.os }}-${{ env.PYTHON_VRSN }}- - name: Create virtual environment run: uv venv --python "${PYTHON_VRSN}" --allow-existing "${VENV_PATH}" @@ -318,9 +321,8 @@ jobs: "${SOURCE_DIR}" \ "${DOCS_DIR}/${BUILD_DIR}/html" - # - name: Minimize uv cache - # if: steps.detect-deps.outputs.source != 'conf' - # run: uv cache prune --ci + - name: Minimize uv cache + run: uv cache prune --ci # -- Deploy to GitHub Pages only on push to upstream main - name: Setup GitHub Pages From 814b8bd7589049115147d42b231dd31d4758d4e1 Mon Sep 17 00:00:00 2001 From: yaswant <2984440+yaswant@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:02:36 +0100 Subject: [PATCH 163/165] Fix typo --- .github/workflows/validate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index 53838cd..cfb2d2d 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -88,7 +88,7 @@ jobs: exit 0 fi - # Run shellcheck on discoverd files, treating warnings as errors + # Run shellcheck on discovered files, treating warnings as errors uv run shellcheck -S warning "${shell_files[@]}" && echo "✅ All checks passed!" continue-on-error: true From 7bf0fc131d77327a067fabbfa0c8850e3279fdc7 Mon Sep 17 00:00:00 2001 From: yaswant <2984440+yaswant@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:15:07 +0100 Subject: [PATCH 164/165] Respond to Copilot review --- .github/pull_request_template.md | 4 +++- .github/workflows/validate.yaml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e9f957e..8fddf4d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,5 +1,7 @@ # PR Summary + + Code Reviewer: @@ -17,7 +19,7 @@ Code Reviewer: ## :robot: AI Assistance and Attribution -- [ ] Some of the content of this change has been produced with the assistance of _Generative AI tool name_ (e.g., Met Office Github Copilot Enterprise, Github Copilot Personal, ChatGPT GPT-4, etc) and I have followed the [Simulation Systems AI policy](https://metoffice.github.io/simulation-systems/FurtherDetails/ai.html) (including attribution labels) +- [ ] Some of the content of this change has been produced with the assistance of _Generative AI tool name_ (e.g., Met Office GitHub Copilot Enterprise, GitHub Copilot Personal, ChatGPT GPT-4, etc) and I have followed the [Simulation Systems AI policy](https://metoffice.github.io/simulation-systems/FurtherDetails/ai.html) (including attribution labels) diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index cfb2d2d..7ea8679 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -94,7 +94,7 @@ jobs: - name: Actionlint if: always() - run: uv run actionlint -shellcheck "-S warning" .github/workflows/*.yaml + run: uv run actionlint -shellcheck "shellcheck -S warning" .github/workflows/*.yaml - name: Zizmor (GitHub Actions security) if: always() From 3c07fd0e564878af0f94828a867c0da6094bca0b Mon Sep 17 00:00:00 2001 From: yaswant <2984440+yaswant@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:35:45 +0100 Subject: [PATCH 165/165] Review comments --- .github/workflows/call-track-review-project.yaml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/call-track-review-project.yaml b/.github/workflows/call-track-review-project.yaml index dc34dab..32f5e16 100644 --- a/.github/workflows/call-track-review-project.yaml +++ b/.github/workflows/call-track-review-project.yaml @@ -18,13 +18,6 @@ permissions: {} jobs: track_review_project: - if: >- - github.event_name == 'workflow_dispatch' || - ( - github.event_name == 'workflow_run' && - github.event.workflow_run.event == 'push' && - github.event.workflow_run.conclusion == 'success' - ) # Granular permissions are safely locked directly to this specific execution context permissions: actions: read # Required to view the status of the upstream triggered workflow run @@ -34,6 +27,6 @@ jobs: PROJECT_ACTION_PAT: ${{ secrets.PROJECT_ACTION_PAT }} # Optional inputs (with default values) with: - runner: "ubuntu-24.04" + runner: "ubuntu-slim" project_org: "MetOffice" project_number: 376