@stream44.studio/dco 0.3.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tsconfig.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "../../../tsconfig.paths.json",
3
+ "compilerOptions": {
4
+ "target": "es2021",
5
+ "module": "esnext",
6
+ "lib": [
7
+ "ES2021",
8
+ "DOM"
9
+ ],
10
+ "types": [
11
+ "bun",
12
+ "node"
13
+ ],
14
+ "moduleResolution": "bundler",
15
+ "strict": true,
16
+ "esModuleInterop": true,
17
+ "skipLibCheck": true,
18
+ "forceConsistentCasingInFileNames": true,
19
+ "resolveJsonModule": true,
20
+ "allowSyntheticDefaultImports": true
21
+ },
22
+ "include": [
23
+ "**/*.ts"
24
+ ],
25
+ "exclude": [
26
+ "node_modules"
27
+ ]
28
+ }
package/validate.sh ADDED
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # DCO Signature Validator for GitHub Actions
4
+ # ===========================================
5
+ # This script validates that all commits in a PR have proper DCO sign-offs.
6
+ #
7
+
8
+ set -e
9
+
10
+ # Color codes for output
11
+ readonly RED='\033[0;31m'
12
+ readonly GREEN='\033[0;32m'
13
+ readonly YELLOW='\033[1;33m'
14
+ readonly BLUE='\033[0;34m'
15
+ readonly CYAN='\033[0;36m'
16
+ readonly NC='\033[0m' # No Color
17
+ readonly BOLD='\033[1m'
18
+
19
+ # Verbose mode: set VERBOSE=1 or pass --verbose
20
+ VERBOSE="${VERBOSE:-0}"
21
+ ENFORCE_SIGNATURE_FINGERPRINTS=0
22
+ for arg in "$@"; do
23
+ [[ "$arg" == "--verbose" ]] && VERBOSE=1
24
+ [[ "$arg" == "--enforce-signature-fingerprints" ]] && ENFORCE_SIGNATURE_FINGERPRINTS=1
25
+ done
26
+
27
+ verbose_log() {
28
+ if [[ "$VERBOSE" == "1" ]]; then
29
+ echo -e "${YELLOW}[VERBOSE] $*${NC}"
30
+ fi
31
+ }
32
+
33
+ # Determine base and head refs
34
+ # If called from GitHub Actions, use environment variables
35
+ # Otherwise use command line arguments
36
+ if [[ -n "${GITHUB_EVENT_NAME:-}" ]]; then
37
+ if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
38
+ BASE_BRANCH="origin/${GITHUB_BASE_REF}"
39
+ HEAD_REF="${GITHUB_HEAD_SHA}"
40
+ else
41
+ # Push event
42
+ if [[ "${GITHUB_BEFORE}" != "0000000000000000000000000000000000000000" ]]; then
43
+ BASE_BRANCH="${GITHUB_BEFORE}"
44
+ HEAD_REF="${GITHUB_SHA}"
45
+ else
46
+ # New branch/repo — validate all commits
47
+ BASE_BRANCH=""
48
+ HEAD_REF="${GITHUB_SHA}"
49
+ fi
50
+ fi
51
+ else
52
+ # Manual invocation
53
+ BASE_BRANCH="${1:-origin/main}"
54
+ HEAD_REF="${2:-HEAD}"
55
+ fi
56
+
57
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
58
+ echo -e "${BOLD}${CYAN} DCO Signature Validation${NC}"
59
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
60
+ echo
61
+
62
+ verbose_log "GITHUB_EVENT_NAME=${GITHUB_EVENT_NAME:-}"
63
+ verbose_log "GITHUB_BASE_REF=${GITHUB_BASE_REF:-}"
64
+ verbose_log "GITHUB_HEAD_SHA=${GITHUB_HEAD_SHA:-}"
65
+ verbose_log "GITHUB_BEFORE=${GITHUB_BEFORE:-}"
66
+ verbose_log "GITHUB_SHA=${GITHUB_SHA:-}"
67
+ verbose_log "BASE_BRANCH=$BASE_BRANCH"
68
+ verbose_log "HEAD_REF=$HEAD_REF"
69
+ verbose_log "Git log (last 10):"
70
+ if [[ "$VERBOSE" == "1" ]]; then
71
+ git log --oneline -10 2>&1 | while IFS= read -r l; do verbose_log " $l"; done
72
+ echo
73
+ fi
74
+
75
+ # Check if we're in a git repository
76
+ if ! git rev-parse --git-dir > /dev/null 2>&1; then
77
+ echo -e "${RED}Error: Not in a git repository${NC}" >&2
78
+ exit 1
79
+ fi
80
+
81
+ # Get list of commits to check
82
+ echo -e "${BLUE}Checking commits between ${BOLD}$BASE_BRANCH${NC}${BLUE} and ${BOLD}$HEAD_REF${NC}"
83
+ echo
84
+
85
+ # Get the commit range
86
+ if [[ -n "$BASE_BRANCH" ]]; then
87
+ COMMITS=$(git rev-list "$BASE_BRANCH..$HEAD_REF" 2>/dev/null || git rev-list "$HEAD_REF")
88
+ else
89
+ # No base branch (new repo/branch) — validate all commits
90
+ COMMITS=$(git rev-list "$HEAD_REF")
91
+ fi
92
+
93
+ if [[ -z "$COMMITS" ]]; then
94
+ echo -e "${GREEN}✓ No commits to validate${NC}"
95
+ exit 0
96
+ fi
97
+
98
+ COMMIT_COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ')
99
+ echo -e "${BLUE}Found ${BOLD}$COMMIT_COUNT${NC}${BLUE} commit(s) to validate${NC}"
100
+ echo
101
+
102
+ # Track validation status
103
+ FAILED_COMMITS=()
104
+ VALID_COMMITS=0
105
+
106
+ # Validate each commit
107
+ while IFS= read -r commit; do
108
+ # Get commit info
109
+ commit_short=$(git rev-parse --short "$commit")
110
+ commit_author=$(git log -1 --format='%an' "$commit")
111
+ commit_email=$(git log -1 --format='%ae' "$commit")
112
+ commit_subject=$(git log -1 --format='%s' "$commit")
113
+ commit_body=$(git log -1 --format='%b' "$commit")
114
+
115
+ # Check for Signed-off-by trailer
116
+ signoff_line=$(echo "$commit_body" | grep -i "^Signed-off-by:" | head -1 || true)
117
+
118
+ if [[ -z "$signoff_line" ]]; then
119
+ # No sign-off found
120
+ FAILED_COMMITS+=("$commit")
121
+ echo -e "${RED}✗ FAIL${NC} $commit_short - ${YELLOW}$commit_subject${NC}"
122
+ echo -e " Author: $commit_author <$commit_email>"
123
+ echo -e " ${RED}Missing: Signed-off-by trailer${NC}"
124
+ echo
125
+ else
126
+ # Validate that the sign-off matches the author
127
+ signoff_name=$(echo "$signoff_line" | sed 's/^Signed-off-by: //' | sed 's/ <.*//')
128
+ signoff_email=$(echo "$signoff_line" | sed 's/.*<\(.*\)>/\1/')
129
+
130
+ if [[ "$signoff_name" != "$commit_author" ]] || [[ "$signoff_email" != "$commit_email" ]]; then
131
+ # Sign-off doesn't match author
132
+ FAILED_COMMITS+=("$commit")
133
+ echo -e "${RED}✗ FAIL${NC} $commit_short - ${YELLOW}$commit_subject${NC}"
134
+ echo -e " Author: $commit_author <$commit_email>"
135
+ echo -e " Signed-off: $signoff_name <$signoff_email>"
136
+ echo -e " ${RED}Mismatch: Sign-off must match commit author${NC}"
137
+ echo
138
+ else
139
+ # Valid sign-off
140
+ ((VALID_COMMITS++)) || true
141
+ echo -e "${GREEN}✓ PASS${NC} $commit_short - $commit_subject"
142
+ echo -e " Signed-off-by: $signoff_name <$signoff_email>"
143
+ echo
144
+ fi
145
+ fi
146
+ done <<< "$COMMITS"
147
+
148
+ # Print summary
149
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
150
+ echo -e "${BOLD}${CYAN} Validation Summary${NC}"
151
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
152
+ echo
153
+
154
+ # Validate .dco-signatures entries
155
+ SIG_FILE_VALID=true
156
+ GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
157
+ SIG_FILE="$GIT_ROOT/.dco-signatures"
158
+
159
+ if [[ -f "$SIG_FILE" ]]; then
160
+ echo
161
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
162
+ echo -e "${BOLD}${CYAN} DCO Signature File Validation${NC}"
163
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
164
+ echo
165
+
166
+ while IFS= read -r line; do
167
+ # Skip empty lines, comments, headers, and separator
168
+ [[ -z "$line" ]] && continue
169
+ [[ "$line" =~ ^# ]] && continue
170
+ [[ "$line" =~ ^--- ]] && continue
171
+ [[ "$line" == "This "* ]] && continue
172
+ [[ "$line" == "Each "* ]] && continue
173
+ [[ "$line" == "Format:"* ]] && continue
174
+
175
+ # Parse signature line: name <email> | signed: <date> | agreement: <commit> (<dco_change_date>)
176
+ sig_identity=$(echo "$line" | sed 's/ | signed:.*//')
177
+ sig_agreement_commit=$(echo "$line" | sed -n 's/.*| agreement: \([a-f0-9]*\).*/\1/p')
178
+
179
+ if [[ -z "$sig_agreement_commit" ]]; then
180
+ echo -e "${RED}✗ FAIL${NC} $sig_identity"
181
+ echo -e " ${RED}Missing agreement commit reference in signature${NC}"
182
+ SIG_FILE_VALID=false
183
+ continue
184
+ fi
185
+
186
+ # Verify the commit exists
187
+ if ! git cat-file -e "$sig_agreement_commit" 2>/dev/null; then
188
+ echo -e "${RED}✗ FAIL${NC} $sig_identity"
189
+ echo -e " ${RED}Agreement commit $sig_agreement_commit does not exist${NC}"
190
+ SIG_FILE_VALID=false
191
+ continue
192
+ fi
193
+
194
+ # Verify DCO.md was present in that commit
195
+ if ! git show "$sig_agreement_commit:DCO.md" >/dev/null 2>&1; then
196
+ echo -e "${RED}✗ FAIL${NC} $sig_identity"
197
+ echo -e " ${RED}DCO.md not found in commit $sig_agreement_commit${NC}"
198
+ SIG_FILE_VALID=false
199
+ continue
200
+ fi
201
+
202
+ echo -e "${GREEN}✓ PASS${NC} $sig_identity"
203
+ echo -e " Agreement commit: ${CYAN}$(git rev-parse --short "$sig_agreement_commit")${NC}"
204
+ done < "$SIG_FILE"
205
+ echo
206
+ fi
207
+
208
+ # ── SSH Signature Fingerprint Enforcement (--enforce-signature-fingerprints) ──
209
+ SIG_VALIDATION_FAILED=false
210
+ if [[ "$ENFORCE_SIGNATURE_FINGERPRINTS" == "1" ]]; then
211
+ echo
212
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
213
+ echo -e "${BOLD}${CYAN} Enforce Signature Fingerprints${NC}"
214
+ echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
215
+ echo
216
+
217
+ # Build a map of contributor email → expected fingerprint from .dco-signatures
218
+ declare -A EXPECTED_FINGERPRINTS
219
+ if [[ -f "$SIG_FILE" ]]; then
220
+ while IFS= read -r line; do
221
+ [[ -z "$line" ]] && continue
222
+ [[ "$line" =~ ^# ]] && continue
223
+ [[ "$line" =~ ^--- ]] && continue
224
+ [[ "$line" == "This "* ]] && continue
225
+ [[ "$line" == "Each "* ]] && continue
226
+ [[ "$line" == "Format:"* ]] && continue
227
+
228
+ # Extract email and fingerprint
229
+ local_email=$(echo "$line" | sed -n 's/.*<\(.*\)>.*/\1/p')
230
+ local_fp=$(echo "$line" | sed -n 's/.*| signature: \([^ ]*\).*/\1/p')
231
+ if [[ -n "$local_email" ]] && [[ -n "$local_fp" ]]; then
232
+ EXPECTED_FINGERPRINTS["$local_email"]="$local_fp"
233
+ verbose_log "Expected fingerprint for $local_email: $local_fp"
234
+ elif [[ -n "$local_email" ]]; then
235
+ echo -e "${RED}✗ FAIL${NC} Contributor $local_email has no signature fingerprint in .dco-signatures"
236
+ SIG_VALIDATION_FAILED=true
237
+ fi
238
+ done < "$SIG_FILE"
239
+ else
240
+ echo -e "${RED}✗ FAIL${NC} --enforce-signature-fingerprints specified but .dco-signatures file not found"
241
+ SIG_VALIDATION_FAILED=true
242
+ fi
243
+
244
+ # Verify each commit has a valid SSH signature matching the expected fingerprint
245
+ if [[ "$SIG_VALIDATION_FAILED" == "false" ]]; then
246
+ while IFS= read -r commit; do
247
+ commit_short=$(git rev-parse --short "$commit")
248
+ commit_email=$(git log -1 --format='%ae' "$commit")
249
+ commit_subject=$(git log -1 --format='%s' "$commit")
250
+
251
+ # Get the SSH signature fingerprint from the commit
252
+ sig_output=$(git log -1 --format='%GK' "$commit" 2>/dev/null || true)
253
+ sig_status=$(git log -1 --format='%G?' "$commit" 2>/dev/null || true)
254
+
255
+ verbose_log "Commit $commit_short: sig_status=$sig_status sig_key=$sig_output email=$commit_email"
256
+
257
+ if [[ -z "$sig_output" ]] || [[ "$sig_status" == "N" ]]; then
258
+ echo -e "${RED}✗ FAIL${NC} $commit_short - ${YELLOW}$commit_subject${NC}"
259
+ echo -e " ${RED}Missing SSH signature on commit${NC}"
260
+ SIG_VALIDATION_FAILED=true
261
+ continue
262
+ fi
263
+
264
+ # Check if we have an expected fingerprint for this contributor
265
+ expected_fp="${EXPECTED_FINGERPRINTS[$commit_email]:-}"
266
+ if [[ -n "$expected_fp" ]]; then
267
+ # Compare the fingerprint from the commit signature with the expected one
268
+ if [[ "$sig_output" == "$expected_fp" ]]; then
269
+ echo -e "${GREEN}✓ PASS${NC} $commit_short - $commit_subject"
270
+ echo -e " SSH signature: ${CYAN}$sig_output${NC}"
271
+ else
272
+ echo -e "${RED}✗ FAIL${NC} $commit_short - ${YELLOW}$commit_subject${NC}"
273
+ echo -e " Expected fingerprint: ${CYAN}$expected_fp${NC}"
274
+ echo -e " Actual fingerprint: ${CYAN}$sig_output${NC}"
275
+ echo -e " ${RED}SSH signature fingerprint mismatch${NC}"
276
+ SIG_VALIDATION_FAILED=true
277
+ fi
278
+ else
279
+ # No expected fingerprint for this email — just verify it has a signature
280
+ if [[ "$sig_status" == "G" ]] || [[ "$sig_status" == "U" ]]; then
281
+ echo -e "${GREEN}✓ PASS${NC} $commit_short - $commit_subject"
282
+ echo -e " SSH signature: ${CYAN}$sig_output${NC} (no fingerprint in .dco-signatures to cross-check)"
283
+ else
284
+ echo -e "${RED}✗ FAIL${NC} $commit_short - ${YELLOW}$commit_subject${NC}"
285
+ echo -e " ${RED}Invalid SSH signature (status: $sig_status)${NC}"
286
+ SIG_VALIDATION_FAILED=true
287
+ fi
288
+ fi
289
+ done <<< "$COMMITS"
290
+ fi
291
+ echo
292
+ fi
293
+
294
+ if [[ ${#FAILED_COMMITS[@]} -eq 0 ]] && [[ "$SIG_FILE_VALID" == "true" ]] && [[ "$SIG_VALIDATION_FAILED" == "false" ]]; then
295
+ echo -e "${GREEN}${BOLD}✓ All commits are properly signed!${NC}"
296
+ echo -e "${GREEN} Valid commits: $VALID_COMMITS/$COMMIT_COUNT${NC}"
297
+ if [[ "$ENFORCE_SIGNATURE_FINGERPRINTS" == "1" ]]; then
298
+ echo -e "${GREEN} Signature fingerprints: enforced${NC}"
299
+ fi
300
+ echo
301
+ exit 0
302
+ else
303
+ echo -e "${RED}${BOLD}✗ DCO validation failed!${NC}"
304
+ echo -e "${RED} Valid commits: $VALID_COMMITS/$COMMIT_COUNT${NC}"
305
+ echo -e "${RED} Invalid commits: ${#FAILED_COMMITS[@]}/$COMMIT_COUNT${NC}"
306
+ echo
307
+ echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
308
+ echo -e "${YELLOW} How to Fix${NC}"
309
+ echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
310
+ echo
311
+ echo -e "${BLUE}Step 1: Revert the last commit and restore changes to working copy${NC}"
312
+ echo -e " ${BOLD}git reset HEAD~1${NC}"
313
+ echo -e " (This undoes the commit and unstages your changes)"
314
+ echo
315
+ echo -e "${BLUE}Step 2: Choose one of the following options:${NC}"
316
+ echo
317
+ echo -e "${BLUE}Option A: Use the commit wrapper script (recommended)${NC}"
318
+ echo -e " 1. Stage your changes:"
319
+ echo -e " ${BOLD}git add .${NC} (or stage specific files)"
320
+ echo
321
+ echo -e " 2. Commit with the wrapper script:"
322
+ echo -e " ${BOLD}./commit.sh -m \"Your commit message\"${NC}"
323
+ echo
324
+ echo -e " This will:"
325
+ echo -e " • Automatically add the sign-off to your commit"
326
+ echo -e " • Update the .dco-signatures file (if first time)"
327
+ echo -e " • Create a DCO signature commit"
328
+ echo
329
+ echo -e "${BLUE}Option B: Add sign-off manually${NC}"
330
+ echo -e " 1. Update .dco-signatures file manually:"
331
+ echo -e " Add this entry to ${BOLD}.dco-signatures${NC}:"
332
+ echo -e " ${CYAN}**Your Name** <your@email.com>${NC}"
333
+ echo -e " ${CYAN}Signed: $(date -u +"%Y-%m-%d %H:%M:%S UTC")${NC}"
334
+ echo
335
+ echo -e " 2. Stage ONLY the signatures file:"
336
+ echo -e " ${BOLD}git add .dco-signatures${NC}"
337
+ echo
338
+ echo -e " 3. Commit the signature record with sign-off:"
339
+ echo -e " ${BOLD}git commit -s -m \"DCO: Add signature for Your Name <your@email.com>\"${NC}"
340
+ echo
341
+ echo -e " 4. Stage your original changes:"
342
+ echo -e " ${BOLD}git add .${NC} (or stage specific files)"
343
+ echo
344
+ echo -e " 5. Commit your original changes with sign-off:"
345
+ echo -e " ${BOLD}git commit -s -m \"Your original commit message\"${NC}"
346
+ echo
347
+ echo -e "${BLUE}What is a DCO signature?${NC}"
348
+ echo -e " A DCO (Developer Certificate of Origin) is your certification that you"
349
+ echo -e " have the right to submit your contribution under the project's license."
350
+ echo -e " See: ${CYAN}https://developercertificate.org/${NC}"
351
+ echo
352
+ exit 1
353
+ fi