Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9012105
Add PR commit signature verification
brooke-hamilton Aug 18, 2026
194aa11
Refactor signature comment orchestration
brooke-hamilton Aug 19, 2026
a3bee1f
Enable manual signature verification
brooke-hamilton Aug 19, 2026
fcbf3e3
Report commit verification results
brooke-hamilton Aug 19, 2026
4dbb978
Retain commit signature guidance comments
brooke-hamilton Aug 19, 2026
d185c64
Avoid workflow command injection
brooke-hamilton Aug 19, 2026
561ebd3
Potential fix for pull request finding
brooke-hamilton Aug 19, 2026
622b615
Validate signature verifier output
brooke-hamilton Aug 19, 2026
9b23913
Merge branch 'main' into brooke-hamilton-commit-signoff-check
brooke-hamilton Aug 19, 2026
6833783
Use github-script for signature verification
brooke-hamilton Aug 19, 2026
af8be6a
Decompose signature verification script
brooke-hamilton Aug 19, 2026
65adad0
Extract signature verification script
brooke-hamilton Aug 20, 2026
c978ccf
Merge origin/main into commit signature verification
brooke-hamilton Aug 26, 2026
925fbf6
Address signature verifier review feedback
brooke-hamilton Aug 26, 2026
c8d6a5c
Apply repository JavaScript formatting
brooke-hamilton Aug 26, 2026
ca94950
Serialize pnpm-based lint checks
brooke-hamilton Aug 26, 2026
0d734a8
Revert "Serialize pnpm-based lint checks"
brooke-hamilton Aug 26, 2026
5b4bab3
Merge branch 'main' into brooke-hamilton-commit-signoff-check
brooke-hamilton Aug 31, 2026
8cb4e64
Merge branch 'main' into brooke-hamilton-commit-signoff-check
brooke-hamilton Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/scripts/verify-commit-signatures.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/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(
Comment thread
brooke-hamilton marked this conversation as resolved.
Outdated
.commit.signature == null or
.commit.signature.isValid != true or
.commit.signature.state != "VALID"
) |
.commit.oid
]
' <<< "${response}"
}

main "$@"
101 changes: 101 additions & 0 deletions .github/workflows/verify-commit-signatures.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# yaml-language-server: $schema=https://www.schemastore.org/github-workflow.json
---
name: Verify Commit Signatures
Comment thread
Copilot marked this conversation as resolved.
Outdated

on:
# This workflow uses the trusted base commit and never executes PR-head code.
pull_request_target:
types: [opened, reopened, synchronize]
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 || inputs.pr_number }}
cancel-in-progress: true

permissions: {}

jobs:
verify-commit-signatures:
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
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.sh
sparse-checkout-cone-mode: false
persist-credentials: false

- name: Verify commit signatures
Comment thread
brooke-hamilton marked this conversation as resolved.
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' "${count}" >> "${GITHUB_OUTPUT}"

- name: Prepare signature guidance
if: steps.verify.outputs.count != '0'
env:
UNVERIFIED_COMMITS: ${{ steps.verify.outputs.unverified_commits }}
run: |
pr_author="$(
gh api "/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
--jq '.user.login'
)"
Comment thread
brooke-hamilton marked this conversation as resolved.
Outdated
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\`."
Comment thread
brooke-hamilton marked this conversation as resolved.
Outdated
echo
printf '%s%s\n' \
"Cryptographic commit signing is separate from the DCO " \
"\`Signed-off-by\` line; both are required."
} > commit-signature-guidance.md
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed

- 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
number_force: ${{ github.event.pull_request.number || inputs.pr_number }}
path: commit-signature-guidance.md
skip_unchanged: true
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion docs/contributing/contributing-pull-requests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

Expand Down
Loading