Repository navigation
145 lines (127 loc) · 6.34 KB
/
Copy pathdco.yml
File metadata and controls
145 lines (127 loc) · 6.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Developer Certificate of Origin (DCO) check.
#
# Every commit pushed to the repository or included in a pull request must
# carry a "Signed-off-by:" trailer certifying the contributor's agreement
# with DCO v1.1 (https://developercertificate.org/). This mirrors the
# check used by the Linux kernel and many other open-source projects.
#
# The check validates per commit:
# 1. A "Signed-off-by:" line is present in the commit message.
# 2. The line carries a non-empty name and a syntactically valid e-mail
# address in "Name <email>" form.
#
# To sign off: git commit -s (or --signoff)
# To fix a past commit: git commit --amend -s
name: DCO
on:
push:
branches: ["**"]
pull_request:
types: [opened, synchronize, reopened]
# Read-only token is sufficient — we only inspect commit metadata.
permissions:
contents: read
concurrency:
group: dco-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
jobs:
dco:
name: DCO sign-off
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check Signed-off-by on every commit
env:
# pull_request: compare PR base → head.
# push: compare before → after (before=zeros on new branches).
EVENT: ${{ github.event_name }}
PR_BASE: ${{ github.event.pull_request.base.sha }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE: ${{ github.event.before }}
PUSH_AFTER: ${{ github.event.after }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
# ── Determine the commit range ──────────────────────────────────────
# Checked: commits reachable from HEAD and from none of BASE.
if [[ "$EVENT" == "pull_request" ]]; then
# GitHub provides the exact base and head SHAs for the PR.
BASE=("$PR_BASE")
HEAD="$PR_HEAD"
else
# push event — HEAD is always the new tip.
HEAD="$PUSH_AFTER"
CURRENT_BRANCH="${GITHUB_REF_NAME:-}"
ZEROS="0000000000000000000000000000000000000000"
if [[ "$CURRENT_BRANCH" == "$DEFAULT_BRANCH" ]]; then
# Direct push to the default branch: check only the commits
# that were just pushed (before..after). Fail closed when the
# range is ambiguous — force-push or initial push — rather than
# silently checking only the tip commit.
if [[ "$PUSH_BEFORE" == "$ZEROS" ]]; then
echo "::error::Cannot determine commit range: initial or force-push to $DEFAULT_BRANCH with no resolvable base."
echo " Open a pull request so commits are verified against a known base."
exit 1
fi
if ! git cat-file -e "$PUSH_BEFORE" 2>/dev/null; then
echo "::error::Cannot determine commit range: PUSH_BEFORE ($PUSH_BEFORE) is not reachable (force-push?)."
echo " Open a pull request so commits are verified against a known base."
exit 1
fi
BASE=("$PUSH_BEFORE")
elif [[ "$CURRENT_BRANCH" == release/* && "$PUSH_BEFORE" != "$ZEROS" ]] \
&& git cat-file -e "$PUSH_BEFORE" 2>/dev/null; then
# Push to a release branch: only the commits just pushed. A new
# or force-pushed release branch falls through to the case below.
BASE=("$PUSH_BEFORE")
else
# Any other push: every commit not yet on a long-lived branch
# (the default branch or release/*), whichever it was forked from.
# Do not infer a parent branch from merge-bases instead: any
# pushed branch could then vouch for an unsigned commit.
git fetch --quiet origin \
"+refs/heads/$DEFAULT_BRANCH:refs/remotes/origin/$DEFAULT_BRANCH" \
'+refs/heads/release/*:refs/remotes/origin/release/*' 2>/dev/null || true
mapfile -t BASE < <(git for-each-ref --format='%(refname)' \
"refs/remotes/origin/$DEFAULT_BRANCH" refs/remotes/origin/release/ \
| grep -vxF "refs/remotes/origin/$CURRENT_BRANCH")
fi
fi
# ── Check each commit ───────────────────────────────────────────────
fail=0
while IFS= read -r sha; do
msg=$(git log -1 --format="%B" "$sha")
author=$(git log -1 --format="%an <%ae>" "$sha")
# 1. Must have at least one Signed-off-by line.
if ! echo "$msg" | grep -qP "^Signed-off-by:\s+\S"; then
echo "::error::Commit $sha by $author is missing a Signed-off-by trailer."
echo " Fix with: git commit --amend -s (then force-push)"
fail=1
continue
fi
# 2. Each Signed-off-by must have a non-empty name and a valid
# email address in "Name <user@host.tld>" form.
while IFS= read -r sob; do
identity="${sob#*Signed-off-by:}"
identity="${identity#"${identity%%[^[:space:]]*}"}" # ltrim all whitespace
if ! echo "$identity" | grep -qP "^\S[^<@>]*<[^@\s]+@[^@\s]+\.[^@\s]+>\s*$"; then
echo "::error::Commit $sha has a malformed Signed-off-by: '$sob'"
echo " Expected: Signed-off-by: Full Name <email@example.com>"
fail=1
fi
done < <(echo "$msg" | grep -P "^Signed-off-by:")
done < <(git rev-list --no-merges "$HEAD" --not "${BASE[@]}")
if [[ "$fail" -ne 0 ]]; then
echo ""
echo "One or more commits are missing a valid DCO sign-off."
echo "See https://developercertificate.org/ and CONTRIBUTING.md."
exit 1
fi
count=$(git rev-list --count --no-merges "$HEAD" --not "${BASE[@]}")
echo "All $count commit(s) carry a valid Signed-off-by. ✓"