@fairgarden/distribution 0.1.0-alpha.1 → 0.1.0-alpha.3

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 (63) hide show
  1. package/Readme.md +3 -3
  2. package/dist/add-module.d.ts +4 -0
  3. package/dist/add-module.d.ts.map +1 -1
  4. package/dist/add-module.js +29 -0
  5. package/dist/add-module.js.map +1 -1
  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.map +1 -1
  11. package/dist/canary.js +15 -2
  12. package/dist/canary.js.map +1 -1
  13. package/dist/changelog.d.ts +153 -0
  14. package/dist/changelog.d.ts.map +1 -0
  15. package/dist/changelog.js +359 -0
  16. package/dist/changelog.js.map +1 -0
  17. package/dist/cli.js +356 -12
  18. package/dist/cli.js.map +1 -1
  19. package/dist/extends.d.ts +3 -2
  20. package/dist/extends.d.ts.map +1 -1
  21. package/dist/extends.js +12 -2
  22. package/dist/extends.js.map +1 -1
  23. package/dist/forks.d.ts +116 -0
  24. package/dist/forks.d.ts.map +1 -0
  25. package/dist/forks.js +357 -0
  26. package/dist/forks.js.map +1 -0
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/inherit.d.ts +18 -0
  32. package/dist/inherit.d.ts.map +1 -0
  33. package/dist/inherit.js +84 -0
  34. package/dist/inherit.js.map +1 -0
  35. package/dist/manifest.d.ts +43 -0
  36. package/dist/manifest.d.ts.map +1 -0
  37. package/dist/manifest.js +64 -0
  38. package/dist/manifest.js.map +1 -0
  39. package/dist/policy.d.ts +5 -1
  40. package/dist/policy.d.ts.map +1 -1
  41. package/dist/policy.js +14 -6
  42. package/dist/policy.js.map +1 -1
  43. package/dist/readme.d.ts +2 -0
  44. package/dist/readme.d.ts.map +1 -1
  45. package/dist/readme.js +6 -1
  46. package/dist/readme.js.map +1 -1
  47. package/dist/release.d.ts +37 -1
  48. package/dist/release.d.ts.map +1 -1
  49. package/dist/release.js +107 -7
  50. package/dist/release.js.map +1 -1
  51. package/dist/scaffold.d.ts +3 -5
  52. package/dist/scaffold.d.ts.map +1 -1
  53. package/dist/scaffold.js +116 -64
  54. package/dist/scaffold.js.map +1 -1
  55. package/dist/submodules.d.ts +49 -0
  56. package/dist/submodules.d.ts.map +1 -1
  57. package/dist/submodules.js +94 -4
  58. package/dist/submodules.js.map +1 -1
  59. package/dist/workflows.d.ts +46 -1
  60. package/dist/workflows.d.ts.map +1 -1
  61. package/dist/workflows.js +401 -20
  62. package/dist/workflows.js.map +1 -1
  63. package/package.json +14 -12
package/dist/cli.js CHANGED
@@ -1,19 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
- import { mkdir } from 'node:fs/promises';
4
+ import { mkdir, readFile } from 'node:fs/promises';
5
5
  import { addModule } from "./add-module.js";
6
6
  import { extractModule } from "./extract.js";
7
7
  import { writeReadmes } from "./readme.js";
8
- import { writeWorkflows } from "./workflows.js";
8
+ import { writeDistributionChangelogCheck, writeDistributionWorkflow, writeWorkflows } from "./workflows.js";
9
9
  import { writeOverrides } from "./overrides.js";
10
- import { checkReleasable, planRelease, readRelease, release, reportToActions as reportRelease, startPrerelease, } from "./release.js";
10
+ import { checkReleasable, planRelease, readRelease, release, releaseDistribution, reportToActions as reportRelease, startPrerelease, } from "./release.js";
11
11
  import { reportToActions as reportCanary, stampCanary } from "./canary.js";
12
12
  import { describeRemote, isSsh, toHttps } from "./git-url.js";
13
13
  import { distributionRepo, isEmpty, moduleRepo, monolithRepo, write, } from "./scaffold.js";
14
14
  import { describeViolations, inspectExtends } from "./extends.js";
15
+ import { contribute, forkModule, integrate, unforkModule } from "./forks.js";
16
+ import { inherit } from "./inherit.js";
17
+ import { bumpEntry, checkChangelog, crossedReleases, holdPullRequests, CHANGELOG, moduleLabel, noteInDistribution, notesFor, pullRequestFromActions, } from "./changelog.js";
15
18
  import { buildPolicy, findPolicy, POLICIES, setupTurbo, testPolicy, usePolicy } from "./policy.js";
16
- import { inspect, isPublic, moveTo, repositoryRoot, setUrl, submodules, target, } from "./submodules.js";
19
+ import { stampModules } from "./manifest.js";
20
+ import { containsAll, inspect, isDistribution, isPublic, moveTo, repositoryRoot, setUrl, submodules, target, } from "./submodules.js";
17
21
  // Piping into `head` closes stdout early; that is ordinary use, not a failure.
18
22
  for (const stream of [process.stdout, process.stderr]) {
19
23
  stream.on('error', (error) => {
@@ -30,14 +34,22 @@ Commands:
30
34
  extract <path> Turn a directory here into its own repository and submodule
31
35
  use-https [name...] Rewrite submodule urls from ssh to https
32
36
  readme Record versions in the readmes; run in a distribution or a module
33
- workflows Give every module a publishing workflow
37
+ workflows [name...] Give every module a publishing workflow and a changelog check;
38
+ --distribution, this one a publishing workflow too
34
39
  overrides Point the workspace at this distribution's own checkouts
35
40
  canary Stamp a canary version into the manifest, for CI
36
41
  release Move a module on from the release it just published
37
42
  prerelease Start the next line on a branch, leaving main where it is
38
43
  check Fail when this ships anything older than what it extends
44
+ inherit Add the modules the distribution this extends ships, and this does not
39
45
  sync Report which modules have newer versions available
40
- bump [name...] Move modules to their newest non-major version
46
+ bump [name...] Move modules to their newest non-major version; a fork merges it
47
+ fork <name> <url> Check a module out from your fork, keeping where it came from
48
+ contribute <name> Push the module's branch and open a pull request upstream
49
+ unfork <name> Go back to the upstream a fork came from, once it has the fork's work
50
+ changelog check Fail a pull request that adds no changelog line linking itself (CI)
51
+ changelog notes [v] Print a version's changelog section, for its release notes
52
+ changelog hold Hold open pull requests until the next version starts (CI, after a release)
41
53
  policy build Build the organization's policy: policies/, on every module's own
42
54
  policy test Test each module's rules, then the organization's on top of them
43
55
  policy use Put the built policy beside this service, for it to run
@@ -49,6 +61,9 @@ Options:
49
61
  --minor Release the next minor, or branch one (release, prerelease)
50
62
  --patch Release the next patch, staying on this line (release)
51
63
  --id <name> Prerelease identifier: alpha, beta, rc (release, prerelease)
64
+ --stable A distribution's release leaves its prerelease stages (release)
65
+ --next <how> prerelease, alpha, beta, rc, patch, minor, major or stable (release)
66
+ --direct Commit main's move on where this runs and push it, no pull request (release)
52
67
  --no-fetch Use the refs already fetched (sync, bump)
53
68
  --dry-run Report what would change without changing it
54
69
  --check Verify instead of writing (readme, release, overrides)
@@ -63,7 +78,12 @@ Options:
63
78
  --no-verify Skip checking whether the https urls can be cloned
64
79
  --no-tag Do not tag the extracted module with its declared version
65
80
  --force Overwrite workflows that are already there (workflows)
66
- --no-push Leave the release branches local, and open no pull request
81
+ --distribution Publish the distribution itself, for others to extend (workflows)
82
+ --no-push Push nothing and open no pull request (release, fork, contribute, bump)
83
+ --to <ref> Upstream tag, branch or commit to go back to (unfork)
84
+ --base <branch> Branch the pull request goes into (contribute); what it merges into (changelog check)
85
+ --pr <number> The pull request to check, outside GitHub Actions (changelog check)
86
+ --repo <owner/name> The repository its link names, outside GitHub Actions (changelog check)
67
87
  --out <file> Where policy build writes the bundle (default: policies/dist/)
68
88
  `;
69
89
  const parseArgs = (argv) => {
@@ -89,6 +109,13 @@ const parseArgs = (argv) => {
89
109
  bump: undefined,
90
110
  id: undefined,
91
111
  out: undefined,
112
+ to: undefined,
113
+ base: undefined,
114
+ distribution: false,
115
+ stable: false,
116
+ pr: undefined,
117
+ repo: undefined,
118
+ direct: false,
92
119
  };
93
120
  for (let index = 0; index < argv.length; index += 1) {
94
121
  const arg = argv[index];
@@ -150,6 +177,47 @@ const parseArgs = (argv) => {
150
177
  else if (arg === '--no-push') {
151
178
  args.push = false;
152
179
  }
180
+ else if (arg === '--direct') {
181
+ args.direct = true;
182
+ }
183
+ else if (arg === '--next') {
184
+ // How main moves on after a release, as the publish workflow is told:
185
+ // carry on the prerelease, change its stage, or bump.
186
+ const next = argv[++index];
187
+ if (next === 'prerelease') {
188
+ // as it is
189
+ }
190
+ else if (next === 'alpha' || next === 'beta' || next === 'rc') {
191
+ args.id = next;
192
+ }
193
+ else if (next === 'patch' || next === 'minor' || next === 'major') {
194
+ args.bump = next;
195
+ }
196
+ else if (next === 'stable') {
197
+ args.stable = true;
198
+ }
199
+ else {
200
+ throw new Error(`--next is prerelease, alpha, beta, rc, patch, minor, major or stable; not ${next}.`);
201
+ }
202
+ }
203
+ else if (arg === '--pr') {
204
+ args.pr = Number(argv[++index]);
205
+ }
206
+ else if (arg === '--repo') {
207
+ args.repo = argv[++index];
208
+ }
209
+ else if (arg === '--stable') {
210
+ args.stable = true;
211
+ }
212
+ else if (arg === '--distribution') {
213
+ args.distribution = true;
214
+ }
215
+ else if (arg === '--to') {
216
+ args.to = argv[++index];
217
+ }
218
+ else if (arg === '--base') {
219
+ args.base = argv[++index];
220
+ }
153
221
  else if (arg === '--out') {
154
222
  args.out = path.resolve(argv[++index] ?? '.');
155
223
  }
@@ -166,6 +234,19 @@ const parseArgs = (argv) => {
166
234
  return args;
167
235
  };
168
236
  /** One line per module, aligned, saying what is available. */
237
+ /**
238
+ * Where a fork could go back to upstream, when upstream has taken what it
239
+ * added: a release past the pinned one, or its branch. Not a fork that has
240
+ * added nothing yet, which is where every fork starts.
241
+ */
242
+ const canReturn = (state) => {
243
+ const merged = state.fork?.merged;
244
+ if (!merged)
245
+ return undefined;
246
+ if (merged === state.current && state.fork?.ahead === 0)
247
+ return undefined;
248
+ return merged.replace(/^upstream\//, '');
249
+ };
169
250
  const describeState = (state) => {
170
251
  const { current, available, untagged, dirty } = state;
171
252
  const parts = [];
@@ -185,6 +266,18 @@ const describeState = (state) => {
185
266
  }
186
267
  if (untagged > 0)
187
268
  parts.push(`${untagged} untagged commit${untagged === 1 ? '' : 's'}`);
269
+ if (state.fork) {
270
+ const { upstream, ahead } = state.fork;
271
+ const from = `fork of ${describeRemote(upstream)}`;
272
+ const returnTo = canReturn(state);
273
+ parts.push(returnTo
274
+ ? `${from}, all in upstream ${returnTo}`
275
+ : ahead === 0
276
+ ? `${from}, nothing of its own`
277
+ : `${from}, ${ahead} commit${ahead === 1 ? '' : 's'} not upstream`);
278
+ }
279
+ if (state.pushed === false)
280
+ parts.push('not pushed');
188
281
  if (dirty)
189
282
  parts.push('uncommitted changes');
190
283
  if (state.fetched === false)
@@ -269,7 +362,9 @@ const main = async () => {
269
362
  }
270
363
  process.stdout.write(kind === 'module'
271
364
  ? 'Next: pnpm install, then commit and push so a distribution can add it.\n'
272
- : 'Next: pnpm install, then `fg-dist add-module <url>` to ship a module.\n');
365
+ : args.extends && kind === 'distribution'
366
+ ? `Next: pnpm install, then \`fg-dist inherit\` to ship what ${args.extends} ships.\n`
367
+ : 'Next: pnpm install, then `fg-dist add-module <url>` to ship a module.\n');
273
368
  return 0;
274
369
  }
275
370
  if (args.command === 'add-module') {
@@ -328,6 +423,9 @@ const main = async () => {
328
423
  else if (result.monolithConfig) {
329
424
  process.stdout.write(`Already mounted at /${result.mount}.\n`);
330
425
  }
426
+ if (result.turbo) {
427
+ process.stdout.write("turbo.json: the monolith's build waits for its libraries, not its own build\n");
428
+ }
331
429
  process.stdout.write(result.monolithPackageJson
332
430
  ? '\nRun pnpm install and `fg-monolith merge-package-json`.\n'
333
431
  : '\nRun pnpm install.\n');
@@ -365,10 +463,34 @@ const main = async () => {
365
463
  const root = repositoryRoot(args.cwd);
366
464
  if (!root)
367
465
  throw new Error(`${args.cwd} is not inside a git repository.`);
466
+ if (args.distribution) {
467
+ const update = await writeDistributionWorkflow(root, { force: args.force, dryRun: args.dryRun });
468
+ if (update.skipped) {
469
+ process.stdout.write('The distribution has a publishing workflow already. Pass --force to overwrite it.\n');
470
+ return 0;
471
+ }
472
+ process.stdout.write(`${update.files.join(', ')}\n`);
473
+ if (update.unprivated) {
474
+ process.stdout.write('\nTook "private": true off package.json — a private package cannot publish.\n');
475
+ }
476
+ if (update.missingAccess) {
477
+ process.stdout.write('Nothing says whether it publishes publicly. For others to extend it, add\n' +
478
+ ' "publishConfig": { "access": "public" }\n');
479
+ }
480
+ process.stdout.write('\nIt publishes the manifest, recording every module it ships, and its policy. ' +
481
+ 'Version it with `fg-dist release`, then run the workflow.\n');
482
+ return 0;
483
+ }
368
484
  const updates = await writeWorkflows(root, {
369
485
  force: args.force,
370
486
  dryRun: args.dryRun,
487
+ names: args.names,
371
488
  });
489
+ // The distribution checks its own changelog too, for policy changes —
490
+ // unless only some modules were named.
491
+ if (isDistribution(root) && args.names.length === 0) {
492
+ updates.push(await writeDistributionChangelogCheck(root, { force: args.force, dryRun: args.dryRun }));
493
+ }
372
494
  if (updates.length === 0) {
373
495
  process.stdout.write('No modules here. Run this in a distribution, which is where the modules are.\n');
374
496
  return 0;
@@ -463,11 +585,40 @@ const main = async () => {
463
585
  throw new Error(`${args.cwd} is not inside a git repository.`);
464
586
  if (args.check) {
465
587
  const releasable = await checkReleasable(root);
588
+ // A distribution records what it ships in what it publishes.
589
+ if (isDistribution(root))
590
+ await stampModules(root, { version: releasable.version });
466
591
  await reportRelease(releasable);
467
592
  process.stdout.write(`${releasable.name}@${releasable.version} is not on npm; ready to release as ` +
468
593
  `${releasable.tag}.\n`);
469
594
  return 0;
470
595
  }
596
+ if (isDistribution(root)) {
597
+ if (args.bump) {
598
+ process.stderr.write("A distribution's version is its month and its release: move it with " +
599
+ '--id <alpha|beta|rc> or --stable, or neither to carry on as it is.\n');
600
+ return 1;
601
+ }
602
+ const next = await releaseDistribution(root, {
603
+ dryRun: args.dryRun,
604
+ id: args.id,
605
+ stable: args.stable,
606
+ direct: args.direct,
607
+ push: args.push,
608
+ });
609
+ const verb = args.dryRun ? 'Would move' : 'Moved';
610
+ process.stdout.write(next.version === next.from
611
+ ? `${next.name} ${next.from} is not published yet; it is what the publish workflow releases next.\n`
612
+ : `${next.published ? `${next.from} is published. ` : `${next.from} was never published. `}` +
613
+ `${verb} ${next.name} to ${next.version}.\n`);
614
+ if (next.pushed) {
615
+ process.stdout.write('Committed and pushed.\n');
616
+ }
617
+ else if (!args.dryRun && next.version !== next.from) {
618
+ process.stdout.write(`Commit it, then run the publish workflow, which publishes it and tags v${next.version}.\n`);
619
+ }
620
+ return 0;
621
+ }
471
622
  // Which way main moves on is a judgement about what changed, so it is asked
472
623
  // for rather than guessed at. Showing all three is the cheapest way to ask.
473
624
  if (!args.bump) {
@@ -492,8 +643,16 @@ const main = async () => {
492
643
  id: args.id,
493
644
  dryRun: args.dryRun,
494
645
  push: args.push,
646
+ direct: args.direct,
495
647
  });
496
648
  const { plan } = outcome;
649
+ // Straight onto the branch this runs on: what the publish workflow does.
650
+ if (args.direct && !args.dryRun) {
651
+ process.stdout.write(`Released ${plan.released}. ${outcome.base} starts ${plan.next.version}` +
652
+ (plan.maintenance ? `; ${plan.maintenance.branch} maintains ${plan.maintenance.version}` : '') +
653
+ (outcome.pushed ? '. Pushed.\n' : '. Left local.\n'));
654
+ return 0;
655
+ }
497
656
  process.stdout.write(`Released ${plan.released}, ` +
498
657
  `${plan.tagged ? `tagged ${plan.base}` : 'which is not tagged here'}\n\n`);
499
658
  const width = pad([plan.maintenance?.branch ?? '', plan.next.branch]);
@@ -707,6 +866,25 @@ const main = async () => {
707
866
  process.stdout.write('Run `fg-dist use-https` to rewrite them. They also have to be ' +
708
867
  'readable without credentials, which means public.\n');
709
868
  }
869
+ // A pin the remote does not have breaks every other clone, a build host's first.
870
+ const unpushed = states.filter((state) => state.pushed === false);
871
+ if (unpushed.length > 0) {
872
+ process.stdout.write(`\n${unpushed.length} module(s) are pinned at a commit their remote does not have, ` +
873
+ 'so no other clone can check them out:\n');
874
+ for (const state of unpushed) {
875
+ process.stdout.write(` ${state.submodule.relativePath} ${state.head.slice(0, 7)}\n`);
876
+ }
877
+ process.stdout.write('Push them — `fg-dist contribute <name>` does, for a change on its way upstream.\n');
878
+ }
879
+ // A fork whose work upstream has taken can go back to upstream.
880
+ const returnable = states.filter((state) => canReturn(state));
881
+ if (returnable.length > 0) {
882
+ process.stdout.write('\nUpstream has everything these forks add:\n');
883
+ for (const state of returnable) {
884
+ process.stdout.write(` ${state.submodule.relativePath} in ${canReturn(state)}\n`);
885
+ }
886
+ process.stdout.write('Keep them, or `fg-dist unfork <name>` to go back to upstream.\n');
887
+ }
710
888
  // An extension may move ahead of what it extends, never behind it.
711
889
  const extendsReport = inspectExtends(root);
712
890
  if (extendsReport.violations.length > 0) {
@@ -742,7 +920,7 @@ const main = async () => {
742
920
  return 0;
743
921
  }
744
922
  if (args.command === 'bump') {
745
- const { modules } = modulesFor(args.cwd, args.names);
923
+ const { root, modules } = modulesFor(args.cwd, args.names);
746
924
  const states = modules.map((module) => inspect(module, { fetch: args.fetch }));
747
925
  const moves = states
748
926
  .map((state) => ({ state, to: target(state, { major: args.major }) }))
@@ -763,18 +941,184 @@ const main = async () => {
763
941
  .join('\n')}\n`);
764
942
  return 1;
765
943
  }
944
+ let failed = 0;
766
945
  for (const { state, to } of moves) {
767
946
  const from = state.current ?? state.head.slice(0, 7);
947
+ const line = `${state.submodule.relativePath} ${from} -> ${to}`;
768
948
  if (args.dryRun) {
769
- process.stdout.write(`${state.submodule.relativePath} ${from} -> ${to}\n`);
949
+ const how = !state.fork
950
+ ? ''
951
+ : containsAll(state.submodule.path, `refs/tags/${to}`, state.head)
952
+ ? ' (fork: upstream has all of it; would move to it)'
953
+ : ' (fork: would merge it in)';
954
+ process.stdout.write(`${line}${how}\n`);
770
955
  continue;
771
956
  }
772
- moveTo(state.submodule, to);
773
- process.stdout.write(`${state.submodule.relativePath} ${from} -> ${to}\n`);
957
+ if (!state.fork) {
958
+ moveTo(state.submodule, to);
959
+ noteInDistribution(root, bumpEntry(moduleLabel(state.submodule.path).name, from, to, state.submodule.url, crossedReleases(state.available, to)));
960
+ process.stdout.write(`${line}\n`);
961
+ continue;
962
+ }
963
+ // A fork takes upstream's release by merging it, keeping its own work on top.
964
+ try {
965
+ const done = integrate(root, state, to, { push: args.push });
966
+ process.stdout.write(done.how === 'moved'
967
+ ? `${line} (fork: upstream has all of it; \`fg-dist unfork ${state.submodule.name}\` goes back)\n`
968
+ : `${line} (fork: merged${done.pushed ? `, pushed to ${done.pushed}` : ', not pushed'})\n`);
969
+ }
970
+ catch (error) {
971
+ failed += 1;
972
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
973
+ }
774
974
  }
775
975
  process.stdout.write(args.dryRun
776
976
  ? '\nDry run; nothing moved.\n'
777
977
  : '\nSubmodule pointers moved. Commit them to record the new versions.\n');
978
+ return failed > 0 ? 1 : 0;
979
+ }
980
+ if (args.command === 'inherit') {
981
+ const root = repositoryRoot(args.cwd);
982
+ if (!root)
983
+ throw new Error(`${args.cwd} is not inside a git repository.`);
984
+ const result = await inherit(root, { dryRun: args.dryRun });
985
+ const width = pad(result.added.map((module) => module.path));
986
+ for (const module of result.added) {
987
+ process.stdout.write(`${module.path.padEnd(width)} ${module.name} ${module.version} at ${module.commit.slice(0, 7)}, ` +
988
+ `from ${describeRemote(module.repository)}` +
989
+ `${module.upstream ? ` (a fork of ${describeRemote(module.upstream)})` : ''}\n`);
990
+ }
991
+ for (const module of result.blocked) {
992
+ process.stderr.write(`${module.path} has something in it already, so ${module.name} was not added there.\n`);
993
+ }
994
+ if (result.added.length === 0 && result.blocked.length === 0) {
995
+ process.stdout.write(`Ships everything ${result.parent}@${result.parentVersion} ships already.\n`);
996
+ return 0;
997
+ }
998
+ process.stdout.write(args.dryRun
999
+ ? '\nDry run; nothing added.\n'
1000
+ : `\nShips what ${result.parent}@${result.parentVersion} ships. Run pnpm install, then commit.\n`);
1001
+ return result.blocked.length > 0 ? 1 : 0;
1002
+ }
1003
+ if (args.command === 'changelog') {
1004
+ const [action, version] = args.names;
1005
+ const root = repositoryRoot(args.cwd) ?? args.cwd;
1006
+ if (action === 'check') {
1007
+ const actions = pullRequestFromActions();
1008
+ const pullRequest = args.pr ?? actions.pullRequest;
1009
+ const base = args.base ?? actions.base;
1010
+ const repository = args.repo ?? actions.repository;
1011
+ if (!pullRequest || !base || !repository) {
1012
+ process.stderr.write('changelog check runs on a pull request: in GitHub Actions, or given ' +
1013
+ '--pr <number> --base <ref> --repo <owner/name>.\n');
1014
+ return 1;
1015
+ }
1016
+ const result = checkChangelog(root, {
1017
+ pullRequest,
1018
+ base,
1019
+ repository,
1020
+ distribution: isDistribution(root),
1021
+ labels: actions.labels,
1022
+ server: actions.server,
1023
+ });
1024
+ (result.ok ? process.stdout : process.stderr).write(`${result.message}\n`);
1025
+ return result.ok ? 0 : 1;
1026
+ }
1027
+ if (action === 'hold') {
1028
+ // The branch just released from: main, or a maintenance branch.
1029
+ const base = args.base ??
1030
+ process.env.GITHUB_REF_NAME ??
1031
+ execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim();
1032
+ const holds = holdPullRequests(root, { base });
1033
+ if (holds.length === 0) {
1034
+ process.stdout.write(`No pull requests are open into ${base}.\n`);
1035
+ return 0;
1036
+ }
1037
+ for (const hold of holds) {
1038
+ process.stdout.write(hold.held
1039
+ ? `#${hold.pullRequest} held${hold.run === undefined ? ', its first check will see the release' : ''}\n`
1040
+ : `#${hold.pullRequest} could not be held: ${hold.problem}\n`);
1041
+ }
1042
+ return holds.every((hold) => hold.held) ? 0 : 1;
1043
+ }
1044
+ if (action === 'notes') {
1045
+ const wanted = version ?? JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')).version;
1046
+ const text = await readFile(path.join(root, CHANGELOG), 'utf8').catch(() => '');
1047
+ const notes = notesFor(text, wanted);
1048
+ if (!notes) {
1049
+ process.stderr.write(`${CHANGELOG} says nothing for ${wanted}.\n`);
1050
+ return 1;
1051
+ }
1052
+ process.stdout.write(notes);
1053
+ return 0;
1054
+ }
1055
+ process.stderr.write('changelog needs `check`, `hold` or `notes`.\n');
1056
+ return 1;
1057
+ }
1058
+ if (args.command === 'fork') {
1059
+ const [name, url] = args.names;
1060
+ if (!name || !url) {
1061
+ process.stderr.write('fork needs a module and the url of your fork: fg-dist fork <name> <url>\n');
1062
+ return 1;
1063
+ }
1064
+ const { root, modules } = modulesFor(args.cwd, [name]);
1065
+ const result = forkModule(root, modules[0], url, { ssh: args.ssh, push: args.push });
1066
+ process.stdout.write(`${modules[0].relativePath} is checked out from ${result.url}\n` +
1067
+ `forked from ${result.upstream}, which .gitmodules records as its upstream\n`);
1068
+ if (result.rewritten) {
1069
+ process.stdout.write(`(rewritten from ${url}; a submodule is cloned without an SSH key)\n`);
1070
+ }
1071
+ process.stdout.write(result.branch
1072
+ ? `Pinned commit pushed to ${result.branch}, where everything this distribution pins in the fork lives.\n`
1073
+ : 'Nothing pushed. The pinned commit has to be on the fork before anyone else can check it out.\n');
1074
+ process.stdout.write(`\nNext: make the change on a branch in ${modules[0].relativePath}, then ` +
1075
+ `\`fg-dist contribute ${modules[0].name}\`.\nCommit .gitmodules and ${modules[0].relativePath} here.\n`);
1076
+ return 0;
1077
+ }
1078
+ if (args.command === 'contribute') {
1079
+ const [name] = args.names;
1080
+ if (!name) {
1081
+ process.stderr.write('contribute needs a module: fg-dist contribute <name>\n');
1082
+ return 1;
1083
+ }
1084
+ const { root, modules } = modulesFor(args.cwd, [name]);
1085
+ const result = contribute(root, modules[0], { push: args.push, base: args.base });
1086
+ if (!args.push) {
1087
+ process.stdout.write(`Would push ${result.branch} and open a pull request into ${describeRemote(result.repository)} ${result.base}.\n`);
1088
+ return 0;
1089
+ }
1090
+ process.stdout.write(`Pushed ${result.branch}.\n`);
1091
+ if (result.pinnedOn)
1092
+ process.stdout.write(`Pinned commit kept on ${result.pinnedOn}, which outlives the pull request's branch.\n`);
1093
+ if (result.pullRequest) {
1094
+ process.stdout.write(`Opened ${result.pullRequest}\n`);
1095
+ }
1096
+ else if (result.compare) {
1097
+ process.stdout.write(`Open the pull request at\n ${result.compare}\n`);
1098
+ }
1099
+ else {
1100
+ process.stdout.write(`Open a pull request from ${result.branch} into ${result.base} on ${describeRemote(result.repository)}.\n`);
1101
+ }
1102
+ if (result.changelog) {
1103
+ process.stdout.write(`\nIts changelog check wants a line in ${result.changelog.file}, under ${result.changelog.version}. ` +
1104
+ 'The words are yours:\n' +
1105
+ ` ${result.changelog.line}\n` +
1106
+ 'Commit it on the branch and push, then run this again to keep the pin current.\n');
1107
+ }
1108
+ process.stdout.write(`\nCommit ${modules[0].relativePath} here to ship the change meanwhile.\n`);
1109
+ return 0;
1110
+ }
1111
+ if (args.command === 'unfork') {
1112
+ const [name] = args.names;
1113
+ if (!name) {
1114
+ process.stderr.write('unfork needs a module: fg-dist unfork <name>\n');
1115
+ return 1;
1116
+ }
1117
+ const { root, modules } = modulesFor(args.cwd, [name]);
1118
+ const state = inspect(modules[0], { fetch: args.fetch });
1119
+ const result = unforkModule(root, state, { to: args.to, major: args.major, force: args.force });
1120
+ process.stdout.write(`${modules[0].relativePath} is checked out from ${result.url} again, at ${result.to}.\n` +
1121
+ `Commit .gitmodules and ${modules[0].relativePath} here. The fork itself is left as it is.\n`);
778
1122
  return 0;
779
1123
  }
780
1124
  process.stderr.write(`Unknown command: ${args.command}\n\n${USAGE}`);