@voxgig/sdkgen 4.28.0 → 4.30.0
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/bin/voxgig-sdkgen +1 -1
- package/dist/admin/npm-trust.d.ts +39 -0
- package/dist/admin/npm-trust.js +308 -0
- package/dist/admin/npm-trust.js.map +1 -0
- package/dist/cmp/PublishWorkflow.js +47 -26
- package/dist/cmp/PublishWorkflow.js.map +1 -1
- package/dist/helpers/packageMeta.d.ts +2 -1
- package/dist/helpers/packageMeta.js +7 -3
- package/dist/helpers/packageMeta.js.map +1 -1
- package/dist/sdkgen.d.ts +3 -2
- package/dist/sdkgen.js +15 -4
- package/dist/sdkgen.js.map +1 -1
- package/dist/testkit.js +16 -6
- package/dist/testkit.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +1 -0
- package/model/sdkgen.aontu +9 -0
- package/package.json +3 -2
- package/project/.sdk/model/target/rb.aontu +1 -2
- package/project/.sdk/src/cmp/c/utility_c.ts +3 -1
- package/project/.sdk/src/cmp/cpp/ReadmeInstall_cpp.ts +3 -3
- package/project/.sdk/src/cmp/elixir/ReadmeInstall_elixir.ts +2 -2
- package/project/.sdk/src/cmp/java/Package_java.ts +2 -1
- package/project/.sdk/src/cmp/java/ReadmeInstall_java.ts +2 -2
- package/project/.sdk/src/cmp/js/Package_js.ts +2 -2
- package/project/.sdk/src/cmp/kotlin/Package_kotlin.ts +2 -1
- package/project/.sdk/src/cmp/kotlin/ReadmeInstall_kotlin.ts +2 -2
- package/project/.sdk/src/cmp/lua/Package_lua.ts +3 -5
- package/project/.sdk/src/cmp/ocaml/utility_ocaml.ts +3 -1
- package/project/.sdk/src/cmp/php/Package_php.ts +0 -2
- package/project/.sdk/src/cmp/py/Package_py.ts +0 -2
- package/project/.sdk/src/cmp/py-data/Main_py-data.ts +1 -3
- package/project/.sdk/src/cmp/rb/Package_rb.ts +15 -23
- package/project/.sdk/src/cmp/rust/utility_rust.ts +3 -1
- package/project/.sdk/src/cmp/scala/ReadmeInstall_scala.ts +2 -2
- package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
- package/project/sdkgen-package.json +1 -1
- package/src/admin/npm-trust.ts +408 -0
- package/src/cmp/PublishWorkflow.ts +56 -26
- package/src/helpers/packageMeta.ts +9 -3
- package/src/sdkgen.ts +14 -0
- package/src/testkit.ts +50 -35
- package/src/types.ts +1 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/* Copyright (c) 2026 Voxgig Ltd, MIT License */
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
type Mode = 'setup' | 'check' | 'dry-run'
|
|
7
|
+
|
|
8
|
+
type Publication = {
|
|
9
|
+
pkg: string
|
|
10
|
+
file: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type TrustSpec = {
|
|
14
|
+
repository: string
|
|
15
|
+
publish: Publication[]
|
|
16
|
+
mode: Mode
|
|
17
|
+
replace: boolean
|
|
18
|
+
otp?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type TrustEntry = {
|
|
22
|
+
id?: string
|
|
23
|
+
type?: string
|
|
24
|
+
file?: string
|
|
25
|
+
repository?: string
|
|
26
|
+
environment?: string
|
|
27
|
+
permissions?: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Assessment = {
|
|
31
|
+
match?: TrustEntry
|
|
32
|
+
extra: TrustEntry[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type NpmPort = {
|
|
36
|
+
list(pkg: string): TrustEntry[]
|
|
37
|
+
create(pkg: string, repository: string, file: string): void
|
|
38
|
+
revoke(pkg: string, id: string): void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
const USAGE =
|
|
43
|
+
'usage: npm-trust --repository <owner/repo> ' +
|
|
44
|
+
'--publish <package>=<workflow.yml> [--publish ...] ' +
|
|
45
|
+
'[--check | --dry-run] [--replace] [--otp <code>]'
|
|
46
|
+
|
|
47
|
+
const REPOSITORY_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/
|
|
48
|
+
const WORKFLOW_RE = /^[A-Za-z0-9_.-]+\.ya?ml$/
|
|
49
|
+
const PACKAGE_RE = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
function parseArgs(argv: string[]): TrustSpec {
|
|
53
|
+
const spec: TrustSpec = { repository: '', publish: [], mode: 'setup', replace: false }
|
|
54
|
+
const modes: Mode[] = []
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const arg = argv[i]
|
|
58
|
+
const value = (): string => {
|
|
59
|
+
const v = argv[++i]
|
|
60
|
+
if (null == v || v.startsWith('--')) {
|
|
61
|
+
throw new Error(`${arg} needs a value\n${USAGE}`)
|
|
62
|
+
}
|
|
63
|
+
return v
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if ('--repository' === arg) {
|
|
67
|
+
if ('' !== spec.repository) {
|
|
68
|
+
throw new Error('--repository is given twice; the generated script already names it')
|
|
69
|
+
}
|
|
70
|
+
spec.repository = value()
|
|
71
|
+
}
|
|
72
|
+
else if ('--publish' === arg) {
|
|
73
|
+
const v = value()
|
|
74
|
+
const eq = v.indexOf('=')
|
|
75
|
+
spec.publish.push({ pkg: v.slice(0, eq < 0 ? v.length : eq), file: eq < 0 ? '' : v.slice(eq + 1) })
|
|
76
|
+
}
|
|
77
|
+
else if ('--check' === arg || '--dry-run' === arg) {
|
|
78
|
+
modes.push(arg.slice(2) as Mode)
|
|
79
|
+
}
|
|
80
|
+
else if ('--replace' === arg) {
|
|
81
|
+
spec.replace = true
|
|
82
|
+
}
|
|
83
|
+
else if ('--otp' === arg) {
|
|
84
|
+
spec.otp = value()
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw new Error(`unknown argument: ${arg}\n${USAGE}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (1 < modes.length) {
|
|
92
|
+
throw new Error(`--check and --dry-run are alternatives\n${USAGE}`)
|
|
93
|
+
}
|
|
94
|
+
spec.mode = modes[0] || 'setup'
|
|
95
|
+
|
|
96
|
+
if ('check' === spec.mode && spec.replace) {
|
|
97
|
+
throw new Error('--check changes nothing, so it cannot --replace')
|
|
98
|
+
}
|
|
99
|
+
if (!REPOSITORY_RE.test(spec.repository)) {
|
|
100
|
+
throw new Error(`--repository must be owner/repo, got: ${spec.repository || '(none)'}\n${USAGE}`)
|
|
101
|
+
}
|
|
102
|
+
if (0 === spec.publish.length) {
|
|
103
|
+
throw new Error(`at least one --publish is required\n${USAGE}`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const seen = new Set<string>()
|
|
107
|
+
for (const { pkg, file } of spec.publish) {
|
|
108
|
+
if (!PACKAGE_RE.test(pkg)) {
|
|
109
|
+
throw new Error(`not an npm package name: ${pkg}`)
|
|
110
|
+
}
|
|
111
|
+
if (!WORKFLOW_RE.test(file)) {
|
|
112
|
+
throw new Error(`${pkg}: the workflow must be a .yml file name, not a path, got: ${file || '(none)'}`)
|
|
113
|
+
}
|
|
114
|
+
if (seen.has(pkg)) {
|
|
115
|
+
throw new Error(`${pkg} is named twice`)
|
|
116
|
+
}
|
|
117
|
+
seen.add(pkg)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return spec
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
// `npm trust list --json` prints one pretty-printed object per configuration
|
|
125
|
+
// rather than an array, so the objects are split out by brace depth.
|
|
126
|
+
function parseTrustList(text: string): TrustEntry[] {
|
|
127
|
+
const src = text.trim()
|
|
128
|
+
if ('' === src) {
|
|
129
|
+
return []
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const whole = JSON.parse(src)
|
|
134
|
+
return Array.isArray(whole) ? whole : [whole]
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Not a single JSON value: fall through to the object stream.
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const entries: TrustEntry[] = []
|
|
141
|
+
let depth = 0
|
|
142
|
+
let start = -1
|
|
143
|
+
let inString = false
|
|
144
|
+
let escaped = false
|
|
145
|
+
|
|
146
|
+
for (let i = 0; i < src.length; i++) {
|
|
147
|
+
const c = src[i]
|
|
148
|
+
if (inString) {
|
|
149
|
+
if (escaped) {
|
|
150
|
+
escaped = false
|
|
151
|
+
}
|
|
152
|
+
else if ('\\' === c) {
|
|
153
|
+
escaped = true
|
|
154
|
+
}
|
|
155
|
+
else if ('"' === c) {
|
|
156
|
+
inString = false
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else if ('"' === c) {
|
|
160
|
+
inString = true
|
|
161
|
+
}
|
|
162
|
+
else if ('{' === c) {
|
|
163
|
+
if (0 === depth) {
|
|
164
|
+
start = i
|
|
165
|
+
}
|
|
166
|
+
depth++
|
|
167
|
+
}
|
|
168
|
+
else if ('}' === c) {
|
|
169
|
+
depth--
|
|
170
|
+
if (0 === depth) {
|
|
171
|
+
entries.push(JSON.parse(src.slice(start, i + 1)))
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (0 !== depth) {
|
|
177
|
+
throw new Error('could not read the output of `npm trust list --json`')
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return entries
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
// The generated workflows declare no GitHub environment, so a configuration
|
|
185
|
+
// that names one would never match the job's OIDC token.
|
|
186
|
+
function assess(pub: Publication, repository: string, entries: TrustEntry[]): Assessment {
|
|
187
|
+
const matches = (e: TrustEntry) =>
|
|
188
|
+
'github' === e.type &&
|
|
189
|
+
repository.toLowerCase() === String(e.repository || '').toLowerCase() &&
|
|
190
|
+
pub.file === e.file &&
|
|
191
|
+
null == e.environment &&
|
|
192
|
+
(null == e.permissions || e.permissions.includes('createPackage'))
|
|
193
|
+
|
|
194
|
+
const match = entries.find(matches)
|
|
195
|
+
return { match, extra: entries.filter((e) => e !== match) }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
function describe(e: TrustEntry): string {
|
|
200
|
+
return [
|
|
201
|
+
e.type || '?',
|
|
202
|
+
e.repository || '?',
|
|
203
|
+
e.file || '?',
|
|
204
|
+
...(null == e.environment ? [] : ['environment ' + e.environment]),
|
|
205
|
+
...(null == e.permissions ? [] : ['[' + e.permissions.join(', ') + ']']),
|
|
206
|
+
...(null == e.id ? [] : ['id ' + e.id]),
|
|
207
|
+
].join(' ')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
function trustCommand(repository: string, pub: Publication): string {
|
|
212
|
+
return `npm trust github ${pub.pkg} --repository ${repository} ` +
|
|
213
|
+
`--file ${pub.file} --allow-publish`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
function run(spec: TrustSpec, npm: NpmPort, out: (line: string) => void): number {
|
|
218
|
+
if ('dry-run' === spec.mode) {
|
|
219
|
+
for (const pub of spec.publish) {
|
|
220
|
+
out(`${pub.pkg}: ${trustCommand(spec.repository, pub)}`)
|
|
221
|
+
}
|
|
222
|
+
if (spec.replace) {
|
|
223
|
+
out('--replace would also revoke any other trusted publisher of these packages.')
|
|
224
|
+
}
|
|
225
|
+
return 0
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let drift = false
|
|
229
|
+
|
|
230
|
+
// Nothing is created beside a configuration this repository did not ask
|
|
231
|
+
// for, so a replacement revokes first and creates after.
|
|
232
|
+
for (const pub of spec.publish) {
|
|
233
|
+
const { match, extra } = assess(pub, spec.repository, npm.list(pub.pkg))
|
|
234
|
+
const wanted = `github ${spec.repository} ${pub.file}`
|
|
235
|
+
|
|
236
|
+
let kept = 0
|
|
237
|
+
for (const e of extra) {
|
|
238
|
+
if ('setup' === spec.mode && spec.replace && null != e.id) {
|
|
239
|
+
npm.revoke(pub.pkg, e.id)
|
|
240
|
+
out(`${pub.pkg}: revoked: ${describe(e)}`)
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
kept++
|
|
244
|
+
out(`${pub.pkg}: also trusted, and not by this repository's workflows: ${describe(e)}` +
|
|
245
|
+
('setup' === spec.mode ? ' (--replace revokes it)' : ''))
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const create = null == match && 'setup' === spec.mode && 0 === kept
|
|
250
|
+
if (null != match) {
|
|
251
|
+
out(`${pub.pkg}: trusted: ${wanted}`)
|
|
252
|
+
}
|
|
253
|
+
else if (create) {
|
|
254
|
+
npm.create(pub.pkg, spec.repository, pub.file)
|
|
255
|
+
out(`${pub.pkg}: now trusted: ${wanted}`)
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
out(`${pub.pkg}: NOT trusted: ${wanted}`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
drift = drift || 0 < kept || (null == match && !create)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return drift ? 1 : 0
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
function npmFailure(action: string, stderr: string): string {
|
|
269
|
+
if (/EOTP|one-time pass/i.test(stderr)) {
|
|
270
|
+
return `${action} needs a one-time password: run again with --otp <code>`
|
|
271
|
+
}
|
|
272
|
+
if (/E404|not found/i.test(stderr)) {
|
|
273
|
+
return `${action}: the package is not on npm yet. Publish its first version ` +
|
|
274
|
+
'by hand, then set up trusted publishing'
|
|
275
|
+
}
|
|
276
|
+
if (/E401|ENEEDAUTH/.test(stderr)) {
|
|
277
|
+
return `${action}: not logged in to npm with publish rights. Run \`npm login\` first`
|
|
278
|
+
}
|
|
279
|
+
return `${action} failed:\n${stderr.trim()}`
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
// An npm without the command still exits 0 from `npm trust --help`, printing
|
|
284
|
+
// "Unknown command", so only the usage text tells a capable npm apart.
|
|
285
|
+
function trustCapableNpm(): string[] {
|
|
286
|
+
const shell = 'win32' === process.platform
|
|
287
|
+
const probe = spawnSync('npm', ['trust', '--help'], { encoding: 'utf8', shell })
|
|
288
|
+
return /npm trust github/.test(String(probe.stdout || '') + String(probe.stderr || '')) ?
|
|
289
|
+
['npm'] : ['npx', '--yes', 'npm@latest']
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
// A captured call has no terminal, so npm raises EOTP instead of prompting;
|
|
294
|
+
// the mutating calls keep the terminal and can prompt.
|
|
295
|
+
function npmPort(otp?: string): NpmPort {
|
|
296
|
+
const npm = trustCapableNpm()
|
|
297
|
+
const shell = 'win32' === process.platform
|
|
298
|
+
const otpArgs = null == otp ? [] : ['--otp', otp]
|
|
299
|
+
|
|
300
|
+
const call = (args: string[], capture: boolean): string => {
|
|
301
|
+
const res = spawnSync(npm[0], [...npm.slice(1), ...args, ...otpArgs], {
|
|
302
|
+
encoding: 'utf8',
|
|
303
|
+
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
304
|
+
shell,
|
|
305
|
+
})
|
|
306
|
+
if (null != res.error) {
|
|
307
|
+
throw new Error(`could not run ${npm[0]}: ${res.error.message}`)
|
|
308
|
+
}
|
|
309
|
+
if (0 !== res.status) {
|
|
310
|
+
throw new Error(npmFailure(`npm ${args.slice(0, 3).join(' ')}`, String(res.stderr || '')))
|
|
311
|
+
}
|
|
312
|
+
return String(res.stdout || '')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
list: (pkg) => parseTrustList(call(['trust', 'list', pkg, '--json'], true)),
|
|
317
|
+
create: (pkg, repository, file) => {
|
|
318
|
+
call(['trust', 'github', pkg, '--repository', repository,
|
|
319
|
+
'--file', file, '--allow-publish', '--yes'], false)
|
|
320
|
+
},
|
|
321
|
+
revoke: (pkg, id) => {
|
|
322
|
+
call(['trust', 'revoke', pkg, '--id=' + id], false)
|
|
323
|
+
},
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
function shellQuote(value: string): string {
|
|
329
|
+
return "'" + value.replace(/'/g, "'\\''") + "'"
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
// A null repository still writes the script, so one generated while the
|
|
334
|
+
// repository was on github.com cannot outlive the move.
|
|
335
|
+
function npmTrustScript(repository: string | null, publish: Publication[]): string {
|
|
336
|
+
if (null == repository) {
|
|
337
|
+
return `#!/usr/bin/env bash
|
|
338
|
+
# Generated by @voxgig/sdkgen. npm trusts GitHub Actions only for a repository
|
|
339
|
+
# on github.com, and this project's repository is elsewhere.
|
|
340
|
+
echo 'npm trusted publishing needs the repository on github.com; see .sdk/PUBLISHING.md.' >&2
|
|
341
|
+
exit 1
|
|
342
|
+
`
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const args = [
|
|
346
|
+
'--repository ' + shellQuote(repository),
|
|
347
|
+
...publish.map((p) => '--publish ' + shellQuote(`${p.pkg}=${p.file}`)),
|
|
348
|
+
'"$@"',
|
|
349
|
+
]
|
|
350
|
+
return `#!/usr/bin/env bash
|
|
351
|
+
# Generated by @voxgig/sdkgen. Registers this repository's npm publish
|
|
352
|
+
# workflows as trusted publishers; --check reports drift, --dry-run changes nothing.
|
|
353
|
+
set -euo pipefail
|
|
354
|
+
sdk_dir="$(cd -- "$(dirname -- "\${BASH_SOURCE[0]}")/.." && pwd)"
|
|
355
|
+
trust="$sdk_dir/node_modules/@voxgig/sdkgen/dist/admin/npm-trust.js"
|
|
356
|
+
if [[ ! -f "$trust" ]]; then
|
|
357
|
+
echo 'Install or update the .sdk dependencies to configure npm trusted publishing.' >&2
|
|
358
|
+
exit 1
|
|
359
|
+
fi
|
|
360
|
+
exec node "$trust" \\
|
|
361
|
+
${args.join(' \\\n ')}
|
|
362
|
+
`
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
const OFFLINE: NpmPort = {
|
|
367
|
+
list() { throw new Error('--dry-run does not contact npm') },
|
|
368
|
+
create() { throw new Error('--dry-run does not contact npm') },
|
|
369
|
+
revoke() { throw new Error('--dry-run does not contact npm') },
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
function main(argv: string[]): number {
|
|
374
|
+
try {
|
|
375
|
+
const spec = parseArgs(argv)
|
|
376
|
+
const npm = 'dry-run' === spec.mode ? OFFLINE : npmPort(spec.otp)
|
|
377
|
+
return run(spec, npm, (line) => console.log(line))
|
|
378
|
+
}
|
|
379
|
+
catch (err: any) {
|
|
380
|
+
console.error(err.message)
|
|
381
|
+
return 2
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
if (require.main === module) {
|
|
387
|
+
process.exitCode = main(process.argv.slice(2))
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
export type {
|
|
392
|
+
Assessment,
|
|
393
|
+
NpmPort,
|
|
394
|
+
Publication,
|
|
395
|
+
TrustEntry,
|
|
396
|
+
TrustSpec,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export {
|
|
400
|
+
assess,
|
|
401
|
+
main,
|
|
402
|
+
npmFailure,
|
|
403
|
+
npmTrustScript,
|
|
404
|
+
parseArgs,
|
|
405
|
+
parseTrustList,
|
|
406
|
+
run,
|
|
407
|
+
trustCommand,
|
|
408
|
+
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
|
|
10
10
|
import { packageName, packageVersion, repoInfo } from '../helpers/packageMeta'
|
|
11
11
|
|
|
12
|
+
import { npmTrustScript } from '../admin/npm-trust'
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
const PublishWorkflow = cmp(function PublishWorkflow(props: any) {
|
|
14
16
|
const { ctx$ } = props
|
|
@@ -34,13 +36,15 @@ const PublishWorkflow = cmp(function PublishWorkflow(props: any) {
|
|
|
34
36
|
return
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// npm trusts GitHub Actions only for a repository on github.com.
|
|
40
|
+
const { host, path } = repoInfo(model)
|
|
41
|
+
const repository = 'github.com' === host.toLowerCase() ? path : null
|
|
38
42
|
|
|
39
43
|
Folder({ name: '.github' }, () => {
|
|
40
44
|
Folder({ name: 'workflows' }, () => {
|
|
41
45
|
for (const target of npmTargets) {
|
|
42
|
-
File({ name:
|
|
43
|
-
Content(publishWorkflow(model, target, npmTargets))
|
|
46
|
+
File({ name: workflowFile(target) }, () => {
|
|
47
|
+
Content(publishWorkflow(model, target, npmTargets, repository))
|
|
44
48
|
})
|
|
45
49
|
}
|
|
46
50
|
})
|
|
@@ -48,7 +52,14 @@ const PublishWorkflow = cmp(function PublishWorkflow(props: any) {
|
|
|
48
52
|
|
|
49
53
|
Folder({ name: '.sdk' }, () => {
|
|
50
54
|
File({ name: 'PUBLISHING.md' }, () => {
|
|
51
|
-
Content(publishingDoc(model, npmTargets,
|
|
55
|
+
Content(publishingDoc(model, npmTargets, repository))
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
Folder({ name: 'admin' }, () => {
|
|
59
|
+
File({ name: 'setup-npm-trust.sh', mode: 0o755 }, () => {
|
|
60
|
+
Content(npmTrustScript(repository, npmTargets.map((t: any) =>
|
|
61
|
+
({ pkg: packageName(model, t.name), file: workflowFile(t) }))))
|
|
62
|
+
})
|
|
52
63
|
})
|
|
53
64
|
})
|
|
54
65
|
|
|
@@ -59,6 +70,11 @@ const PublishWorkflow = cmp(function PublishWorkflow(props: any) {
|
|
|
59
70
|
})
|
|
60
71
|
|
|
61
72
|
|
|
73
|
+
function workflowFile(target: any): string {
|
|
74
|
+
return `publish-${target.name}.yml`
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
62
78
|
function isPrimaryNpm(model: any, target: any, npmTargets: any[]): boolean {
|
|
63
79
|
if (1 === npmTargets.length) {
|
|
64
80
|
return true
|
|
@@ -73,10 +89,16 @@ function releaseTag(model: any, target: any, npmTargets: any[]): string {
|
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
|
|
76
|
-
function publishWorkflow(
|
|
92
|
+
function publishWorkflow(
|
|
93
|
+
model: any, target: any, npmTargets: any[], repository: string | null,
|
|
94
|
+
): string {
|
|
77
95
|
const name = target.name
|
|
78
96
|
const tag = releaseTag(model, target, npmTargets)
|
|
79
97
|
const pkg = packageName(model, name)
|
|
98
|
+
const setup = null == repository ? '' : `
|
|
99
|
+
# .sdk/admin/setup-npm-trust.sh registers exactly this, and with --check
|
|
100
|
+
# reports any drift from it.
|
|
101
|
+
#`
|
|
80
102
|
|
|
81
103
|
return `# Generated by @voxgig/sdkgen. Do not edit.
|
|
82
104
|
#
|
|
@@ -88,10 +110,10 @@ function publishWorkflow(model: any, target: any, npmTargets: any[]): string {
|
|
|
88
110
|
# SET UP ONCE, by a maintainer with publish rights — see .sdk/PUBLISHING.md:
|
|
89
111
|
#
|
|
90
112
|
# npm trust github ${pkg} \\
|
|
91
|
-
# --repository <owner>/<repo> \\
|
|
92
|
-
# --file
|
|
113
|
+
# --repository ${repository ?? '<owner>/<repo>'} \\
|
|
114
|
+
# --file ${workflowFile(target)} \\
|
|
93
115
|
# --allow-publish
|
|
94
|
-
|
|
116
|
+
#${setup}
|
|
95
117
|
# THE FILENAME IS PART OF THAT CONFIGURATION. Renaming this file breaks
|
|
96
118
|
# publishing until the npm side is updated to match.
|
|
97
119
|
#
|
|
@@ -283,18 +305,27 @@ jobs:
|
|
|
283
305
|
}
|
|
284
306
|
|
|
285
307
|
|
|
286
|
-
function publishingDoc(model: any, targets: any[],
|
|
308
|
+
function publishingDoc(model: any, targets: any[], repository: string | null): string {
|
|
287
309
|
const pkg = packageName(model, targets[0].name)
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
.
|
|
292
|
-
|
|
293
|
-
|
|
310
|
+
const first = workflowFile(targets[0])
|
|
311
|
+
|
|
312
|
+
const setupHow = null == repository ? 'run:' :
|
|
313
|
+
'run\n`.sdk/admin/setup-npm-trust.sh`. It registers every workflow in the table\n' +
|
|
314
|
+
'above and leaves a package that is already set up alone; for the\n' +
|
|
315
|
+
`\`${targets[0].name}\` target it runs:`
|
|
316
|
+
|
|
317
|
+
const setupCheck = null == repository ? '' : `
|
|
318
|
+
Run the script again with \`--check\` at any time. It compares what npm holds
|
|
319
|
+
with what these workflows need, changes nothing, and exits non-zero on any
|
|
320
|
+
difference, including a trusted publisher this repository did not ask for.
|
|
321
|
+
\`--replace\` revokes those, \`--dry-run\` prints the commands without contacting
|
|
322
|
+
npm, and \`--otp <code>\` hands npm a one-time password when it asks for one.
|
|
323
|
+
An npm with no \`npm trust\` command is bypassed for \`npx npm@latest\`.
|
|
324
|
+
`
|
|
294
325
|
|
|
295
326
|
const rows = targets.map((t: any) =>
|
|
296
327
|
`| \`${t.name}/\` | ${packageName(model, t.name)} | ` +
|
|
297
|
-
`\`.github/workflows
|
|
328
|
+
`\`.github/workflows/${workflowFile(t)}\` |`).join('\n')
|
|
298
329
|
|
|
299
330
|
// WHICH TAG EACH TARGET CUTS. Worth a table rather than a sentence: the
|
|
300
331
|
// primary npm target owns the bare `v<version>` and every other one is
|
|
@@ -332,7 +363,7 @@ publish credential, and attaches SLSA provenance as it goes.
|
|
|
332
363
|
2. Commit and push to \`main\`, and let CI go green.
|
|
333
364
|
3. Run the workflow from the Actions tab, or:
|
|
334
365
|
|
|
335
|
-
gh workflow run
|
|
366
|
+
gh workflow run ${first} --ref main -f expect_sha=$(git rev-parse HEAD)
|
|
336
367
|
|
|
337
368
|
\`expect_sha\` is optional and worth using: it refuses if \`main\` moved between
|
|
338
369
|
the commit you checked and the run resolving.
|
|
@@ -363,21 +394,20 @@ its token into the git config for the whole job.
|
|
|
363
394
|
|
|
364
395
|
## One-time set-up
|
|
365
396
|
|
|
366
|
-
npm has to be told which repository and which workflow file may publish
|
|
397
|
+
npm has to be told which repository and which workflow file may publish each
|
|
367
398
|
package. From a machine logged in to npm with publish rights (2FA is
|
|
368
|
-
required)
|
|
399
|
+
required), ${setupHow}
|
|
369
400
|
|
|
370
401
|
npm trust github ${pkg} \\
|
|
371
|
-
--repository ${owner} \\
|
|
372
|
-
--file
|
|
402
|
+
--repository ${repository ?? '<owner>/<repo>'} \\
|
|
403
|
+
--file ${first} \\
|
|
373
404
|
--allow-publish
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
\`npm trust revoke ${pkg} --id=<id>\` removes
|
|
405
|
+
${setupCheck}
|
|
406
|
+
\`npm trust list ${pkg}\` shows the configuration, and
|
|
407
|
+
\`npm trust revoke ${pkg} --id=<id>\` removes one.
|
|
377
408
|
|
|
378
409
|
**The workflow filename is part of the configuration.** Renaming
|
|
379
|
-
|
|
380
|
-
updated to match.
|
|
410
|
+
\`${first}\` breaks publishing until the npm side is updated to match.
|
|
381
411
|
|
|
382
412
|
**A brand-new package cannot be set up this way.** npm only offers the
|
|
383
413
|
trusted-publisher settings once a version exists, so the FIRST release of a
|
|
@@ -33,6 +33,11 @@ function originName(model: any, target: string): string {
|
|
|
33
33
|
return (null != orig && '' !== orig) ? String(orig) : target
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function sdkName(slug: string): string {
|
|
37
|
+
return slug.endsWith('-sdk') ? slug : `${slug}-sdk`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
36
41
|
function repoInfo(model: any) {
|
|
37
42
|
const slug = model.name
|
|
38
43
|
const origin = model.origin || 'voxgig-sdk'
|
|
@@ -40,7 +45,7 @@ function repoInfo(model: any) {
|
|
|
40
45
|
const declared = (model && model.main && model.main[KIT] && model.main[KIT].repo) || {}
|
|
41
46
|
|
|
42
47
|
const host = '' === (declared.host || '') ? 'github.com' : (declared.host || 'github.com')
|
|
43
|
-
const path = '' === (declared.path || '') ? `${origin}/${slug}
|
|
48
|
+
const path = '' === (declared.path || '') ? `${origin}/${sdkName(slug)}` : String(declared.path)
|
|
44
49
|
|
|
45
50
|
const seg = path.split('/')
|
|
46
51
|
const repo = seg[seg.length - 1]
|
|
@@ -190,8 +195,8 @@ function apiName(model: any): string {
|
|
|
190
195
|
function packageName(model: any, eco: string): string {
|
|
191
196
|
const slug = model.name
|
|
192
197
|
const origin = model.origin || 'voxgig-sdk'
|
|
193
|
-
const base =
|
|
194
|
-
const npmScoped = `@${origin}/${
|
|
198
|
+
const base = sdkName(slug)
|
|
199
|
+
const npmScoped = `@${origin}/${base}`
|
|
195
200
|
|
|
196
201
|
const ECO_TARGET: Record<string, string> = {
|
|
197
202
|
npm: 'ts', pypi: 'py', gem: 'rb', luarocks: 'lua', composer: 'php',
|
|
@@ -324,6 +329,7 @@ function envName(model: any): string {
|
|
|
324
329
|
}
|
|
325
330
|
|
|
326
331
|
export {
|
|
332
|
+
sdkName,
|
|
327
333
|
PUBLISHER,
|
|
328
334
|
PUBLISHER_URL,
|
|
329
335
|
SECURITY_EMAIL,
|
package/src/sdkgen.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { AgentGuideFeature } from './cmp/AgentGuideFeature'
|
|
|
37
37
|
import { License } from './cmp/License'
|
|
38
38
|
import { Security } from './cmp/Security'
|
|
39
39
|
import { PublishWorkflow } from './cmp/PublishWorkflow'
|
|
40
|
+
import { npmTrustScript } from './admin/npm-trust'
|
|
40
41
|
import { Changelog } from './cmp/Changelog'
|
|
41
42
|
import { Test } from './cmp/Test'
|
|
42
43
|
import { TestControl, TEST_CONTROL_EXCLUDE } from './cmp/TestControl'
|
|
@@ -103,6 +104,7 @@ import {
|
|
|
103
104
|
import type { Manifest, ManifestRead } from './helpers/manifest'
|
|
104
105
|
import {
|
|
105
106
|
packageName,
|
|
107
|
+
sdkName,
|
|
106
108
|
installCommand,
|
|
107
109
|
registryState,
|
|
108
110
|
isPublished,
|
|
@@ -656,6 +658,8 @@ function externalItems(
|
|
|
656
658
|
|
|
657
659
|
if (null != ov.path && '' !== ov.path) {
|
|
658
660
|
output.path = ov.path
|
|
661
|
+
// The override relocates this run's output, root placement included.
|
|
662
|
+
delete output.root
|
|
659
663
|
}
|
|
660
664
|
if (null != ov.sdkrel && '' !== ov.sdkrel) {
|
|
661
665
|
output.sdkrel = ov.sdkrel
|
|
@@ -718,6 +722,14 @@ function checkExternalFolders(external: ExternalPlan[], root: string, fs: any) {
|
|
|
718
722
|
ext.target.output.path + '", which resolves to: ' + ext.folder +
|
|
719
723
|
'\n (SDK project: ' + root + ')'
|
|
720
724
|
|
|
725
|
+
if (true === ext.target.output.root) {
|
|
726
|
+
throw new SdkGenError(
|
|
727
|
+
label + ' "' + ext.name + '" declares both `output: path` and ' +
|
|
728
|
+
'`output: root: true`.\n ' + where +
|
|
729
|
+
'\n `path` generates it into another repository and `root` at the ' +
|
|
730
|
+
'root of this one. Keep one.')
|
|
731
|
+
}
|
|
732
|
+
|
|
721
733
|
if (ext.folder === root || folderContains(root, ext.folder)) {
|
|
722
734
|
throw new SdkGenError(
|
|
723
735
|
'External output path is inside the SDK project.\n ' + where +
|
|
@@ -945,6 +957,7 @@ export {
|
|
|
945
957
|
License,
|
|
946
958
|
Security,
|
|
947
959
|
PublishWorkflow,
|
|
960
|
+
npmTrustScript,
|
|
948
961
|
Changelog,
|
|
949
962
|
Entity,
|
|
950
963
|
Feature,
|
|
@@ -1094,6 +1107,7 @@ export {
|
|
|
1094
1107
|
prefixLeadingDigit,
|
|
1095
1108
|
|
|
1096
1109
|
packageName,
|
|
1110
|
+
sdkName,
|
|
1097
1111
|
installCommand,
|
|
1098
1112
|
registryState,
|
|
1099
1113
|
isPublished,
|