@fairgarden/distribution 0.1.0-0.canary.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 (82) hide show
  1. package/Readme.md +97 -0
  2. package/dist/add-module.d.ts +62 -0
  3. package/dist/add-module.d.ts.map +1 -0
  4. package/dist/add-module.js +279 -0
  5. package/dist/add-module.js.map +1 -0
  6. package/dist/calver.d.ts +65 -0
  7. package/dist/calver.d.ts.map +1 -0
  8. package/dist/calver.js +115 -0
  9. package/dist/calver.js.map +1 -0
  10. package/dist/canary.d.ts +58 -0
  11. package/dist/canary.d.ts.map +1 -0
  12. package/dist/canary.js +130 -0
  13. package/dist/canary.js.map +1 -0
  14. package/dist/changelog.d.ts +111 -0
  15. package/dist/changelog.d.ts.map +1 -0
  16. package/dist/changelog.js +285 -0
  17. package/dist/changelog.js.map +1 -0
  18. package/dist/cli.d.ts +3 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +1076 -0
  21. package/dist/cli.js.map +1 -0
  22. package/dist/config-edit.d.ts +18 -0
  23. package/dist/config-edit.d.ts.map +1 -0
  24. package/dist/config-edit.js +166 -0
  25. package/dist/config-edit.js.map +1 -0
  26. package/dist/extends.d.ts +60 -0
  27. package/dist/extends.d.ts.map +1 -0
  28. package/dist/extends.js +173 -0
  29. package/dist/extends.js.map +1 -0
  30. package/dist/extract.d.ts +31 -0
  31. package/dist/extract.d.ts.map +1 -0
  32. package/dist/extract.js +177 -0
  33. package/dist/extract.js.map +1 -0
  34. package/dist/forks.d.ts +116 -0
  35. package/dist/forks.d.ts.map +1 -0
  36. package/dist/forks.js +357 -0
  37. package/dist/forks.js.map +1 -0
  38. package/dist/git-url.d.ts +19 -0
  39. package/dist/git-url.d.ts.map +1 -0
  40. package/dist/git-url.js +55 -0
  41. package/dist/git-url.js.map +1 -0
  42. package/dist/index.d.ts +21 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +11 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/inherit.d.ts +18 -0
  47. package/dist/inherit.d.ts.map +1 -0
  48. package/dist/inherit.js +84 -0
  49. package/dist/inherit.js.map +1 -0
  50. package/dist/manifest.d.ts +43 -0
  51. package/dist/manifest.d.ts.map +1 -0
  52. package/dist/manifest.js +64 -0
  53. package/dist/manifest.js.map +1 -0
  54. package/dist/overrides.d.ts +32 -0
  55. package/dist/overrides.d.ts.map +1 -0
  56. package/dist/overrides.js +81 -0
  57. package/dist/overrides.js.map +1 -0
  58. package/dist/policy.d.ts +86 -0
  59. package/dist/policy.d.ts.map +1 -0
  60. package/dist/policy.js +269 -0
  61. package/dist/policy.js.map +1 -0
  62. package/dist/readme.d.ts +60 -0
  63. package/dist/readme.d.ts.map +1 -0
  64. package/dist/readme.js +304 -0
  65. package/dist/readme.js.map +1 -0
  66. package/dist/release.d.ts +164 -0
  67. package/dist/release.d.ts.map +1 -0
  68. package/dist/release.js +454 -0
  69. package/dist/release.js.map +1 -0
  70. package/dist/scaffold.d.ts +45 -0
  71. package/dist/scaffold.d.ts.map +1 -0
  72. package/dist/scaffold.js +492 -0
  73. package/dist/scaffold.js.map +1 -0
  74. package/dist/submodules.d.ts +144 -0
  75. package/dist/submodules.d.ts.map +1 -0
  76. package/dist/submodules.js +315 -0
  77. package/dist/submodules.js.map +1 -0
  78. package/dist/workflows.d.ts +102 -0
  79. package/dist/workflows.d.ts.map +1 -0
  80. package/dist/workflows.js +710 -0
  81. package/dist/workflows.js.map +1 -0
  82. package/package.json +54 -0
@@ -0,0 +1,710 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { rm } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import semver from 'semver';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { submodules } from "./submodules.js";
8
+ import { CHANGELOG, newChangelog } from "./changelog.js";
9
+ /**
10
+ * The publishing workflow a module carries.
11
+ *
12
+ * A module releases on its own schedule, so the workflow lives in the module
13
+ * rather than the distribution. Authentication is npm trusted publishing over
14
+ * OIDC — there is no token to leak, and the package must already exist on npm
15
+ * with a trusted publisher pointing at this repository and this file.
16
+ */
17
+ export const publishWorkflow = (packageName) => ({
18
+ '.github/workflows/publish.yml': `name: Publish
19
+
20
+ # npm trusted publishing is configured against one workflow file per package,
21
+ # so canary and release both live here. There is no npm token: the package must
22
+ # already exist on npm with a trusted publisher pointing at this repository and
23
+ # this file. Bootstrap a new package by publishing it once from a workstation,
24
+ # then configure trusted publishing on npmjs.com.
25
+
26
+ on:
27
+ push:
28
+ branches:
29
+ - main
30
+ schedule:
31
+ - cron: '0 0 * * *' # Daily at midnight UTC
32
+ workflow_dispatch:
33
+ inputs:
34
+ dist-tag:
35
+ description: 'npm dist tag to publish the release to'
36
+ required: false
37
+ type: string
38
+ default: 'latest'
39
+ dry-run:
40
+ description: 'Pack and validate without publishing'
41
+ required: false
42
+ type: boolean
43
+ default: false
44
+
45
+ permissions: {}
46
+
47
+ concurrency:
48
+ group: publish-\${{ github.ref }}
49
+ cancel-in-progress: false
50
+
51
+ jobs:
52
+ canary:
53
+ name: Publish canary
54
+ # Lockfile bumps do not warrant a canary.
55
+ if: >
56
+ github.event_name == 'schedule'
57
+ || (github.event_name == 'push' && github.event.head_commit.author.name != 'renovate[bot]')
58
+ runs-on: ubuntu-latest
59
+ permissions:
60
+ contents: read
61
+ id-token: write # Required for provenance and trusted publishing
62
+ steps:
63
+ - name: Checkout
64
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
65
+ with:
66
+ persist-credentials: false
67
+
68
+ - name: Prepare for publishing
69
+ uses: ./.github/actions/publish-prepare
70
+
71
+ - name: Stamp the canary version
72
+ id: canary
73
+ # \`fg-dist canary\` works out the version and whether there is anything
74
+ # to publish: a canary records the commit it was built from, so a
75
+ # nightly run with nothing new is a no-op rather than a version bump.
76
+ run: pnpm run canary
77
+
78
+ - name: Publish to npm
79
+ if: steps.canary.outputs.skip != 'true'
80
+ # Provenance only where npm will generate it: it refuses for anything
81
+ # not published publicly, and refusing is a failed publish.
82
+ run: |
83
+ if [ "\${{ steps.canary.outputs.provenance }}" = "true" ]; then
84
+ npm publish --tag canary --provenance
85
+ else
86
+ npm publish --tag canary
87
+ fi
88
+
89
+ - name: Summary
90
+ run: |
91
+ if [ "\${{ steps.canary.outputs.skip }}" = "true" ]; then
92
+ echo "\\\`\${{ steps.canary.outputs.version }}\\\` is already the canary for this commit." >> "\$GITHUB_STEP_SUMMARY"
93
+ else
94
+ echo "Published \\\`${packageName}@\${{ steps.canary.outputs.version }}\\\` to the \\\`canary\\\` tag." >> "\$GITHUB_STEP_SUMMARY"
95
+ fi
96
+
97
+ release:
98
+ name: Publish release
99
+ if: github.event_name == 'workflow_dispatch'
100
+ runs-on: ubuntu-latest
101
+ permissions:
102
+ contents: write # Required for pushing the tag and creating the release
103
+ id-token: write # Required for provenance and trusted publishing
104
+ environment:
105
+ name: npm-publish
106
+ steps:
107
+ - name: Checkout
108
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
109
+ with:
110
+ fetch-depth: 0 # \`gh release create --generate-notes\` needs history
111
+
112
+ - name: Prepare for publishing
113
+ uses: ./.github/actions/publish-prepare
114
+
115
+ - name: Resolve version
116
+ id: version
117
+ # Refuses when the version is already on npm: main is meant to carry the
118
+ # next unreleased version, so finding it published means \`fg-dist
119
+ # release\` has not run since the last one.
120
+ run: pnpm run release:check
121
+
122
+ - name: Publish to npm
123
+ env:
124
+ DIST_TAG: \${{ inputs.dist-tag }}
125
+ # npm refuses provenance for anything not published publicly.
126
+ PROVENANCE: \${{ steps.version.outputs.provenance }}
127
+ DRY_RUN: \${{ inputs.dry-run }}
128
+ run: |
129
+ flags="--tag \$DIST_TAG"
130
+ [ "\$PROVENANCE" = "true" ] && flags="\$flags --provenance"
131
+ [ "\$DRY_RUN" = "true" ] && flags="\$flags --dry-run"
132
+ npm publish \$flags
133
+
134
+ - name: Tag and create the GitHub release
135
+ if: inputs.dry-run != true
136
+ env:
137
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
138
+ TAG: \${{ steps.version.outputs.tag }}
139
+ run: |
140
+ git tag "\$TAG"
141
+ git push origin "\$TAG"
142
+ # The notes are the changelog's section for this version, written as
143
+ # each pull request merged; GitHub's own where there is none.
144
+ if pnpm run --silent changelog notes > "\$RUNNER_TEMP/notes.md"; then
145
+ notes="--notes-file \$RUNNER_TEMP/notes.md"
146
+ else
147
+ notes="--generate-notes"
148
+ fi
149
+ gh release create "\$TAG" --title "\$TAG" \$notes --verify-tag
150
+
151
+ - name: Summary
152
+ run: |
153
+ echo "Published \\\`${packageName}@\${{ steps.version.outputs.version }}\\\` to the \\\`\${{ inputs.dist-tag }}\\\` tag." >> "\$GITHUB_STEP_SUMMARY"
154
+ `,
155
+ '.github/actions/publish-prepare/action.yml': `name: Prepare for publishing
156
+ description: Install dependencies and build, so the package is ready to publish.
157
+
158
+ # Kept as an action rather than repeated steps: npm trusted publishing pins one
159
+ # workflow file per package, so the canary and release jobs both have to share
160
+ # this.
161
+
162
+ runs:
163
+ using: composite
164
+ steps:
165
+ # Pin these to a commit sha once Renovate is watching the repository. The
166
+ # checkout in the workflow is pinned already, because it handles credentials.
167
+ #
168
+ # No \`version\` here: the \`packageManager\` field in package.json is what
169
+ # pins pnpm, and giving both makes this action refuse to choose.
170
+ - uses: pnpm/action-setup@v4
171
+
172
+ - uses: actions/setup-node@v4
173
+ with:
174
+ # Inline rather than \`node-version-file\`, so this action works in a
175
+ # repository that has no \`.nvmrc\`.
176
+ node-version: 22
177
+ # No \`cache: pnpm\`: it needs a lockfile to hash, and a module developed
178
+ # inside a distribution keeps its lockfile in the distribution.
179
+ #
180
+ # No \`registry-url\` either. Given one, setup-node writes an .npmrc
181
+ # with \`_authToken=\${NODE_AUTH_TOKEN}\` and, with no token to put
182
+ # there, exports a placeholder. npm then publishes with that instead of
183
+ # exchanging the Actions OIDC token, and the registry answers
184
+ # \`E404 Not Found - PUT\`. Trusted publishing needs npm to find no
185
+ # credentials at all; npmjs.org is the default registry anyway.
186
+
187
+ # Trusted publishing needs npm 11.5.1 or later, which Node 22 does not ship.
188
+ - name: Update npm
189
+ shell: bash
190
+ run: npm install -g npm@^11.5.1
191
+
192
+ - name: Install
193
+ shell: bash
194
+ run: |
195
+ if [ -f pnpm-lock.yaml ]; then
196
+ pnpm install --frozen-lockfile
197
+ else
198
+ # A module developed inside a distribution has no lockfile of its
199
+ # own; the distribution's workspace holds it.
200
+ pnpm install --no-frozen-lockfile
201
+ fi
202
+
203
+ - name: Build
204
+ shell: bash
205
+ # A module that is consumed as source has nothing to build; one that ships
206
+ # a \`dist\` does. Neither should have to edit this file.
207
+ run: pnpm run --if-present build
208
+ `,
209
+ });
210
+ /** Steps both of a distribution's publishing jobs start with. */
211
+ const distributionSetup = (fetchDepth) => ` - name: Checkout
212
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
213
+ with:
214
+ persist-credentials: false
215
+ # What it publishes is read from its modules: their versions, and
216
+ # the commits it pins.
217
+ submodules: true${fetchDepth ? '\n fetch-depth: 0 # `gh release create --generate-notes` needs history' : ''}
218
+
219
+ # No build: what a distribution publishes is its manifest and its
220
+ # policy, not its apps.
221
+ - uses: pnpm/action-setup@v4
222
+
223
+ # No \`registry-url\`, and npm new enough for trusted publishing: see
224
+ # .github/actions/publish-prepare in any module for why.
225
+ - uses: actions/setup-node@v4
226
+ with:
227
+ node-version: 22
228
+
229
+ - name: Update npm
230
+ run: npm install -g npm@^11.5.1
231
+
232
+ - name: Install
233
+ run: pnpm install --frozen-lockfile
234
+
235
+ # Only where fg-dist is developed in this workspace, as a module of the
236
+ # distribution: then it is linked from its checkout, which is not built.
237
+ # Everywhere else it is installed from npm, and this matches nothing.
238
+ - name: Build fg-dist
239
+ run: pnpm --filter @fairgarden/distribution run --if-present build
240
+ `;
241
+ /**
242
+ * The publishing workflow a distribution carries, for others to extend it.
243
+ *
244
+ * The same shape as a module's — a canary on every push to main, a release by
245
+ * hand — but checked out with its submodules, since `fg-dist canary` and
246
+ * `release --check` record every module's version, path, repository and
247
+ * commit in the manifest they publish. That is what an extension checks
248
+ * itself against and takes its own modules from.
249
+ */
250
+ export const distributionWorkflow = (packageName) => ({
251
+ '.github/workflows/publish.yml': `name: Publish
252
+
253
+ # Publishes this distribution for others to extend. As for a module, npm
254
+ # trusted publishing is configured against this one workflow file, and there is
255
+ # no npm token: publish once from a workstation, then configure it on npmjs.com.
256
+
257
+ on:
258
+ push:
259
+ branches:
260
+ - main
261
+ workflow_dispatch:
262
+ inputs:
263
+ dist-tag:
264
+ description: 'npm dist tag to publish the release to'
265
+ required: false
266
+ type: string
267
+ default: 'latest'
268
+ dry-run:
269
+ description: 'Pack and validate without publishing'
270
+ required: false
271
+ type: boolean
272
+ default: false
273
+
274
+ permissions: {}
275
+
276
+ concurrency:
277
+ group: publish-\${{ github.ref }}
278
+ cancel-in-progress: false
279
+
280
+ jobs:
281
+ canary:
282
+ name: Publish canary
283
+ if: github.event_name == 'push' && github.event.head_commit.author.name != 'renovate[bot]'
284
+ runs-on: ubuntu-latest
285
+ permissions:
286
+ contents: read
287
+ id-token: write # Required for provenance and trusted publishing
288
+ steps:
289
+ ${distributionSetup(false)}
290
+ - name: Stamp the canary version and what it ships
291
+ id: canary
292
+ run: pnpm run canary
293
+
294
+ - name: Publish to npm
295
+ if: steps.canary.outputs.skip != 'true'
296
+ run: |
297
+ if [ "\${{ steps.canary.outputs.provenance }}" = "true" ]; then
298
+ npm publish --tag canary --provenance
299
+ else
300
+ npm publish --tag canary
301
+ fi
302
+
303
+ release:
304
+ name: Publish release
305
+ if: github.event_name == 'workflow_dispatch'
306
+ runs-on: ubuntu-latest
307
+ permissions:
308
+ contents: write # Required for pushing the tag and creating the release
309
+ id-token: write # Required for provenance and trusted publishing
310
+ environment:
311
+ name: npm-publish
312
+ steps:
313
+ ${distributionSetup(true)}
314
+ - name: Stamp what it ships
315
+ id: version
316
+ # Refuses when this version is on npm already, or names a month that
317
+ # is over: \`fg-dist release\` moves the distribution on.
318
+ run: pnpm run release:check
319
+
320
+ - name: Publish to npm
321
+ env:
322
+ DIST_TAG: \${{ inputs.dist-tag }}
323
+ PROVENANCE: \${{ steps.version.outputs.provenance }}
324
+ DRY_RUN: \${{ inputs.dry-run }}
325
+ run: |
326
+ flags="--tag \$DIST_TAG"
327
+ [ "\$PROVENANCE" = "true" ] && flags="\$flags --provenance"
328
+ [ "\$DRY_RUN" = "true" ] && flags="\$flags --dry-run"
329
+ npm publish \$flags
330
+
331
+ - name: Tag and create the GitHub release
332
+ if: inputs.dry-run != true
333
+ env:
334
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
335
+ TAG: \${{ steps.version.outputs.tag }}
336
+ run: |
337
+ git tag "\$TAG"
338
+ git push origin "\$TAG"
339
+ # The changelog's section for this version: the modules it moved,
340
+ # and what changed in the organization's policy.
341
+ if pnpm run --silent changelog notes > "\$RUNNER_TEMP/notes.md"; then
342
+ notes="--notes-file \$RUNNER_TEMP/notes.md"
343
+ else
344
+ notes="--generate-notes"
345
+ fi
346
+ gh release create "\$TAG" --title "\$TAG" \$notes --verify-tag
347
+
348
+ - name: Summary
349
+ run: |
350
+ echo "Published \\\`${packageName}@\${{ steps.version.outputs.version }}\\\`." >> "\$GITHUB_STEP_SUMMARY"
351
+ `,
352
+ ...changelogWorkflow({ submodules: true }),
353
+ });
354
+ /**
355
+ * The pull request check that keeps a changelog current.
356
+ *
357
+ * Every pull request has to add a line linking itself under the version being
358
+ * worked on — or, in a distribution, every one that changes the
359
+ * organization's policy — so a release's notes are written, and committed,
360
+ * before it is released. A distribution is checked out with its submodules,
361
+ * without which its workspace does not install.
362
+ */
363
+ export const changelogWorkflow = ({ submodules }) => ({
364
+ '.github/workflows/changelog.yml': `name: Changelog
365
+
366
+ # Every pull request says what it changes in CHANGELOG.md, under the version
367
+ # being worked on, with a link to itself — so that version's notes are written,
368
+ # and committed, before it is released: whoever makes the change writes the
369
+ # line, and review sees it with the change. Make this job a required status
370
+ # check on main, or it only advises.
371
+ #
372
+ # A pull request that changes nothing anyone would read about — a lockfile
373
+ # bump — is labelled "skip changelog".
374
+
375
+ on:
376
+ pull_request:
377
+ types: [opened, synchronize, reopened, labeled, unlabeled]
378
+
379
+ permissions:
380
+ contents: read
381
+
382
+ jobs:
383
+ changelog:
384
+ name: Changelog entry
385
+ runs-on: ubuntu-latest
386
+ steps:
387
+ - name: Checkout
388
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
389
+ with:
390
+ persist-credentials: false
391
+ fetch-depth: 0 # to compare with what it merges into${submodules ? '\n submodules: true # a distribution does not install without them' : ''}
392
+
393
+ - uses: pnpm/action-setup@v4
394
+
395
+ - uses: actions/setup-node@v4
396
+ with:
397
+ node-version: 22
398
+
399
+ - name: Install
400
+ run: |
401
+ if [ -f pnpm-lock.yaml ]; then
402
+ pnpm install --frozen-lockfile
403
+ else
404
+ pnpm install --no-frozen-lockfile
405
+ fi
406
+
407
+ # Only where fg-dist is developed in this workspace; see publish.yml.
408
+ - name: Build fg-dist
409
+ run: pnpm --filter @fairgarden/distribution run --if-present build
410
+
411
+ - name: Check for a line linking this pull request
412
+ run: pnpm run changelog check
413
+ `,
414
+ });
415
+ const readManifest = async (root) => {
416
+ try {
417
+ return JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
418
+ }
419
+ catch {
420
+ return undefined;
421
+ }
422
+ };
423
+ /** The package whose CLI a module releases with. */
424
+ const TOOL = '@fairgarden/distribution';
425
+ /**
426
+ * This tool's own version, which is what a module is given to release with.
427
+ *
428
+ * Read rather than written down, so a module is pinned to the CLI that set it
429
+ * up instead of to whatever was current when this file was last edited.
430
+ */
431
+ const toolVersion = async () => {
432
+ const here = path.dirname(fileURLToPath(import.meta.url));
433
+ const own = await readManifest(path.join(here, '..'));
434
+ return typeof own?.version === 'string' ? own.version : undefined;
435
+ };
436
+ /**
437
+ * The scripts the workflow calls, and a module releases by hand with.
438
+ *
439
+ * They are named rather than spelled out in the workflow so that every
440
+ * module's workflow is the same file, and so `pnpm release` is the answer to
441
+ * "how do I release this" wherever you are standing.
442
+ */
443
+ export const releaseScripts = (packageName) => {
444
+ // The tool cannot depend on itself being published to release itself.
445
+ const cli = packageName === TOOL ? 'node dist/cli.js' : 'fg-dist';
446
+ return {
447
+ canary: `${cli} canary`,
448
+ release: `${cli} release`,
449
+ 'release:check': `${cli} release --check`,
450
+ changelog: `${cli} changelog`,
451
+ };
452
+ };
453
+ /**
454
+ * Give a module what it needs to publish itself.
455
+ *
456
+ * One pass over the manifest rather than several: a structural rewrite undoes
457
+ * a careful textual splice, so doing both would mean the second silently
458
+ * discarding the first's formatting. Two-space JSON is what npm and pnpm write
459
+ * anyway.
460
+ */
461
+ export const updateManifest = async (moduleRoot, packageName, version, packageManager, { write = true } = {}) => {
462
+ const manifest = await readManifest(moduleRoot);
463
+ if (!manifest) {
464
+ return { changed: false, unprivated: false, missingFiles: false, missingAccess: false };
465
+ }
466
+ const before = JSON.stringify(manifest);
467
+ const updated = { ...manifest };
468
+ // A private package cannot be published, which is the one thing the workflow
469
+ // being added here is for.
470
+ const unprivated = updated.private === true;
471
+ if (unprivated)
472
+ delete updated.private;
473
+ // `pnpm/action-setup` takes its version from this field and will not run
474
+ // without one. A module that pins its own is left alone.
475
+ if (!updated.packageManager && packageManager) {
476
+ updated.packageManager = packageManager;
477
+ }
478
+ const scripts = { ...updated.scripts };
479
+ for (const [name, command] of Object.entries(releaseScripts(packageName))) {
480
+ // A script the module already defines may call the tool differently on
481
+ // purpose.
482
+ if (!scripts[name])
483
+ scripts[name] = command;
484
+ }
485
+ updated.scripts = scripts;
486
+ const dependencies = updated.dependencies;
487
+ const devDependencies = updated.devDependencies;
488
+ // Whichever list already has it wins; the tool is only needed to release, so
489
+ // a module that does not already depend on it gets it as a devDependency.
490
+ if (packageName !== TOOL && !dependencies?.[TOOL] && !devDependencies?.[TOOL] && version) {
491
+ updated.devDependencies = { ...devDependencies, [TOOL]: `^${version}` };
492
+ }
493
+ // One that does is moved up to this version at least: the workflows written
494
+ // here call commands it has, and an older one — resolved from npm in the
495
+ // module's own CI — would not know them.
496
+ for (const key of ['dependencies', 'devDependencies']) {
497
+ const list = updated[key];
498
+ const range = list?.[TOOL];
499
+ const floor = range ? semver.minVersion(range, { loose: true }) : null;
500
+ if (packageName !== TOOL && version && floor && semver.lt(floor, version)) {
501
+ updated[key] = { ...list, [TOOL]: `^${version}` };
502
+ }
503
+ }
504
+ const changed = JSON.stringify(updated) !== before;
505
+ if (changed && write) {
506
+ await writeFile(path.join(moduleRoot, 'package.json'), `${JSON.stringify(updated, null, 2)}\n`);
507
+ }
508
+ // Guessed at, this would be wrong: a module consumed as source ships `app`
509
+ // and `lib`, one that builds ships `dist`, and npm's default ships both plus
510
+ // whatever else is lying around. Report it instead.
511
+ // Both of these are reported rather than guessed. `files` because a module
512
+ // consumed as source ships `app` and `lib` while one that builds ships
513
+ // `dist`. `publishConfig.access` because npm takes an explicit value as an
514
+ // instruction to *change* an existing package's visibility — writing
515
+ // `restricted` into an already-public package would make it private on the
516
+ // next publish, or fail on an org with no paid plan.
517
+ return {
518
+ changed,
519
+ unprivated,
520
+ missingFiles: !updated.files,
521
+ missingAccess: packageName.startsWith('@') &&
522
+ updated.publishConfig?.access === undefined,
523
+ };
524
+ };
525
+ /**
526
+ * Give every module a publishing workflow.
527
+ *
528
+ * A module that already has one is left alone: it may have been changed on
529
+ * purpose, and overwriting it would be the kind of help nobody asked for.
530
+ */
531
+ export const writeWorkflows = async (root, { force = false, dryRun = false, names = [], } = {}) => {
532
+ const updates = [];
533
+ // What the distribution builds with, which is what its modules are developed
534
+ // against whether or not they say so themselves.
535
+ const packageManager = (await readManifest(root))?.packageManager;
536
+ const spec = typeof packageManager === 'string' ? packageManager : undefined;
537
+ const tool = await toolVersion();
538
+ const unknown = names.filter((name) => !submodules(root).some((submodule) => submodule.name === name || submodule.relativePath === name));
539
+ if (unknown.length > 0)
540
+ throw new Error(`No such module: ${unknown.join(', ')}.`);
541
+ for (const submodule of submodules(root)) {
542
+ if (names.length > 0 && !names.includes(submodule.name) && !names.includes(submodule.relativePath))
543
+ continue;
544
+ const name = (await readManifest(submodule.path))?.name;
545
+ if (typeof name !== 'string')
546
+ continue;
547
+ // File by file: a module that publishes already still gets the changelog
548
+ // check it lacks, and what it has is only replaced when forced.
549
+ const written = [];
550
+ const files = { ...publishWorkflow(name), ...changelogWorkflow({ submodules: false }) };
551
+ for (const [relative, contents] of Object.entries(files)) {
552
+ const full = path.join(submodule.path, relative);
553
+ if (existsSync(full) && !force)
554
+ continue;
555
+ if (!dryRun) {
556
+ await mkdir(path.dirname(full), { recursive: true });
557
+ await writeFile(full, contents);
558
+ }
559
+ written.push(relative);
560
+ }
561
+ if (await startChangelog(submodule.path, dryRun))
562
+ written.push(CHANGELOG);
563
+ // An earlier version of this command shipped the canary stamper as a file
564
+ // in the module. It is a CLI command now, and a stale copy of it would go
565
+ // on being the thing anyone reads.
566
+ const stale = path.join(submodule.path, 'scripts/stampCanaryVersion.mjs');
567
+ if (existsSync(stale)) {
568
+ if (!dryRun) {
569
+ await rm(stale);
570
+ // And the directory, if that was the only thing in it. A module with
571
+ // scripts of its own keeps them.
572
+ await rm(path.dirname(stale), { recursive: false }).catch(() => { });
573
+ }
574
+ written.push('scripts/stampCanaryVersion.mjs (removed)');
575
+ }
576
+ const manifest = await updateManifest(submodule.path, name, tool, spec, {
577
+ write: !dryRun,
578
+ });
579
+ if (manifest.changed)
580
+ written.push('package.json');
581
+ updates.push({
582
+ relativePath: submodule.relativePath,
583
+ files: written.sort(),
584
+ skipped: written.length === 0,
585
+ unprivated: manifest.unprivated,
586
+ missingFiles: manifest.missingFiles,
587
+ missingAccess: manifest.missingAccess,
588
+ });
589
+ }
590
+ return updates;
591
+ };
592
+ /**
593
+ * Give the distribution itself a publishing workflow, so others can extend it.
594
+ *
595
+ * Asked for rather than done by `writeWorkflows`: it makes the distribution
596
+ * public on npm, which a distribution only its owner deploys has no need to be.
597
+ * The tarball is the manifest and the organization's policy, which is what an
598
+ * extension builds its own on.
599
+ */
600
+ export const writeDistributionWorkflow = async (root, { force = false, dryRun = false } = {}) => {
601
+ const manifest = await readManifest(root);
602
+ const name = manifest?.name;
603
+ if (typeof name !== 'string')
604
+ throw new Error('The distribution needs a name in package.json to publish.');
605
+ const relativePath = '.';
606
+ const workflow = path.join(root, '.github/workflows/publish.yml');
607
+ if (existsSync(workflow) && !force) {
608
+ return { relativePath, files: [], skipped: true, unprivated: false, missingFiles: false, missingAccess: false };
609
+ }
610
+ const written = [];
611
+ for (const [relative, contents] of Object.entries(distributionWorkflow(name))) {
612
+ if (!dryRun) {
613
+ await mkdir(path.dirname(path.join(root, relative)), { recursive: true });
614
+ await writeFile(path.join(root, relative), contents);
615
+ }
616
+ written.push(relative);
617
+ }
618
+ if (await startChangelog(root, dryRun))
619
+ written.push(CHANGELOG);
620
+ const packageManager = typeof manifest?.packageManager === 'string' ? manifest.packageManager : undefined;
621
+ const change = await updateManifest(root, name, await toolVersion(), packageManager, { write: !dryRun });
622
+ if (change.changed)
623
+ written.push('package.json');
624
+ // Its policy is what an extension layers its own on; nothing else is needed.
625
+ let missingFiles = change.missingFiles;
626
+ if (missingFiles) {
627
+ const updated = (await readManifest(root)) ?? {};
628
+ const files = existsSync(path.join(root, 'policies')) ? ['policies'] : [];
629
+ if (!dryRun)
630
+ await writeFile(path.join(root, 'package.json'), `${JSON.stringify({ ...updated, files }, null, 2)}\n`);
631
+ if (!written.includes('package.json'))
632
+ written.push('package.json');
633
+ missingFiles = false;
634
+ }
635
+ return {
636
+ relativePath,
637
+ files: written.sort(),
638
+ skipped: false,
639
+ unprivated: change.unprivated,
640
+ missingFiles,
641
+ missingAccess: change.missingAccess,
642
+ };
643
+ };
644
+ /**
645
+ * Start a repository's changelog at the version it carries, unless it keeps one
646
+ * already: a changelog is history, and is never written over.
647
+ */
648
+ const startChangelog = async (root, dryRun) => {
649
+ const file = path.join(root, CHANGELOG);
650
+ if (existsSync(file))
651
+ return false;
652
+ const version = (await readManifest(root))?.version;
653
+ if (typeof version !== 'string')
654
+ return false;
655
+ if (!dryRun)
656
+ await writeFile(file, newChangelog(version));
657
+ return true;
658
+ };
659
+ /**
660
+ * Give the distribution itself the changelog check, for pull requests that
661
+ * change the organization's policy — what else changes in it, fg-dist notes
662
+ * itself. Unlike its publishing, this makes nothing public.
663
+ */
664
+ export const writeDistributionChangelogCheck = async (root, { force = false, dryRun = false } = {}) => {
665
+ const written = [];
666
+ for (const [relative, contents] of Object.entries(changelogWorkflow({ submodules: true }))) {
667
+ const full = path.join(root, relative);
668
+ if (existsSync(full) && !force)
669
+ continue;
670
+ if (!dryRun) {
671
+ await mkdir(path.dirname(full), { recursive: true });
672
+ await writeFile(full, contents);
673
+ }
674
+ written.push(relative);
675
+ }
676
+ if (await startChangelog(root, dryRun))
677
+ written.push(CHANGELOG);
678
+ // The script the check runs, and the tool it runs — nothing that publishes.
679
+ const manifest = await readManifest(root);
680
+ if (manifest) {
681
+ const scripts = { ...manifest.scripts };
682
+ const dev = { ...manifest.devDependencies };
683
+ const deps = manifest.dependencies;
684
+ const tool = await toolVersion();
685
+ let changed = false;
686
+ if (!scripts.changelog) {
687
+ scripts.changelog = releaseScripts(String(manifest.name ?? '')).changelog;
688
+ changed = true;
689
+ }
690
+ if (!dev[TOOL] && !deps?.[TOOL] && tool) {
691
+ dev[TOOL] = `^${tool}`;
692
+ changed = true;
693
+ }
694
+ if (changed) {
695
+ if (!dryRun) {
696
+ await writeFile(path.join(root, 'package.json'), `${JSON.stringify({ ...manifest, scripts, devDependencies: dev }, null, 2)}\n`);
697
+ }
698
+ written.push('package.json');
699
+ }
700
+ }
701
+ return {
702
+ relativePath: '.',
703
+ files: written.sort(),
704
+ skipped: written.length === 0,
705
+ unprivated: false,
706
+ missingFiles: false,
707
+ missingAccess: false,
708
+ };
709
+ };
710
+ //# sourceMappingURL=workflows.js.map