@inditextech/docouture-cli 0.1.0-SNAPSHOT.68.1 → 0.1.0-SNAPSHOT.71.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/build/bin.js +54 -4
- package/build/commands/branch-model.js +211 -0
- package/build/commands/completion.js +1 -0
- package/build/commands/doctor.js +11 -1
- package/build/commands/new.js +137 -8
- package/build/commands/upgrade.js +20 -0
- package/build/lib/branch-detect.js +87 -0
- package/build/lib/copy-template.js +4 -0
- package/build/lib/dev-server.js +17 -3
- package/build/lib/doctor-checks.js +40 -0
- package/build/lib/playbook-yml.js +51 -0
- package/build/templates/starter/antora-playbook.local.yml +12 -10
- package/build/templates/starter/antora-playbook.versioned.yml +10 -9
- package/build/templates/starter/antora-playbook.yml +10 -8
- package/build/templates/starter/package.json +1 -0
- package/build/templates/starter/src/antora.yml +2 -2
- package/build/templates/starter/src/modules/main/pages/components.adoc +2 -2
- package/build/templates/workflows/docouture-kroki-cache-warm.yml +31 -17
- package/build/templates/workflows/docouture-publish-prerelease.yml +22 -17
- package/build/templates/workflows/docouture-publish.yml +1 -1
- package/build/templates/workflows/docouture-release-preview.yml +3 -2
- package/build/templates/workflows/docouture-release.yml +70 -57
- package/package.json +1 -1
package/build/bin.js
CHANGED
|
@@ -7,6 +7,7 @@ import { runBuild } from './commands/build.js';
|
|
|
7
7
|
import { runDoctor } from './commands/doctor.js';
|
|
8
8
|
import { runPublish } from './commands/publish.js';
|
|
9
9
|
import { runUpgrade } from './commands/upgrade.js';
|
|
10
|
+
import { runBranchModel } from './commands/branch-model.js';
|
|
10
11
|
import { runEject } from './commands/eject.js';
|
|
11
12
|
import { runTeardown } from './commands/teardown.js';
|
|
12
13
|
import { runCompletion } from './commands/completion.js';
|
|
@@ -22,6 +23,7 @@ const COMMAND_ORDER = [
|
|
|
22
23
|
'publish',
|
|
23
24
|
'doctor',
|
|
24
25
|
'upgrade',
|
|
26
|
+
'branch-model',
|
|
25
27
|
'eject',
|
|
26
28
|
'teardown',
|
|
27
29
|
'completion',
|
|
@@ -31,7 +33,9 @@ const COMMAND_INFO = {
|
|
|
31
33
|
usageLine: 'new <name>',
|
|
32
34
|
summary: 'Scaffold a new documentation site',
|
|
33
35
|
help: `Usage:
|
|
34
|
-
docouture new <name> [--dir <path>] [--title <title>] [--mode standalone|versioned]
|
|
36
|
+
docouture new <name> [--dir <path>] [--title <title>] [--mode standalone|versioned]
|
|
37
|
+
[--flow trunk-based|git-flow] [--branch <name>] [--integration-branch <name>]
|
|
38
|
+
[--release-branch <name>] [--pm npm|pnpm] [--yes]
|
|
35
39
|
|
|
36
40
|
Scaffold an Antora documentation site into docs/ (and its workflows into
|
|
37
41
|
.github/workflows/), plus AGENTS.md and agent skills (.opencode/skills/,
|
|
@@ -47,6 +51,10 @@ Options:
|
|
|
47
51
|
--dir <path> Repository root to scaffold into (default: cwd, or its enclosing repo)
|
|
48
52
|
--title <title> Site title (default: title-cased from <name>)
|
|
49
53
|
--mode <mode> 'standalone' (default) or 'versioned'
|
|
54
|
+
--flow <flow> 'trunk-based' (default) or 'git-flow' — see 'docouture branch-model --help'
|
|
55
|
+
--branch <name> Trunk-based only: the one branch (default: main)
|
|
56
|
+
--integration-branch <name> Git-flow only: the prerelease branch (default: develop)
|
|
57
|
+
--release-branch <name> Git-flow only: the release branch (default: main)
|
|
50
58
|
--pm <pm> 'npm' or 'pnpm' (default: auto-detected from a lockfile/packageManager
|
|
51
59
|
field, or how docouture itself was invoked)
|
|
52
60
|
--yes Skip the interactive wizard, use defaults for anything unset
|
|
@@ -127,7 +135,9 @@ Check that the environment and site configuration are healthy: Node
|
|
|
127
135
|
version, the four names that must agree (component name, start page,
|
|
128
136
|
content path, package name), git history, that antora is installed,
|
|
129
137
|
and (advisory only) whether AGENTS.md and the scaffolded skills are
|
|
130
|
-
still present
|
|
138
|
+
still present, whether the docs/release label exists, and whether the
|
|
139
|
+
declared branching model (docs/package.json's docouture.branching)
|
|
140
|
+
agrees with what antora-playbook.yml/docouture-release.yml actually say.
|
|
131
141
|
|
|
132
142
|
Options:
|
|
133
143
|
--dir <path> Repository root (default: cwd, or its enclosing repo)
|
|
@@ -153,6 +163,45 @@ Options:
|
|
|
153
163
|
--dir <path> Repository root (default: cwd, or its enclosing repo)
|
|
154
164
|
--title <title> Override the title read back from docs/antora.yml
|
|
155
165
|
--dry-run List what would be written without changing anything
|
|
166
|
+
`,
|
|
167
|
+
},
|
|
168
|
+
'branch-model': {
|
|
169
|
+
usageLine: 'branch-model <model>',
|
|
170
|
+
summary: 'Switch between trunk-based and git-flow branching',
|
|
171
|
+
help: `Usage:
|
|
172
|
+
docouture branch-model <trunk-based|git-flow> [--branch <name>]
|
|
173
|
+
[--integration-branch <name>] [--release-branch <name>] [--dir <path>] [--dry-run]
|
|
174
|
+
|
|
175
|
+
Switch an already-scaffolded repository between the trunk-based and
|
|
176
|
+
git-flow branching models — see docs/src/modules/main/pages/
|
|
177
|
+
guides-branching-model.adoc for the full mechanism. Direction is
|
|
178
|
+
inferred from the site's current branch names (read live from
|
|
179
|
+
antora-playbook.yml/docouture-release.yml, never from a stored config)
|
|
180
|
+
versus the <model> argument given here — the same command handles
|
|
181
|
+
both directions.
|
|
182
|
+
|
|
183
|
+
trunk-based -> git-flow: the current single branch becomes the release
|
|
184
|
+
branch by default (least disruption to anything already tagged off
|
|
185
|
+
it) — --integration-branch <name> (the new prerelease branch) is
|
|
186
|
+
required, since there is nothing on disk to infer that name from.
|
|
187
|
+
|
|
188
|
+
git-flow -> trunk-based: lossy — --branch <name> is required and must
|
|
189
|
+
match one of the two current branches exactly; a third, invented name
|
|
190
|
+
is refused.
|
|
191
|
+
|
|
192
|
+
Re-renders .github/workflows/ (same machinery 'docouture upgrade' uses)
|
|
193
|
+
and patches antora-playbook.yml's content.sources[0].branches and
|
|
194
|
+
docs/package.json's docouture.branching field. Does NOT rename actual
|
|
195
|
+
git branches, touch branch-protection/ruleset rules, or change GitHub's
|
|
196
|
+
configured default branch — these stay manual steps, printed as a
|
|
197
|
+
reminder after every real run.
|
|
198
|
+
|
|
199
|
+
Options:
|
|
200
|
+
--branch <name> Trunk-based target only
|
|
201
|
+
--integration-branch <name> Git-flow target only: the prerelease branch
|
|
202
|
+
--release-branch <name> Git-flow target only: the release branch
|
|
203
|
+
--dir <path> Repository root (default: cwd, or its enclosing repo)
|
|
204
|
+
--dry-run List what would be written without changing anything
|
|
156
205
|
`,
|
|
157
206
|
},
|
|
158
207
|
eject: {
|
|
@@ -207,8 +256,8 @@ Examples:
|
|
|
207
256
|
// 'docouture <command> --help' uses for a single command.
|
|
208
257
|
const CLI_SUMMARY = 'Scaffold, build, and publish Antora documentation sites';
|
|
209
258
|
// Left column width of the command table below, including the 2-space
|
|
210
|
-
// indent — wide enough for the longest usageLine ('
|
|
211
|
-
const COMMAND_COLUMN_WIDTH =
|
|
259
|
+
// indent — wide enough for the longest usageLine ('branch-model <model>').
|
|
260
|
+
const COMMAND_COLUMN_WIDTH = 24;
|
|
212
261
|
const USAGE = `Usage: docouture <command> [options]
|
|
213
262
|
|
|
214
263
|
Commands:
|
|
@@ -234,6 +283,7 @@ const RUNNERS = {
|
|
|
234
283
|
publish: runPublish,
|
|
235
284
|
doctor: runDoctor,
|
|
236
285
|
upgrade: runUpgrade,
|
|
286
|
+
'branch-model': runBranchModel,
|
|
237
287
|
eject: runEject,
|
|
238
288
|
teardown: runTeardown,
|
|
239
289
|
completion: async (argv) => runCompletion(argv),
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { parseArgs } from '../lib/args.js';
|
|
7
|
+
import { readCliInfo } from '../lib/cli-info.js';
|
|
8
|
+
import { copyTemplate, exists } from '../lib/copy-template.js';
|
|
9
|
+
import { detectPackageManager, packageManagerPlan } from '../lib/detect-package-manager.js';
|
|
10
|
+
import { findRepoRoot } from '../lib/repo-root.js';
|
|
11
|
+
import { readBranches, writeBranches } from '../lib/playbook-yml.js';
|
|
12
|
+
import { cacheWarmBranchesYaml, detectBranches } from '../lib/branch-detect.js';
|
|
13
|
+
const BRANCH_MODELS = ['trunk-based', 'git-flow'];
|
|
14
|
+
// Same duplication precedent upgrade.ts's own isInsideGitWorkTree comment
|
|
15
|
+
// documents (doctor-checks.ts already sets it) — a small piece of new.ts's
|
|
16
|
+
// logic reused rather than importing the wizard-heavy module it lives in.
|
|
17
|
+
function isInsideGitWorkTree(dir) {
|
|
18
|
+
try {
|
|
19
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir, stdio: 'ignore' });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function readAntoraField(content, key) {
|
|
27
|
+
const match = new RegExp(`^${key}:.*$`, 'm').exec(content);
|
|
28
|
+
if (!match)
|
|
29
|
+
return null;
|
|
30
|
+
const value = match[0].slice(key.length + 1).trim();
|
|
31
|
+
return value.length > 0 ? value : null;
|
|
32
|
+
}
|
|
33
|
+
// The one JSON write this command performs — a single field
|
|
34
|
+
// (`docouture.branching`), read-modify-write rather than a regex patch
|
|
35
|
+
// (unlike antora-playbook.yml/docouture-release.yml, package.json is
|
|
36
|
+
// already JSON, so there is no comment-preservation reason to avoid
|
|
37
|
+
// JSON.parse/stringify here the way playbook-yml.ts avoids a full YAML
|
|
38
|
+
// round-trip). Re-serializes the whole file at 2-space indent — matching
|
|
39
|
+
// every template's own style — rather than only touching the one line, so
|
|
40
|
+
// key ordering is preserved (JSON.parse/stringify both preserve insertion
|
|
41
|
+
// order) but incidental whitespace a human introduced is not.
|
|
42
|
+
async function writeBranching(packageJsonFile, branching) {
|
|
43
|
+
const content = await readFile(packageJsonFile, 'utf8');
|
|
44
|
+
const pkg = JSON.parse(content);
|
|
45
|
+
pkg.docouture = { ...pkg.docouture, branching };
|
|
46
|
+
await writeFile(packageJsonFile, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
export async function runBranchModel(argv) {
|
|
49
|
+
const { positional, flags } = parseArgs(argv);
|
|
50
|
+
const target = positional[0];
|
|
51
|
+
if (!target || !BRANCH_MODELS.includes(target)) {
|
|
52
|
+
console.error(`usage: docouture branch-model <trunk-based|git-flow> [--branch <name>] [--integration-branch <name>] [--release-branch <name>] [--dir <path>] [--dry-run]`);
|
|
53
|
+
console.error(`invalid target: '${target ?? ''}' — expected 'trunk-based' or 'git-flow'`);
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
const targetModel = target;
|
|
57
|
+
const dryRun = flags['dry-run'] === true;
|
|
58
|
+
const branchFlag = typeof flags.branch === 'string' ? flags.branch : undefined;
|
|
59
|
+
const integrationBranchFlag = typeof flags['integration-branch'] === 'string' ? flags['integration-branch'] : undefined;
|
|
60
|
+
const releaseBranchFlag = typeof flags['release-branch'] === 'string' ? flags['release-branch'] : undefined;
|
|
61
|
+
const startDir = typeof flags.dir === 'string' ? resolve(flags.dir) : resolve(process.cwd());
|
|
62
|
+
if (!isInsideGitWorkTree(startDir)) {
|
|
63
|
+
console.error(`'${startDir}' is not inside a git repository`);
|
|
64
|
+
console.error('docouture branch-model re-configures an already-scaffolded repository — run it from your repo root, or pass --dir <path> to one');
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const repoRoot = await findRepoRoot(startDir);
|
|
68
|
+
const siteRoot = join(repoRoot, 'docs');
|
|
69
|
+
if (!(await exists(siteRoot))) {
|
|
70
|
+
console.error(`no site found at '${siteRoot}'`);
|
|
71
|
+
console.error('pass --dir <path> to a component root (the parent of docs/), or run docouture new first');
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
// Always derived live, never read back from a stored config — see
|
|
75
|
+
// lib/branch-detect.ts's own comment on why. Falls back to nothing
|
|
76
|
+
// special for a site scaffolded before this feature existed: its
|
|
77
|
+
// antora-playbook.yml/docouture-release.yml already have the exact same
|
|
78
|
+
// shape (one literal branch, `main` in both places), so this is the only
|
|
79
|
+
// path, not a legacy-only fallback.
|
|
80
|
+
const detected = await detectBranches(siteRoot, repoRoot);
|
|
81
|
+
if (!detected.prerelease || !detected.release) {
|
|
82
|
+
console.error('could not determine this site\u2019s current branch names');
|
|
83
|
+
console.error(` antora-playbook.yml content.sources[0].branches: ${detected.prerelease ?? '(not found)'}`);
|
|
84
|
+
console.error(` docouture-release.yml checkout ref: ${detected.release ?? '(not found)'}`);
|
|
85
|
+
console.error('is this a docouture-scaffolded site? (run docouture new first, or check --dir)');
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
const currentModel = detected.prerelease === detected.release ? 'trunk-based' : 'git-flow';
|
|
89
|
+
let newPrerelease;
|
|
90
|
+
let newRelease;
|
|
91
|
+
if (targetModel === 'trunk-based') {
|
|
92
|
+
if (currentModel === 'git-flow') {
|
|
93
|
+
if (!branchFlag) {
|
|
94
|
+
console.error('switching from git-flow to trunk-based requires --branch <name>');
|
|
95
|
+
console.error(` must match one of the current two branches: '${detected.prerelease}' or '${detected.release}'`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
if (branchFlag !== detected.prerelease && branchFlag !== detected.release) {
|
|
99
|
+
console.error(`--branch '${branchFlag}' does not match either current branch`);
|
|
100
|
+
console.error(` current prerelease branch: '${detected.prerelease}'`);
|
|
101
|
+
console.error(` current release branch: '${detected.release}'`);
|
|
102
|
+
console.error('refusing to invent a third branch name — collapsing two branches into one is lossy: pick one of the two that already exist');
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
newPrerelease = branchFlag;
|
|
106
|
+
newRelease = branchFlag;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const branch = branchFlag ?? detected.release;
|
|
110
|
+
newPrerelease = branch;
|
|
111
|
+
newRelease = branch;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (currentModel === 'trunk-based') {
|
|
116
|
+
// Least-disruption default (see the issue's own design note): the
|
|
117
|
+
// current single branch becomes *release* — whatever already got
|
|
118
|
+
// tagged off it keeps working unchanged — and a NEW name is required
|
|
119
|
+
// for the integration/prerelease branch, since there is nothing
|
|
120
|
+
// already on disk to infer that from.
|
|
121
|
+
if (!integrationBranchFlag) {
|
|
122
|
+
console.error('switching from trunk-based to git-flow requires --integration-branch <name>');
|
|
123
|
+
console.error(` the current branch ('${detected.release}') becomes the release branch by default`);
|
|
124
|
+
console.error(' (override with --release-branch if you want a different name for it too)');
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
newPrerelease = integrationBranchFlag;
|
|
128
|
+
newRelease = releaseBranchFlag ?? detected.release;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
newPrerelease = integrationBranchFlag ?? detected.prerelease;
|
|
132
|
+
newRelease = releaseBranchFlag ?? detected.release;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (targetModel === currentModel && newPrerelease === detected.prerelease && newRelease === detected.release) {
|
|
136
|
+
console.log(`already ${currentModel} (${newPrerelease === newRelease ? newPrerelease : `${newPrerelease}/${newRelease}`}) — nothing to do`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
140
|
+
const templatesRoot = join(here, '..', 'templates');
|
|
141
|
+
const workflowsTemplateDir = join(templatesRoot, 'workflows');
|
|
142
|
+
const workflowsDir = join(repoRoot, '.github', 'workflows');
|
|
143
|
+
const { version: cliVersion } = await readCliInfo(import.meta.url, 2);
|
|
144
|
+
const pm = packageManagerPlan(detectPackageManager(repoRoot));
|
|
145
|
+
const descriptorPath = join(siteRoot, 'src', 'antora.yml');
|
|
146
|
+
let name = 'docs';
|
|
147
|
+
let title = 'Docs';
|
|
148
|
+
if (await exists(descriptorPath)) {
|
|
149
|
+
const content = await readFile(descriptorPath, 'utf8');
|
|
150
|
+
name = readAntoraField(content, 'name') ?? name;
|
|
151
|
+
title = readAntoraField(content, 'title') ?? title;
|
|
152
|
+
}
|
|
153
|
+
const values = {
|
|
154
|
+
name,
|
|
155
|
+
title,
|
|
156
|
+
// Same reasoning as upgrade.ts's own comment: neither placeholder
|
|
157
|
+
// appears in any file this command re-renders (workflows only), so
|
|
158
|
+
// there is nothing meaningful to compute either from.
|
|
159
|
+
componentName: 'unused-by-branch-model',
|
|
160
|
+
cliVersion,
|
|
161
|
+
pmName: pm.pm,
|
|
162
|
+
pmCacheName: pm.cacheName,
|
|
163
|
+
pmLockfile: pm.lockfile,
|
|
164
|
+
pmCiCmd: pm.ciCmd,
|
|
165
|
+
pmSetupStepYaml: pm.setupStepYaml,
|
|
166
|
+
pmPackageManagerField: 'unused-by-branch-model',
|
|
167
|
+
repoIgnoreGlob: 'unused-by-branch-model',
|
|
168
|
+
prereleaseBranch: newPrerelease,
|
|
169
|
+
releaseBranch: newRelease,
|
|
170
|
+
// docs/package.json is patched directly below (writeBranching), not via
|
|
171
|
+
// copyTemplate — this placeholder only ever appears in that file.
|
|
172
|
+
branching: 'unused-by-branch-model',
|
|
173
|
+
cacheWarmBranchesYaml: cacheWarmBranchesYaml(newPrerelease, newRelease),
|
|
174
|
+
};
|
|
175
|
+
const playbookFile = join(siteRoot, 'antora-playbook.yml');
|
|
176
|
+
const packageJsonFile = join(siteRoot, 'package.json');
|
|
177
|
+
if (dryRun) {
|
|
178
|
+
const plannedWorkflows = await copyTemplate(workflowsTemplateDir, workflowsDir, values, { dryRun: true });
|
|
179
|
+
console.log(`would switch ${currentModel} -> ${targetModel}`);
|
|
180
|
+
console.log(` prerelease branch: '${detected.prerelease}' -> '${newPrerelease}'`);
|
|
181
|
+
console.log(` release branch: '${detected.release}' -> '${newRelease}'`);
|
|
182
|
+
console.log('would write:');
|
|
183
|
+
for (const path of [...plannedWorkflows, playbookFile, packageJsonFile]) {
|
|
184
|
+
console.log(` ${relative(repoRoot, path)}`);
|
|
185
|
+
}
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
await copyTemplate(workflowsTemplateDir, workflowsDir, values);
|
|
189
|
+
const playbookContent = await readFile(playbookFile, 'utf8');
|
|
190
|
+
// Only rewritten if the prerelease branch actually changed and the line
|
|
191
|
+
// is still there in the shape readBranches/writeBranches expect —
|
|
192
|
+
// guards against a hand-edited playbook this regex can't safely touch.
|
|
193
|
+
if (newPrerelease !== detected.prerelease && readBranches(playbookContent)) {
|
|
194
|
+
await writeFile(playbookFile, writeBranches(playbookContent, newPrerelease), 'utf8');
|
|
195
|
+
}
|
|
196
|
+
await writeBranching(packageJsonFile, targetModel);
|
|
197
|
+
console.log(`switched ${currentModel} -> ${targetModel}`);
|
|
198
|
+
console.log(` prerelease branch: '${detected.prerelease}' -> '${newPrerelease}'`);
|
|
199
|
+
console.log(` release branch: '${detected.release}' -> '${newRelease}'`);
|
|
200
|
+
console.log(`updated ${relative(repoRoot, workflowsDir)}`);
|
|
201
|
+
console.log(`updated ${relative(repoRoot, playbookFile)}`);
|
|
202
|
+
console.log(`updated ${relative(repoRoot, packageJsonFile)}`);
|
|
203
|
+
console.log('');
|
|
204
|
+
console.log('this command does NOT:');
|
|
205
|
+
console.log(' - rename actual git branches');
|
|
206
|
+
console.log(' - touch branch-protection/ruleset rules');
|
|
207
|
+
console.log(" - change GitHub's configured default branch");
|
|
208
|
+
console.log('these are still manual steps — see docs/src/modules/main/pages/guides-branching-model.adoc');
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=branch-model.js.map
|
package/build/commands/doctor.js
CHANGED
|
@@ -7,7 +7,8 @@ import { findRepoRoot } from '../lib/repo-root.js';
|
|
|
7
7
|
import { getContext } from '../lib/cli-context.js';
|
|
8
8
|
import { theme } from '../lib/theme.js';
|
|
9
9
|
import { readSourceUrl, readStartPageComponent, readStartPath } from '../lib/playbook-yml.js';
|
|
10
|
-
import {
|
|
10
|
+
import { detectBranches, inferBranching } from '../lib/branch-detect.js';
|
|
11
|
+
import { checkAgentFilesPresent, checkAntoraAvailable, checkBranchingAgrees, checkGitHasCommit, checkNamesAgree, checkNodeVersion, checkReleaseLabelExists, } from '../lib/doctor-checks.js';
|
|
11
12
|
async function readJson(file) {
|
|
12
13
|
try {
|
|
13
14
|
return JSON.parse(await readFile(file, 'utf8'));
|
|
@@ -148,6 +149,15 @@ export async function runDoctor(argv) {
|
|
|
148
149
|
record(report, 'release', releaseResult, 'warn');
|
|
149
150
|
if (!json)
|
|
150
151
|
printAdvisory(releaseResult);
|
|
152
|
+
log('branching');
|
|
153
|
+
const detectedBranches = await detectBranches(siteRoot, target);
|
|
154
|
+
const branchingResult = checkBranchingAgrees({
|
|
155
|
+
declaredBranching: pkg?.docouture?.branching ?? null,
|
|
156
|
+
actualBranching: inferBranching(detectedBranches),
|
|
157
|
+
});
|
|
158
|
+
record(report, 'branching', branchingResult, 'warn');
|
|
159
|
+
if (!json)
|
|
160
|
+
printAdvisory(branchingResult);
|
|
151
161
|
if (json) {
|
|
152
162
|
process.stdout.write(`${JSON.stringify({ status: status === 0 ? 'ok' : 'fail', checks: report }, null, 2)}\n`);
|
|
153
163
|
}
|
package/build/commands/new.js
CHANGED
|
@@ -11,6 +11,7 @@ import { copyTemplate, exists, isEmptyOrMissing, renderTemplateFile, writeTempla
|
|
|
11
11
|
import { detectPackageManager, packageManagerPlan, } from '../lib/detect-package-manager.js';
|
|
12
12
|
import { findRepoRoot } from '../lib/repo-root.js';
|
|
13
13
|
import { AGENTS_MD_FILENAME, hasManagedSection, mergeAgentsMd } from '../lib/agents-md.js';
|
|
14
|
+
import { cacheWarmBranchesYaml } from '../lib/branch-detect.js';
|
|
14
15
|
// Matches the rule an npm package name (and, not coincidentally, an Antora
|
|
15
16
|
// component name — both end up as URL segments) can safely be: this is
|
|
16
17
|
// stricter than npm's own rule, which also allows dots and a leading `@scope/`
|
|
@@ -30,6 +31,15 @@ const NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
|
30
31
|
// here: it only ever comes up as an ad-hoc `docouture dev` preview before a mode
|
|
31
32
|
// is chosen, never as something worth releasing.)
|
|
32
33
|
const MODES = ['standalone', 'versioned'];
|
|
34
|
+
// The two branching models `docouture new` can scaffold for — see the
|
|
35
|
+
// guides-branching-model guide, and GH #175. 'trunk-based' (the default):
|
|
36
|
+
// one branch plays both roles (prerelease AND release) — the degenerate
|
|
37
|
+
// case, not a second code path anywhere prereleaseBranch/releaseBranch are
|
|
38
|
+
// used. 'git-flow': two independently-named branches, e.g. `develop`
|
|
39
|
+
// (prerelease) and `main` (release) — see antora-playbook.yml's own comment
|
|
40
|
+
// on what "prerelease branch" means, and docouture-release.yml's on
|
|
41
|
+
// "release branch".
|
|
42
|
+
const BRANCH_MODELS = ['trunk-based', 'git-flow'];
|
|
33
43
|
const PACKAGE_MANAGERS = ['npm', 'pnpm'];
|
|
34
44
|
// Filenames docouture-publish.yml / docouture-publish-prerelease.yml /
|
|
35
45
|
// docouture-release.yml / docouture-release-preview.yml / docouture-pr-verify.yml /
|
|
@@ -229,6 +239,47 @@ async function promptWizard(io, initial, defaults) {
|
|
|
229
239
|
},
|
|
230
240
|
],
|
|
231
241
|
}, context));
|
|
242
|
+
// Branching model — see BRANCH_MODELS' own comment. Unconditional, same
|
|
243
|
+
// as Versioning mode above; the follow-up branch-name question(s) below
|
|
244
|
+
// are this wizard's first CONDITIONAL prompt.
|
|
245
|
+
const branching = initial.branching ??
|
|
246
|
+
(await select({
|
|
247
|
+
message: 'Branching model:\n',
|
|
248
|
+
default: 'trunk-based',
|
|
249
|
+
choices: [
|
|
250
|
+
{
|
|
251
|
+
name: 'Trunk-based',
|
|
252
|
+
value: 'trunk-based',
|
|
253
|
+
description: 'One long-lived branch plays both the prerelease and release roles — e.g. main.',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'Git-flow',
|
|
257
|
+
value: 'git-flow',
|
|
258
|
+
description: 'Two independently-named branches — an integration branch (e.g. develop) for prerelease, ' +
|
|
259
|
+
'a release branch (e.g. main) for cutting releases.',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
}, context));
|
|
263
|
+
let prereleaseBranch;
|
|
264
|
+
let releaseBranch;
|
|
265
|
+
if (branching === 'git-flow') {
|
|
266
|
+
releaseBranch = (await input({
|
|
267
|
+
message: 'Release branch (docouture-release.yml checks this out and cuts tags from it):\n',
|
|
268
|
+
default: initial.releaseBranch ?? 'main',
|
|
269
|
+
}, context)).trim();
|
|
270
|
+
prereleaseBranch = (await input({
|
|
271
|
+
message: 'Integration/prerelease branch (antora-playbook.yml tracks this as the live prerelease version):\n',
|
|
272
|
+
default: initial.prereleaseBranch ?? 'develop',
|
|
273
|
+
}, context)).trim();
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const branch = (await input({
|
|
277
|
+
message: 'Branch (plays both the prerelease and release roles):\n',
|
|
278
|
+
default: initial.branch ?? 'main',
|
|
279
|
+
}, context)).trim();
|
|
280
|
+
prereleaseBranch = branch;
|
|
281
|
+
releaseBranch = branch;
|
|
282
|
+
}
|
|
232
283
|
const pm = initial.pm ??
|
|
233
284
|
(await select({
|
|
234
285
|
message: 'Package manager:\n',
|
|
@@ -238,7 +289,16 @@ async function promptWizard(io, initial, defaults) {
|
|
|
238
289
|
{ name: 'pnpm', value: 'pnpm' },
|
|
239
290
|
],
|
|
240
291
|
}, context));
|
|
241
|
-
return {
|
|
292
|
+
return {
|
|
293
|
+
name,
|
|
294
|
+
title: title.length > 0 ? title : defaultTitle,
|
|
295
|
+
urlSegment,
|
|
296
|
+
mode,
|
|
297
|
+
branching,
|
|
298
|
+
prereleaseBranch,
|
|
299
|
+
releaseBranch,
|
|
300
|
+
pm,
|
|
301
|
+
};
|
|
242
302
|
}
|
|
243
303
|
export async function runNew(argv, io = defaultIO()) {
|
|
244
304
|
const { positional, flags } = parseArgs(argv);
|
|
@@ -248,6 +308,12 @@ export async function runNew(argv, io = defaultIO()) {
|
|
|
248
308
|
// already "off", so the only thing worth scripting is turning it on.
|
|
249
309
|
let urlSegment = flags['url-segment'] === true ? true : undefined;
|
|
250
310
|
let mode;
|
|
311
|
+
let branching;
|
|
312
|
+
let prereleaseBranch;
|
|
313
|
+
let releaseBranch;
|
|
314
|
+
const branchFlag = typeof flags.branch === 'string' ? flags.branch : undefined;
|
|
315
|
+
const integrationBranchFlag = typeof flags['integration-branch'] === 'string' ? flags['integration-branch'] : undefined;
|
|
316
|
+
const releaseBranchFlag = typeof flags['release-branch'] === 'string' ? flags['release-branch'] : undefined;
|
|
251
317
|
let pmChoice;
|
|
252
318
|
if (typeof flags.mode === 'string') {
|
|
253
319
|
if (!MODES.includes(flags.mode)) {
|
|
@@ -256,6 +322,13 @@ export async function runNew(argv, io = defaultIO()) {
|
|
|
256
322
|
}
|
|
257
323
|
mode = flags.mode;
|
|
258
324
|
}
|
|
325
|
+
if (typeof flags.flow === 'string') {
|
|
326
|
+
if (!BRANCH_MODELS.includes(flags.flow)) {
|
|
327
|
+
console.error(`invalid --flow: '${flags.flow}' — expected 'trunk-based' or 'git-flow'`);
|
|
328
|
+
return 1;
|
|
329
|
+
}
|
|
330
|
+
branching = flags.flow;
|
|
331
|
+
}
|
|
259
332
|
if (typeof flags.pm === 'string') {
|
|
260
333
|
if (!PACKAGE_MANAGERS.includes(flags.pm)) {
|
|
261
334
|
console.error(`invalid --pm: '${flags.pm}' — expected 'npm' or 'pnpm'`);
|
|
@@ -275,19 +348,48 @@ export async function runNew(argv, io = defaultIO()) {
|
|
|
275
348
|
// gets exactly today's non-interactive behaviour, defaults and all.
|
|
276
349
|
const skipWizard = flags.yes === true || !io.isTTY;
|
|
277
350
|
if (!skipWizard) {
|
|
278
|
-
const answers = await promptWizard(io, {
|
|
351
|
+
const answers = await promptWizard(io, {
|
|
352
|
+
name,
|
|
353
|
+
title,
|
|
354
|
+
urlSegment,
|
|
355
|
+
mode,
|
|
356
|
+
branching,
|
|
357
|
+
branch: branchFlag,
|
|
358
|
+
prereleaseBranch: integrationBranchFlag,
|
|
359
|
+
releaseBranch: releaseBranchFlag,
|
|
360
|
+
pm: pmChoice,
|
|
361
|
+
}, { pm: pmGuess });
|
|
279
362
|
name = answers.name;
|
|
280
363
|
title = answers.title;
|
|
281
364
|
urlSegment = answers.urlSegment;
|
|
282
365
|
mode = answers.mode;
|
|
366
|
+
branching = answers.branching;
|
|
367
|
+
prereleaseBranch = answers.prereleaseBranch;
|
|
368
|
+
releaseBranch = answers.releaseBranch;
|
|
283
369
|
pmChoice = answers.pm;
|
|
284
370
|
io.output.write('\n');
|
|
285
371
|
}
|
|
286
372
|
mode = mode ?? 'standalone';
|
|
373
|
+
branching = branching ?? 'trunk-based';
|
|
287
374
|
pmChoice = pmChoice ?? pmGuess;
|
|
288
375
|
urlSegment = urlSegment ?? false;
|
|
376
|
+
// Non-interactive path (--yes, or no TTY): resolve from flags/defaults
|
|
377
|
+
// the same way the wizard would have, rather than leaving these unset —
|
|
378
|
+
// trunk-based collapses to one branch name playing both roles, same as
|
|
379
|
+
// BRANCH_MODELS' own comment describes.
|
|
380
|
+
if (branching === 'git-flow') {
|
|
381
|
+
prereleaseBranch = prereleaseBranch ?? integrationBranchFlag ?? 'develop';
|
|
382
|
+
releaseBranch = releaseBranch ?? releaseBranchFlag ?? 'main';
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const branch = branchFlag ?? 'main';
|
|
386
|
+
prereleaseBranch = prereleaseBranch ?? branch;
|
|
387
|
+
releaseBranch = releaseBranch ?? branch;
|
|
388
|
+
}
|
|
289
389
|
if (!name) {
|
|
290
|
-
console.error('usage: docouture new <name> [--dir <path>] [--title <title>] [--url-segment] [--mode standalone|versioned]
|
|
390
|
+
console.error('usage: docouture new <name> [--dir <path>] [--title <title>] [--url-segment] [--mode standalone|versioned] ' +
|
|
391
|
+
'[--flow trunk-based|git-flow] [--branch <name>] [--integration-branch <name>] [--release-branch <name>] ' +
|
|
392
|
+
'[--pm npm|pnpm]');
|
|
291
393
|
return 1;
|
|
292
394
|
}
|
|
293
395
|
if (!NAME_PATTERN.test(name)) {
|
|
@@ -396,6 +498,10 @@ export async function runNew(argv, io = defaultIO()) {
|
|
|
396
498
|
pmSetupStepYaml: pm.setupStepYaml,
|
|
397
499
|
pmPackageManagerField: pm.packageManagerField,
|
|
398
500
|
repoIgnoreGlob: repoIgnoreGlob(target),
|
|
501
|
+
prereleaseBranch,
|
|
502
|
+
releaseBranch,
|
|
503
|
+
branching,
|
|
504
|
+
cacheWarmBranchesYaml: cacheWarmBranchesYaml(prereleaseBranch, releaseBranch),
|
|
399
505
|
};
|
|
400
506
|
// The whole starter subtree — package.json, antora-playbook.yml, its own
|
|
401
507
|
// nested src/antora.yml — lands under <repo-root>/docs/ as one piece,
|
|
@@ -418,7 +524,17 @@ export async function runNew(argv, io = defaultIO()) {
|
|
|
418
524
|
await writeTemplateFile(join(starterDir, 'antora-playbook.versioned.yml'), join(docsDir, 'antora-playbook.yml'), values);
|
|
419
525
|
await writeTemplateFile(join(starterDir, 'src', 'release-version.versioned'), join(docsDir, '.release-version'), values);
|
|
420
526
|
}
|
|
421
|
-
printNextSteps({
|
|
527
|
+
printNextSteps({
|
|
528
|
+
mode,
|
|
529
|
+
branching,
|
|
530
|
+
prereleaseBranch,
|
|
531
|
+
releaseBranch,
|
|
532
|
+
pm,
|
|
533
|
+
target,
|
|
534
|
+
docsDir,
|
|
535
|
+
workflowsDir,
|
|
536
|
+
ghPagesUrl: githubPagesUrl(target),
|
|
537
|
+
});
|
|
422
538
|
return 0;
|
|
423
539
|
}
|
|
424
540
|
// Everything printed after scaffolding finishes, grouped into labeled
|
|
@@ -426,7 +542,7 @@ export async function runNew(argv, io = defaultIO()) {
|
|
|
426
542
|
// "created X" lines. Headers use the shared theme's bold, matching bin.ts's
|
|
427
543
|
// own banner — see lib/theme.ts for why that's not picocolors directly.
|
|
428
544
|
function printNextSteps(args) {
|
|
429
|
-
const { mode, pm, target, docsDir, workflowsDir, ghPagesUrl } = args;
|
|
545
|
+
const { mode, branching, prereleaseBranch, releaseBranch, pm, target, docsDir, workflowsDir, ghPagesUrl } = args;
|
|
430
546
|
const created = (path) => ` ${theme.success('✓')} ${path}`;
|
|
431
547
|
// Relative to `target` (the repo root), not process.cwd() — cwd may be a
|
|
432
548
|
// nested subdirectory findRepoRoot walked up from (see runNew), in which
|
|
@@ -453,9 +569,22 @@ function printNextSteps(args) {
|
|
|
453
569
|
console.log(' docouture-documenting-changes, docouture-authoring-guides, docouture-writing-docs-pages,');
|
|
454
570
|
console.log(' docouture-docs-internals and, for a versioned-mode site, docouture-docs-versioning)');
|
|
455
571
|
console.log('');
|
|
572
|
+
if (branching === 'git-flow') {
|
|
573
|
+
console.log(theme.bold('Branching: Git-flow'));
|
|
574
|
+
console.log(` ${prereleaseBranch} is the prerelease/integration branch; ${releaseBranch} is the release branch.`);
|
|
575
|
+
console.log(` docouture-release.yml only runs against ${releaseBranch}; ordinary content merges into`);
|
|
576
|
+
console.log(` ${prereleaseBranch} publish continuously via docouture-publish-prerelease.yml.`);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
console.log(theme.bold('Branching: Trunk-based'));
|
|
580
|
+
console.log(` ${releaseBranch} plays both the prerelease and release roles.`);
|
|
581
|
+
}
|
|
582
|
+
console.log(' See docs/src/modules/main/pages/guides-branching-model.adoc for the full lifecycle,');
|
|
583
|
+
console.log(" and 'docouture branch-model' to switch later.");
|
|
584
|
+
console.log('');
|
|
456
585
|
if (mode === 'versioned') {
|
|
457
586
|
console.log(theme.bold('Versioning: Versioned (Full History)'));
|
|
458
|
-
console.log(
|
|
587
|
+
console.log(` ${prereleaseBranch} is the prerelease channel.`);
|
|
459
588
|
console.log('');
|
|
460
589
|
console.log(' To cut your first release:');
|
|
461
590
|
console.log(" 1. Create the 'docs/release' label (once): gh label create docs/release");
|
|
@@ -470,11 +599,11 @@ function printNextSteps(args) {
|
|
|
470
599
|
}
|
|
471
600
|
else {
|
|
472
601
|
console.log(theme.bold('Versioning: Standalone (Stable + Prerelease)'));
|
|
473
|
-
console.log(
|
|
602
|
+
console.log(` ${prereleaseBranch} is the prerelease channel.`);
|
|
474
603
|
console.log('');
|
|
475
604
|
console.log(' To cut your first stable release:');
|
|
476
605
|
console.log(" 1. Create the 'docs/release' label (once): gh label create docs/release");
|
|
477
|
-
console.log(
|
|
606
|
+
console.log(` 2. Merge any PR labeled 'docs/release' into ${releaseBranch} — docouture-release.yml runs automatically`);
|
|
478
607
|
console.log('');
|
|
479
608
|
console.log(' (or skip the label/PR entirely: run docouture-release.yml manually via workflow_dispatch,');
|
|
480
609
|
console.log(' default input is fine)');
|
|
@@ -10,6 +10,7 @@ import { detectPackageManager, packageManagerPlan } from '../lib/detect-package-
|
|
|
10
10
|
import { resolveConfig } from '../lib/config-resolver.js';
|
|
11
11
|
import { findRepoRoot } from '../lib/repo-root.js';
|
|
12
12
|
import { AGENTS_MD_FILENAME, mergeAgentsMd } from '../lib/agents-md.js';
|
|
13
|
+
import { cacheWarmBranchesYaml, detectBranches } from '../lib/branch-detect.js';
|
|
13
14
|
// Same check `new.ts` uses, duplicated rather than imported — it's three
|
|
14
15
|
// lines, and `doctor-checks.ts` already sets the precedent of duplicating a
|
|
15
16
|
// small piece of `new.ts`'s own logic instead of reaching across into a
|
|
@@ -93,6 +94,18 @@ export async function runUpgrade(argv) {
|
|
|
93
94
|
// --title flag > the value already recorded in docs/antora.yml > a plain
|
|
94
95
|
// title-cased fallback computed from the name above.
|
|
95
96
|
const { title } = resolveConfig({ title: titleCase(name) }, { title: fromYmlTitle }, { title: typeof flags.title === 'string' ? flags.title : undefined });
|
|
97
|
+
// Re-derived live too, same reasoning as name/title above — upgrade
|
|
98
|
+
// re-copies every workflow template (unlike docs/), and those templates
|
|
99
|
+
// now carry __DOCOUTURE_PRERELEASE_BRANCH__/__DOCOUTURE_RELEASE_BRANCH__
|
|
100
|
+
// (GH #175) — real branch names are required here, not stubs, or an
|
|
101
|
+
// upgrade would silently overwrite a git-flow site's workflows back to
|
|
102
|
+
// whatever these placeholders' un-substituted default would be. See
|
|
103
|
+
// lib/branch-detect.ts's own comment on why these are derived live
|
|
104
|
+
// rather than read back from a stored config.
|
|
105
|
+
const siteRoot = join(target, 'docs');
|
|
106
|
+
const detected = await detectBranches(siteRoot, target);
|
|
107
|
+
const prereleaseBranch = detected.prerelease ?? 'main';
|
|
108
|
+
const releaseBranch = detected.release ?? 'main';
|
|
96
109
|
const values = {
|
|
97
110
|
name,
|
|
98
111
|
title,
|
|
@@ -119,6 +132,13 @@ export async function runUpgrade(argv) {
|
|
|
119
132
|
// pattern matching every URL, which would be a silent, dangerous no-op
|
|
120
133
|
// if this value were ever actually substituted somewhere.
|
|
121
134
|
repoIgnoreGlob: 'unused-by-upgrade',
|
|
135
|
+
prereleaseBranch,
|
|
136
|
+
releaseBranch,
|
|
137
|
+
// docs/package.json (the only template file this placeholder appears
|
|
138
|
+
// in) is never re-copied by upgrade — same reasoning as
|
|
139
|
+
// pmPackageManagerField/repoIgnoreGlob above.
|
|
140
|
+
branching: 'unused-by-upgrade',
|
|
141
|
+
cacheWarmBranchesYaml: cacheWarmBranchesYaml(prereleaseBranch, releaseBranch),
|
|
122
142
|
};
|
|
123
143
|
const workflowsDir = join(target, '.github', 'workflows');
|
|
124
144
|
const agentsMdFile = join(target, AGENTS_MD_FILENAME);
|