@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.
Files changed (43) hide show
  1. package/bin/voxgig-sdkgen +1 -1
  2. package/dist/admin/npm-trust.d.ts +39 -0
  3. package/dist/admin/npm-trust.js +308 -0
  4. package/dist/admin/npm-trust.js.map +1 -0
  5. package/dist/cmp/PublishWorkflow.js +47 -26
  6. package/dist/cmp/PublishWorkflow.js.map +1 -1
  7. package/dist/helpers/packageMeta.d.ts +2 -1
  8. package/dist/helpers/packageMeta.js +7 -3
  9. package/dist/helpers/packageMeta.js.map +1 -1
  10. package/dist/sdkgen.d.ts +3 -2
  11. package/dist/sdkgen.js +15 -4
  12. package/dist/sdkgen.js.map +1 -1
  13. package/dist/testkit.js +16 -6
  14. package/dist/testkit.js.map +1 -1
  15. package/dist/tsconfig.tsbuildinfo +1 -1
  16. package/dist/types.d.ts +1 -0
  17. package/model/sdkgen.aontu +9 -0
  18. package/package.json +3 -2
  19. package/project/.sdk/model/target/rb.aontu +1 -2
  20. package/project/.sdk/src/cmp/c/utility_c.ts +3 -1
  21. package/project/.sdk/src/cmp/cpp/ReadmeInstall_cpp.ts +3 -3
  22. package/project/.sdk/src/cmp/elixir/ReadmeInstall_elixir.ts +2 -2
  23. package/project/.sdk/src/cmp/java/Package_java.ts +2 -1
  24. package/project/.sdk/src/cmp/java/ReadmeInstall_java.ts +2 -2
  25. package/project/.sdk/src/cmp/js/Package_js.ts +2 -2
  26. package/project/.sdk/src/cmp/kotlin/Package_kotlin.ts +2 -1
  27. package/project/.sdk/src/cmp/kotlin/ReadmeInstall_kotlin.ts +2 -2
  28. package/project/.sdk/src/cmp/lua/Package_lua.ts +3 -5
  29. package/project/.sdk/src/cmp/ocaml/utility_ocaml.ts +3 -1
  30. package/project/.sdk/src/cmp/php/Package_php.ts +0 -2
  31. package/project/.sdk/src/cmp/py/Package_py.ts +0 -2
  32. package/project/.sdk/src/cmp/py-data/Main_py-data.ts +1 -3
  33. package/project/.sdk/src/cmp/rb/Package_rb.ts +15 -23
  34. package/project/.sdk/src/cmp/rust/utility_rust.ts +3 -1
  35. package/project/.sdk/src/cmp/scala/ReadmeInstall_scala.ts +2 -2
  36. package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
  37. package/project/sdkgen-package.json +1 -1
  38. package/src/admin/npm-trust.ts +408 -0
  39. package/src/cmp/PublishWorkflow.ts +56 -26
  40. package/src/helpers/packageMeta.ts +9 -3
  41. package/src/sdkgen.ts +14 -0
  42. package/src/testkit.ts +50 -35
  43. 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
- const { repoUrl } = repoInfo(model)
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: `publish-${target.name}.yml` }, () => {
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, repoUrl))
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(model: any, target: any, npmTargets: any[]): string {
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 publish-${name}.yml \\
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[], repoUrl: string): string {
308
+ function publishingDoc(model: any, targets: any[], repository: string | null): string {
287
309
  const pkg = packageName(model, targets[0].name)
288
- const repo = String(repoUrl || '')
289
- .replace(/^git\+/, '')
290
- .replace(/^(https?:\/\/)?(www\.)?github\.com[/:]/, '')
291
- .replace(/\.git$/, '')
292
- .replace(/\/+$/, '')
293
- const owner = '' === repo ? '<owner>/<repo>' : repo
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/publish-${t.name}.yml\` |`).join('\n')
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 publish-${targets[0].name}.yml --ref main -f expect_sha=$(git rev-parse HEAD)
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 this
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 publish-${targets[0].name}.yml \\
402
+ --repository ${repository ?? '<owner>/<repo>'} \\
403
+ --file ${first} \\
373
404
  --allow-publish
374
-
375
- Then \`npm trust list ${pkg}\` shows it, and
376
- \`npm trust revoke ${pkg} --id=<id>\` removes it.
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
- \`publish-${targets[0].name}.yml\` breaks publishing until the npm side is
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}-sdk` : String(declared.path)
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 = origin.endsWith('-sdk') ? slug : `${slug}-sdk`
194
- const npmScoped = `@${origin}/${slug}${origin.endsWith('-sdk') ? '' : '-sdk'}`
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,