From 791b6e43b18f8cf869b13905b0e3fde803674ba3 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Wed, 8 Oct 2025 09:14:08 +0200 Subject: [PATCH 01/15] add check authors to ci --- .github/workflows/nestbuildmatrix.yml | 241 ++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index 329f5baa29..8c5e225c39 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -473,6 +473,247 @@ jobs: run: | flake8 . + pr-authors: + runs-on: "ubuntu-22.04" + if: github.event_name == 'pull_request' + steps: + - name: Harden Runner + uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0 + with: + egress-policy: audit + disable-telemetry: true + + - name: "Checkout repository content" + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + fetch-depth: 0 + + - name: "Get PR authors from commits" + id: pr_authors + run: | + # Get the base and head refs for the PR + BASE_REF="${{ github.event.pull_request.base.ref }}" + HEAD_REF="${{ github.event.pull_request.head.ref }}" + PR_NUMBER="${{ github.event.pull_request.number }}" + + echo "Analyzing PR #${PR_NUMBER}: ${HEAD_REF} -> ${BASE_REF}" + + # Get all commits in the PR + COMMITS=$(git log --pretty=format:"%H|%an|%ae" ${BASE_REF}..${HEAD_REF}) + + if [ -z "$COMMITS" ]; then + echo "No commits found in PR" + echo "authors=[]" >> $GITHUB_OUTPUT + echo "author_count=0" >> $GITHUB_OUTPUT + exit 0 + fi + + # Extract unique authors (name and email pairs) + UNIQUE_AUTHORS=$(echo "$COMMITS" | sort -u -t'|' -k2,3 | cut -d'|' -f2,3) + + # Count authors + AUTHOR_COUNT=$(echo "$UNIQUE_AUTHORS" | wc -l) + + # Create JSON array of authors + AUTHORS_JSON="[" + FIRST=true + while IFS='|' read -r name email; do + if [ "$FIRST" = true ]; then + FIRST=falseh + else + AUTHORS_JSON="${AUTHORS_JSON}," + fi + AUTHORS_JSON="${AUTHORS_JSON}{\"name\":\"${name}\",\"email\":\"${email}\"}" + done <<< "$UNIQUE_AUTHORS" + AUTHORS_JSON="${AUTHORS_JSON}]" + + echo "Found ${AUTHOR_COUNT} unique author(s):" + echo "$UNIQUE_AUTHORS" | while IFS='|' read -r name email; do + echo " - ${name} <${email}>" + done + + # Output results + echo "authors=${AUTHORS_JSON}" >> $GITHUB_OUTPUT + echo "author_count=${AUTHOR_COUNT}" >> $GITHUB_OUTPUT + + # Also output as multiline for easy reading + { + echo "authors_multiline<" + done + echo "EOF" + } >> $GITHUB_OUTPUT + + - name: "Fetch authorized authors from private repository" + id: fetch_authorized_authors + if: env.PRIVATE_REPO_TOKEN != '' + env: + PRIVATE_REPO_TOKEN: ${{ secrets.PRIVATE_REPO_TOKEN }} + run: | + # Configuration - Update these values for your private repository + PRIVATE_REPO_OWNER="${{ vars.PRIVATE_REPO_OWNER || 'your-org' }}" + PRIVATE_REPO_NAME="${{ vars.PRIVATE_REPO_NAME || 'your-private-repo' }}" + AUTHORIZED_AUTHORS_FILE="${{ vars.AUTHORIZED_AUTHORS_FILE || 'authorized_authors.txt' }}" + + echo "Fetching authorized authors from ${PRIVATE_REPO_OWNER}/${PRIVATE_REPO_NAME}/${AUTHORIZED_AUTHORS_FILE}" + + # Fetch the authorized authors file from private repository + RESPONSE=$(curl -s -H "Authorization: token ${PRIVATE_REPO_TOKEN}" \ + -H "Accept: application/vnd.github.v3.raw" \ + "https://api.github.com/repos/${PRIVATE_REPO_OWNER}/${PRIVATE_REPO_NAME}/contents/${AUTHORIZED_AUTHORS_FILE}") + + # Check if the request was successful + if echo "$RESPONSE" | grep -q '"message": "Not Found"'; then + echo "Warning: Could not find authorized authors file at ${AUTHORIZED_AUTHORS_FILE}" + echo "authorized_authors=[]" >> $GITHUB_OUTPUT + echo "authorized_count=0" >> $GITHUB_OUTPUT + echo "fetch_success=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Extract the content (assuming it's base64 encoded) + AUTHORIZED_CONTENT=$(echo "$RESPONSE" | jq -r '.content // empty' | base64 -d) + + if [ -z "$AUTHORIZED_CONTENT" ]; then + echo "Warning: Could not decode authorized authors file content" + echo "authorized_authors=[]" >> $GITHUB_OUTPUT + echo "authorized_count=0" >> $GITHUB_OUTPUT + echo "fetch_success=false" >> $GITHUB_OUTPUT + exit 0 + fi + + # Parse authorized authors from YAML format: "Name : githubhandle" + AUTHORIZED_AUTHORS=$(echo "$AUTHORIZED_CONTENT" | grep -v '^#' | grep -v '^$' | sed 's/:.*$//' | sort -u) + AUTHORIZED_COUNT=$(echo "$AUTHORIZED_AUTHORS" | wc -l) + + echo "Found ${AUTHORIZED_COUNT} authorized author(s)" + echo "$AUTHORIZED_AUTHORS" | while read -r line; do + echo " - ${line}" + done + + # Output results + echo "authorized_authors<> $GITHUB_OUTPUT + echo "$AUTHORIZED_AUTHORS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "authorized_count=${AUTHORIZED_COUNT}" >> $GITHUB_OUTPUT + echo "fetch_success=true" >> $GITHUB_OUTPUT + + - name: "Compare PR authors with authorized list" + id: compare_authors + if: steps.fetch_authorized_authors.outputs.fetch_success == 'true' + run: | + echo "Comparing PR authors with authorized list..." + + # Get PR authors and authorized authors + PR_AUTHORS="${{ steps.pr_authors.outputs.authors_multiline }}" + AUTHORIZED_AUTHORS="${{ steps.fetch_authorized_authors.outputs.authorized_authors }}" + + # Create temporary files for comparison + echo "$PR_AUTHORS" > /tmp/pr_authors.txt + echo "$AUTHORIZED_AUTHORS" > /tmp/authorized_authors.txt + + # Find authors in PR but not in authorized list + UNAUTHORIZED_AUTHORS="" + UNAUTHORIZED_COUNT=0 + + while IFS= read -r pr_author; do + if [ -n "$pr_author" ]; then + # Check if this exact author (name and email) is in authorized list + # We need exact match of the full "Name " format + if ! echo "$AUTHORIZED_AUTHORS" | grep -Fxq "$pr_author"; then + UNAUTHORIZED_AUTHORS="${UNAUTHORIZED_AUTHORS}${pr_author}\n" + UNAUTHORIZED_COUNT=$((UNAUTHORIZED_COUNT + 1)) + fi + fi + done < /tmp/pr_authors.txt + + # Find authorized authors not in PR + MISSING_AUTHORS="" + MISSING_COUNT=0 + + while IFS= read -r auth_author; do + if [ -n "$auth_author" ]; then + # Check if this exact authorized author is in PR + if ! echo "$PR_AUTHORS" | grep -Fxq "$auth_author"; then + MISSING_AUTHORS="${MISSING_AUTHORS}${auth_author}\n" + MISSING_COUNT=$((MISSING_COUNT + 1)) + fi + fi + done < /tmp/authorized_authors.txt + + # Output results + echo "unauthorized_authors<> $GITHUB_OUTPUT + echo -e "$UNAUTHORIZED_AUTHORS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "unauthorized_count=${UNAUTHORIZED_COUNT}" >> $GITHUB_OUTPUT + + echo "missing_authors<> $GITHUB_OUTPUT + echo -e "$MISSING_AUTHORS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "missing_count=${MISSING_COUNT}" >> $GITHUB_OUTPUT + + # Determine overall status + if [ $UNAUTHORIZED_COUNT -eq 0 ]; then + echo "comparison_status=success" >> $GITHUB_OUTPUT + else + echo "comparison_status=failure" >> $GITHUB_OUTPUT + echo "❌ FAILURE: Found $UNAUTHORIZED_COUNT unauthorized author(s) in this PR" + echo "The following authors are not in the authorized list:" + echo -e "$UNAUTHORIZED_AUTHORS" + + # Check if we should fail the build (configurable via repository variable) + FAIL_ON_UNAUTHORIZED="${{ vars.FAIL_ON_UNAUTHORIZED_AUTHORS || 'true' }}" + if [ "$FAIL_ON_UNAUTHORIZED" = "true" ]; then + echo "Build failed due to unauthorized authors (set FAIL_ON_UNAUTHORIZED_AUTHORS=false to disable)" + exit 1 + else + echo "Build continues despite unauthorized authors (FAIL_ON_UNAUTHORIZED_AUTHORS=false)" + fi + fi + + - name: "Display PR authors summary" + run: | + echo "## PR Authors Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Total unique authors:** ${{ steps.pr_authors.outputs.author_count }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Authors:**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.pr_authors.outputs.authors_multiline }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + # Add comparison results if available + if [ "${{ steps.fetch_authorized_authors.outputs.fetch_success }}" = "true" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Author Authorization Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Authorized authors in repository:** ${{ steps.fetch_authorized_authors.outputs.authorized_count }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ steps.compare_authors.outputs.unauthorized_count }}" -gt 0 ]; then + echo "⚠️ **Warning:** ${{ steps.compare_authors.outputs.unauthorized_count }} unauthorized author(s) found:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "These authors are not in the authorized list (exact name and email match required):" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.compare_authors.outputs.unauthorized_authors }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + else + echo "✅ **All PR authors are authorized**" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ steps.compare_authors.outputs.missing_count }}" -gt 0 ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Note:** ${{ steps.compare_authors.outputs.missing_count }} authorized author(s) not contributing to this PR:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.compare_authors.outputs.missing_authors }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "ℹ️ **Note:** Author authorization check skipped (private repository access not configured)" >> $GITHUB_STEP_SUMMARY + fi + sphinx-rtd: # as close as possible to the Readthedocs setup (system install cmake, pip install -r doc/requirements.txt) runs-on: "ubuntu-22.04" From 3b9ed82fc6c75ef706b61ce755d2c6cd6574a688 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Wed, 8 Oct 2025 10:57:22 +0200 Subject: [PATCH 02/15] add a author check to ci workflow --- .github/workflows/nestbuildmatrix.yml | 64 +++++++-------------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index 8c5e225c39..474aceb014 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -551,10 +551,10 @@ jobs: env: PRIVATE_REPO_TOKEN: ${{ secrets.PRIVATE_REPO_TOKEN }} run: | - # Configuration - Update these values for your private repository - PRIVATE_REPO_OWNER="${{ vars.PRIVATE_REPO_OWNER || 'your-org' }}" - PRIVATE_REPO_NAME="${{ vars.PRIVATE_REPO_NAME || 'your-private-repo' }}" - AUTHORIZED_AUTHORS_FILE="${{ vars.AUTHORIZED_AUTHORS_FILE || 'authorized_authors.txt' }}" + # Configuration for nest/nest-release-tools repository + PRIVATE_REPO_OWNER="nest" + PRIVATE_REPO_NAME="nest-release-tools" + AUTHORIZED_AUTHORS_FILE="data/gitlognames.yaml" echo "Fetching authorized authors from ${PRIVATE_REPO_OWNER}/${PRIVATE_REPO_NAME}/${AUTHORIZED_AUTHORS_FILE}" @@ -584,13 +584,11 @@ jobs: fi # Parse authorized authors from YAML format: "Name : githubhandle" + # Extract the "Name " part before the colon AUTHORIZED_AUTHORS=$(echo "$AUTHORIZED_CONTENT" | grep -v '^#' | grep -v '^$' | sed 's/:.*$//' | sort -u) AUTHORIZED_COUNT=$(echo "$AUTHORIZED_AUTHORS" | wc -l) - echo "Found ${AUTHORIZED_COUNT} authorized author(s)" - echo "$AUTHORIZED_AUTHORS" | while read -r line; do - echo " - ${line}" - done + echo "Found ${AUTHORIZED_COUNT} authorized author(s) (list not displayed for security)" # Output results echo "authorized_authors<> $GITHUB_OUTPUT @@ -599,11 +597,11 @@ jobs: echo "authorized_count=${AUTHORIZED_COUNT}" >> $GITHUB_OUTPUT echo "fetch_success=true" >> $GITHUB_OUTPUT - - name: "Compare PR authors with authorized list" - id: compare_authors + - name: "Check if PR authors are authorized" + id: check_authorization if: steps.fetch_authorized_authors.outputs.fetch_success == 'true' run: | - echo "Comparing PR authors with authorized list..." + echo "Checking if PR authors are in the authorized list..." # Get PR authors and authorized authors PR_AUTHORS="${{ steps.pr_authors.outputs.authors_multiline }}" @@ -628,36 +626,18 @@ jobs: fi done < /tmp/pr_authors.txt - # Find authorized authors not in PR - MISSING_AUTHORS="" - MISSING_COUNT=0 - - while IFS= read -r auth_author; do - if [ -n "$auth_author" ]; then - # Check if this exact authorized author is in PR - if ! echo "$PR_AUTHORS" | grep -Fxq "$auth_author"; then - MISSING_AUTHORS="${MISSING_AUTHORS}${auth_author}\n" - MISSING_COUNT=$((MISSING_COUNT + 1)) - fi - fi - done < /tmp/authorized_authors.txt - # Output results echo "unauthorized_authors<> $GITHUB_OUTPUT echo -e "$UNAUTHORIZED_AUTHORS" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT echo "unauthorized_count=${UNAUTHORIZED_COUNT}" >> $GITHUB_OUTPUT - echo "missing_authors<> $GITHUB_OUTPUT - echo -e "$MISSING_AUTHORS" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - echo "missing_count=${MISSING_COUNT}" >> $GITHUB_OUTPUT - # Determine overall status if [ $UNAUTHORIZED_COUNT -eq 0 ]; then - echo "comparison_status=success" >> $GITHUB_OUTPUT + echo "authorization_status=success" >> $GITHUB_OUTPUT + echo "✅ SUCCESS: All PR authors are authorized" else - echo "comparison_status=failure" >> $GITHUB_OUTPUT + echo "authorization_status=failure" >> $GITHUB_OUTPUT echo "❌ FAILURE: Found $UNAUTHORIZED_COUNT unauthorized author(s) in this PR" echo "The following authors are not in the authorized list:" echo -e "$UNAUTHORIZED_AUTHORS" @@ -683,31 +663,21 @@ jobs: echo "${{ steps.pr_authors.outputs.authors_multiline }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - # Add comparison results if available + # Add authorization check results if available if [ "${{ steps.fetch_authorized_authors.outputs.fetch_success }}" = "true" ]; then echo "" >> $GITHUB_STEP_SUMMARY echo "## Author Authorization Check" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Authorized authors in repository:** ${{ steps.fetch_authorized_authors.outputs.authorized_count }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ "${{ steps.compare_authors.outputs.unauthorized_count }}" -gt 0 ]; then - echo "⚠️ **Warning:** ${{ steps.compare_authors.outputs.unauthorized_count }} unauthorized author(s) found:" >> $GITHUB_STEP_SUMMARY + if [ "${{ steps.check_authorization.outputs.unauthorized_count }}" -gt 0 ]; then + echo "❌ **FAILURE:** ${{ steps.check_authorization.outputs.unauthorized_count }} unauthorized author(s) found:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "These authors are not in the authorized list (exact name and email match required):" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.compare_authors.outputs.unauthorized_authors }}" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.check_authorization.outputs.unauthorized_authors }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY else - echo "✅ **All PR authors are authorized**" >> $GITHUB_STEP_SUMMARY - fi - - if [ "${{ steps.compare_authors.outputs.missing_count }}" -gt 0 ]; then - echo "" >> $GITHUB_STEP_SUMMARY - echo "**Note:** ${{ steps.compare_authors.outputs.missing_count }} authorized author(s) not contributing to this PR:" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.compare_authors.outputs.missing_authors }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY + echo "✅ **SUCCESS: All PR authors are authorized**" >> $GITHUB_STEP_SUMMARY fi else echo "" >> $GITHUB_STEP_SUMMARY From bf8805638f5898a1a989d7cbbcb2700844a103ab Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Thu, 9 Oct 2025 10:52:41 +0200 Subject: [PATCH 03/15] fix jq line --- .github/workflows/nestbuildmatrix.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index 474aceb014..dade618a2f 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -563,8 +563,8 @@ jobs: -H "Accept: application/vnd.github.v3.raw" \ "https://api.github.com/repos/${PRIVATE_REPO_OWNER}/${PRIVATE_REPO_NAME}/contents/${AUTHORIZED_AUTHORS_FILE}") - # Check if the request was successful - if echo "$RESPONSE" | grep -q '"message": "Not Found"'; then + # Check if the request was successful (raw API returns 404 for not found) + if [ -z "$RESPONSE" ] || echo "$RESPONSE" | grep -q "Not Found"; then echo "Warning: Could not find authorized authors file at ${AUTHORIZED_AUTHORS_FILE}" echo "authorized_authors=[]" >> $GITHUB_OUTPUT echo "authorized_count=0" >> $GITHUB_OUTPUT @@ -572,8 +572,8 @@ jobs: exit 0 fi - # Extract the content (assuming it's base64 encoded) - AUTHORIZED_CONTENT=$(echo "$RESPONSE" | jq -r '.content // empty' | base64 -d) + # Extract the content (raw content is already plain text when using v3.raw) + AUTHORIZED_CONTENT="$RESPONSE" if [ -z "$AUTHORIZED_CONTENT" ]; then echo "Warning: Could not decode authorized authors file content" From e7a483b37b7f9b0f7127cab11633769be6f1cac7 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Mon, 13 Oct 2025 10:00:11 +0200 Subject: [PATCH 04/15] changes --- .github/workflows/nestbuildmatrix.yml | 45 ++++++++++----------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index dade618a2f..8d4fdb8a42 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -566,8 +566,6 @@ jobs: # Check if the request was successful (raw API returns 404 for not found) if [ -z "$RESPONSE" ] || echo "$RESPONSE" | grep -q "Not Found"; then echo "Warning: Could not find authorized authors file at ${AUTHORIZED_AUTHORS_FILE}" - echo "authorized_authors=[]" >> $GITHUB_OUTPUT - echo "authorized_count=0" >> $GITHUB_OUTPUT echo "fetch_success=false" >> $GITHUB_OUTPUT exit 0 fi @@ -577,23 +575,18 @@ jobs: if [ -z "$AUTHORIZED_CONTENT" ]; then echo "Warning: Could not decode authorized authors file content" - echo "authorized_authors=[]" >> $GITHUB_OUTPUT - echo "authorized_count=0" >> $GITHUB_OUTPUT echo "fetch_success=false" >> $GITHUB_OUTPUT exit 0 fi # Parse authorized authors from YAML format: "Name : githubhandle" - # Extract the "Name " part before the colon - AUTHORIZED_AUTHORS=$(echo "$AUTHORIZED_CONTENT" | grep -v '^#' | grep -v '^$' | sed 's/:.*$//' | sort -u) - AUTHORIZED_COUNT=$(echo "$AUTHORIZED_AUTHORS" | wc -l) + # Extract the "Name " part before the colon and store in secure temp file + echo "$AUTHORIZED_CONTENT" | grep -v '^#' | grep -v '^$' | sed 's/:.*$//' | sort -u > /tmp/authorized_authors_secure.txt + AUTHORIZED_COUNT=$(wc -l < /tmp/authorized_authors_secure.txt) - echo "Found ${AUTHORIZED_COUNT} authorized author(s) (list not displayed for security)" + echo "Found ${AUTHORIZED_COUNT} authorized author(s) (list stored securely, not exposed)" - # Output results - echo "authorized_authors<> $GITHUB_OUTPUT - echo "$AUTHORIZED_AUTHORS" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + # Only output success status and count - never the actual names echo "authorized_count=${AUTHORIZED_COUNT}" >> $GITHUB_OUTPUT echo "fetch_success=true" >> $GITHUB_OUTPUT @@ -603,15 +596,14 @@ jobs: run: | echo "Checking if PR authors are in the authorized list..." - # Get PR authors and authorized authors + # Get PR authors from previous step output PR_AUTHORS="${{ steps.pr_authors.outputs.authors_multiline }}" - AUTHORIZED_AUTHORS="${{ steps.fetch_authorized_authors.outputs.authorized_authors }}" - # Create temporary files for comparison + # Create temporary file for PR authors echo "$PR_AUTHORS" > /tmp/pr_authors.txt - echo "$AUTHORIZED_AUTHORS" > /tmp/authorized_authors.txt # Find authors in PR but not in authorized list + # Use the secure temp file created in previous step UNAUTHORIZED_AUTHORS="" UNAUTHORIZED_COUNT=0 @@ -619,17 +611,14 @@ jobs: if [ -n "$pr_author" ]; then # Check if this exact author (name and email) is in authorized list # We need exact match of the full "Name " format - if ! echo "$AUTHORIZED_AUTHORS" | grep -Fxq "$pr_author"; then + if ! grep -Fxq "$pr_author" /tmp/authorized_authors_secure.txt; then UNAUTHORIZED_AUTHORS="${UNAUTHORIZED_AUTHORS}${pr_author}\n" UNAUTHORIZED_COUNT=$((UNAUTHORIZED_COUNT + 1)) fi fi done < /tmp/pr_authors.txt - # Output results - echo "unauthorized_authors<> $GITHUB_OUTPUT - echo -e "$UNAUTHORIZED_AUTHORS" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT + # Only output count and status - never store unauthorized names echo "unauthorized_count=${UNAUTHORIZED_COUNT}" >> $GITHUB_OUTPUT # Determine overall status @@ -639,8 +628,7 @@ jobs: else echo "authorization_status=failure" >> $GITHUB_OUTPUT echo "❌ FAILURE: Found $UNAUTHORIZED_COUNT unauthorized author(s) in this PR" - echo "The following authors are not in the authorized list:" - echo -e "$UNAUTHORIZED_AUTHORS" + echo "Unauthorized authors found (names not displayed for security)" # Check if we should fail the build (configurable via repository variable) FAIL_ON_UNAUTHORIZED="${{ vars.FAIL_ON_UNAUTHORIZED_AUTHORS || 'true' }}" @@ -652,6 +640,9 @@ jobs: fi fi + # Clean up temporary files for security + rm -f /tmp/authorized_authors_secure.txt /tmp/pr_authors.txt + - name: "Display PR authors summary" run: | echo "## PR Authors Summary" >> $GITHUB_STEP_SUMMARY @@ -670,12 +661,10 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY if [ "${{ steps.check_authorization.outputs.unauthorized_count }}" -gt 0 ]; then - echo "❌ **FAILURE:** ${{ steps.check_authorization.outputs.unauthorized_count }} unauthorized author(s) found:" >> $GITHUB_STEP_SUMMARY + echo "❌ **FAILURE:** ${{ steps.check_authorization.outputs.unauthorized_count }} unauthorized author(s) found" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "These authors are not in the authorized list (exact name and email match required):" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.check_authorization.outputs.unauthorized_authors }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY + echo "Some authors in this PR are not in the authorized list (exact name and email match required)." >> $GITHUB_STEP_SUMMARY + echo "Contact repository administrators for access." >> $GITHUB_STEP_SUMMARY else echo "✅ **SUCCESS: All PR authors are authorized**" >> $GITHUB_STEP_SUMMARY fi From 964ff7f8c394105b4a65f163eb71d706ecb76289 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Mon, 13 Oct 2025 10:59:03 +0200 Subject: [PATCH 05/15] use api --- .github/workflows/nestbuildmatrix.yml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index 8d4fdb8a42..bee5e53ac3 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -491,15 +491,28 @@ jobs: - name: "Get PR authors from commits" id: pr_authors run: | - # Get the base and head refs for the PR - BASE_REF="${{ github.event.pull_request.base.ref }}" - HEAD_REF="${{ github.event.pull_request.head.ref }}" + # Get PR information PR_NUMBER="${{ github.event.pull_request.number }}" + REPO_OWNER="${{ github.repository_owner }}" + REPO_NAME="${{ github.event.repository.name }}" + + echo "Analyzing PR #${PR_NUMBER} in ${REPO_OWNER}/${REPO_NAME}" + + # Get commits from GitHub API + echo "Fetching commits from GitHub API..." + COMMITS_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUMBER}/commits") + + # Check if the API call was successful + if [ -z "$COMMITS_RESPONSE" ] || echo "$COMMITS_RESPONSE" | grep -q '"message"'; then + echo "Error: Failed to fetch commits from GitHub API" + echo "$COMMITS_RESPONSE" + exit 1 + fi - echo "Analyzing PR #${PR_NUMBER}: ${HEAD_REF} -> ${BASE_REF}" - - # Get all commits in the PR - COMMITS=$(git log --pretty=format:"%H|%an|%ae" ${BASE_REF}..${HEAD_REF}) + # Extract commit information using jq + COMMITS=$(echo "$COMMITS_RESPONSE" | jq -r '.[] | "\(.sha)|\(.commit.author.name)|\(.commit.author.email)"') if [ -z "$COMMITS" ]; then echo "No commits found in PR" From 7967778d1f37e827d2bdbf1f77a365128bae54c6 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Mon, 13 Oct 2025 11:09:25 +0200 Subject: [PATCH 06/15] fix jq --- .github/workflows/nestbuildmatrix.yml | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index bee5e53ac3..4c10ecb7a3 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -495,24 +495,36 @@ jobs: PR_NUMBER="${{ github.event.pull_request.number }}" REPO_OWNER="${{ github.repository_owner }}" REPO_NAME="${{ github.event.repository.name }}" + TOKEN="${{ github.token }}" echo "Analyzing PR #${PR_NUMBER} in ${REPO_OWNER}/${REPO_NAME}" # Get commits from GitHub API echo "Fetching commits from GitHub API..." - COMMITS_RESPONSE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + RESPONSE_FILE="/tmp/pr_commits.json" + HTTP_STATUS=$(curl -sS \ + -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/vnd.github.v3+json" \ + -o "${RESPONSE_FILE}" \ + -w "%{http_code}" \ "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUMBER}/commits") - # Check if the API call was successful - if [ -z "$COMMITS_RESPONSE" ] || echo "$COMMITS_RESPONSE" | grep -q '"message"'; then - echo "Error: Failed to fetch commits from GitHub API" - echo "$COMMITS_RESPONSE" + # Check HTTP status code + if [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then + echo "Error: Failed to fetch commits from GitHub API (HTTP ${HTTP_STATUS})" + head -c 500 "${RESPONSE_FILE}" || true + exit 1 + fi + + # Validate that the response is a JSON array + if ! jq -e 'type == "array"' "${RESPONSE_FILE}" > /dev/null; then + echo "Error: Unexpected API response (not a JSON array)" + head -c 500 "${RESPONSE_FILE}" || true exit 1 fi # Extract commit information using jq - COMMITS=$(echo "$COMMITS_RESPONSE" | jq -r '.[] | "\(.sha)|\(.commit.author.name)|\(.commit.author.email)"') + COMMITS=$(jq -r '.[] | "\(.sha)|\(.commit.author.name)|\(.commit.author.email)"' "${RESPONSE_FILE}") if [ -z "$COMMITS" ]; then echo "No commits found in PR" From d6afc87d363830ed737c57f0a1cd2edfa15bc920 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Tue, 14 Oct 2025 09:24:44 +0200 Subject: [PATCH 07/15] rm unused json --- .github/workflows/nestbuildmatrix.yml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index 4c10ecb7a3..0de47bb776 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -539,26 +539,12 @@ jobs: # Count authors AUTHOR_COUNT=$(echo "$UNIQUE_AUTHORS" | wc -l) - # Create JSON array of authors - AUTHORS_JSON="[" - FIRST=true - while IFS='|' read -r name email; do - if [ "$FIRST" = true ]; then - FIRST=falseh - else - AUTHORS_JSON="${AUTHORS_JSON}," - fi - AUTHORS_JSON="${AUTHORS_JSON}{\"name\":\"${name}\",\"email\":\"${email}\"}" - done <<< "$UNIQUE_AUTHORS" - AUTHORS_JSON="${AUTHORS_JSON}]" - echo "Found ${AUTHOR_COUNT} unique author(s):" echo "$UNIQUE_AUTHORS" | while IFS='|' read -r name email; do echo " - ${name} <${email}>" done # Output results - echo "authors=${AUTHORS_JSON}" >> $GITHUB_OUTPUT echo "author_count=${AUTHOR_COUNT}" >> $GITHUB_OUTPUT # Also output as multiline for easy reading From e90863e546a2191ba1ef7e477c017f4bc461d83c Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Tue, 14 Oct 2025 10:56:42 +0200 Subject: [PATCH 08/15] modify summary --- .github/workflows/nestbuildmatrix.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index 0de47bb776..37e681f2c7 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -656,6 +656,10 @@ jobs: - name: "Display PR authors summary" run: | + echo "Starting PR authors summary display..." + echo "Author count: ${{ steps.pr_authors.outputs.author_count }}" + echo "Authors multiline: ${{ steps.pr_authors.outputs.authors_multiline }}" + echo "## PR Authors Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Total unique authors:** ${{ steps.pr_authors.outputs.author_count }}" >> $GITHUB_STEP_SUMMARY From 82252e1baffd030b55156550dbc482bfc3ec0d44 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Thu, 23 Oct 2025 22:43:13 +0200 Subject: [PATCH 09/15] move script from CI to Python in build_support, alter text --- .github/workflows/nestbuildmatrix.yml | 230 ++++++-------------------- build_support/check_pr_authors.py | 224 +++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 175 deletions(-) create mode 100755 build_support/check_pr_authors.py diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index 37e681f2c7..e89a5b2308 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -488,204 +488,84 @@ jobs: with: fetch-depth: 0 - - name: "Get PR authors from commits" - id: pr_authors - run: | - # Get PR information - PR_NUMBER="${{ github.event.pull_request.number }}" - REPO_OWNER="${{ github.repository_owner }}" - REPO_NAME="${{ github.event.repository.name }}" - TOKEN="${{ github.token }}" - - echo "Analyzing PR #${PR_NUMBER} in ${REPO_OWNER}/${REPO_NAME}" - - # Get commits from GitHub API - echo "Fetching commits from GitHub API..." - RESPONSE_FILE="/tmp/pr_commits.json" - HTTP_STATUS=$(curl -sS \ - -H "Authorization: Bearer ${TOKEN}" \ - -H "Accept: application/vnd.github.v3+json" \ - -o "${RESPONSE_FILE}" \ - -w "%{http_code}" \ - "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUMBER}/commits") - - # Check HTTP status code - if [ "${HTTP_STATUS}" -lt 200 ] || [ "${HTTP_STATUS}" -ge 300 ]; then - echo "Error: Failed to fetch commits from GitHub API (HTTP ${HTTP_STATUS})" - head -c 500 "${RESPONSE_FILE}" || true - exit 1 - fi - - # Validate that the response is a JSON array - if ! jq -e 'type == "array"' "${RESPONSE_FILE}" > /dev/null; then - echo "Error: Unexpected API response (not a JSON array)" - head -c 500 "${RESPONSE_FILE}" || true - exit 1 - fi - - # Extract commit information using jq - COMMITS=$(jq -r '.[] | "\(.sha)|\(.commit.author.name)|\(.commit.author.email)"' "${RESPONSE_FILE}") - - if [ -z "$COMMITS" ]; then - echo "No commits found in PR" - echo "authors=[]" >> $GITHUB_OUTPUT - echo "author_count=0" >> $GITHUB_OUTPUT - exit 0 - fi - - # Extract unique authors (name and email pairs) - UNIQUE_AUTHORS=$(echo "$COMMITS" | sort -u -t'|' -k2,3 | cut -d'|' -f2,3) - - # Count authors - AUTHOR_COUNT=$(echo "$UNIQUE_AUTHORS" | wc -l) - - echo "Found ${AUTHOR_COUNT} unique author(s):" - echo "$UNIQUE_AUTHORS" | while IFS='|' read -r name email; do - echo " - ${name} <${email}>" - done - - # Output results - echo "author_count=${AUTHOR_COUNT}" >> $GITHUB_OUTPUT + - name: "Set up Python 3.x" + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.10" - # Also output as multiline for easy reading - { - echo "authors_multiline<" - done - echo "EOF" - } >> $GITHUB_OUTPUT + - name: "Install dependencies" + run: | + pip install requests - - name: "Fetch authorized authors from private repository" - id: fetch_authorized_authors - if: env.PRIVATE_REPO_TOKEN != '' + - name: "Check PR authors" + id: check_authors env: + GITHUB_TOKEN: ${{ github.token }} PRIVATE_REPO_TOKEN: ${{ secrets.PRIVATE_REPO_TOKEN }} + PRIVATE_REPO_OWNER: ${{ secrets.PRIVATE_REPO_OWNER }} + PRIVATE_REPO_NAME: ${{ secrets.PRIVATE_REPO_NAME }} + FAIL_ON_UNKNOWN_AUTHORS: ${{ vars.FAIL_ON_UNKNOWN_AUTHORS || 'true' }} run: | - # Configuration for nest/nest-release-tools repository - PRIVATE_REPO_OWNER="nest" - PRIVATE_REPO_NAME="nest-release-tools" - AUTHORIZED_AUTHORS_FILE="data/gitlognames.yaml" - - echo "Fetching authorized authors from ${PRIVATE_REPO_OWNER}/${PRIVATE_REPO_NAME}/${AUTHORIZED_AUTHORS_FILE}" - - # Fetch the authorized authors file from private repository - RESPONSE=$(curl -s -H "Authorization: token ${PRIVATE_REPO_TOKEN}" \ - -H "Accept: application/vnd.github.v3.raw" \ - "https://api.github.com/repos/${PRIVATE_REPO_OWNER}/${PRIVATE_REPO_NAME}/contents/${AUTHORIZED_AUTHORS_FILE}") - - # Check if the request was successful (raw API returns 404 for not found) - if [ -z "$RESPONSE" ] || echo "$RESPONSE" | grep -q "Not Found"; then - echo "Warning: Could not find authorized authors file at ${AUTHORIZED_AUTHORS_FILE}" - echo "fetch_success=false" >> $GITHUB_OUTPUT - exit 0 - fi - - # Extract the content (raw content is already plain text when using v3.raw) - AUTHORIZED_CONTENT="$RESPONSE" - - if [ -z "$AUTHORIZED_CONTENT" ]; then - echo "Warning: Could not decode authorized authors file content" - echo "fetch_success=false" >> $GITHUB_OUTPUT - exit 0 - fi - - # Parse authorized authors from YAML format: "Name : githubhandle" - # Extract the "Name " part before the colon and store in secure temp file - echo "$AUTHORIZED_CONTENT" | grep -v '^#' | grep -v '^$' | sed 's/:.*$//' | sort -u > /tmp/authorized_authors_secure.txt - AUTHORIZED_COUNT=$(wc -l < /tmp/authorized_authors_secure.txt) - - echo "Found ${AUTHORIZED_COUNT} authorized author(s) (list stored securely, not exposed)" - - # Only output success status and count - never the actual names - echo "authorized_count=${AUTHORIZED_COUNT}" >> $GITHUB_OUTPUT - echo "fetch_success=true" >> $GITHUB_OUTPUT + # Run the script and capture outputs + python build_support/check_pr_authors.py \ + --pr-number ${{ github.event.pull_request.number }} \ + --repo-owner ${{ github.repository_owner }} \ + --repo-name ${{ github.event.repository.name }} \ + --github-token ${{ github.token }} \ + --private-repo-owner "${{ secrets.PRIVATE_REPO_OWNER }}" \ + --private-repo-name "${{ secrets.PRIVATE_REPO_NAME }}" \ + --private-repo-token "${{ secrets.PRIVATE_REPO_TOKEN }}" \ + --authors-file-path "${{ vars.VALIDATED_AUTHORS_FILE_PATH || 'data/gitlognames.yaml' }}" \ + --fail-on-unknown ${{ vars.FAIL_ON_UNKNOWN_AUTHORS == 'true' }} > /tmp/script_output.txt 2>&1 + + # Extract outputs for GitHub Actions + if [ -f /tmp/script_output.txt ]; then + # Extract key=value pairs and set as GitHub outputs + grep "^[a-zA-Z_][a-zA-Z0-9_]*=" /tmp/script_output.txt | while IFS='=' read -r key value; do + echo "${key}=${value}" >> $GITHUB_OUTPUT + done - - name: "Check if PR authors are authorized" - id: check_authorization - if: steps.fetch_authorized_authors.outputs.fetch_success == 'true' - run: | - echo "Checking if PR authors are in the authorized list..." - - # Get PR authors from previous step output - PR_AUTHORS="${{ steps.pr_authors.outputs.authors_multiline }}" - - # Create temporary file for PR authors - echo "$PR_AUTHORS" > /tmp/pr_authors.txt - - # Find authors in PR but not in authorized list - # Use the secure temp file created in previous step - UNAUTHORIZED_AUTHORS="" - UNAUTHORIZED_COUNT=0 - - while IFS= read -r pr_author; do - if [ -n "$pr_author" ]; then - # Check if this exact author (name and email) is in authorized list - # We need exact match of the full "Name " format - if ! grep -Fxq "$pr_author" /tmp/authorized_authors_secure.txt; then - UNAUTHORIZED_AUTHORS="${UNAUTHORIZED_AUTHORS}${pr_author}\n" - UNAUTHORIZED_COUNT=$((UNAUTHORIZED_COUNT + 1)) - fi + # Handle formatted authors list for GitHub Actions step summary + if grep -q "authors_formatted< /tmp/authors_formatted.txt + echo "authors_formatted<> $GITHUB_OUTPUT + cat /tmp/authors_formatted.txt >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT fi - done < /tmp/pr_authors.txt - - # Only output count and status - never store unauthorized names - echo "unauthorized_count=${UNAUTHORIZED_COUNT}" >> $GITHUB_OUTPUT - # Determine overall status - if [ $UNAUTHORIZED_COUNT -eq 0 ]; then - echo "authorization_status=success" >> $GITHUB_OUTPUT - echo "✅ SUCCESS: All PR authors are authorized" - else - echo "authorization_status=failure" >> $GITHUB_OUTPUT - echo "❌ FAILURE: Found $UNAUTHORIZED_COUNT unauthorized author(s) in this PR" - echo "Unauthorized authors found (names not displayed for security)" - - # Check if we should fail the build (configurable via repository variable) - FAIL_ON_UNAUTHORIZED="${{ vars.FAIL_ON_UNAUTHORIZED_AUTHORS || 'true' }}" - if [ "$FAIL_ON_UNAUTHORIZED" = "true" ]; then - echo "Build failed due to unauthorized authors (set FAIL_ON_UNAUTHORIZED_AUTHORS=false to disable)" - exit 1 - else - echo "Build continues despite unauthorized authors (FAIL_ON_UNAUTHORIZED_AUTHORS=false)" - fi + # Display the script output + cat /tmp/script_output.txt fi - # Clean up temporary files for security - rm -f /tmp/authorized_authors_secure.txt /tmp/pr_authors.txt - - name: "Display PR authors summary" run: | - echo "Starting PR authors summary display..." - echo "Author count: ${{ steps.pr_authors.outputs.author_count }}" - echo "Authors multiline: ${{ steps.pr_authors.outputs.authors_multiline }}" - echo "## PR Authors Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Total unique authors:** ${{ steps.pr_authors.outputs.author_count }}" >> $GITHUB_STEP_SUMMARY + echo "**Total unique authors:** ${{ steps.check_authors.outputs.author_count }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Authors:**" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - echo "${{ steps.pr_authors.outputs.authors_multiline }}" >> $GITHUB_STEP_SUMMARY + echo "${{ steps.check_authors.outputs.authors_formatted }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY - # Add authorization check results if available - if [ "${{ steps.fetch_authorized_authors.outputs.fetch_success }}" = "true" ]; then + # Add validation check results if available + if [ "${{ steps.check_authors.outputs.validation_status }}" = "success" ]; then echo "" >> $GITHUB_STEP_SUMMARY - echo "## Author Authorization Check" >> $GITHUB_STEP_SUMMARY + echo "## Author Validation Check" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ steps.check_authorization.outputs.unauthorized_count }}" -gt 0 ]; then - echo "❌ **FAILURE:** ${{ steps.check_authorization.outputs.unauthorized_count }} unauthorized author(s) found" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "Some authors in this PR are not in the authorized list (exact name and email match required)." >> $GITHUB_STEP_SUMMARY - echo "Contact repository administrators for access." >> $GITHUB_STEP_SUMMARY - else - echo "✅ **SUCCESS: All PR authors are authorized**" >> $GITHUB_STEP_SUMMARY - fi + echo "✅ **SUCCESS: All PR authors are validated**" >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check_authors.outputs.validation_status }}" = "failure" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "## Author Validation Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ **FAILURE:** ${{ steps.check_authors.outputs.unknown_count }} unknown author(s) found" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "The authors of this PR may be contributing for the first time or may have modified their author information. Author information requires review." >> $GITHUB_STEP_SUMMARY else echo "" >> $GITHUB_STEP_SUMMARY - echo "ℹ️ **Note:** Author authorization check skipped (private repository access not configured)" >> $GITHUB_STEP_SUMMARY + echo "ℹ️ **Note:** Author validation check skipped (private repository access not configured)" >> $GITHUB_STEP_SUMMARY fi sphinx-rtd: diff --git a/build_support/check_pr_authors.py b/build_support/check_pr_authors.py new file mode 100755 index 0000000000..4b7e367a06 --- /dev/null +++ b/build_support/check_pr_authors.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +Check PR authors against validated author list. + +This script fetches PR commits, extracts author information, and compares +against a validated author list from the NEST release dataset. + +SECURITY NOTE: This script is designed to protect sensitive data: +- Private repository information is not logged +- Validated author names from private repo are not exposed in logs +- PR author names can be logged (they're already public in the PR) +- Sensitive data from private repository is cleaned up from memory after use +""" + +import argparse +import json +import logging +import os +import sys +import tempfile +from typing import List, Optional, Tuple + +import requests + + +def secure_cleanup(data: any) -> None: + """Ensure sensitive data is not retained in memory longer than necessary.""" + if isinstance(data, str): + # Overwrite string data with zeros + data = "0" * len(data) + elif isinstance(data, list): + for item in data: + secure_cleanup(item) + elif isinstance(data, dict): + for key, value in data.items(): + secure_cleanup(value) + # Let garbage collector handle the rest + + +def get_pr_commits(pr_number: int, repo_owner: str, repo_name: str, token: str) -> List[dict]: + """Fetch commits from a PR using GitHub API.""" + url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls/{pr_number}/commits" + headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github.v3+json"} + + print(f"Fetching commits from GitHub API for PR #{pr_number} in {repo_owner}/{repo_name}") + + try: + response = requests.get(url, headers=headers) + response.raise_for_status() + + commits = response.json() + if not isinstance(commits, list): + raise ValueError("Unexpected API response (not a JSON array)") + + return commits + except requests.exceptions.RequestException as e: + print(f"Error: Failed to fetch commits from GitHub API: {e}") + sys.exit(1) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + + +def extract_unique_authors(commits: List[dict]) -> List[Tuple[str, str]]: + """Extract unique authors from commits.""" + authors = set() + + for commit in commits: + author_name = commit.get("commit", {}).get("author", {}).get("name", "") + author_email = commit.get("commit", {}).get("author", {}).get("email", "") + + if author_name and author_email: + authors.add((author_name, author_email)) + + return sorted(list(authors)) + + +def fetch_validated_authors( + private_repo_owner: str, private_repo_name: str, authors_file_path: str, token: str +) -> Optional[List[str]]: + """Fetch validated authors from private repository.""" + url = f"https://api.github.com/repos/{private_repo_owner}/{private_repo_name}/contents/{authors_file_path}" + headers = {"Authorization": f"token {token}", "Accept": "application/vnd.github.v3.raw"} + + # Don't log private repository information for security + print("Fetching validated authors from private repository...") + + try: + response = requests.get(url, headers=headers) + + if response.status_code == 404: + print("Warning: Could not find validated authors file") + return None + + response.raise_for_status() + content = response.text + + if not content: + print("Warning: Could not decode validated authors file content") + return None + + # Parse YAML format: "Name : githubhandle" + # Extract the "Name " part before the colon + validated_authors = [] + for line in content.split("\n"): + line = line.strip() + if line and not line.startswith("#") and ":" in line: + author_part = line.split(":")[0].strip() + if author_part: + validated_authors.append(author_part) + + # Only log count, never the actual names + print(f"Found {len(validated_authors)} validated author(s) (list stored securely, not exposed)") + return validated_authors + + except requests.exceptions.RequestException as e: + print(f"Warning: Failed to fetch validated authors: {e}") + return None + + +def check_authors_against_validated_list( + pr_authors: List[Tuple[str, str]], validated_authors: List[str] +) -> Tuple[List[str], int]: + """Check PR authors against validated list.""" + unknown_authors = [] + + for name, email in pr_authors: + author_string = f"{name} <{email}>" + if author_string not in validated_authors: + unknown_authors.append(author_string) + + return unknown_authors, len(unknown_authors) + + +def main(): + parser = argparse.ArgumentParser(description="Check PR authors against validated author list") + parser.add_argument("--pr-number", type=int, required=True, help="Pull request number") + parser.add_argument("--repo-owner", required=True, help="Repository owner") + parser.add_argument("--repo-name", required=True, help="Repository name") + parser.add_argument("--github-token", required=True, help="GitHub token") + parser.add_argument("--private-repo-owner", help="Private repository owner") + parser.add_argument("--private-repo-name", help="Private repository name") + parser.add_argument("--private-repo-token", help="Private repository token") + parser.add_argument( + "--authors-file-path", default="data/gitlognames.yaml", help="Path to authors file in private repo" + ) + parser.add_argument("--fail-on-unknown", action="store_true", help="Fail if unknown authors are found") + + args = parser.parse_args() + + # Get PR commits and extract authors + commits = get_pr_commits(args.pr_number, args.repo_owner, args.repo_name, args.github_token) + + if not commits: + print("No commits found in PR") + print("author_count=0") + print("unknown_count=0") + print("validation_status=skipped") + return + + pr_authors = extract_unique_authors(commits) + author_count = len(pr_authors) + + print(f"Found {author_count} unique author(s):") + for name, email in pr_authors: + print(f" - {name} <{email}>") + + # Output author count and authors list for GitHub Actions + print(f"author_count={author_count}") + + # Output authors list in GitHub Actions format for step summary display + authors_formatted = "\n".join([f"{name} <{email}>" for name, email in pr_authors]) + print(f"authors_formatted< 0: + print("validation_status=failure") + print("❌ FAILURE: Found unknown author(s) in this PR") + print( + "The authors of this PR may be contributing for the first time or may have modified their author information. Author information requires review." + ) + + # Log unknown authors (these are PR authors, so it's safe to show them) + print(f"Unknown authors in this PR:") + for author in unknown_authors: + print(f" - {author}") + + # Clean up sensitive data (only validated_authors, not unknown_authors since they're PR data) + secure_cleanup(validated_authors) + + if args.fail_on_unknown: + print("Build failed due to unknown authors") + sys.exit(1) + else: + print("Build continues despite unknown authors") + else: + print("validation_status=success") + print("✅ SUCCESS: All PR authors are validated") + + # Clean up sensitive data + secure_cleanup(validated_authors) + + +if __name__ == "__main__": + main() From 11ce215bab2c54e44bfe4e15313a499542881088 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Thu, 23 Oct 2025 22:50:17 +0200 Subject: [PATCH 10/15] fix flake, copyright --- build_support/check_pr_authors.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/build_support/check_pr_authors.py b/build_support/check_pr_authors.py index 4b7e367a06..19c6965791 100755 --- a/build_support/check_pr_authors.py +++ b/build_support/check_pr_authors.py @@ -1,3 +1,23 @@ +# -*- coding: utf-8 -*- +# +# check_pr_authors.py +# +# This file is part of NEST. +# +# Copyright (C) 2004 The NEST Initiative +# +# NEST is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# NEST is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NEST. If not, see . #!/usr/bin/env python3 """ Check PR authors against validated author list. @@ -196,7 +216,8 @@ def main(): print("validation_status=failure") print("❌ FAILURE: Found unknown author(s) in this PR") print( - "The authors of this PR may be contributing for the first time or may have modified their author information. Author information requires review." + "The authors of this PR may be contributing for the first time or may have " + "modified their author information. Author information requires review." ) # Log unknown authors (these are PR authors, so it's safe to show them) From d286916d6deac83d1c3dfaba69fbcffabedb7810 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Thu, 23 Oct 2025 22:56:42 +0200 Subject: [PATCH 11/15] fix up script --- build_support/check_pr_authors.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/build_support/check_pr_authors.py b/build_support/check_pr_authors.py index 19c6965791..2714719947 100755 --- a/build_support/check_pr_authors.py +++ b/build_support/check_pr_authors.py @@ -75,9 +75,19 @@ def get_pr_commits(pr_number: int, repo_owner: str, repo_name: str, token: str) return commits except requests.exceptions.RequestException as e: print(f"Error: Failed to fetch commits from GitHub API: {e}") + print(f"URL: {url}") + if "response" in locals(): + print(f"Response status: {response.status_code}") + if response.status_code == 404: + print("This might indicate the PR number is invalid or the repository doesn't exist") + elif response.status_code == 401: + print("This might indicate the GitHub token doesn't have the required permissions") + else: + print("No response received") sys.exit(1) except ValueError as e: print(f"Error: {e}") + print(f"URL: {url}") sys.exit(1) @@ -169,6 +179,7 @@ def main(): args = parser.parse_args() # Get PR commits and extract authors + print(f"Debug: Fetching commits for PR #{args.pr_number} in {args.repo_owner}/{args.repo_name}") commits = get_pr_commits(args.pr_number, args.repo_owner, args.repo_name, args.github_token) if not commits: @@ -190,12 +201,15 @@ def main(): # Output authors list in GitHub Actions format for step summary display authors_formatted = "\n".join([f"{name} <{email}>" for name, email in pr_authors]) - print(f"authors_formatted< Date: Thu, 23 Oct 2025 23:10:11 +0200 Subject: [PATCH 12/15] update script --- build_support/check_pr_authors.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/build_support/check_pr_authors.py b/build_support/check_pr_authors.py index 2714719947..65a6e0d5a6 100755 --- a/build_support/check_pr_authors.py +++ b/build_support/check_pr_authors.py @@ -34,7 +34,6 @@ import argparse import json -import logging import os import sys import tempfile @@ -75,19 +74,9 @@ def get_pr_commits(pr_number: int, repo_owner: str, repo_name: str, token: str) return commits except requests.exceptions.RequestException as e: print(f"Error: Failed to fetch commits from GitHub API: {e}") - print(f"URL: {url}") - if "response" in locals(): - print(f"Response status: {response.status_code}") - if response.status_code == 404: - print("This might indicate the PR number is invalid or the repository doesn't exist") - elif response.status_code == 401: - print("This might indicate the GitHub token doesn't have the required permissions") - else: - print("No response received") sys.exit(1) except ValueError as e: print(f"Error: {e}") - print(f"URL: {url}") sys.exit(1) @@ -179,7 +168,6 @@ def main(): args = parser.parse_args() # Get PR commits and extract authors - print(f"Debug: Fetching commits for PR #{args.pr_number} in {args.repo_owner}/{args.repo_name}") commits = get_pr_commits(args.pr_number, args.repo_owner, args.repo_name, args.github_token) if not commits: @@ -199,7 +187,7 @@ def main(): # Output author count and authors list for GitHub Actions print(f"author_count={author_count}") - # Output authors list in GitHub Actions format for step summary display + # Output authors as formatted list for GitHub Actions step summary authors_formatted = "\n".join([f"{name} <{email}>" for name, email in pr_authors]) print("authors_formatted< Date: Thu, 23 Oct 2025 23:14:33 +0200 Subject: [PATCH 13/15] debuggin error --- .github/workflows/nestbuildmatrix.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index e89a5b2308..a8b6a81e16 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -507,6 +507,7 @@ jobs: FAIL_ON_UNKNOWN_AUTHORS: ${{ vars.FAIL_ON_UNKNOWN_AUTHORS || 'true' }} run: | # Run the script and capture outputs + echo "Running PR authors check script..." python build_support/check_pr_authors.py \ --pr-number ${{ github.event.pull_request.number }} \ --repo-owner ${{ github.repository_owner }} \ @@ -516,7 +517,16 @@ jobs: --private-repo-name "${{ secrets.PRIVATE_REPO_NAME }}" \ --private-repo-token "${{ secrets.PRIVATE_REPO_TOKEN }}" \ --authors-file-path "${{ vars.VALIDATED_AUTHORS_FILE_PATH || 'data/gitlognames.yaml' }}" \ - --fail-on-unknown ${{ vars.FAIL_ON_UNKNOWN_AUTHORS == 'true' }} > /tmp/script_output.txt 2>&1 + --fail-on-unknown ${{ vars.FAIL_ON_UNKNOWN_AUTHORS == 'true' }} 2>&1 | tee /tmp/script_output.txt + + # Check if script failed + SCRIPT_EXIT_CODE=${PIPESTATUS[0]} + if [ $SCRIPT_EXIT_CODE -ne 0 ]; then + echo "Script failed with exit code $SCRIPT_EXIT_CODE" + echo "Script output:" + cat /tmp/script_output.txt + exit $SCRIPT_EXIT_CODE + fi # Extract outputs for GitHub Actions if [ -f /tmp/script_output.txt ]; then @@ -534,9 +544,6 @@ jobs: cat /tmp/authors_formatted.txt >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT fi - - # Display the script output - cat /tmp/script_output.txt fi - name: "Display PR authors summary" From ae2b1dafb79a725f394c7da75fa72811459ebc55 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Thu, 23 Oct 2025 23:21:27 +0200 Subject: [PATCH 14/15] debug2 --- .github/workflows/nestbuildmatrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index a8b6a81e16..fb6ed40c41 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -517,7 +517,7 @@ jobs: --private-repo-name "${{ secrets.PRIVATE_REPO_NAME }}" \ --private-repo-token "${{ secrets.PRIVATE_REPO_TOKEN }}" \ --authors-file-path "${{ vars.VALIDATED_AUTHORS_FILE_PATH || 'data/gitlognames.yaml' }}" \ - --fail-on-unknown ${{ vars.FAIL_ON_UNKNOWN_AUTHORS == 'true' }} 2>&1 | tee /tmp/script_output.txt + ${{ vars.FAIL_ON_UNKNOWN_AUTHORS == 'true' && '--fail-on-unknown' || '' }} 2>&1 | tee /tmp/script_output.txt # Check if script failed SCRIPT_EXIT_CODE=${PIPESTATUS[0]} From 9bd3fbe7b29dee375bcbda4ce0c446b665f10626 Mon Sep 17 00:00:00 2001 From: Jessica Mitchell Date: Thu, 23 Oct 2025 23:37:00 +0200 Subject: [PATCH 15/15] display summary during build --- .github/workflows/nestbuildmatrix.yml | 30 +++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/nestbuildmatrix.yml b/.github/workflows/nestbuildmatrix.yml index fb6ed40c41..dd1d1664ec 100644 --- a/.github/workflows/nestbuildmatrix.yml +++ b/.github/workflows/nestbuildmatrix.yml @@ -548,6 +548,7 @@ jobs: - name: "Display PR authors summary" run: | + # Create summary content echo "## PR Authors Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Total unique authors:** ${{ steps.check_authors.outputs.author_count }}" >> $GITHUB_STEP_SUMMARY @@ -575,6 +576,35 @@ jobs: echo "ℹ️ **Note:** Author validation check skipped (private repository access not configured)" >> $GITHUB_STEP_SUMMARY fi + # Also display in build logs for visibility + echo "=== PR AUTHORS SUMMARY ===" + echo "## PR Authors Summary" + echo "" + echo "**Total unique authors:** ${{ steps.check_authors.outputs.author_count }}" + echo "" + echo "**Authors:**" + echo '```' + echo "${{ steps.check_authors.outputs.authors_formatted }}" + echo '```' + + if [ "${{ steps.check_authors.outputs.validation_status }}" = "success" ]; then + echo "" + echo "## Author Validation Check" + echo "" + echo "✅ **SUCCESS: All PR authors are validated**" + elif [ "${{ steps.check_authors.outputs.validation_status }}" = "failure" ]; then + echo "" + echo "## Author Validation Check" + echo "" + echo "❌ **FAILURE:** ${{ steps.check_authors.outputs.unknown_count }} unknown author(s) found" + echo "" + echo "The authors of this PR may be contributing for the first time or may have modified their author information. Author information requires review." + else + echo "" + echo "ℹ️ **Note:** Author validation check skipped (private repository access not configured)" + fi + echo "=========================" + sphinx-rtd: # as close as possible to the Readthedocs setup (system install cmake, pip install -r doc/requirements.txt) runs-on: "ubuntu-22.04"