From 9012105f80d681adcaef7973fe8c45d42ac447f3 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:25:51 -0400 Subject: [PATCH 01/15] Add PR commit signature verification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/verify-commit-signatures.sh | 216 ++++++++++++++++++ .../workflows/verify-commit-signatures.yml | 41 ++++ CONTRIBUTING.md | 2 + .../first-commit-06-creating-a-pr/index.md | 2 +- .../contributing-pull-requests/README.md | 4 +- 5 files changed, 263 insertions(+), 2 deletions(-) create mode 100755 .github/scripts/verify-commit-signatures.sh create mode 100644 .github/workflows/verify-commit-signatures.yml diff --git a/.github/scripts/verify-commit-signatures.sh b/.github/scripts/verify-commit-signatures.sh new file mode 100755 index 00000000000..495b72bfffe --- /dev/null +++ b/.github/scripts/verify-commit-signatures.sh @@ -0,0 +1,216 @@ +#!/bin/bash + +# ------------------------------------------------------------ +# Copyright 2026 The Radius Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------ + +# Verifies that every pull request commit has a GitHub Verified signature. +# Requires the GitHub CLI and jq. + +set -euo pipefail + +readonly COMMENT_MARKER="" +readonly GUIDE_DIR="docs/contributing/contributing-code" +readonly GUIDE_SECTION="contributing-code-first-commit" +readonly GUIDE_PAGE="first-commit-06-creating-a-pr/index.md" +readonly GUIDE_PATH="${GUIDE_DIR}/${GUIDE_SECTION}/${GUIDE_PAGE}" +readonly GITHUB_ORIGIN="${GITHUB_SERVER_URL:-https://github.com}" +readonly GUIDE_REPO="${GITHUB_ORIGIN}/radius-project/radius" +readonly GUIDE_BLOB_URL="${GUIDE_REPO}/blob/main/${GUIDE_PATH}" +readonly GUIDE_URL="${GUIDE_BLOB_URL}#signing-your-commits" +# shellcheck disable=SC2016 # GraphQL variables must remain literal. +readonly GRAPHQL_QUERY=' +query( + $endCursor: String + $number: Int! + $owner: String! + $repo: String! +) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + commits(first: 100, after: $endCursor) { + nodes { + commit { + oid + signature { + isValid + state + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +}' + +TEMP_DIR="" + +cleanup() { + if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then + rm -rf "${TEMP_DIR}" + fi +} + +trap cleanup EXIT + +require_environment() { + local name + for name in GH_TOKEN GITHUB_REPOSITORY PR_AUTHOR PR_NUMBER; do + if [[ -z "${!name:-}" ]]; then + echo "Error: ${name} is required" >&2 + return 1 + fi + done +} + +require_commands() { + local command + for command in gh jq; do + if ! command -v "${command}" > /dev/null; then + echo "Error: ${command} is required" >&2 + return 1 + fi + done +} + +delete_comment() { + local comment_id="$1" + gh api --method DELETE \ + "/repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" +} + +main() { + require_environment + require_commands + + local owner + local repo + IFS="/" read -r owner repo <<< "${GITHUB_REPOSITORY}" + if [[ -z "${owner}" || -z "${repo}" ]]; then + echo "Error: GITHUB_REPOSITORY must use owner/repo format" >&2 + return 1 + fi + + TEMP_DIR="$(mktemp -d)" + local commits_file="${TEMP_DIR}/commits.json" + local unverified_file="${TEMP_DIR}/unverified.txt" + local comments_file="${TEMP_DIR}/comments.json" + local comment_ids_file="${TEMP_DIR}/comment-ids.txt" + local comment_body_file="${TEMP_DIR}/comment.md" + local issue_endpoint="/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}" + local comments_endpoint="/repos/${GITHUB_REPOSITORY}/issues/comments" + + gh api graphql --paginate --slurp \ + -f query="${GRAPHQL_QUERY}" \ + -F owner="${owner}" \ + -F repo="${repo}" \ + -F number="${PR_NUMBER}" > "${commits_file}" + + if ! jq -e \ + 'all(.[]; .data.repository.pullRequest != null)' \ + "${commits_file}" > /dev/null; then + echo "Error: pull request ${PR_NUMBER} was not found" >&2 + return 1 + fi + + jq -r ' + .[] | + .data.repository.pullRequest.commits.nodes[] | + select( + .commit.signature == null or + .commit.signature.isValid != true or + .commit.signature.state != "VALID" + ) | + .commit.oid + ' "${commits_file}" > "${unverified_file}" + + gh api --paginate --slurp \ + "${issue_endpoint}/comments?per_page=100" > "${comments_file}" + + jq -r --arg marker "${COMMENT_MARKER}" ' + .[][] | + select(.user.login == "github-actions[bot]") | + select(.body | contains($marker)) | + .id + ' "${comments_file}" > "${comment_ids_file}" + + local -a unverified_commits + local -a comment_ids + mapfile -t unverified_commits < "${unverified_file}" + mapfile -t comment_ids < "${comment_ids_file}" + + if ((${#unverified_commits[@]} == 0)); then + local comment_id + for comment_id in "${comment_ids[@]}"; do + delete_comment "${comment_id}" + done + echo "All pull request commits have Verified signatures." + return + fi + + { + echo "Hi @${PR_AUTHOR}, thank you for contributing to Radius." + echo + echo "The following commits do not currently show a GitHub" \ + "**Verified** signature:" + echo + local commit + for commit in "${unverified_commits[@]}"; do + printf -- "- [\`%s\`](%s/%s/commit/%s)\n" \ + "${commit:0:7}" \ + "${GITHUB_ORIGIN}" \ + "${GITHUB_REPOSITORY}" \ + "${commit}" + done + echo + echo "Please follow our [commit-signing guide](${GUIDE_URL}) to" \ + "configure GPG, SSH, or S/MIME signing. Then re-sign the affected" \ + "commits and force-push the rewritten branch with" \ + "\`--force-with-lease\`." + echo + echo "Cryptographic commit signing is separate from the DCO" \ + "\`Signed-off-by\` line; both are required." + echo + echo "${COMMENT_MARKER}" + } > "${comment_body_file}" + + if ((${#comment_ids[@]} == 0)); then + jq -n --rawfile body "${comment_body_file}" '{body: $body}' | + gh api --method POST \ + "${issue_endpoint}/comments" \ + --input - > /dev/null + else + jq -n --rawfile body "${comment_body_file}" '{body: $body}' | + gh api --method PATCH \ + "${comments_endpoint}/${comment_ids[0]}" \ + --input - > /dev/null + + local duplicate_id + for duplicate_id in "${comment_ids[@]:1}"; do + delete_comment "${duplicate_id}" + done + fi + + printf '%s%d%s\n' \ + "::warning title=Unverified commit signatures::" \ + "${#unverified_commits[@]}" \ + " commit(s) do not show a Verified signature." +} + +main "$@" diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml new file mode 100644 index 00000000000..e0d020fdd0b --- /dev/null +++ b/.github/workflows/verify-commit-signatures.yml @@ -0,0 +1,41 @@ +# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json +--- +name: Verify Commit Signatures + +on: + # This workflow uses the trusted base commit and never executes PR-head code. + pull_request_target: + types: [opened, reopened, synchronize] + # workflow_dispatch: # Enable during development only. + +concurrency: + group: verify-commit-signatures-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: {} + +jobs: + verify-commit-signatures: + name: Verify Commit Signatures + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + issues: write + pull-requests: read + steps: + - name: Checkout signature verifier + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.base.sha }} + sparse-checkout: | + .github/scripts/verify-commit-signatures.sh + sparse-checkout-cone-mode: false + persist-credentials: false + + - name: Verify commit signatures + env: + GH_TOKEN: ${{ github.token }} + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: ./.github/scripts/verify-commit-signatures.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2f8b5c6806f..0b06352be6c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,8 @@ git commit -s -m 'This is my commit message' Visual Studio Code has a setting, `git.alwaysSignOff` to automatically add a Signed-off-by line to commit messages. Search for "sign-off" in VS Code settings to find it and enable it. +Every commit must also be cryptographically signed so that GitHub displays a **Verified** badge. Cryptographic signing is separate from the DCO `Signed-off-by` line; both are required. See [Signing your commits](docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md#signing-your-commits) for setup and remediation guidance. + ## Code of conduct This project has adopted the [Contributor Covenant](http://contributor-covenant.org/). diff --git a/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md b/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md index a5a98838126..e74b2167de7 100644 --- a/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md +++ b/docs/contributing/contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md @@ -48,7 +48,7 @@ Visual Studio Code has a setting, `git.alwaysSignOff` to automatically add a Sig ## Signing your commits -> 💡 Commit signing is **separate from** the DCO `Signed-off-by` line described above. The `Signed-off-by` line is a textual attestation, while commit signing proves that the commit was signed with the private key corresponding to your configured public key. Both are recommended. +> 💡 Commit signing is **separate from** the DCO `Signed-off-by` line described above. The `Signed-off-by` line is a textual attestation, while commit signing proves that the commit was signed with the private key corresponding to your configured public key. Both are required. We require all contributors to **cryptographically sign their commits** so that they show as **Verified** on GitHub. On GitHub, **Verified** means GitHub could validate the signature and that the signing key is associated with the account. This gives reviewers and the community additional confidence in the integrity and provenance of commits, which is an important supply-chain safeguard. diff --git a/docs/contributing/contributing-pull-requests/README.md b/docs/contributing/contributing-pull-requests/README.md index a6e3ea57a16..45a34d5ce92 100644 --- a/docs/contributing/contributing-pull-requests/README.md +++ b/docs/contributing/contributing-pull-requests/README.md @@ -45,7 +45,9 @@ We **squash** pull requests as part of the merge process, so intermediate commit ### 3. Sign your commits -The Developer Certificate of Origin (DCO) check requires every commit to be signed off. See [Signing your commits](../contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md#signing-your-commits) in the first commit guide for how to do this. +The Developer Certificate of Origin (DCO) check requires every commit to include a `Signed-off-by` line. Every commit must also be cryptographically signed so that GitHub displays a **Verified** badge. These are separate requirements; see [Signing your commits](../contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md#signing-your-commits) in the first commit guide for setup and remediation guidance. + +The commit-signature verification workflow posts a non-blocking reminder when any commit does not show a **Verified** signature. It updates that reminder after each push and removes it when all commits are verified. ### 4. Open the pull request and fill out the template From 194aa116d1094de1b7116e4aff42d62be0439e29 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:58:23 -0400 Subject: [PATCH 02/15] Refactor signature comment orchestration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/verify-commit-signatures.sh | 147 +++--------------- .../workflows/verify-commit-signatures.yml | 63 +++++++- 2 files changed, 82 insertions(+), 128 deletions(-) diff --git a/.github/scripts/verify-commit-signatures.sh b/.github/scripts/verify-commit-signatures.sh index 495b72bfffe..b6421af9ca0 100755 --- a/.github/scripts/verify-commit-signatures.sh +++ b/.github/scripts/verify-commit-signatures.sh @@ -16,20 +16,11 @@ # limitations under the License. # ------------------------------------------------------------ -# Verifies that every pull request commit has a GitHub Verified signature. +# Lists pull request commits that do not have a GitHub Verified signature. # Requires the GitHub CLI and jq. set -euo pipefail -readonly COMMENT_MARKER="" -readonly GUIDE_DIR="docs/contributing/contributing-code" -readonly GUIDE_SECTION="contributing-code-first-commit" -readonly GUIDE_PAGE="first-commit-06-creating-a-pr/index.md" -readonly GUIDE_PATH="${GUIDE_DIR}/${GUIDE_SECTION}/${GUIDE_PAGE}" -readonly GITHUB_ORIGIN="${GITHUB_SERVER_URL:-https://github.com}" -readonly GUIDE_REPO="${GITHUB_ORIGIN}/radius-project/radius" -readonly GUIDE_BLOB_URL="${GUIDE_REPO}/blob/main/${GUIDE_PATH}" -readonly GUIDE_URL="${GUIDE_BLOB_URL}#signing-your-commits" # shellcheck disable=SC2016 # GraphQL variables must remain literal. readonly GRAPHQL_QUERY=' query( @@ -59,19 +50,9 @@ query( } }' -TEMP_DIR="" - -cleanup() { - if [[ -n "${TEMP_DIR}" && -d "${TEMP_DIR}" ]]; then - rm -rf "${TEMP_DIR}" - fi -} - -trap cleanup EXIT - require_environment() { local name - for name in GH_TOKEN GITHUB_REPOSITORY PR_AUTHOR PR_NUMBER; do + for name in GH_TOKEN GITHUB_REPOSITORY PR_NUMBER; do if [[ -z "${!name:-}" ]]; then echo "Error: ${name} is required" >&2 return 1 @@ -89,12 +70,6 @@ require_commands() { done } -delete_comment() { - local comment_id="$1" - gh api --method DELETE \ - "/repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" -} - main() { require_environment require_commands @@ -107,110 +82,34 @@ main() { return 1 fi - TEMP_DIR="$(mktemp -d)" - local commits_file="${TEMP_DIR}/commits.json" - local unverified_file="${TEMP_DIR}/unverified.txt" - local comments_file="${TEMP_DIR}/comments.json" - local comment_ids_file="${TEMP_DIR}/comment-ids.txt" - local comment_body_file="${TEMP_DIR}/comment.md" - local issue_endpoint="/repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}" - local comments_endpoint="/repos/${GITHUB_REPOSITORY}/issues/comments" - - gh api graphql --paginate --slurp \ - -f query="${GRAPHQL_QUERY}" \ - -F owner="${owner}" \ - -F repo="${repo}" \ - -F number="${PR_NUMBER}" > "${commits_file}" + local response + response="$( + gh api graphql --paginate --slurp \ + -f query="${GRAPHQL_QUERY}" \ + -F owner="${owner}" \ + -F repo="${repo}" \ + -F number="${PR_NUMBER}" + )" if ! jq -e \ 'all(.[]; .data.repository.pullRequest != null)' \ - "${commits_file}" > /dev/null; then + <<< "${response}" > /dev/null; then echo "Error: pull request ${PR_NUMBER} was not found" >&2 return 1 fi - jq -r ' - .[] | - .data.repository.pullRequest.commits.nodes[] | - select( - .commit.signature == null or - .commit.signature.isValid != true or - .commit.signature.state != "VALID" - ) | - .commit.oid - ' "${commits_file}" > "${unverified_file}" - - gh api --paginate --slurp \ - "${issue_endpoint}/comments?per_page=100" > "${comments_file}" - - jq -r --arg marker "${COMMENT_MARKER}" ' - .[][] | - select(.user.login == "github-actions[bot]") | - select(.body | contains($marker)) | - .id - ' "${comments_file}" > "${comment_ids_file}" - - local -a unverified_commits - local -a comment_ids - mapfile -t unverified_commits < "${unverified_file}" - mapfile -t comment_ids < "${comment_ids_file}" - - if ((${#unverified_commits[@]} == 0)); then - local comment_id - for comment_id in "${comment_ids[@]}"; do - delete_comment "${comment_id}" - done - echo "All pull request commits have Verified signatures." - return - fi - - { - echo "Hi @${PR_AUTHOR}, thank you for contributing to Radius." - echo - echo "The following commits do not currently show a GitHub" \ - "**Verified** signature:" - echo - local commit - for commit in "${unverified_commits[@]}"; do - printf -- "- [\`%s\`](%s/%s/commit/%s)\n" \ - "${commit:0:7}" \ - "${GITHUB_ORIGIN}" \ - "${GITHUB_REPOSITORY}" \ - "${commit}" - done - echo - echo "Please follow our [commit-signing guide](${GUIDE_URL}) to" \ - "configure GPG, SSH, or S/MIME signing. Then re-sign the affected" \ - "commits and force-push the rewritten branch with" \ - "\`--force-with-lease\`." - echo - echo "Cryptographic commit signing is separate from the DCO" \ - "\`Signed-off-by\` line; both are required." - echo - echo "${COMMENT_MARKER}" - } > "${comment_body_file}" - - if ((${#comment_ids[@]} == 0)); then - jq -n --rawfile body "${comment_body_file}" '{body: $body}' | - gh api --method POST \ - "${issue_endpoint}/comments" \ - --input - > /dev/null - else - jq -n --rawfile body "${comment_body_file}" '{body: $body}' | - gh api --method PATCH \ - "${comments_endpoint}/${comment_ids[0]}" \ - --input - > /dev/null - - local duplicate_id - for duplicate_id in "${comment_ids[@]:1}"; do - delete_comment "${duplicate_id}" - done - fi - - printf '%s%d%s\n' \ - "::warning title=Unverified commit signatures::" \ - "${#unverified_commits[@]}" \ - " commit(s) do not show a Verified signature." + jq -c ' + [ + .[] | + .data.repository.pullRequest.commits.nodes[] | + select( + .commit.signature == null or + .commit.signature.isValid != true or + .commit.signature.state != "VALID" + ) | + .commit.oid + ] + ' <<< "${response}" } main "$@" diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index e0d020fdd0b..8ddfe66d447 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -21,8 +21,7 @@ jobs: timeout-minutes: 5 permissions: contents: read - issues: write - pull-requests: read + pull-requests: write steps: - name: Checkout signature verifier uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -34,8 +33,64 @@ jobs: persist-credentials: false - name: Verify commit signatures + id: verify env: GH_TOKEN: ${{ github.token }} - PR_AUTHOR: ${{ github.event.pull_request.user.login }} PR_NUMBER: ${{ github.event.pull_request.number }} - run: ./.github/scripts/verify-commit-signatures.sh + run: | + unverified_commits="$(./.github/scripts/verify-commit-signatures.sh)" + printf 'unverified_commits=%s\n' "${unverified_commits}" >> "${GITHUB_OUTPUT}" + printf 'count=%s\n' "$(jq 'length' <<< "${unverified_commits}")" >> "${GITHUB_OUTPUT}" + + - name: Prepare signature guidance + if: steps.verify.outputs.count != '0' + env: + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + UNVERIFIED_COMMITS: ${{ steps.verify.outputs.unverified_commits }} + UNVERIFIED_COUNT: ${{ steps.verify.outputs.count }} + run: | + guide_path="docs/contributing/contributing-code" + guide_path+="/contributing-code-first-commit" + guide_path+="/first-commit-06-creating-a-pr/index.md" + guide_url="${GITHUB_SERVER_URL}/radius-project/radius/blob/main" + guide_url+="/${guide_path}#signing-your-commits" + + { + printf 'Hi @%s, thank you for contributing to Radius.\n\n' "${PR_AUTHOR}" + echo "The following commits do not currently show a GitHub **Verified** signature:" + echo + jq -r \ + --arg origin "${GITHUB_SERVER_URL}" \ + --arg repo "${GITHUB_REPOSITORY}" \ + '.[] | "- [`\(.[:7])`](\($origin)/\($repo)/commit/\(.))"' \ + <<< "${UNVERIFIED_COMMITS}" + echo + printf '%s%s%s\n' \ + "Please follow our [commit-signing guide](${guide_url}) to " \ + "configure GPG, SSH, or S/MIME signing. Then re-sign the affected " \ + "commits and force-push the rewritten branch with \`--force-with-lease\`." + echo + printf '%s%s\n' \ + "Cryptographic commit signing is separate from the DCO " \ + "\`Signed-off-by\` line; both are required." + } > commit-signature-guidance.md + + printf '%s%s%s\n' \ + "::warning title=Unverified commit signatures::" \ + "${UNVERIFIED_COUNT}" \ + " commit(s) do not show a Verified signature." + + - name: Post signature guidance + if: steps.verify.outputs.count != '0' + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 + with: + header: commit-signature-verification + path: commit-signature-guidance.md + skip_unchanged: true + + - name: Remove resolved signature guidance + if: steps.verify.outputs.count == '0' + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 + with: + header: commit-signature-verification + delete: true From a3bee1f74de234219c13b0583235c2ddf80f4018 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:22:14 -0400 Subject: [PATCH 03/15] Enable manual signature verification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .../workflows/verify-commit-signatures.yml | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index 8ddfe66d447..095fd2d1b97 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -6,10 +6,15 @@ on: # This workflow uses the trusted base commit and never executes PR-head code. pull_request_target: types: [opened, reopened, synchronize] - # workflow_dispatch: # Enable during development only. + workflow_dispatch: + inputs: + pr_number: + description: Pull request number to verify + required: true + type: number concurrency: - group: verify-commit-signatures-${{ github.event.pull_request.number }} + group: verify-commit-signatures-${{ github.event.pull_request.number || inputs.pr_number }} cancel-in-progress: true permissions: {} @@ -19,6 +24,9 @@ jobs: name: Verify Commit Signatures runs-on: ubuntu-24.04 timeout-minutes: 5 + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} permissions: contents: read pull-requests: write @@ -26,7 +34,7 @@ jobs: - name: Checkout signature verifier uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.pull_request.base.sha }} + ref: ${{ github.event.pull_request.base.sha || github.sha }} sparse-checkout: | .github/scripts/verify-commit-signatures.sh sparse-checkout-cone-mode: false @@ -34,9 +42,6 @@ jobs: - name: Verify commit signatures id: verify - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} run: | unverified_commits="$(./.github/scripts/verify-commit-signatures.sh)" printf 'unverified_commits=%s\n' "${unverified_commits}" >> "${GITHUB_OUTPUT}" @@ -45,10 +50,13 @@ jobs: - name: Prepare signature guidance if: steps.verify.outputs.count != '0' env: - PR_AUTHOR: ${{ github.event.pull_request.user.login }} UNVERIFIED_COMMITS: ${{ steps.verify.outputs.unverified_commits }} UNVERIFIED_COUNT: ${{ steps.verify.outputs.count }} run: | + pr_author="$( + gh api "/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ + --jq '.user.login' + )" guide_path="docs/contributing/contributing-code" guide_path+="/contributing-code-first-commit" guide_path+="/first-commit-06-creating-a-pr/index.md" @@ -56,7 +64,7 @@ jobs: guide_url+="/${guide_path}#signing-your-commits" { - printf 'Hi @%s, thank you for contributing to Radius.\n\n' "${PR_AUTHOR}" + printf 'Hi @%s, thank you for contributing to Radius.\n\n' "${pr_author}" echo "The following commits do not currently show a GitHub **Verified** signature:" echo jq -r \ @@ -85,6 +93,7 @@ jobs: uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: commit-signature-verification + number_force: ${{ github.event.pull_request.number || inputs.pr_number }} path: commit-signature-guidance.md skip_unchanged: true @@ -93,4 +102,5 @@ jobs: uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: commit-signature-verification + number_force: ${{ github.event.pull_request.number || inputs.pr_number }} delete: true From fcbf3e3699ae1a9b51e940f361c58f2772352e28 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:36:24 -0400 Subject: [PATCH 04/15] Report commit verification results Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/verify-commit-signatures.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index 095fd2d1b97..dd8577d8387 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -44,8 +44,17 @@ jobs: id: verify run: | unverified_commits="$(./.github/scripts/verify-commit-signatures.sh)" + count="$(jq 'length' <<< "${unverified_commits}")" + + if [[ "${count}" == "0" ]]; then + echo "All pull request commits have Verified signatures." + else + printf 'Found %s commit(s) without Verified signatures:\n' "${count}" + jq -r '.[] | "- \(.[0:7])"' <<< "${unverified_commits}" + fi + printf 'unverified_commits=%s\n' "${unverified_commits}" >> "${GITHUB_OUTPUT}" - printf 'count=%s\n' "$(jq 'length' <<< "${unverified_commits}")" >> "${GITHUB_OUTPUT}" + printf 'count=%s\n' "${count}" >> "${GITHUB_OUTPUT}" - name: Prepare signature guidance if: steps.verify.outputs.count != '0' From 4dbb9788221ebb14512f109d1ccb6709c66bf82c Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:04:25 -0400 Subject: [PATCH 05/15] Retain commit signature guidance comments Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/verify-commit-signatures.yml | 8 -------- docs/contributing/contributing-pull-requests/README.md | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index dd8577d8387..536fe6f9fe3 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -105,11 +105,3 @@ jobs: number_force: ${{ github.event.pull_request.number || inputs.pr_number }} path: commit-signature-guidance.md skip_unchanged: true - - - name: Remove resolved signature guidance - if: steps.verify.outputs.count == '0' - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 - with: - header: commit-signature-verification - number_force: ${{ github.event.pull_request.number || inputs.pr_number }} - delete: true diff --git a/docs/contributing/contributing-pull-requests/README.md b/docs/contributing/contributing-pull-requests/README.md index 45a34d5ce92..eabd3afaff9 100644 --- a/docs/contributing/contributing-pull-requests/README.md +++ b/docs/contributing/contributing-pull-requests/README.md @@ -47,7 +47,7 @@ We **squash** pull requests as part of the merge process, so intermediate commit The Developer Certificate of Origin (DCO) check requires every commit to include a `Signed-off-by` line. Every commit must also be cryptographically signed so that GitHub displays a **Verified** badge. These are separate requirements; see [Signing your commits](../contributing-code/contributing-code-first-commit/first-commit-06-creating-a-pr/index.md#signing-your-commits) in the first commit guide for setup and remediation guidance. -The commit-signature verification workflow posts a non-blocking reminder when any commit does not show a **Verified** signature. It updates that reminder after each push and removes it when all commits are verified. +The commit-signature verification workflow posts a non-blocking reminder when any commit does not show a **Verified** signature. It updates that reminder while commits remain unverified and leaves it on the pull request after all commits are verified. ### 4. Open the pull request and fill out the template From d185c644d7fb12f0899fb172fb70eb176c5bf135 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:07:19 -0400 Subject: [PATCH 06/15] Avoid workflow command injection Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/verify-commit-signatures.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index 536fe6f9fe3..4730a390b30 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -60,7 +60,6 @@ jobs: if: steps.verify.outputs.count != '0' env: UNVERIFIED_COMMITS: ${{ steps.verify.outputs.unverified_commits }} - UNVERIFIED_COUNT: ${{ steps.verify.outputs.count }} run: | pr_author="$( gh api "/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ @@ -92,11 +91,6 @@ jobs: "\`Signed-off-by\` line; both are required." } > commit-signature-guidance.md - printf '%s%s%s\n' \ - "::warning title=Unverified commit signatures::" \ - "${UNVERIFIED_COUNT}" \ - " commit(s) do not show a Verified signature." - - name: Post signature guidance if: steps.verify.outputs.count != '0' uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 From 561ebd3c7082d88c5f5046b182c4fb0f42b05d4e Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:26:16 -0400 Subject: [PATCH 07/15] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/verify-commit-signatures.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index 4730a390b30..a0559ea1c6f 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json --- -name: Verify Commit Signatures +name: verify-commit-signatures on: # This workflow uses the trusted base commit and never executes PR-head code. From 622b615c3cf81064004e82693273b0737a51c541 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:29:13 -0400 Subject: [PATCH 08/15] Validate signature verifier output Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .../workflows/verify-commit-signatures.yml | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index a0559ea1c6f..86da844d6c0 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -43,28 +43,29 @@ jobs: - name: Verify commit signatures id: verify run: | - unverified_commits="$(./.github/scripts/verify-commit-signatures.sh)" - count="$(jq 'length' <<< "${unverified_commits}")" + ./.github/scripts/verify-commit-signatures.sh > unverified-commits.json + if ! jq -e ' + type == "array" and + all(.[]; type == "string" and test("^[0-9a-f]{40}$")) + ' unverified-commits.json > /dev/null; then + echo "Signature verifier returned invalid commit data." >&2 + exit 1 + fi + + count="$(jq 'length' unverified-commits.json)" if [[ "${count}" == "0" ]]; then echo "All pull request commits have Verified signatures." + echo "has_unverified=false" >> "${GITHUB_OUTPUT}" else printf 'Found %s commit(s) without Verified signatures:\n' "${count}" - jq -r '.[] | "- \(.[0:7])"' <<< "${unverified_commits}" + jq -r '.[] | "- \(.[0:7])"' unverified-commits.json + echo "has_unverified=true" >> "${GITHUB_OUTPUT}" fi - printf 'unverified_commits=%s\n' "${unverified_commits}" >> "${GITHUB_OUTPUT}" - printf 'count=%s\n' "${count}" >> "${GITHUB_OUTPUT}" - - name: Prepare signature guidance - if: steps.verify.outputs.count != '0' - env: - UNVERIFIED_COMMITS: ${{ steps.verify.outputs.unverified_commits }} + if: steps.verify.outputs.has_unverified == 'true' run: | - pr_author="$( - gh api "/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ - --jq '.user.login' - )" guide_path="docs/contributing/contributing-code" guide_path+="/contributing-code-first-commit" guide_path+="/first-commit-06-creating-a-pr/index.md" @@ -72,14 +73,15 @@ jobs: guide_url+="/${guide_path}#signing-your-commits" { - printf 'Hi @%s, thank you for contributing to Radius.\n\n' "${pr_author}" + echo "Thank you for contributing to Radius." + echo echo "The following commits do not currently show a GitHub **Verified** signature:" echo jq -r \ --arg origin "${GITHUB_SERVER_URL}" \ --arg repo "${GITHUB_REPOSITORY}" \ '.[] | "- [`\(.[:7])`](\($origin)/\($repo)/commit/\(.))"' \ - <<< "${UNVERIFIED_COMMITS}" + unverified-commits.json echo printf '%s%s%s\n' \ "Please follow our [commit-signing guide](${guide_url}) to " \ @@ -92,7 +94,7 @@ jobs: } > commit-signature-guidance.md - name: Post signature guidance - if: steps.verify.outputs.count != '0' + if: steps.verify.outputs.has_unverified == 'true' uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 with: header: commit-signature-verification From 68337830ee14a1522adfa26b78d7fe2fc007ed9e Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 14:09:19 -0400 Subject: [PATCH 09/15] Use github-script for signature verification Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/verify-commit-signatures.sh | 115 ---------- .../workflows/verify-commit-signatures.yml | 209 ++++++++++++------ 2 files changed, 145 insertions(+), 179 deletions(-) delete mode 100755 .github/scripts/verify-commit-signatures.sh diff --git a/.github/scripts/verify-commit-signatures.sh b/.github/scripts/verify-commit-signatures.sh deleted file mode 100755 index b6421af9ca0..00000000000 --- a/.github/scripts/verify-commit-signatures.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/bin/bash - -# ------------------------------------------------------------ -# Copyright 2026 The Radius Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# ------------------------------------------------------------ - -# Lists pull request commits that do not have a GitHub Verified signature. -# Requires the GitHub CLI and jq. - -set -euo pipefail - -# shellcheck disable=SC2016 # GraphQL variables must remain literal. -readonly GRAPHQL_QUERY=' -query( - $endCursor: String - $number: Int! - $owner: String! - $repo: String! -) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - commits(first: 100, after: $endCursor) { - nodes { - commit { - oid - signature { - isValid - state - } - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - } -}' - -require_environment() { - local name - for name in GH_TOKEN GITHUB_REPOSITORY PR_NUMBER; do - if [[ -z "${!name:-}" ]]; then - echo "Error: ${name} is required" >&2 - return 1 - fi - done -} - -require_commands() { - local command - for command in gh jq; do - if ! command -v "${command}" > /dev/null; then - echo "Error: ${command} is required" >&2 - return 1 - fi - done -} - -main() { - require_environment - require_commands - - local owner - local repo - IFS="/" read -r owner repo <<< "${GITHUB_REPOSITORY}" - if [[ -z "${owner}" || -z "${repo}" ]]; then - echo "Error: GITHUB_REPOSITORY must use owner/repo format" >&2 - return 1 - fi - - local response - response="$( - gh api graphql --paginate --slurp \ - -f query="${GRAPHQL_QUERY}" \ - -F owner="${owner}" \ - -F repo="${repo}" \ - -F number="${PR_NUMBER}" - )" - - if ! jq -e \ - 'all(.[]; .data.repository.pullRequest != null)' \ - <<< "${response}" > /dev/null; then - echo "Error: pull request ${PR_NUMBER} was not found" >&2 - return 1 - fi - - jq -c ' - [ - .[] | - .data.repository.pullRequest.commits.nodes[] | - select( - .commit.signature == null or - .commit.signature.isValid != true or - .commit.signature.state != "VALID" - ) | - .commit.oid - ] - ' <<< "${response}" -} - -main "$@" diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index 86da844d6c0..19fdc16d96e 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -25,79 +25,160 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 5 env: - GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} permissions: contents: read pull-requests: write steps: - - name: Checkout signature verifier - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Verify commit signatures + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - ref: ${{ github.event.pull_request.base.sha || github.sha }} - sparse-checkout: | - .github/scripts/verify-commit-signatures.sh - sparse-checkout-cone-mode: false - persist-credentials: false + github-token: ${{ github.token }} + script: | + const prNumber = Number(process.env.PR_NUMBER) + if (!Number.isSafeInteger(prNumber) || prNumber <= 0) { + throw new Error('PR_NUMBER must be a positive integer') + } - - name: Verify commit signatures - id: verify - run: | - ./.github/scripts/verify-commit-signatures.sh > unverified-commits.json - if ! jq -e ' - type == "array" and - all(.[]; type == "string" and test("^[0-9a-f]{40}$")) - ' unverified-commits.json > /dev/null; then - echo "Signature verifier returned invalid commit data." >&2 - exit 1 - fi + const query = ` + query( + $owner: String! + $repo: String! + $number: Int! + $cursor: String + ) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + commits(first: 100, after: $cursor) { + nodes { + commit { + oid + signature { + isValid + state + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + ` - count="$(jq 'length' unverified-commits.json)" + const unverified = [] + let cursor = null + do { + const result = await github.graphql(query, { + ...context.repo, + number: prNumber, + cursor + }) + const pullRequest = result.repository.pullRequest + if (!pullRequest) { + throw new Error(`Pull request #${prNumber} was not found`) + } - if [[ "${count}" == "0" ]]; then - echo "All pull request commits have Verified signatures." - echo "has_unverified=false" >> "${GITHUB_OUTPUT}" - else - printf 'Found %s commit(s) without Verified signatures:\n' "${count}" - jq -r '.[] | "- \(.[0:7])"' unverified-commits.json - echo "has_unverified=true" >> "${GITHUB_OUTPUT}" - fi + const commits = pullRequest.commits + for (const { commit } of commits.nodes) { + if (!/^[0-9a-f]{40}$/.test(commit.oid)) { + throw new Error('GitHub returned an invalid commit OID') + } - - name: Prepare signature guidance - if: steps.verify.outputs.has_unverified == 'true' - run: | - guide_path="docs/contributing/contributing-code" - guide_path+="/contributing-code-first-commit" - guide_path+="/first-commit-06-creating-a-pr/index.md" - guide_url="${GITHUB_SERVER_URL}/radius-project/radius/blob/main" - guide_url+="/${guide_path}#signing-your-commits" + const signature = commit.signature + if (!signature || !signature.isValid || signature.state !== 'VALID') { + unverified.push(commit.oid) + } + } - { - echo "Thank you for contributing to Radius." - echo - echo "The following commits do not currently show a GitHub **Verified** signature:" - echo - jq -r \ - --arg origin "${GITHUB_SERVER_URL}" \ - --arg repo "${GITHUB_REPOSITORY}" \ - '.[] | "- [`\(.[:7])`](\($origin)/\($repo)/commit/\(.))"' \ - unverified-commits.json - echo - printf '%s%s%s\n' \ - "Please follow our [commit-signing guide](${guide_url}) to " \ - "configure GPG, SSH, or S/MIME signing. Then re-sign the affected " \ - "commits and force-push the rewritten branch with \`--force-with-lease\`." - echo - printf '%s%s\n' \ - "Cryptographic commit signing is separate from the DCO " \ - "\`Signed-off-by\` line; both are required." - } > commit-signature-guidance.md + if ( + commits.pageInfo.hasNextPage && + !commits.pageInfo.endCursor + ) { + throw new Error('GitHub returned an invalid pagination cursor') + } + cursor = commits.pageInfo.hasNextPage + ? commits.pageInfo.endCursor + : null + } while (cursor) - - name: Post signature guidance - if: steps.verify.outputs.has_unverified == 'true' - uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3.0.5 - with: - header: commit-signature-verification - number_force: ${{ github.event.pull_request.number || inputs.pr_number }} - path: commit-signature-guidance.md - skip_unchanged: true + if (unverified.length === 0) { + core.info( + 'All pull request commits have Verified signatures. ' + + 'Existing guidance comments are retained.' + ) + return + } + + core.warning( + `${unverified.length} commit(s) do not have Verified signatures.` + ) + for (const oid of unverified) { + core.info(`- ${oid.slice(0, 7)}`) + } + + const marker = + '' + const serverUrl = + process.env.GITHUB_SERVER_URL || 'https://github.com' + const guideUrl = + `${serverUrl}/radius-project/radius/blob/main/` + + 'docs/contributing/contributing-code/' + + 'contributing-code-first-commit/' + + 'first-commit-06-creating-a-pr/index.md#signing-your-commits' + const commitLines = unverified.map( + oid => + `- [\`${oid.slice(0, 7)}\`](` + + `${serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${oid})` + ) + const body = [ + 'Thank you for contributing to Radius.', + '', + 'The following commits do not currently show a GitHub **Verified** signature:', + '', + ...commitLines, + '', + `Please follow our [commit-signing guide](${guideUrl}) to configure ` + + 'GPG, SSH, or S/MIME signing. Then re-sign the affected commits ' + + 'and force-push the rewritten branch with `--force-with-lease`.', + '', + 'Cryptographic commit signing is separate from the DCO ' + + '`Signed-off-by` line; both are required.', + '', + marker + ].join('\n') + + const comments = await github.paginate( + github.rest.issues.listComments, + { + ...context.repo, + issue_number: prNumber, + per_page: 100 + } + ) + const existing = comments.find( + comment => + comment.user?.login === 'github-actions[bot]' && + comment.body?.includes(marker) + ) + + if (!existing) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body + }) + core.info('Posted the signature guidance comment.') + } else if (existing.body !== body) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: existing.id, + body + }) + core.info('Updated the signature guidance comment.') + } else { + core.info('The signature guidance comment is already current.') + } From af8be6a19b60b83c362af8f32539a794a98e2b65 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:15:26 -0400 Subject: [PATCH 10/15] Decompose signature verification script Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .../workflows/verify-commit-signatures.yml | 127 ++++++++++-------- 1 file changed, 73 insertions(+), 54 deletions(-) diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index 19fdc16d96e..b5818eab3c8 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -69,41 +69,80 @@ jobs: } ` - const unverified = [] - let cursor = null - do { - const result = await github.graphql(query, { - ...context.repo, - number: prNumber, - cursor - }) - const pullRequest = result.repository.pullRequest - if (!pullRequest) { - throw new Error(`Pull request #${prNumber} was not found`) + function hasVerifiedSignature(commit) { + if (!/^[0-9a-f]{40}$/.test(commit.oid)) { + throw new Error('GitHub returned an invalid commit OID') } - const commits = pullRequest.commits - for (const { commit } of commits.nodes) { - if (!/^[0-9a-f]{40}$/.test(commit.oid)) { - throw new Error('GitHub returned an invalid commit OID') + return ( + commit.signature?.isValid === true && + commit.signature.state === 'VALID' + ) + } + + async function listPullRequestCommits(number) { + const commits = [] + let cursor = null + + do { + const result = await github.graphql(query, { + ...context.repo, + number, + cursor + }) + const pullRequest = result.repository.pullRequest + if (!pullRequest) { + throw new Error(`Pull request #${number} was not found`) } - const signature = commit.signature - if (!signature || !signature.isValid || signature.state !== 'VALID') { - unverified.push(commit.oid) + const page = pullRequest.commits + commits.push(...page.nodes.map(node => node.commit)) + if (page.pageInfo.hasNextPage && !page.pageInfo.endCursor) { + throw new Error('GitHub returned an invalid pagination cursor') } - } + cursor = page.pageInfo.hasNextPage + ? page.pageInfo.endCursor + : null + } while (cursor) - if ( - commits.pageInfo.hasNextPage && - !commits.pageInfo.endCursor - ) { - throw new Error('GitHub returned an invalid pagination cursor') - } - cursor = commits.pageInfo.hasNextPage - ? commits.pageInfo.endCursor - : null - } while (cursor) + return commits + } + + function formatGuidance(unverified, repository, serverUrl, marker) { + const guideUrl = + `${serverUrl}/radius-project/radius/blob/main/` + + 'docs/contributing/contributing-code/' + + 'contributing-code-first-commit/' + + 'first-commit-06-creating-a-pr/index.md#signing-your-commits' + const commitLines = unverified.map( + oid => + `- [\`${oid.slice(0, 7)}\`](` + + `${serverUrl}/${repository.owner}/` + + `${repository.repo}/commit/${oid})` + ) + + return [ + 'Thank you for contributing to Radius.', + '', + 'The following commits do not currently show a GitHub **Verified** signature:', + '', + ...commitLines, + '', + `Please follow our [commit-signing guide](${guideUrl}) to configure ` + + 'GPG, SSH, or S/MIME signing. Then re-sign the affected commits ' + + 'and force-push the rewritten branch with `--force-with-lease`.', + '', + 'Cryptographic commit signing is separate from the DCO ' + + '`Signed-off-by` line; both are required.', + '', + marker + ].join('\n') + } + + const commits = await listPullRequestCommits(prNumber) + const unverified = commits + .filter(commit => !hasVerifiedSignature(commit)) + .map(commit => commit.oid) if (unverified.length === 0) { core.info( @@ -124,32 +163,12 @@ jobs: '' const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com' - const guideUrl = - `${serverUrl}/radius-project/radius/blob/main/` + - 'docs/contributing/contributing-code/' + - 'contributing-code-first-commit/' + - 'first-commit-06-creating-a-pr/index.md#signing-your-commits' - const commitLines = unverified.map( - oid => - `- [\`${oid.slice(0, 7)}\`](` + - `${serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${oid})` - ) - const body = [ - 'Thank you for contributing to Radius.', - '', - 'The following commits do not currently show a GitHub **Verified** signature:', - '', - ...commitLines, - '', - `Please follow our [commit-signing guide](${guideUrl}) to configure ` + - 'GPG, SSH, or S/MIME signing. Then re-sign the affected commits ' + - 'and force-push the rewritten branch with `--force-with-lease`.', - '', - 'Cryptographic commit signing is separate from the DCO ' + - '`Signed-off-by` line; both are required.', - '', + const body = formatGuidance( + unverified, + context.repo, + serverUrl, marker - ].join('\n') + ) const comments = await github.paginate( github.rest.issues.listComments, From 65adad044f4331f2fe7b0fede31b600f722ed710 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:27:30 -0400 Subject: [PATCH 11/15] Extract signature verification script Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/verify-commit-signatures.mjs | 189 ++++++++++++++++++ .../workflows/verify-commit-signatures.yml | 177 ++-------------- 2 files changed, 201 insertions(+), 165 deletions(-) create mode 100644 .github/scripts/verify-commit-signatures.mjs diff --git a/.github/scripts/verify-commit-signatures.mjs b/.github/scripts/verify-commit-signatures.mjs new file mode 100644 index 00000000000..1e84894aebc --- /dev/null +++ b/.github/scripts/verify-commit-signatures.mjs @@ -0,0 +1,189 @@ +// Copyright 2026 The Radius Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// @ts-check + +const COMMENT_MARKER = + ""; + +const COMMITS_QUERY = ` + query( + $owner: String! + $repo: String! + $number: Int! + $cursor: String + ) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + commits(first: 100, after: $cursor) { + nodes { + commit { + oid + signature { + isValid + state + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } +`; + +/** + * @param {{ oid: string, signature?: { isValid: boolean, state: string } | null }} commit + * @returns {boolean} + */ +export function hasVerifiedSignature(commit) { + if (!/^[0-9a-f]{40}$/.test(commit.oid)) { + throw new Error("GitHub returned an invalid commit OID"); + } + + return ( + commit.signature?.isValid === true && commit.signature.state === "VALID" + ); +} + +/** + * @param {import('@actions/github-script').AsyncFunctionArguments['github']} github + * @param {{ owner: string, repo: string }} repository + * @param {number} number + * @returns {Promise>} + */ +export async function listPullRequestCommits(github, repository, number) { + const commits = []; + let cursor = null; + + do { + const result = await github.graphql(COMMITS_QUERY, { + ...repository, + number, + cursor, + }); + const pullRequest = result.repository.pullRequest; + if (!pullRequest) { + throw new Error(`Pull request #${number} was not found`); + } + + const page = pullRequest.commits; + commits.push(...page.nodes.map((node) => node.commit)); + if (page.pageInfo.hasNextPage && !page.pageInfo.endCursor) { + throw new Error("GitHub returned an invalid pagination cursor"); + } + cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null; + } while (cursor); + + return commits; +} + +/** + * @param {string[]} unverified + * @param {{ owner: string, repo: string }} repository + * @param {string} serverUrl + * @returns {string} + */ +export function formatGuidance(unverified, repository, serverUrl) { + const guideUrl = + `${serverUrl}/radius-project/radius/blob/main/` + + "docs/contributing/contributing-code/" + + "contributing-code-first-commit/" + + "first-commit-06-creating-a-pr/index.md#signing-your-commits"; + const commitLines = unverified.map( + (oid) => + `- [\`${oid.slice(0, 7)}\`](` + + `${serverUrl}/${repository.owner}/` + + `${repository.repo}/commit/${oid})`, + ); + + return [ + "Thank you for contributing to Radius.", + "", + "The following commits do not currently show a GitHub **Verified** signature:", + "", + ...commitLines, + "", + `Please follow our [commit-signing guide](${guideUrl}) to configure ` + + "GPG, SSH, or S/MIME signing. Then re-sign the affected commits " + + "and force-push the rewritten branch with `--force-with-lease`.", + "", + "Cryptographic commit signing is separate from the DCO " + + "`Signed-off-by` line; both are required.", + "", + COMMENT_MARKER, + ].join("\n"); +} + +/** @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments */ +export default async ({ github, context, core }) => { + const prNumber = Number(process.env.PR_NUMBER); + if (!Number.isSafeInteger(prNumber) || prNumber <= 0) { + throw new Error("PR_NUMBER must be a positive integer"); + } + + const commits = await listPullRequestCommits(github, context.repo, prNumber); + const unverified = commits + .filter((commit) => !hasVerifiedSignature(commit)) + .map((commit) => commit.oid); + + if (unverified.length === 0) { + core.info( + "All pull request commits have Verified signatures. " + + "Existing guidance comments are retained.", + ); + return; + } + + core.warning( + `${unverified.length} commit(s) do not have Verified signatures.`, + ); + for (const oid of unverified) { + core.info(`- ${oid.slice(0, 7)}`); + } + + const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + const body = formatGuidance(unverified, context.repo, serverUrl); + const comments = await github.paginate(github.rest.issues.listComments, { + ...context.repo, + issue_number: prNumber, + per_page: 100, + }); + const existing = comments.find( + (comment) => + comment.user?.login === "github-actions[bot]" && + comment.body?.includes(COMMENT_MARKER), + ); + + if (!existing) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body, + }); + core.info("Posted the signature guidance comment."); + } else if (existing.body !== body) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: existing.id, + body, + }); + core.info("Updated the signature guidance comment."); + } else { + core.info("The signature guidance comment is already current."); + } +}; diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index b5818eab3c8..c444a94f939 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -30,174 +30,21 @@ jobs: contents: read pull-requests: write steps: + - name: Checkout signature verifier + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.base.sha || github.sha }} + sparse-checkout: | + .github/scripts/verify-commit-signatures.mjs + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Verify commit signatures uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ github.token }} script: | - const prNumber = Number(process.env.PR_NUMBER) - if (!Number.isSafeInteger(prNumber) || prNumber <= 0) { - throw new Error('PR_NUMBER must be a positive integer') - } - - const query = ` - query( - $owner: String! - $repo: String! - $number: Int! - $cursor: String - ) { - repository(owner: $owner, name: $repo) { - pullRequest(number: $number) { - commits(first: 100, after: $cursor) { - nodes { - commit { - oid - signature { - isValid - state - } - } - } - pageInfo { - hasNextPage - endCursor - } - } - } - } - } - ` - - function hasVerifiedSignature(commit) { - if (!/^[0-9a-f]{40}$/.test(commit.oid)) { - throw new Error('GitHub returned an invalid commit OID') - } - - return ( - commit.signature?.isValid === true && - commit.signature.state === 'VALID' - ) - } - - async function listPullRequestCommits(number) { - const commits = [] - let cursor = null - - do { - const result = await github.graphql(query, { - ...context.repo, - number, - cursor - }) - const pullRequest = result.repository.pullRequest - if (!pullRequest) { - throw new Error(`Pull request #${number} was not found`) - } - - const page = pullRequest.commits - commits.push(...page.nodes.map(node => node.commit)) - if (page.pageInfo.hasNextPage && !page.pageInfo.endCursor) { - throw new Error('GitHub returned an invalid pagination cursor') - } - cursor = page.pageInfo.hasNextPage - ? page.pageInfo.endCursor - : null - } while (cursor) - - return commits - } - - function formatGuidance(unverified, repository, serverUrl, marker) { - const guideUrl = - `${serverUrl}/radius-project/radius/blob/main/` + - 'docs/contributing/contributing-code/' + - 'contributing-code-first-commit/' + - 'first-commit-06-creating-a-pr/index.md#signing-your-commits' - const commitLines = unverified.map( - oid => - `- [\`${oid.slice(0, 7)}\`](` + - `${serverUrl}/${repository.owner}/` + - `${repository.repo}/commit/${oid})` - ) - - return [ - 'Thank you for contributing to Radius.', - '', - 'The following commits do not currently show a GitHub **Verified** signature:', - '', - ...commitLines, - '', - `Please follow our [commit-signing guide](${guideUrl}) to configure ` + - 'GPG, SSH, or S/MIME signing. Then re-sign the affected commits ' + - 'and force-push the rewritten branch with `--force-with-lease`.', - '', - 'Cryptographic commit signing is separate from the DCO ' + - '`Signed-off-by` line; both are required.', - '', - marker - ].join('\n') - } - - const commits = await listPullRequestCommits(prNumber) - const unverified = commits - .filter(commit => !hasVerifiedSignature(commit)) - .map(commit => commit.oid) - - if (unverified.length === 0) { - core.info( - 'All pull request commits have Verified signatures. ' + - 'Existing guidance comments are retained.' - ) - return - } - - core.warning( - `${unverified.length} commit(s) do not have Verified signatures.` - ) - for (const oid of unverified) { - core.info(`- ${oid.slice(0, 7)}`) - } - - const marker = - '' - const serverUrl = - process.env.GITHUB_SERVER_URL || 'https://github.com' - const body = formatGuidance( - unverified, - context.repo, - serverUrl, - marker - ) - - const comments = await github.paginate( - github.rest.issues.listComments, - { - ...context.repo, - issue_number: prNumber, - per_page: 100 - } + const { default: script } = await import( + `${process.env.GITHUB_WORKSPACE}/.github/scripts/verify-commit-signatures.mjs` ) - const existing = comments.find( - comment => - comment.user?.login === 'github-actions[bot]' && - comment.body?.includes(marker) - ) - - if (!existing) { - await github.rest.issues.createComment({ - ...context.repo, - issue_number: prNumber, - body - }) - core.info('Posted the signature guidance comment.') - } else if (existing.body !== body) { - await github.rest.issues.updateComment({ - ...context.repo, - comment_id: existing.id, - body - }) - core.info('Updated the signature guidance comment.') - } else { - core.info('The signature guidance comment is already current.') - } + await script({ github, context, core }) From 925fbf6ee58e5a2a15d68b9a3bd9dc8369f8805c Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:39:43 -0400 Subject: [PATCH 12/15] Address signature verifier review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/verify-commit-signatures.mjs | 110 ++++++++++-------- .../workflows/verify-commit-signatures.yml | 4 +- 2 files changed, 65 insertions(+), 49 deletions(-) diff --git a/.github/scripts/verify-commit-signatures.mjs b/.github/scripts/verify-commit-signatures.mjs index 1e84894aebc..0adc21fc91f 100644 --- a/.github/scripts/verify-commit-signatures.mjs +++ b/.github/scripts/verify-commit-signatures.mjs @@ -131,59 +131,75 @@ export function formatGuidance(unverified, repository, serverUrl) { /** @param {import('@actions/github-script').AsyncFunctionArguments} AsyncFunctionArguments */ export default async ({ github, context, core }) => { - const prNumber = Number(process.env.PR_NUMBER); - if (!Number.isSafeInteger(prNumber) || prNumber <= 0) { - throw new Error("PR_NUMBER must be a positive integer"); - } - - const commits = await listPullRequestCommits(github, context.repo, prNumber); - const unverified = commits - .filter((commit) => !hasVerifiedSignature(commit)) - .map((commit) => commit.oid); - - if (unverified.length === 0) { - core.info( - "All pull request commits have Verified signatures. " + - "Existing guidance comments are retained.", + try { + const prNumber = Number( + core.getInput("PR_NUMBER", { + required: true, + }), ); - return; - } + if (!Number.isSafeInteger(prNumber) || prNumber <= 0) { + throw new Error("PR_NUMBER must be a positive integer"); + } - core.warning( - `${unverified.length} commit(s) do not have Verified signatures.`, - ); - for (const oid of unverified) { - core.info(`- ${oid.slice(0, 7)}`); - } + const commits = await listPullRequestCommits( + github, + context.repo, + prNumber, + ); + const unverified = commits + .filter((commit) => !hasVerifiedSignature(commit)) + .map((commit) => commit.oid); + + if (unverified.length === 0) { + core.info( + "All pull request commits have Verified signatures. " + + "Existing guidance comments are retained.", + ); + return; + } - const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; - const body = formatGuidance(unverified, context.repo, serverUrl); - const comments = await github.paginate(github.rest.issues.listComments, { - ...context.repo, - issue_number: prNumber, - per_page: 100, - }); - const existing = comments.find( - (comment) => - comment.user?.login === "github-actions[bot]" && - comment.body?.includes(COMMENT_MARKER), - ); + core.warning( + `${unverified.length} commit(s) do not have Verified signatures.`, + ); + for (const oid of unverified) { + core.info(`- ${oid.slice(0, 7)}`); + } - if (!existing) { - await github.rest.issues.createComment({ + const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com"; + const body = formatGuidance(unverified, context.repo, serverUrl); + const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: prNumber, - body, - }); - core.info("Posted the signature guidance comment."); - } else if (existing.body !== body) { - await github.rest.issues.updateComment({ - ...context.repo, - comment_id: existing.id, - body, + per_page: 100, }); - core.info("Updated the signature guidance comment."); - } else { - core.info("The signature guidance comment is already current."); + const existing = comments.find( + (comment) => + comment.user?.login === "github-actions[bot]" && + comment.body?.includes(COMMENT_MARKER), + ); + + if (!existing) { + await github.rest.issues.createComment({ + ...context.repo, + issue_number: prNumber, + body, + }); + core.info("Posted the signature guidance comment."); + } else if (existing.body !== body) { + await github.rest.issues.updateComment({ + ...context.repo, + comment_id: existing.id, + body, + }); + core.info("Updated the signature guidance comment."); + } else { + core.info("The signature guidance comment is already current."); + } + } catch (error) { + core.setFailed( + error instanceof Error + ? error.message + : `Unexpected error: ${String(error)}`, + ); } }; diff --git a/.github/workflows/verify-commit-signatures.yml b/.github/workflows/verify-commit-signatures.yml index c444a94f939..2e64db36fbe 100644 --- a/.github/workflows/verify-commit-signatures.yml +++ b/.github/workflows/verify-commit-signatures.yml @@ -24,8 +24,6 @@ jobs: name: Verify Commit Signatures runs-on: ubuntu-24.04 timeout-minutes: 5 - env: - PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} permissions: contents: read pull-requests: write @@ -41,6 +39,8 @@ jobs: - name: Verify commit signatures uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + INPUT_PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }} with: github-token: ${{ github.token }} script: | From c8d6a5ce6c488bcdb370729d9a3c673f9212ef1a Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:46:25 -0400 Subject: [PATCH 13/15] Apply repository JavaScript formatting Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/scripts/verify-commit-signatures.mjs | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/scripts/verify-commit-signatures.mjs b/.github/scripts/verify-commit-signatures.mjs index 0adc21fc91f..d27a67d160b 100644 --- a/.github/scripts/verify-commit-signatures.mjs +++ b/.github/scripts/verify-commit-signatures.mjs @@ -74,7 +74,7 @@ export async function listPullRequestCommits(github, repository, number) { const result = await github.graphql(COMMITS_QUERY, { ...repository, number, - cursor, + cursor }); const pullRequest = result.repository.pullRequest; if (!pullRequest) { @@ -108,7 +108,7 @@ export function formatGuidance(unverified, repository, serverUrl) { (oid) => `- [\`${oid.slice(0, 7)}\`](` + `${serverUrl}/${repository.owner}/` + - `${repository.repo}/commit/${oid})`, + `${repository.repo}/commit/${oid})` ); return [ @@ -125,7 +125,7 @@ export function formatGuidance(unverified, repository, serverUrl) { "Cryptographic commit signing is separate from the DCO " + "`Signed-off-by` line; both are required.", "", - COMMENT_MARKER, + COMMENT_MARKER ].join("\n"); } @@ -134,8 +134,8 @@ export default async ({ github, context, core }) => { try { const prNumber = Number( core.getInput("PR_NUMBER", { - required: true, - }), + required: true + }) ); if (!Number.isSafeInteger(prNumber) || prNumber <= 0) { throw new Error("PR_NUMBER must be a positive integer"); @@ -144,7 +144,7 @@ export default async ({ github, context, core }) => { const commits = await listPullRequestCommits( github, context.repo, - prNumber, + prNumber ); const unverified = commits .filter((commit) => !hasVerifiedSignature(commit)) @@ -153,13 +153,13 @@ export default async ({ github, context, core }) => { if (unverified.length === 0) { core.info( "All pull request commits have Verified signatures. " + - "Existing guidance comments are retained.", + "Existing guidance comments are retained." ); return; } core.warning( - `${unverified.length} commit(s) do not have Verified signatures.`, + `${unverified.length} commit(s) do not have Verified signatures.` ); for (const oid of unverified) { core.info(`- ${oid.slice(0, 7)}`); @@ -170,26 +170,26 @@ export default async ({ github, context, core }) => { const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: prNumber, - per_page: 100, + per_page: 100 }); const existing = comments.find( (comment) => comment.user?.login === "github-actions[bot]" && - comment.body?.includes(COMMENT_MARKER), + comment.body?.includes(COMMENT_MARKER) ); if (!existing) { await github.rest.issues.createComment({ ...context.repo, issue_number: prNumber, - body, + body }); core.info("Posted the signature guidance comment."); } else if (existing.body !== body) { await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, - body, + body }); core.info("Updated the signature guidance comment."); } else { @@ -197,9 +197,9 @@ export default async ({ github, context, core }) => { } } catch (error) { core.setFailed( - error instanceof Error - ? error.message - : `Unexpected error: ${String(error)}`, + error instanceof Error ? + error.message + : `Unexpected error: ${String(error)}` ); } }; From ca94950fe0b6054f217a21bba8fc9a4e9c0a2dd4 Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:08:25 -0400 Subject: [PATCH 14/15] Serialize pnpm-based lint checks Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 67182a39-6aba-466e-b1c0-ced6f91ac5a2 Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/lint.yaml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 3485ceae40e..cb78af9ecce 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -95,10 +95,11 @@ jobs: run: make install-shellcheck # Run the independent, read-only linters concurrently. Each parallel step is - # self-contained; the TypeSpec and Prettier checks share a pnpm/corepack - # toolchain, so they are grouped into one step to avoid racing on that - # shared install. The group has an implicit wait that aggregates results: - # every linter runs and any failure fails this job (a single status check). + # self-contained; the TypeSpec, Prettier, and deploy progress uploader + # checks share a pnpm/corepack toolchain, so they are grouped into one step + # to avoid racing on that shared install. The group has an implicit wait + # that aggregates results: every linter runs and any failure fails this job + # (a single status check). - parallel: - name: Go lint run: make lint-go @@ -108,13 +109,11 @@ jobs: helm lint "${HELM_CHARTS_DIR}" make test-helm - - name: TypeSpec and Prettier format checks + - name: TypeSpec, Prettier, and deploy progress uploader run: | make tsp-format-check make format-check || { echo "Format check failed. Run 'make format-write' to fix the formatting issues."; exit 1; } - - - name: Deploy progress artifact uploader - run: make test-deploy-progress-uploader + make test-deploy-progress-uploader - name: Shell lint run: make lint-shell From 0d734a8047bf5305af5ddf1bb47f50fedf5c254a Mon Sep 17 00:00:00 2001 From: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:33:35 -0400 Subject: [PATCH 15/15] Revert "Serialize pnpm-based lint checks" This reverts commit ca94950fe0b6054f217a21bba8fc9a4e9c0a2dd4. Signed-off-by: Brooke Hamilton <45323234+brooke-hamilton@users.noreply.github.com> --- .github/workflows/lint.yaml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index cb78af9ecce..3485ceae40e 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -95,11 +95,10 @@ jobs: run: make install-shellcheck # Run the independent, read-only linters concurrently. Each parallel step is - # self-contained; the TypeSpec, Prettier, and deploy progress uploader - # checks share a pnpm/corepack toolchain, so they are grouped into one step - # to avoid racing on that shared install. The group has an implicit wait - # that aggregates results: every linter runs and any failure fails this job - # (a single status check). + # self-contained; the TypeSpec and Prettier checks share a pnpm/corepack + # toolchain, so they are grouped into one step to avoid racing on that + # shared install. The group has an implicit wait that aggregates results: + # every linter runs and any failure fails this job (a single status check). - parallel: - name: Go lint run: make lint-go @@ -109,11 +108,13 @@ jobs: helm lint "${HELM_CHARTS_DIR}" make test-helm - - name: TypeSpec, Prettier, and deploy progress uploader + - name: TypeSpec and Prettier format checks run: | make tsp-format-check make format-check || { echo "Format check failed. Run 'make format-write' to fix the formatting issues."; exit 1; } - make test-deploy-progress-uploader + + - name: Deploy progress artifact uploader + run: make test-deploy-progress-uploader - name: Shell lint run: make lint-shell