@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/.dco-signatures +9 -0
- package/.github/workflows/dco.yml +12 -0
- package/.o/GordianOpenIntegrity-CurrentLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity-InceptionLifehash.svg +1026 -0
- package/.o/GordianOpenIntegrity.yaml +25 -0
- package/DCO.md +34 -0
- package/README.md +122 -0
- package/action.yml +32 -0
- package/caps/Dco.test.ts +288 -0
- package/caps/Dco.ts +269 -0
- package/commit.sh +468 -0
- package/dco.sh +49 -0
- package/examples/01-Lifecycle/main.test.ts +223 -0
- package/package.json +39 -0
- package/test.sh +422 -0
- package/tsconfig.json +28 -0
- package/validate.sh +353 -0
package/caps/Dco.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
|
|
2
|
+
import { join, dirname } from 'path'
|
|
3
|
+
import { readFile, writeFile, access, mkdir, copyFile } from 'fs/promises'
|
|
4
|
+
import { constants } from 'fs'
|
|
5
|
+
import { $ } from 'bun'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
// ── Constants ────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const DCO_FILE = 'DCO.md'
|
|
11
|
+
const SIGNATURES_FILE = '.dco-signatures'
|
|
12
|
+
const MARKER_FILE = '.git/.dco-agreed'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
// ── Capsule ──────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export async function capsule({
|
|
18
|
+
encapsulate,
|
|
19
|
+
CapsulePropertyTypes,
|
|
20
|
+
makeImportStack
|
|
21
|
+
}: {
|
|
22
|
+
encapsulate: any
|
|
23
|
+
CapsulePropertyTypes: any
|
|
24
|
+
makeImportStack: any
|
|
25
|
+
}) {
|
|
26
|
+
|
|
27
|
+
return encapsulate({
|
|
28
|
+
'#@stream44.studio/encapsulate/spine-contracts/CapsuleSpineContract.v0': {
|
|
29
|
+
'#@stream44.studio/encapsulate/structs/Capsule': {},
|
|
30
|
+
'#': {
|
|
31
|
+
|
|
32
|
+
// ══════════════════════════════════════════════════════
|
|
33
|
+
// sign — Run the DCO signing process
|
|
34
|
+
// ══════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
sign: {
|
|
37
|
+
type: CapsulePropertyTypes.Function,
|
|
38
|
+
value: async function (this: any, context: {
|
|
39
|
+
repoDir: string
|
|
40
|
+
autoAgree?: boolean
|
|
41
|
+
signingKeyPath?: string
|
|
42
|
+
}) {
|
|
43
|
+
const { repoDir, autoAgree } = context
|
|
44
|
+
|
|
45
|
+
// Verify DCO.md exists
|
|
46
|
+
const dcoPath = join(repoDir, DCO_FILE)
|
|
47
|
+
try {
|
|
48
|
+
await access(dcoPath, constants.F_OK)
|
|
49
|
+
} catch {
|
|
50
|
+
throw new Error(`DCO.md not found in ${repoDir}`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Resolve commit.sh from this package
|
|
54
|
+
const packageDir = dirname(require.resolve('@stream44.studio/dco/package.json'))
|
|
55
|
+
const dcoScript = join(packageDir, 'dco.sh')
|
|
56
|
+
|
|
57
|
+
const args = ['bash', dcoScript, 'commit']
|
|
58
|
+
if (autoAgree) {
|
|
59
|
+
args.push('--yes-signoff')
|
|
60
|
+
}
|
|
61
|
+
if (context.signingKeyPath) {
|
|
62
|
+
args.push('--signing-key', context.signingKeyPath)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const proc = Bun.spawn(args, {
|
|
66
|
+
cwd: repoDir,
|
|
67
|
+
stdin: autoAgree ? 'pipe' : 'inherit',
|
|
68
|
+
stdout: 'inherit',
|
|
69
|
+
stderr: 'inherit',
|
|
70
|
+
})
|
|
71
|
+
const exitCode = await proc.exited
|
|
72
|
+
|
|
73
|
+
if (exitCode !== 0) {
|
|
74
|
+
throw new Error(`DCO signing failed with exit code ${exitCode}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { signed: true }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// ══════════════════════════════════════════════════════
|
|
82
|
+
// validate — Validate DCO sign-offs on commits
|
|
83
|
+
// ══════════════════════════════════════════════════════
|
|
84
|
+
|
|
85
|
+
validate: {
|
|
86
|
+
type: CapsulePropertyTypes.Function,
|
|
87
|
+
value: async function (this: any, context: {
|
|
88
|
+
repoDir: string
|
|
89
|
+
baseBranch?: string
|
|
90
|
+
headRef?: string
|
|
91
|
+
}) {
|
|
92
|
+
const packageDir = dirname(require.resolve('@stream44.studio/dco/package.json'))
|
|
93
|
+
const dcoScript = join(packageDir, 'dco.sh')
|
|
94
|
+
|
|
95
|
+
const args = ['bash', dcoScript, 'validate']
|
|
96
|
+
if (context.baseBranch) args.push(context.baseBranch)
|
|
97
|
+
if (context.headRef) args.push(context.headRef)
|
|
98
|
+
|
|
99
|
+
const proc = Bun.spawn(args, {
|
|
100
|
+
cwd: context.repoDir,
|
|
101
|
+
stdout: 'pipe',
|
|
102
|
+
stderr: 'pipe',
|
|
103
|
+
})
|
|
104
|
+
const exitCode = await proc.exited
|
|
105
|
+
const stdout = await new Response(proc.stdout).text()
|
|
106
|
+
const stderr = await new Response(proc.stderr).text()
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
valid: exitCode === 0,
|
|
110
|
+
exitCode,
|
|
111
|
+
stdout,
|
|
112
|
+
stderr,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// ══════════════════════════════════════════════════════
|
|
118
|
+
// hasDco — Check if a repository has DCO.md
|
|
119
|
+
// ══════════════════════════════════════════════════════
|
|
120
|
+
|
|
121
|
+
hasDco: {
|
|
122
|
+
type: CapsulePropertyTypes.Function,
|
|
123
|
+
value: async function (this: any, context: {
|
|
124
|
+
repoDir: string
|
|
125
|
+
}) {
|
|
126
|
+
const dcoPath = join(context.repoDir, DCO_FILE)
|
|
127
|
+
try {
|
|
128
|
+
await access(dcoPath, constants.F_OK)
|
|
129
|
+
return true
|
|
130
|
+
} catch {
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
// ══════════════════════════════════════════════════════
|
|
137
|
+
// isSigned — Check if the current user has signed the DCO
|
|
138
|
+
// ══════════════════════════════════════════════════════
|
|
139
|
+
|
|
140
|
+
isSigned: {
|
|
141
|
+
type: CapsulePropertyTypes.Function,
|
|
142
|
+
value: async function (this: any, context: {
|
|
143
|
+
repoDir: string
|
|
144
|
+
}) {
|
|
145
|
+
const markerPath = join(context.repoDir, MARKER_FILE)
|
|
146
|
+
try {
|
|
147
|
+
await access(markerPath, constants.F_OK)
|
|
148
|
+
const content = await readFile(markerPath, 'utf-8')
|
|
149
|
+
const name = content.match(/^name=(.*)$/m)?.[1] || ''
|
|
150
|
+
const email = content.match(/^email=(.*)$/m)?.[1] || ''
|
|
151
|
+
const date = content.match(/^date=(.*)$/m)?.[1] || ''
|
|
152
|
+
const agreementCommit = content.match(/^agreement_commit=(.*)$/m)?.[1] || ''
|
|
153
|
+
return {
|
|
154
|
+
signed: true,
|
|
155
|
+
name,
|
|
156
|
+
email,
|
|
157
|
+
date,
|
|
158
|
+
agreementCommit,
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
return { signed: false }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// ══════════════════════════════════════════════════════
|
|
167
|
+
// getSignatures — Read all signatures from .dco-signatures
|
|
168
|
+
// ══════════════════════════════════════════════════════
|
|
169
|
+
|
|
170
|
+
getSignatures: {
|
|
171
|
+
type: CapsulePropertyTypes.Function,
|
|
172
|
+
value: async function (this: any, context: {
|
|
173
|
+
repoDir: string
|
|
174
|
+
}) {
|
|
175
|
+
const sigPath = join(context.repoDir, SIGNATURES_FILE)
|
|
176
|
+
try {
|
|
177
|
+
const content = await readFile(sigPath, 'utf-8')
|
|
178
|
+
const signatures: Array<{
|
|
179
|
+
name: string
|
|
180
|
+
email: string
|
|
181
|
+
signedDate: string
|
|
182
|
+
agreementCommit: string
|
|
183
|
+
agreementChangeDate: string
|
|
184
|
+
}> = []
|
|
185
|
+
|
|
186
|
+
for (const line of content.split('\n')) {
|
|
187
|
+
// Skip empty lines, comments, headers, separator
|
|
188
|
+
if (!line.trim()) continue
|
|
189
|
+
if (line.startsWith('#')) continue
|
|
190
|
+
if (line.startsWith('---')) continue
|
|
191
|
+
if (line.startsWith('This ')) continue
|
|
192
|
+
if (line.startsWith('Each ')) continue
|
|
193
|
+
if (line.startsWith('Format:')) continue
|
|
194
|
+
|
|
195
|
+
// Parse: name <email> | signed: <date> | agreement: <commit> (<date>)
|
|
196
|
+
const nameMatch = line.match(/^(.+?)\s*<(.+?)>/)
|
|
197
|
+
const signedMatch = line.match(/\|\s*signed:\s*(.+?)\s*\|/)
|
|
198
|
+
const agreementMatch = line.match(/\|\s*agreement:\s*([a-f0-9]+)\s*\((.+?)\)/)
|
|
199
|
+
|
|
200
|
+
if (nameMatch) {
|
|
201
|
+
signatures.push({
|
|
202
|
+
name: nameMatch[1].trim(),
|
|
203
|
+
email: nameMatch[2].trim(),
|
|
204
|
+
signedDate: signedMatch?.[1]?.trim() || '',
|
|
205
|
+
agreementCommit: agreementMatch?.[1]?.trim() || '',
|
|
206
|
+
agreementChangeDate: agreementMatch?.[2]?.trim() || '',
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { found: true, signatures }
|
|
212
|
+
} catch {
|
|
213
|
+
return { found: false, signatures: [] }
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
// ══════════════════════════════════════════════════════
|
|
219
|
+
// signAndCommit — Full DCO flow for publishing pipelines
|
|
220
|
+
// ══════════════════════════════════════════════════════
|
|
221
|
+
|
|
222
|
+
signAndCommit: {
|
|
223
|
+
type: CapsulePropertyTypes.Function,
|
|
224
|
+
value: async function (this: any, context: {
|
|
225
|
+
repoDir: string
|
|
226
|
+
message: string
|
|
227
|
+
autoAgree?: boolean
|
|
228
|
+
signingKeyPath?: string
|
|
229
|
+
projectSourceDir?: string
|
|
230
|
+
}) {
|
|
231
|
+
const { repoDir, message } = context
|
|
232
|
+
|
|
233
|
+
// 1. Run DCO signing process
|
|
234
|
+
await this.sign({
|
|
235
|
+
repoDir,
|
|
236
|
+
autoAgree: context.autoAgree,
|
|
237
|
+
signingKeyPath: context.signingKeyPath,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
// 2. Stage all files and commit with --signoff
|
|
241
|
+
await $`git add -A`.cwd(repoDir).quiet()
|
|
242
|
+
if (context.signingKeyPath) {
|
|
243
|
+
await $`git -c gpg.format=ssh -c user.signingkey=${context.signingKeyPath} commit --gpg-sign --signoff -m ${message}`.cwd(repoDir).quiet().nothrow()
|
|
244
|
+
} else {
|
|
245
|
+
await $`git commit --signoff -m ${message}`.cwd(repoDir).quiet().nothrow()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 3. Copy .dco-signatures back to project source if provided
|
|
249
|
+
if (context.projectSourceDir) {
|
|
250
|
+
const stageSigFile = join(repoDir, SIGNATURES_FILE)
|
|
251
|
+
try {
|
|
252
|
+
await access(stageSigFile, constants.F_OK)
|
|
253
|
+
await copyFile(stageSigFile, join(context.projectSourceDir, SIGNATURES_FILE))
|
|
254
|
+
} catch { }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { committed: true }
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}, {
|
|
264
|
+
importMeta: import.meta,
|
|
265
|
+
importStack: makeImportStack(),
|
|
266
|
+
capsuleName: capsule['#'],
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
capsule['#'] = '@stream44.studio/dco/caps/Dco'
|