@voxgig/sdkgen 4.12.0 → 4.13.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.
@@ -0,0 +1,324 @@
1
+ /* Copyright (c) 2024-2026 Voxgig Ltd, MIT License */
2
+
3
+ import { cmp, each, Content, File, Folder } from 'jostraca'
4
+
5
+ import {
6
+ KIT,
7
+ getModelPath
8
+ } from '../types'
9
+
10
+ import { packageName, packageVersion, repoInfo } from '../helpers/packageMeta'
11
+
12
+
13
+ // PUBLISHING AN npm TARGET FROM GITHUB ACTIONS, WITH NO TOKEN.
14
+ //
15
+ // A generated SDK is published from CI, and the credential is the whole
16
+ // problem: an NPM_TOKEN in repository secrets is long-lived, copyable, and
17
+ // readable by every workflow in the repo. GitHub OIDC Trusted Publishing
18
+ // replaces it — npm exchanges a short-lived OIDC token minted for one job in
19
+ // one repository for a publish credential, and attaches SLSA provenance on
20
+ // the way through.
21
+ //
22
+ // DISPATCH, NOT A TAG PUSH. The deploy Makefile in this same repo publishes
23
+ // with a vault-injected token, and a tag-triggered workflow racing it would
24
+ // publish the same version twice from two mechanisms. A dispatch is a
25
+ // deliberate act by someone who can see the result, which is what a release
26
+ // should be.
27
+ //
28
+ // THE WORKFLOW FILE NAME IS PART OF THE TRUST CONFIGURATION. npm binds a
29
+ // trusted publisher to the repository AND the workflow filename, so renaming
30
+ // this file silently breaks publishing until the npm side is updated to
31
+ // match. That is also why one file per target: `publish-ts.yml` can be
32
+ // trusted for the ts package without granting anything to another target's.
33
+ //
34
+ // SET-UP IS A MAINTAINER TASK, so what a human must do once is written to
35
+ // `.sdk/PUBLISHING.md` — beside the generator, not into the target's own
36
+ // docs, which describe the SDK to the people who INSTALL it and have no
37
+ // business being told how this repo releases.
38
+ const PublishWorkflow = cmp(function PublishWorkflow(props: any) {
39
+ const { ctx$ } = props
40
+ const { model } = ctx$
41
+
42
+ const targetMap = getModelPath(model, `main.${KIT}.target`) || {}
43
+
44
+ // npm ONLY. The mechanics here are npm's — `npm publish`, `npm trust`,
45
+ // npm's OIDC exchange — and a PyPI or RubyGems target needs a different
46
+ // workflow rather than this one with a word changed. Emitting nothing for
47
+ // them is the honest answer until those exist.
48
+ const npmTargets: any[] = []
49
+ each(targetMap, (t: any) => {
50
+ if (false === t.active) {
51
+ return
52
+ }
53
+ if ('npm' === (t.publish?.registry?.name || '')) {
54
+ npmTargets.push(t)
55
+ }
56
+ })
57
+
58
+ if (0 === npmTargets.length) {
59
+ return
60
+ }
61
+
62
+ const { repoUrl } = repoInfo(model)
63
+
64
+ Folder({ name: '.github' }, () => {
65
+ Folder({ name: 'workflows' }, () => {
66
+ for (const target of npmTargets) {
67
+ File({ name: `publish-${target.name}.yml` }, () => {
68
+ Content(publishWorkflow(model, target))
69
+ })
70
+ }
71
+ })
72
+ })
73
+
74
+ Folder({ name: '.sdk' }, () => {
75
+ File({ name: 'PUBLISHING.md' }, () => {
76
+ Content(publishingDoc(model, npmTargets, repoUrl))
77
+ })
78
+ })
79
+
80
+ ctx$.log.info({
81
+ point: 'generate-publish-workflow',
82
+ note: 'npm targets: ' + npmTargets.map((t: any) => t.name).join(','),
83
+ })
84
+ })
85
+
86
+
87
+ function publishWorkflow(model: any, target: any): string {
88
+ const name = target.name
89
+ const pkg = packageName(model, 'npm')
90
+
91
+ return `# Generated by @voxgig/sdkgen. Do not edit.
92
+ #
93
+ # Publishes ${pkg} (the \`${name}/\` target) to npm, via GitHub OIDC Trusted
94
+ # Publishing — no NPM_TOKEN. npm exchanges a short-lived OIDC token minted
95
+ # for THIS job in THIS repository for a publish credential, and attaches SLSA
96
+ # provenance automatically.
97
+ #
98
+ # SET UP ONCE, by a maintainer with publish rights — see .sdk/PUBLISHING.md:
99
+ #
100
+ # npm trust github ${pkg} \\
101
+ # --repository <owner>/<repo> \\
102
+ # --file publish-${name}.yml \\
103
+ # --allow-publish
104
+ #
105
+ # THE FILENAME IS PART OF THAT CONFIGURATION. Renaming this file breaks
106
+ # publishing until the npm side is updated to match.
107
+ #
108
+ # BY DISPATCH, deliberately. The deploy Makefile publishes with a
109
+ # vault-injected token; a tag-triggered workflow racing it would publish one
110
+ # version by two mechanisms. Releasing is an act someone performs and
111
+ # watches.
112
+ #
113
+ # TWO JOBS, BECAUSE THEY NEED DIFFERENT PRIVILEGES. A dependency lifecycle
114
+ # script can ask the runner for any OIDC token its job is permitted to mint,
115
+ # so a job that both installs dependencies and holds \`id-token: write\` can be
116
+ # made to publish before its own gates finish.
117
+ #
118
+ # verify contents: read, nothing else. Installs, builds and tests.
119
+ # publish id-token: write. Installs no dependencies and runs no project
120
+ # code; it packs what is already in the tree.
121
+
122
+ name: publish-${name}
123
+
124
+ on:
125
+ workflow_dispatch:
126
+ inputs:
127
+ expect_sha:
128
+ description: 'Optional: refuse unless main is still at this commit'
129
+ type: string
130
+ default: ''
131
+
132
+ jobs:
133
+ verify:
134
+ name: verify
135
+ runs-on: ubuntu-latest
136
+ timeout-minutes: 20
137
+
138
+ # Deliberately the default-minimum. This job runs third-party code.
139
+ permissions:
140
+ contents: read
141
+
142
+ outputs:
143
+ version: \${{ steps.version.outputs.version }}
144
+
145
+ steps:
146
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
147
+
148
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
149
+ with:
150
+ node-version: 24.x
151
+
152
+ # RELEASES COME FROM main, INCLUDING FROM THE BUTTON. workflow_dispatch
153
+ # accepts any ref, so without this the Actions UI can release a branch.
154
+ - name: This ref is main, and is where the caller thinks it is
155
+ env:
156
+ EXPECT: \${{ inputs.expect_sha }}
157
+ run: |
158
+ set -euo pipefail
159
+ if [ "\${GITHUB_REF_NAME}" != "main" ]; then
160
+ echo "::error::releases come from main, not \${GITHUB_REF_NAME}"
161
+ exit 1
162
+ fi
163
+ if [ -n "\$EXPECT" ] && [ "\$EXPECT" != "\$GITHUB_SHA" ]; then
164
+ echo "::error::main is at \$GITHUB_SHA, not \$EXPECT"
165
+ exit 1
166
+ fi
167
+
168
+ - name: Resolve the version
169
+ id: version
170
+ working-directory: ${name}
171
+ run: |
172
+ set -euo pipefail
173
+ VERSION="\$(node -p "require('./package.json').version")"
174
+ test -n "\$VERSION" || { echo "::error::no version in ${name}/package.json"; exit 1; }
175
+ echo "version=\$VERSION" >> "\$GITHUB_OUTPUT"
176
+ echo "${pkg} \$VERSION"
177
+
178
+ - run: npm install
179
+ working-directory: ${name}
180
+
181
+ - run: npm run build
182
+ working-directory: ${name}
183
+
184
+ - run: npm test
185
+ working-directory: ${name}
186
+
187
+ publish:
188
+ name: npm publish
189
+ needs: verify
190
+ runs-on: ubuntu-latest
191
+ timeout-minutes: 15
192
+
193
+ # The ONLY job holding the publish credential — and it installs no
194
+ # dependencies and runs no project code. See the header.
195
+ permissions:
196
+ id-token: write
197
+ contents: read
198
+
199
+ steps:
200
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
201
+
202
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
203
+ with:
204
+ node-version: 24.x
205
+ registry-url: 'https://registry.npmjs.org'
206
+
207
+ # Trusted publishing requires npm >= 11.5.1. This is npm itself, not a
208
+ # project dependency: no package.json here is consulted.
209
+ - name: Use a trusted-publishing capable npm
210
+ run: npm install -g npm@latest
211
+
212
+ # THE REGISTRY IS THE SOURCE OF TRUTH FOR "IS THIS RELEASED", not the
213
+ # dispatch. A re-run would otherwise fail on a version conflict and
214
+ # report a red release that in fact succeeded.
215
+ - name: Is this version already on npm?
216
+ id: registry
217
+ env:
218
+ VERSION: \${{ needs.verify.outputs.version }}
219
+ run: |
220
+ set -euo pipefail
221
+ if npm view "${pkg}@\$VERSION" version >/dev/null 2>&1; then
222
+ echo "published=true" >> "\$GITHUB_OUTPUT"
223
+ echo "\$VERSION is already on npm — skipping publish"
224
+ else
225
+ echo "published=false" >> "\$GITHUB_OUTPUT"
226
+ fi
227
+
228
+ - name: Publish to npm
229
+ if: steps.registry.outputs.published == 'false'
230
+ working-directory: ${name}
231
+ run: npm publish --access public
232
+ `
233
+ }
234
+
235
+
236
+ function publishingDoc(model: any, targets: any[], repoUrl: string): string {
237
+ const pkg = packageName(model, 'npm')
238
+ const repo = String(repoUrl || '')
239
+ .replace(/^git\+/, '')
240
+ .replace(/^(https?:\/\/)?(www\.)?github\.com[/:]/, '')
241
+ .replace(/\.git$/, '')
242
+ .replace(/\/+$/, '')
243
+ const owner = '' === repo ? '<owner>/<repo>' : repo
244
+
245
+ const rows = targets.map((t: any) =>
246
+ `| \`${t.name}/\` | ${packageName(model, 'npm')} | ` +
247
+ `\`.github/workflows/publish-${t.name}.yml\` |`).join('\n')
248
+
249
+ return `# Publishing
250
+
251
+ GENERATED by @voxgig/sdkgen — regenerated on every \`npm run generate\`.
252
+
253
+ This file is for MAINTAINERS of this repository. It is in \`.sdk/\` on
254
+ purpose: the target directories document the SDK to the people who install
255
+ it, and how this repo releases is none of their business.
256
+
257
+ | target | package | workflow |
258
+ |---|---|---|
259
+ ${rows}
260
+
261
+ ## How a release happens
262
+
263
+ Publishing runs from GitHub Actions with **no NPM_TOKEN**. npm exchanges a
264
+ short-lived OIDC token — minted for one job in this repository — for a
265
+ publish credential, and attaches SLSA provenance as it goes.
266
+
267
+ 1. Bump the version in the model, then regenerate:
268
+
269
+ main: kit: target: ts: publish: version: '<x.y.z>'
270
+
271
+ 2. Commit and push to \`main\`, and let CI go green.
272
+ 3. Run the workflow from the Actions tab, or:
273
+
274
+ gh workflow run publish-ts.yml --ref main -f expect_sha=$(git rev-parse HEAD)
275
+
276
+ \`expect_sha\` is optional and worth using: it refuses if \`main\` moved between
277
+ the commit you checked and the run resolving.
278
+
279
+ It is a DISPATCH rather than a tag push because the root \`Makefile\` also
280
+ publishes, with a vault-injected token. A tag-triggered workflow racing it
281
+ would publish one version by two mechanisms.
282
+
283
+ ## One-time set-up
284
+
285
+ npm has to be told which repository and which workflow file may publish this
286
+ package. From a machine logged in to npm with publish rights (2FA is
287
+ required):
288
+
289
+ npm trust github ${pkg} \\
290
+ --repository ${owner} \\
291
+ --file publish-ts.yml \\
292
+ --allow-publish
293
+
294
+ Then \`npm trust list ${pkg}\` shows it, and
295
+ \`npm trust revoke ${pkg} --id=<id>\` removes it.
296
+
297
+ **The workflow filename is part of the configuration.** Renaming
298
+ \`publish-ts.yml\` breaks publishing until the npm side is updated to match.
299
+
300
+ **A brand-new package cannot be set up this way.** npm only offers the
301
+ trusted-publisher settings once a version exists, so the FIRST release of a
302
+ package goes out by hand from an authenticated machine:
303
+
304
+ cd ts && npm publish --access public
305
+
306
+ After that, register the publisher and every later release is a dispatch.
307
+
308
+ ## Why the workflow has two jobs
309
+
310
+ A dependency lifecycle script can ask the runner for any OIDC token the job
311
+ it runs in is permitted to mint. A job that both installs dependencies and
312
+ holds \`id-token: write\` can therefore be made to publish as this package
313
+ before its own gates finish.
314
+
315
+ So \`verify\` installs, builds and tests under \`contents: read\` and holds no
316
+ credential, and \`publish\` holds \`id-token: write\` while installing nothing
317
+ and running no project code.
318
+ `
319
+ }
320
+
321
+
322
+ export {
323
+ PublishWorkflow
324
+ }
package/src/sdkgen.ts CHANGED
@@ -38,6 +38,7 @@ import { AgentGuide } from './cmp/AgentGuide'
38
38
  import { AgentGuideFeature } from './cmp/AgentGuideFeature'
39
39
  import { License } from './cmp/License'
40
40
  import { Security } from './cmp/Security'
41
+ import { PublishWorkflow } from './cmp/PublishWorkflow'
41
42
  import { Changelog } from './cmp/Changelog'
42
43
  import { Test } from './cmp/Test'
43
44
  import { TestControl, TEST_CONTROL_EXCLUDE } from './cmp/TestControl'
@@ -1083,6 +1084,7 @@ export {
1083
1084
  Deploy,
1084
1085
  License,
1085
1086
  Security,
1087
+ PublishWorkflow,
1086
1088
  Changelog,
1087
1089
  Entity,
1088
1090
  Feature,