@moku-labs/ci 1.1.2 → 1.2.1
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 +20 -2
- package/dist/release.mjs +354 -156
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ everything else lives here, once. Not a build tool and not a framework — it ca
|
|
|
22
22
|
[Install](#install) ·
|
|
23
23
|
[How it works](#how-it-works) ·
|
|
24
24
|
[Workflows](#workflows) ·
|
|
25
|
+
[PR previews](#pr-previews) ·
|
|
25
26
|
[CLI](#cli) ·
|
|
26
27
|
[The contract](#the-contract) ·
|
|
27
28
|
[Versioning](#versioning) ·
|
|
@@ -93,7 +94,7 @@ flowchart LR
|
|
|
93
94
|
|
|
94
95
|
| Workflow | Called from | Jobs | Inputs (all optional) |
|
|
95
96
|
|---|---|---|---|
|
|
96
|
-
| [`package-ci.yml`](.github/workflows/package-ci.yml) | [`examples/package/ci.yml`](examples/package/ci.yml) | `lint` · `types` · `test` · `build` | `runs_on`, `bun_version`, `validate` |
|
|
97
|
+
| [`package-ci.yml`](.github/workflows/package-ci.yml) | [`examples/package/ci.yml`](examples/package/ci.yml) | `lint` · `types` · `test` · `build` · `preview` | `runs_on`, `bun_version`, `validate`, `preview` |
|
|
97
98
|
| [`package-release.yml`](.github/workflows/package-release.yml) | [`examples/package/publish.yml`](examples/package/publish.yml) | `check` → `release` → `package` → `publish` | `release_type`, `publish`, `runs_on`, `bun_version`, `node_version`, `artifact_name`, `validate` |
|
|
98
99
|
| [`app-deploy.yml`](.github/workflows/app-deploy.yml) | [`examples/app/ci.yml`](examples/app/ci.yml) | `validate` → `deploy` to Cloudflare | script names (`lint_script`, `build_script`, `deploy_script`, …) and two required secrets |
|
|
99
100
|
| [`self-test.yml`](.github/workflows/self-test.yml) | this repo only | `actionlint` over workflows and examples | — |
|
|
@@ -109,12 +110,29 @@ flowchart LR
|
|
|
109
110
|
> A Layer-3 app copies `examples/app/ci.yml` by hand and needs a `deploy` script. The CLI
|
|
110
111
|
> sets up packages only.
|
|
111
112
|
|
|
113
|
+
## PR previews
|
|
114
|
+
|
|
115
|
+
Every pull request commit is published to [pkg.pr.new](https://pkg.pr.new) by the `preview`
|
|
116
|
+
job. Nothing reaches npm and no token is in scope. The bot comments the install command on
|
|
117
|
+
the PR.
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
bun add https://pkg.pr.new/@moku-labs/core@42 # 42 = PR number, a commit sha works too
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Rule | Where |
|
|
124
|
+
|---|---|
|
|
125
|
+
| The [pkg.pr.new GitHub App](https://github.com/apps/pkg-pr-new) must be installed on the repo. | once per org |
|
|
126
|
+
| The package repo must be public. The consuming project may be private. | pkg.pr.new |
|
|
127
|
+
| A preview URL never reaches `main`: the `lint` job and `doctor` both refuse it. | `package-ci.yml`, `preview-deps` |
|
|
128
|
+
| `preview` is not a required check. Turn it off with `with: { preview: false }`. | caller `ci.yml` |
|
|
129
|
+
|
|
112
130
|
## CLI
|
|
113
131
|
|
|
114
132
|
| Command | When | What it does |
|
|
115
133
|
|---|---|---|
|
|
116
134
|
| `bun run release:setup` | once per project | Idempotent wizard: workflows, script contract, first publish, first tag, trusted publisher, branch ruleset, then `doctor`. `--dry-run` prints every action and changes nothing. |
|
|
117
|
-
| `bun run release:doctor` | any time |
|
|
135
|
+
| `bun run release:doctor` | any time | Changes nothing in the project; it only runs `git fetch --tags` first. Twelve checks, one line each, and the exact `fix:` command for every red line. `--json` for machines. |
|
|
118
136
|
| `bun run release <patch\|minor\|major\|prerelease>` | each release | Refuses unless the tree is clean and `HEAD == origin/main`. Dispatches `publish.yml`, watches the run, verifies the version and dist-tag on npm. |
|
|
119
137
|
|
|
120
138
|
The scripts are plain aliases of the `moku-release` bin. Internals and the list of checks:
|
package/dist/release.mjs
CHANGED
|
@@ -644,6 +644,61 @@ function parseJsonArray(stdout) {
|
|
|
644
644
|
function hasMainBranchRuleset(stdout) {
|
|
645
645
|
return parseJsonArray(stdout).some((ruleset) => ruleset.target === BRANCH_TARGET && ruleset.enforcement !== "disabled");
|
|
646
646
|
}
|
|
647
|
+
/** Rule type GitHub uses for the required status checks of a ruleset. */
|
|
648
|
+
const REQUIRED_CHECKS_RULE = "required_status_checks";
|
|
649
|
+
/**
|
|
650
|
+
* Parse the `rules` of one full ruleset, tolerating error text and non-object payloads.
|
|
651
|
+
*
|
|
652
|
+
* @param stdout - Output of `gh api repos/{owner}/{repo}/rulesets/{id}`, or a ruleset template.
|
|
653
|
+
* @returns The rules, or an empty array when the payload has none.
|
|
654
|
+
* @example
|
|
655
|
+
* parseRules('{"rules":[{"type":"deletion"}]}');
|
|
656
|
+
*/
|
|
657
|
+
function parseRules(stdout) {
|
|
658
|
+
try {
|
|
659
|
+
const parsed = JSON.parse(stdout);
|
|
660
|
+
return Array.isArray(parsed?.rules) ? parsed.rules : [];
|
|
661
|
+
} catch {
|
|
662
|
+
return [];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* The ids of every active BRANCH ruleset, in listing order.
|
|
667
|
+
*
|
|
668
|
+
* @param stdout - Output of `gh api repos/{owner}/{repo}/rulesets`.
|
|
669
|
+
* @returns The ruleset ids; a ruleset without an id is left out.
|
|
670
|
+
* @example
|
|
671
|
+
* activeBranchRulesetIds('[{"id":7,"target":"branch","enforcement":"active"}]'); // [7]
|
|
672
|
+
*/
|
|
673
|
+
function activeBranchRulesetIds(stdout) {
|
|
674
|
+
return parseJsonArray(stdout).filter((ruleset) => ruleset.target === BRANCH_TARGET && ruleset.enforcement !== "disabled").flatMap((ruleset) => ruleset.id === void 0 ? [] : [ruleset.id]);
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* The status check contexts one full ruleset requires.
|
|
678
|
+
*
|
|
679
|
+
* @param stdout - One full ruleset as JSON.
|
|
680
|
+
* @returns The required contexts, e.g. `["ci / lint"]`.
|
|
681
|
+
* @example
|
|
682
|
+
* requiredCheckContexts(renderMainRuleset()); // ["ci / lint", "ci / types", …]
|
|
683
|
+
*/
|
|
684
|
+
function requiredCheckContexts(stdout) {
|
|
685
|
+
return parseRules(stdout).filter((rule) => rule.type === REQUIRED_CHECKS_RULE).flatMap((rule) => rule.parameters?.required_status_checks ?? []).flatMap((check) => check.context === void 0 ? [] : [check.context]);
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* The body that moves an existing ruleset onto the central required checks. Every other
|
|
689
|
+
* rule of the existing ruleset is kept as it is.
|
|
690
|
+
*
|
|
691
|
+
* @param existing - The full existing ruleset as JSON.
|
|
692
|
+
* @param template - The central ruleset template as JSON.
|
|
693
|
+
* @returns The `PUT` body: the existing rules with the template's required checks.
|
|
694
|
+
* @example
|
|
695
|
+
* const body = withCentralRequiredChecks(existingJson, renderMainRuleset());
|
|
696
|
+
*/
|
|
697
|
+
function withCentralRequiredChecks(existing, template) {
|
|
698
|
+
const kept = parseRules(existing).filter((rule) => rule.type !== REQUIRED_CHECKS_RULE);
|
|
699
|
+
const central = parseRules(template).filter((rule) => rule.type === REQUIRED_CHECKS_RULE);
|
|
700
|
+
return JSON.stringify({ rules: [...kept, ...central] });
|
|
701
|
+
}
|
|
647
702
|
/**
|
|
648
703
|
* The id of the newest workflow run from `gh run list --json databaseId`.
|
|
649
704
|
*
|
|
@@ -894,6 +949,143 @@ function skip(detail) {
|
|
|
894
949
|
};
|
|
895
950
|
}
|
|
896
951
|
//#endregion
|
|
952
|
+
//#region src/templates/central.ts
|
|
953
|
+
/**
|
|
954
|
+
* @file `moku-release` — read a central definition from this package's own files.
|
|
955
|
+
*
|
|
956
|
+
* The CLI ships in the same package as `examples/` and `rulesets/`, so a template is read
|
|
957
|
+
* from disk instead of being copied into a string. One file, one version, nothing to drift.
|
|
958
|
+
*/
|
|
959
|
+
/** A file that only exists at the package root, used to find it from `src/` and from `dist/`. */
|
|
960
|
+
const ROOT_MARKER = path.join("rulesets", "main.json");
|
|
961
|
+
/**
|
|
962
|
+
* The package root: the closest parent directory that holds `rulesets/main.json`.
|
|
963
|
+
*
|
|
964
|
+
* @returns The absolute path of the package root.
|
|
965
|
+
* @throws {Error} When no parent directory holds the marker, which means a broken install.
|
|
966
|
+
* @example
|
|
967
|
+
* path.join(packageRoot(), "examples");
|
|
968
|
+
*/
|
|
969
|
+
function packageRoot() {
|
|
970
|
+
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
971
|
+
while (!existsSync(path.join(directory, ROOT_MARKER))) {
|
|
972
|
+
const parent = path.dirname(directory);
|
|
973
|
+
if (parent === directory) throw new Error(`moku-release: ${ROOT_MARKER} not found above ${import.meta.url}`);
|
|
974
|
+
directory = parent;
|
|
975
|
+
}
|
|
976
|
+
return directory;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Read one central definition, for example `examples/package/ci.yml`.
|
|
980
|
+
*
|
|
981
|
+
* @param relativePath - The path inside the package.
|
|
982
|
+
* @returns The file contents.
|
|
983
|
+
* @example
|
|
984
|
+
* readCentral("rulesets/main.json");
|
|
985
|
+
*/
|
|
986
|
+
function readCentral(relativePath) {
|
|
987
|
+
return readFileSync(path.join(packageRoot(), relativePath), "utf8");
|
|
988
|
+
}
|
|
989
|
+
//#endregion
|
|
990
|
+
//#region src/templates/ci.ts
|
|
991
|
+
/**
|
|
992
|
+
* @file `moku-release` — the `.github/workflows/ci.yml` template.
|
|
993
|
+
*
|
|
994
|
+
* Read from this package's own `examples/package/ci.yml`. It is a thin caller: the
|
|
995
|
+
* whole check matrix lives once in the central reusable workflow, so a pipeline change is
|
|
996
|
+
* one PR there instead of one per repository. Two details are load-bearing and must not be
|
|
997
|
+
* "tidied": the caller job id is `ci` (GitHub prefixes the reused jobs with it, so the
|
|
998
|
+
* required checks are `ci / lint`, `ci / types`, `ci / test`, `ci / build`), and there is
|
|
999
|
+
* deliberately NO concurrency block — the called workflow already groups by
|
|
1000
|
+
* `github.workflow`, and a caller group with the same value deadlocks against its own child.
|
|
1001
|
+
*/
|
|
1002
|
+
/** Repo-relative path this template is written to. */
|
|
1003
|
+
const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
|
|
1004
|
+
/** The reusable workflow ref `doctor` recognizes a migrated `ci.yml` by. */
|
|
1005
|
+
const CI_WORKFLOW_REF = "moku-labs/ci/.github/workflows/package-ci.yml@v1";
|
|
1006
|
+
/** The `ci.yml` body, read from `examples/package/ci.yml`. */
|
|
1007
|
+
const CI_WORKFLOW = readCentral("examples/package/ci.yml");
|
|
1008
|
+
//#endregion
|
|
1009
|
+
//#region src/templates/publish.ts
|
|
1010
|
+
/**
|
|
1011
|
+
* @file `moku-release` — the `.github/workflows/publish.yml` template.
|
|
1012
|
+
*
|
|
1013
|
+
* Read from this package's own `examples/package/publish.yml`. The FILE NAME is the
|
|
1014
|
+
* contract: npm Trusted Publishing validates the CALLING workflow's filename, so this file
|
|
1015
|
+
* must stay `publish.yml` even though the publish itself happens inside the central
|
|
1016
|
+
* reusable workflow. There is no `NPM_TOKEN` anywhere — `id-token: write` is the whole
|
|
1017
|
+
* credential.
|
|
1018
|
+
*/
|
|
1019
|
+
/** Repo-relative path this template is written to. Registered with npm — never rename it. */
|
|
1020
|
+
const PUBLISH_WORKFLOW_PATH = ".github/workflows/publish.yml";
|
|
1021
|
+
/**
|
|
1022
|
+
* The reusable workflow ref `doctor` recognizes a migrated `publish.yml` by. The
|
|
1023
|
+
* `publish.local-publish.yml` fallback variant calls the same ref, so it is recognized too.
|
|
1024
|
+
*/
|
|
1025
|
+
const PUBLISH_WORKFLOW_REF = "moku-labs/ci/.github/workflows/package-release.yml@v1";
|
|
1026
|
+
/** The `publish.yml` body, read from `examples/package/publish.yml`. */
|
|
1027
|
+
const PUBLISH_WORKFLOW = readCentral("examples/package/publish.yml");
|
|
1028
|
+
//#endregion
|
|
1029
|
+
//#region src/templates/ruleset.ts
|
|
1030
|
+
/**
|
|
1031
|
+
* @file `moku-release` — the branch ruleset `setup` applies to `main`.
|
|
1032
|
+
*
|
|
1033
|
+
* Read from this package's own `rulesets/main.json`: PR-only main, no deletion, no
|
|
1034
|
+
* force-push, and the four required checks the thin `ci.yml` produces (`ci / lint`,
|
|
1035
|
+
* `ci / types`, `ci / test`, `ci / build`). Tags are deliberately NOT restricted — the
|
|
1036
|
+
* release workflow pushes `v*` tags, and a tag ruleset would block every release.
|
|
1037
|
+
*/
|
|
1038
|
+
/** The ruleset payload for `gh api repos/{owner}/{repo}/rulesets --input -`, read from `rulesets/main.json`. */
|
|
1039
|
+
const MAIN_RULESET_JSON = readCentral("rulesets/main.json");
|
|
1040
|
+
//#endregion
|
|
1041
|
+
//#region src/lib/templates.ts
|
|
1042
|
+
/**
|
|
1043
|
+
* @file `moku-release` — the workflow template surface: what to write, where, and how to
|
|
1044
|
+
* tell a migrated workflow from a hand-written legacy one.
|
|
1045
|
+
*
|
|
1046
|
+
* `setup` writes what {@link workflowTemplates} lists; `doctor` reads the same list back
|
|
1047
|
+
* and asks {@link isThinWorkflow} whether the file on disk still calls the pinned central
|
|
1048
|
+
* workflow. One list, both directions — a template can never drift from its check.
|
|
1049
|
+
*
|
|
1050
|
+
* The bodies are read from this package's own `examples/package/*.yml` and
|
|
1051
|
+
* `rulesets/main.json`: this CLI distributes the central definitions, it does not copy them.
|
|
1052
|
+
*/
|
|
1053
|
+
/** Both generated workflows, in the order `setup` writes and `doctor` reports them. */
|
|
1054
|
+
const workflowTemplates = [{
|
|
1055
|
+
path: CI_WORKFLOW_PATH,
|
|
1056
|
+
content: CI_WORKFLOW,
|
|
1057
|
+
ref: CI_WORKFLOW_REF
|
|
1058
|
+
}, {
|
|
1059
|
+
path: PUBLISH_WORKFLOW_PATH,
|
|
1060
|
+
content: PUBLISH_WORKFLOW,
|
|
1061
|
+
ref: PUBLISH_WORKFLOW_REF
|
|
1062
|
+
}];
|
|
1063
|
+
/**
|
|
1064
|
+
* Whether a workflow file on disk is the thin caller — i.e. it delegates to the pinned
|
|
1065
|
+
* central reusable workflow. A file that does not is a legacy hand-written pipeline.
|
|
1066
|
+
* Matching on the `@v1` ref rather than on the whole body is deliberate: the
|
|
1067
|
+
* `publish.local-publish.yml` fallback variant calls the same ref and must also pass.
|
|
1068
|
+
*
|
|
1069
|
+
* @param content - The file contents read from disk.
|
|
1070
|
+
* @param template - The template the file is expected to match.
|
|
1071
|
+
* @returns `true` when the file calls the pinned reusable workflow.
|
|
1072
|
+
* @example
|
|
1073
|
+
* isThinWorkflow(onDisk, workflowTemplates[0]);
|
|
1074
|
+
*/
|
|
1075
|
+
function isThinWorkflow(content, template) {
|
|
1076
|
+
return content.includes(template.ref);
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* The branch-ruleset payload `gh api … --input -` reads — the central definition verbatim.
|
|
1080
|
+
*
|
|
1081
|
+
* @returns The ruleset JSON.
|
|
1082
|
+
* @example
|
|
1083
|
+
* const body = renderMainRuleset();
|
|
1084
|
+
*/
|
|
1085
|
+
function renderMainRuleset() {
|
|
1086
|
+
return MAIN_RULESET_JSON;
|
|
1087
|
+
}
|
|
1088
|
+
//#endregion
|
|
897
1089
|
//#region src/checks/branch-ruleset.ts
|
|
898
1090
|
/**
|
|
899
1091
|
* @file `moku-release` — check: a branch ruleset protects the default branch.
|
|
@@ -901,15 +1093,40 @@ function skip(detail) {
|
|
|
901
1093
|
* Advisory. The release pipeline works without it; what it buys is that main can only
|
|
902
1094
|
* move through a PR, so a release always describes a reviewed state.
|
|
903
1095
|
*/
|
|
904
|
-
/**
|
|
1096
|
+
/**
|
|
1097
|
+
* Find the first active branch ruleset whose required checks are not the central ones. A
|
|
1098
|
+
* legacy ruleset requires `lint`; the thin caller reports `ci / lint`, so no PR can merge.
|
|
1099
|
+
*
|
|
1100
|
+
* @param ctx - The injected ports.
|
|
1101
|
+
* @param ownerRepo - The `owner/repo` slug.
|
|
1102
|
+
* @param listing - Output of `gh api repos/{owner}/{repo}/rulesets`.
|
|
1103
|
+
* @returns The stale ruleset, or `undefined` when every ruleset is on the central checks.
|
|
1104
|
+
* @example
|
|
1105
|
+
* const stale = await findStaleRuleset(ctx, "moku-labs/system", listing.stdout);
|
|
1106
|
+
*/
|
|
1107
|
+
async function findStaleRuleset(ctx, ownerRepo, listing) {
|
|
1108
|
+
const central = new Set(requiredCheckContexts(renderMainRuleset()));
|
|
1109
|
+
for (const id of activeBranchRulesetIds(listing)) {
|
|
1110
|
+
const detail = await ctx.exec.capture("gh", ["api", `repos/${ownerRepo}/rulesets/${id}`]);
|
|
1111
|
+
if (detail.code !== 0) continue;
|
|
1112
|
+
const legacy = requiredCheckContexts(detail.stdout).filter((context) => !central.has(context));
|
|
1113
|
+
if (legacy.length > 0) return {
|
|
1114
|
+
id,
|
|
1115
|
+
body: detail.stdout,
|
|
1116
|
+
legacy
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
/** Verifies `gh api repos/{owner}/{repo}/rulesets` lists an active, up-to-date branch ruleset. */
|
|
905
1121
|
const branchRulesetCheck = {
|
|
906
1122
|
id: "branch-ruleset",
|
|
907
1123
|
title: "branch ruleset on main",
|
|
908
1124
|
/**
|
|
909
|
-
* List the repository's rulesets
|
|
1125
|
+
* List the repository's rulesets, look for an active branch-targeting one, and make sure
|
|
1126
|
+
* it does not require legacy check names.
|
|
910
1127
|
*
|
|
911
1128
|
* @param ctx - The injected ports.
|
|
912
|
-
* @returns Pass when one exists, warn
|
|
1129
|
+
* @returns Pass when one exists on the central checks, warn otherwise.
|
|
913
1130
|
* @example
|
|
914
1131
|
* await branchRulesetCheck.run(ctx);
|
|
915
1132
|
*/
|
|
@@ -921,6 +1138,8 @@ const branchRulesetCheck = {
|
|
|
921
1138
|
const rulesets = await ctx.exec.capture("gh", ["api", `repos/${ownerRepo}/rulesets`]);
|
|
922
1139
|
if (rulesets.code !== 0) return skip("`gh api` could not read the rulesets");
|
|
923
1140
|
if (!hasMainBranchRuleset(rulesets.stdout)) return warn("main has no branch ruleset", "moku-release setup");
|
|
1141
|
+
const stale = await findStaleRuleset(ctx, ownerRepo, rulesets.stdout);
|
|
1142
|
+
if (stale !== void 0) return warn(`ruleset requires legacy checks: ${stale.legacy.join(", ")}`, "moku-release setup");
|
|
924
1143
|
return pass("active branch ruleset present");
|
|
925
1144
|
}
|
|
926
1145
|
};
|
|
@@ -1216,6 +1435,59 @@ const packageContractCheck = {
|
|
|
1216
1435
|
}
|
|
1217
1436
|
};
|
|
1218
1437
|
//#endregion
|
|
1438
|
+
//#region src/checks/preview-deps.ts
|
|
1439
|
+
/**
|
|
1440
|
+
* @file `moku-release` — check: no dependency points at a pkg.pr.new preview build.
|
|
1441
|
+
*
|
|
1442
|
+
* A preview URL is for testing an unreleased upstream change. It has no provenance and dies
|
|
1443
|
+
* with its pull request, so a release must never be cut with one in the manifest. The `lint`
|
|
1444
|
+
* job of package-ci.yml refuses the same thing on every PR; this is the local view of it.
|
|
1445
|
+
*/
|
|
1446
|
+
/** Host every pkg.pr.new install URL contains. */
|
|
1447
|
+
const PREVIEW_HOST = "pkg.pr.new";
|
|
1448
|
+
/** The manifest tables a preview URL can sit in. */
|
|
1449
|
+
const DEPENDENCY_FIELDS = [
|
|
1450
|
+
"dependencies",
|
|
1451
|
+
"devDependencies",
|
|
1452
|
+
"peerDependencies",
|
|
1453
|
+
"optionalDependencies"
|
|
1454
|
+
];
|
|
1455
|
+
/**
|
|
1456
|
+
* Names of every dependency whose range is a pkg.pr.new URL.
|
|
1457
|
+
*
|
|
1458
|
+
* @param manifest - The manifest to scan.
|
|
1459
|
+
* @returns The dependency names, in manifest order.
|
|
1460
|
+
* @example
|
|
1461
|
+
* previewDependencies({ dependencies: { "@moku-labs/core": "https://pkg.pr.new/@moku-labs/core@42" } });
|
|
1462
|
+
*/
|
|
1463
|
+
function previewDependencies(manifest) {
|
|
1464
|
+
return DEPENDENCY_FIELDS.flatMap((field) => {
|
|
1465
|
+
const table = manifest[field];
|
|
1466
|
+
if (typeof table !== "object" || table === null) return [];
|
|
1467
|
+
return Object.entries(table).filter(([, range]) => typeof range === "string" && range.includes(PREVIEW_HOST)).map(([name]) => name);
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
/** Verifies no dependency is installed from a pkg.pr.new preview. */
|
|
1471
|
+
const previewDepsCheck = {
|
|
1472
|
+
id: "preview-deps",
|
|
1473
|
+
title: "no pkg.pr.new preview dependencies",
|
|
1474
|
+
/**
|
|
1475
|
+
* Scan the dependency tables for preview URLs.
|
|
1476
|
+
*
|
|
1477
|
+
* @param ctx - The injected ports.
|
|
1478
|
+
* @returns Pass when none is found, otherwise every preview dependency by name.
|
|
1479
|
+
* @example
|
|
1480
|
+
* await previewDepsCheck.run(ctx);
|
|
1481
|
+
*/
|
|
1482
|
+
async run(ctx) {
|
|
1483
|
+
const manifest = await readManifest(ctx.files);
|
|
1484
|
+
if (!manifest) return skip("package.json is missing or malformed");
|
|
1485
|
+
const names = previewDependencies(manifest);
|
|
1486
|
+
if (names.length > 0) return fail(`preview build of ${names.join(", ")}`, `release upstream, then: bun add ${names.join(" ")}`);
|
|
1487
|
+
return pass("every dependency comes from the registry");
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
//#endregion
|
|
1219
1491
|
//#region src/checks/repository-url.ts
|
|
1220
1492
|
/**
|
|
1221
1493
|
* @file `moku-release` — check: `repository.url` points at the real origin.
|
|
@@ -1296,64 +1568,6 @@ const tagSyncCheck = {
|
|
|
1296
1568
|
}
|
|
1297
1569
|
};
|
|
1298
1570
|
//#endregion
|
|
1299
|
-
//#region src/templates/central.ts
|
|
1300
|
-
/**
|
|
1301
|
-
* @file `moku-release` — read a central definition from this package's own files.
|
|
1302
|
-
*
|
|
1303
|
-
* The CLI ships in the same package as `examples/` and `rulesets/`, so a template is read
|
|
1304
|
-
* from disk instead of being copied into a string. One file, one version, nothing to drift.
|
|
1305
|
-
*/
|
|
1306
|
-
/** A file that only exists at the package root, used to find it from `src/` and from `dist/`. */
|
|
1307
|
-
const ROOT_MARKER = path.join("rulesets", "main.json");
|
|
1308
|
-
/**
|
|
1309
|
-
* The package root: the closest parent directory that holds `rulesets/main.json`.
|
|
1310
|
-
*
|
|
1311
|
-
* @returns The absolute path of the package root.
|
|
1312
|
-
* @throws {Error} When no parent directory holds the marker, which means a broken install.
|
|
1313
|
-
* @example
|
|
1314
|
-
* path.join(packageRoot(), "examples");
|
|
1315
|
-
*/
|
|
1316
|
-
function packageRoot() {
|
|
1317
|
-
let directory = path.dirname(fileURLToPath(import.meta.url));
|
|
1318
|
-
while (!existsSync(path.join(directory, ROOT_MARKER))) {
|
|
1319
|
-
const parent = path.dirname(directory);
|
|
1320
|
-
if (parent === directory) throw new Error(`moku-release: ${ROOT_MARKER} not found above ${import.meta.url}`);
|
|
1321
|
-
directory = parent;
|
|
1322
|
-
}
|
|
1323
|
-
return directory;
|
|
1324
|
-
}
|
|
1325
|
-
/**
|
|
1326
|
-
* Read one central definition, for example `examples/package/ci.yml`.
|
|
1327
|
-
*
|
|
1328
|
-
* @param relativePath - The path inside the package.
|
|
1329
|
-
* @returns The file contents.
|
|
1330
|
-
* @example
|
|
1331
|
-
* readCentral("rulesets/main.json");
|
|
1332
|
-
*/
|
|
1333
|
-
function readCentral(relativePath) {
|
|
1334
|
-
return readFileSync(path.join(packageRoot(), relativePath), "utf8");
|
|
1335
|
-
}
|
|
1336
|
-
//#endregion
|
|
1337
|
-
//#region src/templates/publish.ts
|
|
1338
|
-
/**
|
|
1339
|
-
* @file `moku-release` — the `.github/workflows/publish.yml` template.
|
|
1340
|
-
*
|
|
1341
|
-
* Read from this package's own `examples/package/publish.yml`. The FILE NAME is the
|
|
1342
|
-
* contract: npm Trusted Publishing validates the CALLING workflow's filename, so this file
|
|
1343
|
-
* must stay `publish.yml` even though the publish itself happens inside the central
|
|
1344
|
-
* reusable workflow. There is no `NPM_TOKEN` anywhere — `id-token: write` is the whole
|
|
1345
|
-
* credential.
|
|
1346
|
-
*/
|
|
1347
|
-
/** Repo-relative path this template is written to. Registered with npm — never rename it. */
|
|
1348
|
-
const PUBLISH_WORKFLOW_PATH = ".github/workflows/publish.yml";
|
|
1349
|
-
/**
|
|
1350
|
-
* The reusable workflow ref `doctor` recognizes a migrated `publish.yml` by. The
|
|
1351
|
-
* `publish.local-publish.yml` fallback variant calls the same ref, so it is recognized too.
|
|
1352
|
-
*/
|
|
1353
|
-
const PUBLISH_WORKFLOW_REF = "moku-labs/ci/.github/workflows/package-release.yml@v1";
|
|
1354
|
-
/** The `publish.yml` body, read from `examples/package/publish.yml`. */
|
|
1355
|
-
const PUBLISH_WORKFLOW = readCentral("examples/package/publish.yml");
|
|
1356
|
-
//#endregion
|
|
1357
1571
|
//#region src/checks/trusted-publisher.ts
|
|
1358
1572
|
/**
|
|
1359
1573
|
* @file `moku-release` — check: npm knows this repo's `publish.yml` as a trusted publisher.
|
|
@@ -1387,6 +1601,18 @@ function isUnauthorized(output) {
|
|
|
1387
1601
|
return /E401|ENEEDAUTH|401 Unauthorized/i.test(output);
|
|
1388
1602
|
}
|
|
1389
1603
|
/**
|
|
1604
|
+
* Whether npm refused the listing because the account needs a one-time password. Output is
|
|
1605
|
+
* captured here, so npm cannot prompt: the answer is unknown, not "missing".
|
|
1606
|
+
*
|
|
1607
|
+
* @param output - The combined stdout/stderr npm produced.
|
|
1608
|
+
* @returns `true` when npm asked for an OTP.
|
|
1609
|
+
* @example
|
|
1610
|
+
* isOtpRequired("npm error code EOTP");
|
|
1611
|
+
*/
|
|
1612
|
+
function isOtpRequired(output) {
|
|
1613
|
+
return /EOTP|one-time password/i.test(output);
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1390
1616
|
* The exact registration command for this package and repository.
|
|
1391
1617
|
*
|
|
1392
1618
|
* @param name - The package name.
|
|
@@ -1396,7 +1622,7 @@ function isUnauthorized(output) {
|
|
|
1396
1622
|
* trustCommand("@moku-labs/common", "moku-labs/common");
|
|
1397
1623
|
*/
|
|
1398
1624
|
function trustCommand(name, ownerRepo) {
|
|
1399
|
-
return `npm trust github ${name} --file ${PUBLISH_WORKFLOW_FILE$1} --repo ${ownerRepo} --yes`;
|
|
1625
|
+
return `npm trust github ${name} --file ${PUBLISH_WORKFLOW_FILE$1} --repo ${ownerRepo} --allow-publish --yes`;
|
|
1400
1626
|
}
|
|
1401
1627
|
/** Verifies a trusted publisher is registered for the package. */
|
|
1402
1628
|
const trustedPublisherCheck = {
|
|
@@ -1423,90 +1649,12 @@ const trustedPublisherCheck = {
|
|
|
1423
1649
|
]);
|
|
1424
1650
|
if (isUnknownCommand(`${listing.stdout}${listing.stderr}`)) return warn("this npm has no `trust` command", "upgrade npm");
|
|
1425
1651
|
if (isUnauthorized(`${listing.stdout}${listing.stderr}`)) return skip("cannot list trusted publishers without `npm login`");
|
|
1652
|
+
if (isOtpRequired(`${listing.stdout}${listing.stderr}`)) return warn("npm asks for an OTP, cannot verify from here", `npm trust list ${manifest.name}`);
|
|
1426
1653
|
if (listing.code !== 0 || !listing.stdout.includes(PUBLISH_WORKFLOW_FILE$1)) return fail("no trusted publisher registered", trustCommand(manifest.name, ownerRepo));
|
|
1427
1654
|
return pass(`${ownerRepo} · ${PUBLISH_WORKFLOW_FILE$1}`);
|
|
1428
1655
|
}
|
|
1429
1656
|
};
|
|
1430
1657
|
//#endregion
|
|
1431
|
-
//#region src/templates/ci.ts
|
|
1432
|
-
/**
|
|
1433
|
-
* @file `moku-release` — the `.github/workflows/ci.yml` template.
|
|
1434
|
-
*
|
|
1435
|
-
* Read from this package's own `examples/package/ci.yml`. It is a thin caller: the
|
|
1436
|
-
* whole check matrix lives once in the central reusable workflow, so a pipeline change is
|
|
1437
|
-
* one PR there instead of one per repository. Two details are load-bearing and must not be
|
|
1438
|
-
* "tidied": the caller job id is `ci` (GitHub prefixes the reused jobs with it, so the
|
|
1439
|
-
* required checks are `ci / lint`, `ci / types`, `ci / test`, `ci / build`), and there is
|
|
1440
|
-
* deliberately NO concurrency block — the called workflow already groups by
|
|
1441
|
-
* `github.workflow`, and a caller group with the same value deadlocks against its own child.
|
|
1442
|
-
*/
|
|
1443
|
-
/** Repo-relative path this template is written to. */
|
|
1444
|
-
const CI_WORKFLOW_PATH = ".github/workflows/ci.yml";
|
|
1445
|
-
/** The reusable workflow ref `doctor` recognizes a migrated `ci.yml` by. */
|
|
1446
|
-
const CI_WORKFLOW_REF = "moku-labs/ci/.github/workflows/package-ci.yml@v1";
|
|
1447
|
-
/** The `ci.yml` body, read from `examples/package/ci.yml`. */
|
|
1448
|
-
const CI_WORKFLOW = readCentral("examples/package/ci.yml");
|
|
1449
|
-
//#endregion
|
|
1450
|
-
//#region src/templates/ruleset.ts
|
|
1451
|
-
/**
|
|
1452
|
-
* @file `moku-release` — the branch ruleset `setup` applies to `main`.
|
|
1453
|
-
*
|
|
1454
|
-
* Read from this package's own `rulesets/main.json`: PR-only main, no deletion, no
|
|
1455
|
-
* force-push, and the four required checks the thin `ci.yml` produces (`ci / lint`,
|
|
1456
|
-
* `ci / types`, `ci / test`, `ci / build`). Tags are deliberately NOT restricted — the
|
|
1457
|
-
* release workflow pushes `v*` tags, and a tag ruleset would block every release.
|
|
1458
|
-
*/
|
|
1459
|
-
/** The ruleset payload for `gh api repos/{owner}/{repo}/rulesets --input -`, read from `rulesets/main.json`. */
|
|
1460
|
-
const MAIN_RULESET_JSON = readCentral("rulesets/main.json");
|
|
1461
|
-
//#endregion
|
|
1462
|
-
//#region src/lib/templates.ts
|
|
1463
|
-
/**
|
|
1464
|
-
* @file `moku-release` — the workflow template surface: what to write, where, and how to
|
|
1465
|
-
* tell a migrated workflow from a hand-written legacy one.
|
|
1466
|
-
*
|
|
1467
|
-
* `setup` writes what {@link workflowTemplates} lists; `doctor` reads the same list back
|
|
1468
|
-
* and asks {@link isThinWorkflow} whether the file on disk still calls the pinned central
|
|
1469
|
-
* workflow. One list, both directions — a template can never drift from its check.
|
|
1470
|
-
*
|
|
1471
|
-
* The bodies are read from this package's own `examples/package/*.yml` and
|
|
1472
|
-
* `rulesets/main.json`: this CLI distributes the central definitions, it does not copy them.
|
|
1473
|
-
*/
|
|
1474
|
-
/** Both generated workflows, in the order `setup` writes and `doctor` reports them. */
|
|
1475
|
-
const workflowTemplates = [{
|
|
1476
|
-
path: CI_WORKFLOW_PATH,
|
|
1477
|
-
content: CI_WORKFLOW,
|
|
1478
|
-
ref: CI_WORKFLOW_REF
|
|
1479
|
-
}, {
|
|
1480
|
-
path: PUBLISH_WORKFLOW_PATH,
|
|
1481
|
-
content: PUBLISH_WORKFLOW,
|
|
1482
|
-
ref: PUBLISH_WORKFLOW_REF
|
|
1483
|
-
}];
|
|
1484
|
-
/**
|
|
1485
|
-
* Whether a workflow file on disk is the thin caller — i.e. it delegates to the pinned
|
|
1486
|
-
* central reusable workflow. A file that does not is a legacy hand-written pipeline.
|
|
1487
|
-
* Matching on the `@v1` ref rather than on the whole body is deliberate: the
|
|
1488
|
-
* `publish.local-publish.yml` fallback variant calls the same ref and must also pass.
|
|
1489
|
-
*
|
|
1490
|
-
* @param content - The file contents read from disk.
|
|
1491
|
-
* @param template - The template the file is expected to match.
|
|
1492
|
-
* @returns `true` when the file calls the pinned reusable workflow.
|
|
1493
|
-
* @example
|
|
1494
|
-
* isThinWorkflow(onDisk, workflowTemplates[0]);
|
|
1495
|
-
*/
|
|
1496
|
-
function isThinWorkflow(content, template) {
|
|
1497
|
-
return content.includes(template.ref);
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* The branch-ruleset payload `gh api … --input -` reads — the central definition verbatim.
|
|
1501
|
-
*
|
|
1502
|
-
* @returns The ruleset JSON.
|
|
1503
|
-
* @example
|
|
1504
|
-
* const body = renderMainRuleset();
|
|
1505
|
-
*/
|
|
1506
|
-
function renderMainRuleset() {
|
|
1507
|
-
return MAIN_RULESET_JSON;
|
|
1508
|
-
}
|
|
1509
|
-
//#endregion
|
|
1510
1658
|
//#region src/checks/workflows.ts
|
|
1511
1659
|
/**
|
|
1512
1660
|
* @file `moku-release` — check: both workflows exist and are the thin callers.
|
|
@@ -1576,6 +1724,7 @@ const allChecks = [
|
|
|
1576
1724
|
npmAuthCheck,
|
|
1577
1725
|
npmVersionCheck,
|
|
1578
1726
|
packageContractCheck,
|
|
1727
|
+
previewDepsCheck,
|
|
1579
1728
|
repositoryUrlCheck,
|
|
1580
1729
|
workflowsCheck,
|
|
1581
1730
|
npmPackageCheck,
|
|
@@ -1671,6 +1820,11 @@ async function evaluate(check, ctx) {
|
|
|
1671
1820
|
async function runDoctor(options) {
|
|
1672
1821
|
const { ctx, ui, json = false, checks = allChecks } = options;
|
|
1673
1822
|
const entries = [];
|
|
1823
|
+
await ctx.exec.capture("git", [
|
|
1824
|
+
"fetch",
|
|
1825
|
+
"--tags",
|
|
1826
|
+
"--prune"
|
|
1827
|
+
]);
|
|
1674
1828
|
for (const check of checks) entries.push(await evaluate(check, ctx));
|
|
1675
1829
|
const failed = entries.some((entry) => entry.status === "fail");
|
|
1676
1830
|
if (json) {
|
|
@@ -1848,11 +2002,6 @@ async function runRelease(options) {
|
|
|
1848
2002
|
ui.error("package.json is missing, malformed, or has no `name`");
|
|
1849
2003
|
return 1;
|
|
1850
2004
|
}
|
|
1851
|
-
await ctx.exec.capture("git", [
|
|
1852
|
-
"fetch",
|
|
1853
|
-
"--tags",
|
|
1854
|
-
"--prune"
|
|
1855
|
-
]);
|
|
1856
2005
|
if (!await preflight(ctx, ui)) return 1;
|
|
1857
2006
|
const declared = repositoryUrlOf(manifest);
|
|
1858
2007
|
const ownerRepo = declared === void 0 ? void 0 : ownerRepoFrom(declared);
|
|
@@ -2122,6 +2271,21 @@ async function applyBranchRuleset(setup, ownerRepo) {
|
|
|
2122
2271
|
setup.ui.check(true, result.detail);
|
|
2123
2272
|
return;
|
|
2124
2273
|
}
|
|
2274
|
+
const listing = await setup.ctx.exec.capture("gh", ["api", `repos/${ownerRepo}/rulesets`]);
|
|
2275
|
+
const stale = await findStaleRuleset(setup.ctx, ownerRepo, listing.stdout);
|
|
2276
|
+
if (stale !== void 0) {
|
|
2277
|
+
if (deferred(setup, `move ruleset ${stale.id} on ${ownerRepo} to the ci / … checks`)) return;
|
|
2278
|
+
const updated = await setup.ctx.exec.capture("gh", [
|
|
2279
|
+
"api",
|
|
2280
|
+
`repos/${ownerRepo}/rulesets/${stale.id}`,
|
|
2281
|
+
"--method",
|
|
2282
|
+
"PUT",
|
|
2283
|
+
"--input",
|
|
2284
|
+
"-"
|
|
2285
|
+
], { input: withCentralRequiredChecks(stale.body, renderMainRuleset()) });
|
|
2286
|
+
setup.ui.check(updated.code === 0, "branch ruleset updated", updated.stderr.trim() || void 0);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2125
2289
|
if (deferred(setup, `create the PR-only branch ruleset on ${ownerRepo}`)) return;
|
|
2126
2290
|
const created = await setup.ctx.exec.capture("gh", [
|
|
2127
2291
|
"api",
|
|
@@ -2181,7 +2345,7 @@ async function runSetup(options) {
|
|
|
2181
2345
|
* dispatcher.
|
|
2182
2346
|
*
|
|
2183
2347
|
* The grammar is deliberately tiny: one positional (a command name, or a semver bump that
|
|
2184
|
-
* implies the `release` command) plus
|
|
2348
|
+
* implies the `release` command) plus four flags. Anything unrecognized resolves to
|
|
2185
2349
|
* `help` with an `error` set — the CLI never guesses what an operator meant.
|
|
2186
2350
|
*/
|
|
2187
2351
|
/** The semver bumps `moku-release <type>` accepts, in menu order. */
|
|
@@ -2191,6 +2355,13 @@ const RELEASE_TYPES = [
|
|
|
2191
2355
|
"major",
|
|
2192
2356
|
"prerelease"
|
|
2193
2357
|
];
|
|
2358
|
+
/** Every flag the CLI accepts, besides `--help`. */
|
|
2359
|
+
const KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
2360
|
+
"--json",
|
|
2361
|
+
"--dry-run",
|
|
2362
|
+
"--yes",
|
|
2363
|
+
"-y"
|
|
2364
|
+
]);
|
|
2194
2365
|
/**
|
|
2195
2366
|
* Whether a positional argument is one of the semver bumps.
|
|
2196
2367
|
*
|
|
@@ -2215,6 +2386,7 @@ function rejected(error) {
|
|
|
2215
2386
|
command: "help",
|
|
2216
2387
|
json: false,
|
|
2217
2388
|
dryRun: false,
|
|
2389
|
+
yes: false,
|
|
2218
2390
|
error
|
|
2219
2391
|
};
|
|
2220
2392
|
}
|
|
@@ -2225,7 +2397,7 @@ function rejected(error) {
|
|
|
2225
2397
|
* @returns What to run, and with which flags.
|
|
2226
2398
|
* @example
|
|
2227
2399
|
* parseArgv(["patch", "--dry-run"]);
|
|
2228
|
-
* // { command: "release", releaseType: "patch", json: false, dryRun: true }
|
|
2400
|
+
* // { command: "release", releaseType: "patch", json: false, dryRun: true, yes: false }
|
|
2229
2401
|
*/
|
|
2230
2402
|
function parseArgv(argv) {
|
|
2231
2403
|
const flags = argv.filter((argument) => argument.startsWith("-"));
|
|
@@ -2233,34 +2405,40 @@ function parseArgv(argv) {
|
|
|
2233
2405
|
if (flags.includes("--help") || flags.includes("-h")) return {
|
|
2234
2406
|
command: "help",
|
|
2235
2407
|
json: false,
|
|
2236
|
-
dryRun: false
|
|
2408
|
+
dryRun: false,
|
|
2409
|
+
yes: false
|
|
2237
2410
|
};
|
|
2238
|
-
const unknownFlag = flags.find((flag) => flag
|
|
2411
|
+
const unknownFlag = flags.find((flag) => !KNOWN_FLAGS.has(flag));
|
|
2239
2412
|
if (unknownFlag) return rejected(`unknown flag \`${unknownFlag}\``);
|
|
2240
2413
|
if (positionals.length > 1) return rejected(`unexpected argument \`${positionals[1]}\``);
|
|
2241
2414
|
const json = flags.includes("--json");
|
|
2242
2415
|
const dryRun = flags.includes("--dry-run");
|
|
2416
|
+
const yes = flags.includes("--yes") || flags.includes("-y");
|
|
2243
2417
|
const [first] = positionals;
|
|
2244
2418
|
if (first === void 0) return {
|
|
2245
2419
|
command: "help",
|
|
2246
2420
|
json,
|
|
2247
|
-
dryRun
|
|
2421
|
+
dryRun,
|
|
2422
|
+
yes
|
|
2248
2423
|
};
|
|
2249
2424
|
if (first === "doctor" || first === "setup") return {
|
|
2250
2425
|
command: first,
|
|
2251
2426
|
json,
|
|
2252
|
-
dryRun
|
|
2427
|
+
dryRun,
|
|
2428
|
+
yes
|
|
2253
2429
|
};
|
|
2254
2430
|
if (first === "help") return {
|
|
2255
2431
|
command: "help",
|
|
2256
2432
|
json,
|
|
2257
|
-
dryRun
|
|
2433
|
+
dryRun,
|
|
2434
|
+
yes
|
|
2258
2435
|
};
|
|
2259
2436
|
if (isReleaseType(first)) return {
|
|
2260
2437
|
command: "release",
|
|
2261
2438
|
releaseType: first,
|
|
2262
2439
|
json,
|
|
2263
|
-
dryRun
|
|
2440
|
+
dryRun,
|
|
2441
|
+
yes
|
|
2264
2442
|
};
|
|
2265
2443
|
return rejected(`unknown command \`${first}\``);
|
|
2266
2444
|
}
|
|
@@ -2445,12 +2623,32 @@ const USAGE = [
|
|
|
2445
2623
|
" moku-release doctor [--json] read-only diagnosis of the release setup",
|
|
2446
2624
|
` moku-release <${RELEASE_TYPES.join("|")}>`,
|
|
2447
2625
|
"",
|
|
2448
|
-
" --dry-run print every action, mutate nothing",
|
|
2626
|
+
" --dry-run print every action, mutate nothing, ask nothing",
|
|
2627
|
+
" --yes, -y answer every setup confirmation with yes",
|
|
2449
2628
|
"",
|
|
2450
2629
|
" Two steps are yours alone — this CLI never handles a credential:",
|
|
2451
2630
|
" gh auth login",
|
|
2452
2631
|
" npm login"
|
|
2453
2632
|
].join("\n");
|
|
2633
|
+
/** Prompts that never read stdin: every confirmation is a yes, every menu its first entry. */
|
|
2634
|
+
const ASSENTING_PROMPTS = {
|
|
2635
|
+
/**
|
|
2636
|
+
* Answer a confirmation with yes.
|
|
2637
|
+
*
|
|
2638
|
+
* @returns Always `true`.
|
|
2639
|
+
* @example
|
|
2640
|
+
* await ASSENTING_PROMPTS.confirm("Replace ci.yml?"); // true
|
|
2641
|
+
*/
|
|
2642
|
+
confirm: async () => true,
|
|
2643
|
+
/**
|
|
2644
|
+
* Pick the first entry of a menu.
|
|
2645
|
+
*
|
|
2646
|
+
* @returns Always `0`.
|
|
2647
|
+
* @example
|
|
2648
|
+
* await ASSENTING_PROMPTS.select("Bump?", ["patch", "minor"]); // 0
|
|
2649
|
+
*/
|
|
2650
|
+
select: async () => 0
|
|
2651
|
+
};
|
|
2454
2652
|
/**
|
|
2455
2653
|
* Dispatch a parsed invocation to its command.
|
|
2456
2654
|
*
|
|
@@ -2476,7 +2674,7 @@ async function dispatch(parsed, ctx, ui) {
|
|
|
2476
2674
|
if (parsed.command === "setup") return runSetup({
|
|
2477
2675
|
ctx,
|
|
2478
2676
|
ui,
|
|
2479
|
-
prompts: createBrandPrompts(),
|
|
2677
|
+
prompts: parsed.yes || parsed.dryRun ? ASSENTING_PROMPTS : createBrandPrompts(),
|
|
2480
2678
|
dryRun: parsed.dryRun
|
|
2481
2679
|
});
|
|
2482
2680
|
if (parsed.command === "release" && parsed.releaseType !== void 0) return runRelease({
|