@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.
- package/Readme.md +97 -0
- package/dist/add-module.d.ts +62 -0
- package/dist/add-module.d.ts.map +1 -0
- package/dist/add-module.js +279 -0
- package/dist/add-module.js.map +1 -0
- package/dist/calver.d.ts +65 -0
- package/dist/calver.d.ts.map +1 -0
- package/dist/calver.js +115 -0
- package/dist/calver.js.map +1 -0
- package/dist/canary.d.ts +58 -0
- package/dist/canary.d.ts.map +1 -0
- package/dist/canary.js +130 -0
- package/dist/canary.js.map +1 -0
- package/dist/changelog.d.ts +111 -0
- package/dist/changelog.d.ts.map +1 -0
- package/dist/changelog.js +285 -0
- package/dist/changelog.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1076 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-edit.d.ts +18 -0
- package/dist/config-edit.d.ts.map +1 -0
- package/dist/config-edit.js +166 -0
- package/dist/config-edit.js.map +1 -0
- package/dist/extends.d.ts +60 -0
- package/dist/extends.d.ts.map +1 -0
- package/dist/extends.js +173 -0
- package/dist/extends.js.map +1 -0
- package/dist/extract.d.ts +31 -0
- package/dist/extract.d.ts.map +1 -0
- package/dist/extract.js +177 -0
- package/dist/extract.js.map +1 -0
- package/dist/forks.d.ts +116 -0
- package/dist/forks.d.ts.map +1 -0
- package/dist/forks.js +357 -0
- package/dist/forks.js.map +1 -0
- package/dist/git-url.d.ts +19 -0
- package/dist/git-url.d.ts.map +1 -0
- package/dist/git-url.js +55 -0
- package/dist/git-url.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/inherit.d.ts +18 -0
- package/dist/inherit.d.ts.map +1 -0
- package/dist/inherit.js +84 -0
- package/dist/inherit.js.map +1 -0
- package/dist/manifest.d.ts +43 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +64 -0
- package/dist/manifest.js.map +1 -0
- package/dist/overrides.d.ts +32 -0
- package/dist/overrides.d.ts.map +1 -0
- package/dist/overrides.js +81 -0
- package/dist/overrides.js.map +1 -0
- package/dist/policy.d.ts +86 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +269 -0
- package/dist/policy.js.map +1 -0
- package/dist/readme.d.ts +60 -0
- package/dist/readme.d.ts.map +1 -0
- package/dist/readme.js +304 -0
- package/dist/readme.js.map +1 -0
- package/dist/release.d.ts +164 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +454 -0
- package/dist/release.js.map +1 -0
- package/dist/scaffold.d.ts +45 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +492 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/submodules.d.ts +144 -0
- package/dist/submodules.d.ts.map +1 -0
- package/dist/submodules.js +315 -0
- package/dist/submodules.js.map +1 -0
- package/dist/workflows.d.ts +102 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +710 -0
- package/dist/workflows.js.map +1 -0
- 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
|