@fuzdev/fuz_gitops 0.72.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/PageHeader.svelte +3 -3
- package/dist/PageHeader.svelte.d.ts +3 -3
- package/dist/PageHeader.svelte.d.ts.map +1 -1
- package/dist/changeset_generator.d.ts.map +1 -1
- package/dist/changeset_generator.js +2 -16
- package/dist/changeset_reader.d.ts +1 -1
- package/dist/changeset_reader.d.ts.map +1 -1
- package/dist/changeset_reader.js +1 -1
- package/dist/dependency_graph.d.ts.map +1 -1
- package/dist/dependency_graph.js +6 -6
- package/dist/fetch_repo_data.d.ts.map +1 -1
- package/dist/fetch_repo_data.js +3 -2
- package/dist/git_operations.d.ts +5 -0
- package/dist/git_operations.d.ts.map +1 -1
- package/dist/git_operations.js +5 -0
- package/dist/gitops_analyze.task.d.ts +1 -0
- package/dist/gitops_analyze.task.d.ts.map +1 -1
- package/dist/gitops_analyze.task.js +12 -17
- package/dist/gitops_plan.task.d.ts +1 -0
- package/dist/gitops_plan.task.d.ts.map +1 -1
- package/dist/gitops_plan.task.js +9 -2
- package/dist/gitops_publish.task.d.ts +2 -2
- package/dist/gitops_publish.task.d.ts.map +1 -1
- package/dist/gitops_publish.task.js +83 -28
- package/dist/gitops_run.task.d.ts +2 -1
- package/dist/gitops_run.task.d.ts.map +1 -1
- package/dist/gitops_run.task.js +40 -7
- package/dist/gitops_sync.task.d.ts +1 -0
- package/dist/gitops_sync.task.d.ts.map +1 -1
- package/dist/gitops_sync.task.js +16 -2
- package/dist/gitops_task_helpers.d.ts +16 -2
- package/dist/gitops_task_helpers.d.ts.map +1 -1
- package/dist/gitops_task_helpers.js +11 -3
- package/dist/gitops_validate.task.d.ts +1 -0
- package/dist/gitops_validate.task.d.ts.map +1 -1
- package/dist/gitops_validate.task.js +20 -20
- package/dist/graph_validation.d.ts +16 -1
- package/dist/graph_validation.d.ts.map +1 -1
- package/dist/graph_validation.js +15 -0
- package/dist/local_repo.d.ts +25 -12
- package/dist/local_repo.d.ts.map +1 -1
- package/dist/local_repo.js +99 -79
- package/dist/log_helpers.d.ts +0 -5
- package/dist/log_helpers.d.ts.map +1 -1
- package/dist/log_helpers.js +8 -19
- package/dist/multi_repo_publisher.d.ts +21 -2
- package/dist/multi_repo_publisher.d.ts.map +1 -1
- package/dist/multi_repo_publisher.js +279 -287
- package/dist/operations.d.ts +6 -26
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations_defaults.d.ts.map +1 -1
- package/dist/operations_defaults.js +4 -20
- package/dist/output_helpers.d.ts.map +1 -1
- package/dist/output_helpers.js +7 -6
- package/dist/paths.d.ts +0 -4
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +0 -4
- package/dist/publish_gate.d.ts +44 -0
- package/dist/publish_gate.d.ts.map +1 -0
- package/dist/publish_gate.js +32 -0
- package/dist/publish_steps.d.ts +60 -0
- package/dist/publish_steps.d.ts.map +1 -0
- package/dist/publish_steps.js +113 -0
- package/dist/publishing_event.d.ts +12 -19
- package/dist/publishing_event.d.ts.map +1 -1
- package/dist/publishing_event.js +16 -25
- package/dist/publishing_plan.d.ts +1 -2
- package/dist/publishing_plan.d.ts.map +1 -1
- package/dist/publishing_plan.js +21 -4
- package/dist/publishing_plan_helpers.d.ts +1 -1
- package/dist/publishing_plan_helpers.d.ts.map +1 -1
- package/dist/publishing_plan_helpers.js +2 -15
- package/dist/publishing_plan_logging.d.ts.map +1 -1
- package/dist/publishing_plan_logging.js +15 -23
- package/dist/repo.svelte.d.ts +10 -6
- package/dist/repo.svelte.d.ts.map +1 -1
- package/dist/repo.svelte.js +6 -14
- package/dist/version_utils.d.ts +17 -3
- package/dist/version_utils.d.ts.map +1 -1
- package/dist/version_utils.js +22 -0
- package/package.json +14 -15
- package/src/lib/changeset_generator.ts +10 -19
- package/src/lib/changeset_reader.ts +2 -3
- package/src/lib/dependency_graph.ts +6 -8
- package/src/lib/fetch_repo_data.ts +3 -2
- package/src/lib/git_operations.ts +5 -0
- package/src/lib/gitops_analyze.task.ts +18 -23
- package/src/lib/gitops_plan.task.ts +10 -2
- package/src/lib/gitops_publish.task.ts +92 -27
- package/src/lib/gitops_run.task.ts +39 -7
- package/src/lib/gitops_sync.task.ts +18 -3
- package/src/lib/gitops_task_helpers.ts +20 -3
- package/src/lib/gitops_validate.task.ts +30 -22
- package/src/lib/graph_validation.ts +26 -0
- package/src/lib/local_repo.ts +124 -88
- package/src/lib/log_helpers.ts +8 -26
- package/src/lib/multi_repo_publisher.ts +336 -353
- package/src/lib/operations.ts +6 -22
- package/src/lib/operations_defaults.ts +4 -20
- package/src/lib/output_helpers.ts +7 -6
- package/src/lib/paths.ts +0 -5
- package/src/lib/publish_gate.ts +53 -0
- package/src/lib/publish_steps.ts +149 -0
- package/src/lib/publishing_event.ts +16 -25
- package/src/lib/publishing_plan.ts +26 -6
- package/src/lib/publishing_plan_helpers.ts +5 -18
- package/src/lib/publishing_plan_logging.ts +19 -39
- package/src/lib/repo.svelte.ts +12 -15
- package/src/lib/version_utils.ts +30 -9
- package/dist/npm_install_helpers.d.ts +0 -23
- package/dist/npm_install_helpers.d.ts.map +0 -1
- package/dist/npm_install_helpers.js +0 -60
- package/dist/semver.d.ts +0 -26
- package/dist/semver.d.ts.map +0 -1
- package/dist/semver.js +0 -137
- package/dist/serialization_types.d.ts +0 -59
- package/dist/serialization_types.d.ts.map +0 -1
- package/dist/serialization_types.js +0 -42
- package/src/lib/npm_install_helpers.ts +0 -85
- package/src/lib/semver.ts +0 -170
- package/src/lib/serialization_types.ts +0 -92
|
@@ -4,9 +4,9 @@ import {styleText as st} from 'node:util';
|
|
|
4
4
|
import type {Logger} from '@fuzdev/fuz_util/log.js';
|
|
5
5
|
|
|
6
6
|
import {get_gitops_ready} from './gitops_task_helpers.js';
|
|
7
|
-
import
|
|
7
|
+
import type {DependencyGraph} from './dependency_graph.js';
|
|
8
8
|
import type {LocalRepo} from './local_repo.js';
|
|
9
|
-
import {
|
|
9
|
+
import {analyze_repos, type DependencyAnalysis} from './graph_validation.js';
|
|
10
10
|
import {
|
|
11
11
|
format_wildcard_dependencies,
|
|
12
12
|
format_dev_cycles,
|
|
@@ -30,6 +30,13 @@ export const Args = z.strictObject({
|
|
|
30
30
|
.meta({description: 'output format'})
|
|
31
31
|
.default('stdout'),
|
|
32
32
|
outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
|
|
33
|
+
sync: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.meta({
|
|
36
|
+
description:
|
|
37
|
+
'sync repos (switch branch, pull, install) before analyzing instead of reading the working tree as-is',
|
|
38
|
+
})
|
|
39
|
+
.default(false),
|
|
33
40
|
});
|
|
34
41
|
export type Args = z.infer<typeof Args>;
|
|
35
42
|
|
|
@@ -38,25 +45,13 @@ export const task: Task<Args> = {
|
|
|
38
45
|
Args,
|
|
39
46
|
summary: 'analyze dependency structure and relationships across repos',
|
|
40
47
|
run: async ({args, log}) => {
|
|
41
|
-
const {config, dir, format, outfile} = args;
|
|
48
|
+
const {config, dir, format, outfile, sync} = args;
|
|
42
49
|
|
|
43
|
-
// Get repos ready (without downloading)
|
|
44
|
-
const {local_repos} = await get_gitops_ready({config, dir, download: false, log});
|
|
50
|
+
// Get repos ready (without downloading); read the working tree as-is unless `--sync`
|
|
51
|
+
const {local_repos} = await get_gitops_ready({config, dir, download: false, sync, log});
|
|
45
52
|
|
|
46
|
-
// Build dependency graph and
|
|
47
|
-
const {graph, publishing_order
|
|
48
|
-
log,
|
|
49
|
-
throw_on_prod_cycles: false, // Analyze should report, not throw
|
|
50
|
-
log_cycles: false, // We'll show cycles in our formatted output
|
|
51
|
-
log_order: false, // We'll show order in our formatted output
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Perform additional analysis
|
|
55
|
-
const builder = new DependencyGraphBuilder();
|
|
56
|
-
const analysis = builder.analyze(graph);
|
|
57
|
-
|
|
58
|
-
// Publishing order (may be null if prod cycles exist)
|
|
59
|
-
const publishing_order = order.length > 0 ? order : null;
|
|
53
|
+
// Build the dependency graph and analyze cycles/wildcards (tolerating cycles)
|
|
54
|
+
const {graph, analysis, publishing_order} = analyze_repos(local_repos);
|
|
60
55
|
|
|
61
56
|
// Format and output using output_helpers
|
|
62
57
|
const data = {
|
|
@@ -74,7 +69,7 @@ export const task: Task<Args> = {
|
|
|
74
69
|
interface AnalysisData {
|
|
75
70
|
repos: Array<LocalRepo>;
|
|
76
71
|
graph: DependencyGraph;
|
|
77
|
-
analysis:
|
|
72
|
+
analysis: DependencyAnalysis;
|
|
78
73
|
publishing_order: Array<string> | null;
|
|
79
74
|
}
|
|
80
75
|
|
|
@@ -102,7 +97,7 @@ const calculate_stats = (graph: DependencyGraph) => {
|
|
|
102
97
|
|
|
103
98
|
const format_json = (
|
|
104
99
|
graph: DependencyGraph,
|
|
105
|
-
analysis:
|
|
100
|
+
analysis: DependencyAnalysis,
|
|
106
101
|
publishing_order: Array<string> | null,
|
|
107
102
|
): string => {
|
|
108
103
|
const output = {
|
|
@@ -116,7 +111,7 @@ const format_json = (
|
|
|
116
111
|
const format_markdown = (
|
|
117
112
|
repos: Array<LocalRepo>,
|
|
118
113
|
graph: DependencyGraph,
|
|
119
|
-
analysis:
|
|
114
|
+
analysis: DependencyAnalysis,
|
|
120
115
|
publishing_order: Array<string> | null,
|
|
121
116
|
): Array<string> => {
|
|
122
117
|
const lines: Array<string> = ['# Dependency Analysis'];
|
|
@@ -192,7 +187,7 @@ const format_markdown = (
|
|
|
192
187
|
const format_stdout = (
|
|
193
188
|
repos: Array<LocalRepo>,
|
|
194
189
|
graph: DependencyGraph,
|
|
195
|
-
analysis:
|
|
190
|
+
analysis: DependencyAnalysis,
|
|
196
191
|
publishing_order: Array<string> | null,
|
|
197
192
|
log: Logger,
|
|
198
193
|
): void => {
|
|
@@ -28,6 +28,13 @@ export const Args = z.strictObject({
|
|
|
28
28
|
.default('stdout'),
|
|
29
29
|
outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
|
|
30
30
|
verbose: z.boolean().meta({description: 'show additional details'}).default(false),
|
|
31
|
+
sync: z
|
|
32
|
+
.boolean()
|
|
33
|
+
.meta({
|
|
34
|
+
description:
|
|
35
|
+
'sync repos (switch branch, pull, install) before planning instead of reading the working tree as-is',
|
|
36
|
+
})
|
|
37
|
+
.default(false),
|
|
31
38
|
});
|
|
32
39
|
export type Args = z.infer<typeof Args>;
|
|
33
40
|
|
|
@@ -46,15 +53,16 @@ export const task: Task<Args> = {
|
|
|
46
53
|
summary: 'generate a publishing plan based on changesets',
|
|
47
54
|
Args,
|
|
48
55
|
run: async ({args, log}): Promise<void> => {
|
|
49
|
-
const {dir, config, format, outfile, verbose} = args;
|
|
56
|
+
const {dir, config, format, outfile, verbose, sync} = args;
|
|
50
57
|
|
|
51
58
|
log.info(st('cyan', 'Generating multi-repo publishing plan...'));
|
|
52
59
|
|
|
53
|
-
// Load local repos
|
|
60
|
+
// Load local repos; read the working tree as-is unless `--sync`
|
|
54
61
|
const {local_repos} = await get_gitops_ready({
|
|
55
62
|
config,
|
|
56
63
|
dir,
|
|
57
64
|
download: false, // Don't download if missing
|
|
65
|
+
sync,
|
|
58
66
|
log,
|
|
59
67
|
});
|
|
60
68
|
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import type {Task} from '@fuzdev/gro';
|
|
2
|
+
import type {Logger} from '@fuzdev/fuz_util/log.js';
|
|
2
3
|
import {z} from 'zod';
|
|
3
4
|
import {createInterface} from 'node:readline/promises';
|
|
4
5
|
import {styleText as st} from 'node:util';
|
|
5
6
|
|
|
6
7
|
import {get_gitops_ready} from './gitops_task_helpers.js';
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
+
execute_publishing_plan,
|
|
9
10
|
type PublishingOptions,
|
|
10
11
|
type PublishingResult,
|
|
11
12
|
} from './multi_repo_publisher.js';
|
|
12
13
|
import {stdout_handler} from './publishing_event_handler.js';
|
|
13
14
|
import {generate_publishing_plan, log_publishing_plan} from './publishing_plan.js';
|
|
15
|
+
import {derive_publish_steps, format_publish_steps, type PublishStep} from './publish_steps.js';
|
|
16
|
+
import {decide_publish_gate, publish_run_failed} from './publish_gate.js';
|
|
14
17
|
import {format_and_output, type OutputFormatters} from './output_helpers.js';
|
|
15
18
|
import {GITOPS_CONFIG_PATH_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT} from './gitops_constants.js';
|
|
16
19
|
|
|
@@ -34,25 +37,31 @@ export const Args = z.strictObject({
|
|
|
34
37
|
.meta({description: 'output format'})
|
|
35
38
|
.default('stdout'),
|
|
36
39
|
deploy: z.boolean().meta({description: 'deploy all repos after publishing'}).default(false),
|
|
37
|
-
plan: z
|
|
38
|
-
'no-plan': z
|
|
40
|
+
plan: z
|
|
39
41
|
.boolean()
|
|
40
|
-
.meta({description: '
|
|
41
|
-
.default(
|
|
42
|
+
.meta({description: 'show the plan and confirm before publishing; --no-plan to skip'})
|
|
43
|
+
.default(true),
|
|
42
44
|
max_wait: z
|
|
43
45
|
.number()
|
|
44
46
|
.meta({description: 'max time to wait for npm propagation in ms'})
|
|
45
47
|
.default(GITOPS_NPM_WAIT_TIMEOUT_DEFAULT),
|
|
46
|
-
skip_install: z
|
|
47
|
-
.boolean()
|
|
48
|
-
.meta({description: 'skip npm install after dependency updates'})
|
|
49
|
-
.default(false),
|
|
50
48
|
emit_json: z
|
|
51
49
|
.boolean()
|
|
52
50
|
.meta({description: 'stream structured publishing events as JSON-lines to stdout'})
|
|
53
51
|
.default(false),
|
|
54
52
|
outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
|
|
55
53
|
verbose: z.boolean().meta({description: 'show additional details in plan output'}).default(false),
|
|
54
|
+
sync: z
|
|
55
|
+
.boolean()
|
|
56
|
+
.meta({
|
|
57
|
+
description:
|
|
58
|
+
'sync repos (switch branch, pull, install) before the dry run instead of reading the working tree as-is; always on for --wetrun',
|
|
59
|
+
})
|
|
60
|
+
.default(false),
|
|
61
|
+
preview: z
|
|
62
|
+
.boolean()
|
|
63
|
+
.meta({description: 'show the ordered side-effects a --wetrun would perform'})
|
|
64
|
+
.default(false),
|
|
56
65
|
});
|
|
57
66
|
export type Args = z.infer<typeof Args>;
|
|
58
67
|
|
|
@@ -70,29 +79,42 @@ export const task: Task<Args> = {
|
|
|
70
79
|
deploy,
|
|
71
80
|
plan,
|
|
72
81
|
max_wait,
|
|
73
|
-
skip_install,
|
|
74
82
|
emit_json,
|
|
75
83
|
outfile,
|
|
76
84
|
verbose,
|
|
85
|
+
sync,
|
|
86
|
+
preview,
|
|
77
87
|
} = args;
|
|
78
88
|
|
|
79
|
-
// Load repos
|
|
89
|
+
// Load repos. A dry run reads the working tree as-is unless `--sync`;
|
|
90
|
+
// a real publish (`--wetrun`) always syncs so preflight sees the canonical branches.
|
|
80
91
|
const {local_repos: repos} = await get_gitops_ready({
|
|
81
92
|
config,
|
|
82
93
|
dir,
|
|
83
94
|
download: false, // Don't download if missing
|
|
95
|
+
sync: sync || wetrun,
|
|
84
96
|
log,
|
|
85
97
|
});
|
|
86
98
|
|
|
87
|
-
//
|
|
88
|
-
|
|
99
|
+
// Generate the plan once; the executor consumes this exact plan (no second pass).
|
|
100
|
+
const publishing_plan = await generate_publishing_plan(repos, {verbose});
|
|
101
|
+
const preview_steps = preview ? derive_publish_steps(publishing_plan, {deploy}) : null;
|
|
102
|
+
|
|
103
|
+
// Decide whether to show the plan + confirm, block, or proceed (the decision table lives
|
|
104
|
+
// in `decide_publish_gate`; readline + exit stay here at the edge).
|
|
105
|
+
const gate = decide_publish_gate({wetrun, show_plan: plan, plan: publishing_plan});
|
|
106
|
+
|
|
107
|
+
// A real publish that shows its plan prints it first — including before a `blocked` throw,
|
|
108
|
+
// so the operator sees the errors that blocked it.
|
|
109
|
+
if (gate.action !== 'proceed') {
|
|
89
110
|
log.info(st('cyan', 'Publishing Plan'));
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
log_publishing_plan(publishing_plan, log, {verbose});
|
|
112
|
+
}
|
|
92
113
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
114
|
+
if (gate.action === 'blocked') {
|
|
115
|
+
throw new Error(gate.message);
|
|
116
|
+
} else if (gate.action === 'confirm') {
|
|
117
|
+
if (preview_steps) log_preview(preview_steps, log);
|
|
96
118
|
|
|
97
119
|
// Ask for confirmation
|
|
98
120
|
log.info(st('yellow', '⚠️ This will publish the packages shown above.'));
|
|
@@ -102,16 +124,19 @@ export const task: Task<Args> = {
|
|
|
102
124
|
log.info('Publishing cancelled');
|
|
103
125
|
process.exit(0);
|
|
104
126
|
}
|
|
127
|
+
} else if (preview_steps && format === 'stdout') {
|
|
128
|
+
// proceed (dry run or --no-plan): only render to stdout for the human format;
|
|
129
|
+
// json/markdown carry the preview in their structured output, so logging here too
|
|
130
|
+
// would corrupt that stream.
|
|
131
|
+
log_preview(preview_steps, log);
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
// Publishing options
|
|
108
135
|
const options: PublishingOptions = {
|
|
109
136
|
wetrun,
|
|
110
|
-
update_deps: true, // Always update dependencies
|
|
111
137
|
version_strategy: peer_strategy,
|
|
112
138
|
deploy,
|
|
113
139
|
max_wait,
|
|
114
|
-
skip_install,
|
|
115
140
|
log,
|
|
116
141
|
// Live JSON-lines stream when requested; events also surface on the result.
|
|
117
142
|
events: emit_json ? stdout_handler() : undefined,
|
|
@@ -122,7 +147,7 @@ export const task: Task<Args> = {
|
|
|
122
147
|
let fatal_error: Error | null = null;
|
|
123
148
|
|
|
124
149
|
try {
|
|
125
|
-
result = await
|
|
150
|
+
result = await execute_publishing_plan(repos, publishing_plan, options);
|
|
126
151
|
} catch (error) {
|
|
127
152
|
// Construct a failure result for fatal errors so output can still be generated
|
|
128
153
|
fatal_error = error instanceof Error ? error : new Error(String(error));
|
|
@@ -134,13 +159,15 @@ export const task: Task<Args> = {
|
|
|
134
159
|
duration: 0,
|
|
135
160
|
events: [],
|
|
136
161
|
summary: {total: 0, published: 0, failed: 1, skipped: 0, duration: 0},
|
|
162
|
+
plan_errors: publishing_plan.errors,
|
|
163
|
+
plan_warnings: publishing_plan.warnings,
|
|
137
164
|
};
|
|
138
165
|
}
|
|
139
166
|
|
|
140
167
|
// Format and output result (always runs, even on fatal errors)
|
|
141
|
-
// Note: stdout format is handled by
|
|
168
|
+
// Note: stdout format is handled by the executor's logging
|
|
142
169
|
if (format !== 'stdout') {
|
|
143
|
-
await format_and_output({result, fatal_error}, create_publish_formatters(), {
|
|
170
|
+
await format_and_output({result, fatal_error, preview_steps}, create_publish_formatters(), {
|
|
144
171
|
format,
|
|
145
172
|
outfile,
|
|
146
173
|
log,
|
|
@@ -148,7 +175,7 @@ export const task: Task<Args> = {
|
|
|
148
175
|
}
|
|
149
176
|
|
|
150
177
|
// Exit with error if failed
|
|
151
|
-
if (
|
|
178
|
+
if (publish_run_failed(result, fatal_error)) {
|
|
152
179
|
process.exit(1);
|
|
153
180
|
}
|
|
154
181
|
},
|
|
@@ -157,21 +184,36 @@ export const task: Task<Args> = {
|
|
|
157
184
|
interface PublishResultData {
|
|
158
185
|
result: PublishingResult;
|
|
159
186
|
fatal_error: Error | null;
|
|
187
|
+
preview_steps: Array<PublishStep> | null;
|
|
160
188
|
}
|
|
161
189
|
|
|
162
190
|
const create_publish_formatters = (): OutputFormatters<PublishResultData> => ({
|
|
163
|
-
json: (data) =>
|
|
164
|
-
|
|
191
|
+
json: (data) =>
|
|
192
|
+
JSON.stringify(
|
|
193
|
+
data.preview_steps ? {...data.result, preview: data.preview_steps} : data.result,
|
|
194
|
+
null,
|
|
195
|
+
2,
|
|
196
|
+
),
|
|
197
|
+
markdown: (data) => format_result_markdown(data.result, data.fatal_error, data.preview_steps),
|
|
165
198
|
stdout: () => {
|
|
166
|
-
// stdout format is handled by
|
|
199
|
+
// stdout format is handled by the executor's logging
|
|
167
200
|
// This should never be called due to early return in task
|
|
168
201
|
},
|
|
169
202
|
});
|
|
170
203
|
|
|
204
|
+
/** Logs the ordered side-effect preview to stdout. */
|
|
205
|
+
const log_preview = (steps: Array<PublishStep>, log: Logger): void => {
|
|
206
|
+
log.info(st('cyan', '\nSide-effect preview (what a real publish would perform):'));
|
|
207
|
+
for (const line of format_publish_steps(steps)) {
|
|
208
|
+
log.info(st('dim', ` ${line}`));
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
171
212
|
// Format the publishing result as markdown
|
|
172
213
|
const format_result_markdown = (
|
|
173
214
|
result: PublishingResult,
|
|
174
215
|
fatal_error: Error | null,
|
|
216
|
+
preview_steps: Array<PublishStep> | null,
|
|
175
217
|
): Array<string> => {
|
|
176
218
|
const lines: Array<string> = [];
|
|
177
219
|
|
|
@@ -215,6 +257,29 @@ const format_result_markdown = (
|
|
|
215
257
|
}
|
|
216
258
|
}
|
|
217
259
|
|
|
260
|
+
if (result.plan_warnings.length > 0) {
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push('## Plan Warnings');
|
|
263
|
+
lines.push('');
|
|
264
|
+
for (const warning of result.plan_warnings) lines.push(`- ${warning}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (result.plan_errors.length > 0) {
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push('## Plan Errors');
|
|
270
|
+
lines.push('');
|
|
271
|
+
for (const plan_error of result.plan_errors) lines.push(`- ${plan_error}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (preview_steps) {
|
|
275
|
+
lines.push('');
|
|
276
|
+
lines.push('## Side-Effect Preview');
|
|
277
|
+
lines.push('');
|
|
278
|
+
lines.push('```');
|
|
279
|
+
for (const line of format_publish_steps(preview_steps)) lines.push(line);
|
|
280
|
+
lines.push('```');
|
|
281
|
+
}
|
|
282
|
+
|
|
218
283
|
return lines;
|
|
219
284
|
};
|
|
220
285
|
|
|
@@ -2,6 +2,7 @@ import {TaskError, type Task} from '@fuzdev/gro';
|
|
|
2
2
|
import {z} from 'zod';
|
|
3
3
|
import {map_concurrent_settled} from '@fuzdev/fuz_util/async.js';
|
|
4
4
|
import {spawn_out} from '@fuzdev/fuz_util/process.js';
|
|
5
|
+
import {writeFile} from 'node:fs/promises';
|
|
5
6
|
import {styleText as st} from 'node:util';
|
|
6
7
|
import {resolve} from 'node:path';
|
|
7
8
|
|
|
@@ -9,7 +10,9 @@ import {get_repo_paths} from './repo_ops.js';
|
|
|
9
10
|
import {GITOPS_CONCURRENCY_DEFAULT, GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
|
|
10
11
|
|
|
11
12
|
export const Args = z.strictObject({
|
|
12
|
-
|
|
13
|
+
// Positional rest args (gro convention) so `gro gitops_run "npm test"` works;
|
|
14
|
+
// joined with spaces and passed to `sh -c`, so quote commands that contain flags.
|
|
15
|
+
_: z.array(z.string()).meta({description: 'shell command to run in each repo'}).default([]),
|
|
13
16
|
config: z
|
|
14
17
|
.string()
|
|
15
18
|
.meta({description: 'path to the gitops config file'})
|
|
@@ -21,6 +24,10 @@ export const Args = z.strictObject({
|
|
|
21
24
|
.meta({description: 'maximum number of repos to run in parallel'})
|
|
22
25
|
.default(GITOPS_CONCURRENCY_DEFAULT),
|
|
23
26
|
format: z.enum(['text', 'json']).meta({description: 'output format'}).default('text'),
|
|
27
|
+
outfile: z
|
|
28
|
+
.string()
|
|
29
|
+
.meta({description: 'with --format json, write clean JSON to this file instead of stdout'})
|
|
30
|
+
.optional(),
|
|
24
31
|
});
|
|
25
32
|
export type Args = z.infer<typeof Args>;
|
|
26
33
|
|
|
@@ -39,7 +46,12 @@ export const task: Task<Args> = {
|
|
|
39
46
|
Args,
|
|
40
47
|
summary: 'run a shell command across all repos in parallel',
|
|
41
48
|
run: async ({args, log}) => {
|
|
42
|
-
const {
|
|
49
|
+
const {_, config, concurrency, format, outfile} = args;
|
|
50
|
+
|
|
51
|
+
const command = _.join(' ').trim();
|
|
52
|
+
if (!command) {
|
|
53
|
+
throw new TaskError('No command provided, e.g. `gro gitops_run "npm test"`');
|
|
54
|
+
}
|
|
43
55
|
|
|
44
56
|
// Get repo paths (lightweight, no library-metadata loading needed)
|
|
45
57
|
const config_path = resolve(config);
|
|
@@ -140,18 +152,38 @@ export const task: Task<Args> = {
|
|
|
140
152
|
duration_ms: Math.round(total_duration_ms),
|
|
141
153
|
},
|
|
142
154
|
};
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
const json = JSON.stringify(json_output, null, 2);
|
|
156
|
+
if (outfile) {
|
|
157
|
+
// Clean machine-readable output, free of gro's logging prefixes.
|
|
158
|
+
await writeFile(outfile, json);
|
|
159
|
+
log.info(`wrote JSON output to ${outfile}`);
|
|
160
|
+
} else {
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.log(json);
|
|
163
|
+
}
|
|
145
164
|
} else {
|
|
146
165
|
// Text format
|
|
147
166
|
log.info(''); // blank line
|
|
148
167
|
|
|
149
|
-
// Show successes
|
|
168
|
+
// Show successes, including the command's output so read-only commands
|
|
169
|
+
// (`git rev-parse`, `cat package.json`, …) are useful without `--format json`.
|
|
150
170
|
if (successes.length > 0) {
|
|
151
171
|
log.info(st('green', `✓ ${successes.length} succeeded:`));
|
|
152
172
|
for (const result of successes) {
|
|
153
|
-
const duration =
|
|
154
|
-
|
|
173
|
+
const duration = st('blue', `(${Math.round(result.duration_ms)}ms)`);
|
|
174
|
+
const label = st('gray', ` ${result.repo_name}`);
|
|
175
|
+
const out = result.stdout.trim();
|
|
176
|
+
const out_lines = out ? out.split('\n') : [];
|
|
177
|
+
if (out_lines.length <= 1) {
|
|
178
|
+
// single-line (or empty) output inline after the repo name
|
|
179
|
+
log.info(out ? `${label} ${out} ${duration}` : `${label} ${duration}`);
|
|
180
|
+
} else {
|
|
181
|
+
// multi-line output indented under the repo
|
|
182
|
+
log.info(`${label} ${duration}`);
|
|
183
|
+
for (const line of out_lines) {
|
|
184
|
+
log.info(st('gray', ` ${line}`));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
155
187
|
}
|
|
156
188
|
}
|
|
157
189
|
|
|
@@ -35,6 +35,13 @@ export const Args = z.strictObject({
|
|
|
35
35
|
.boolean()
|
|
36
36
|
.meta({description: 'check repos are ready without fetching remote data'})
|
|
37
37
|
.default(false),
|
|
38
|
+
allow_dirty: z
|
|
39
|
+
.boolean()
|
|
40
|
+
.meta({
|
|
41
|
+
description:
|
|
42
|
+
'sync (switch branch, pull) tolerating uncommitted changes instead of failing on a dirty workspace',
|
|
43
|
+
})
|
|
44
|
+
.default(false),
|
|
38
45
|
});
|
|
39
46
|
export type Args = z.infer<typeof Args>;
|
|
40
47
|
|
|
@@ -47,9 +54,17 @@ export const task: Task<Args> = {
|
|
|
47
54
|
Args,
|
|
48
55
|
summary: 'syncs local repos and generates UI data from repo metadata',
|
|
49
56
|
run: async ({args, log, svelte_config, invoke_task}) => {
|
|
50
|
-
const {config, dir, outdir = svelte_config.routes_path, download, check} = args;
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
const {config, dir, outdir = svelte_config.routes_path, download, check, allow_dirty} = args;
|
|
58
|
+
|
|
59
|
+
// `gitops_sync` is the task whose job is to mutate working trees, so it always syncs.
|
|
60
|
+
const {local_repos} = await get_gitops_ready({
|
|
61
|
+
config,
|
|
62
|
+
dir,
|
|
63
|
+
download,
|
|
64
|
+
sync: true,
|
|
65
|
+
allow_dirty,
|
|
66
|
+
log,
|
|
67
|
+
});
|
|
53
68
|
|
|
54
69
|
const outfile_json = resolve(outdir, 'repos.json');
|
|
55
70
|
const outfile_ts = resolve(outdir, 'repos.ts');
|
|
@@ -37,6 +37,14 @@ export interface GetGitopsReadyOptions {
|
|
|
37
37
|
npm_ops?: NpmOperations;
|
|
38
38
|
parallel?: boolean;
|
|
39
39
|
concurrency?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Sync each repo's working tree to its configured branch before loading
|
|
42
|
+
* (switch branch, pull, install). When `false`, repos load exactly as they
|
|
43
|
+
* sit on disk — the safe default for read-only diagnostics. Defaults to `true`.
|
|
44
|
+
*/
|
|
45
|
+
sync?: boolean;
|
|
46
|
+
/** When syncing, tolerate uncommitted changes instead of throwing. Defaults to `false`. */
|
|
47
|
+
allow_dirty?: boolean;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
/**
|
|
@@ -45,8 +53,12 @@ export interface GetGitopsReadyOptions {
|
|
|
45
53
|
* Initialization sequence:
|
|
46
54
|
* 1. Loads and normalizes config from `gitops.config.ts`
|
|
47
55
|
* 2. Resolves local repo paths (creates missing with `--download`)
|
|
48
|
-
* 3.
|
|
49
|
-
* 4.
|
|
56
|
+
* 3. If `sync`, switches branches and pulls latest changes (in parallel by default)
|
|
57
|
+
* 4. If `sync`, auto-installs deps if `package.json` changed during pull
|
|
58
|
+
*
|
|
59
|
+
* With `sync: false` (the default for read-only diagnostics), steps 3-4 are
|
|
60
|
+
* skipped and repos are loaded exactly as checked out — no branch switch, pull,
|
|
61
|
+
* install, or clean-workspace check.
|
|
50
62
|
*
|
|
51
63
|
* Priority for path resolution:
|
|
52
64
|
* - `dir` argument (explicit override)
|
|
@@ -57,6 +69,8 @@ export interface GetGitopsReadyOptions {
|
|
|
57
69
|
* @param options.npm_ops - for testing (defaults to real npm operations)
|
|
58
70
|
* @param options.parallel - whether to load repos in parallel (default: true)
|
|
59
71
|
* @param options.concurrency - max concurrent repo loads (default: 5)
|
|
72
|
+
* @param options.sync - sync working trees before loading (default: true)
|
|
73
|
+
* @param options.allow_dirty - when syncing, tolerate uncommitted changes (default: false)
|
|
60
74
|
* @returns initialized config and fully loaded repos ready for operations
|
|
61
75
|
* @throws {TaskError} if config loading or repo resolution fails
|
|
62
76
|
*/
|
|
@@ -68,7 +82,8 @@ export const get_gitops_ready = async (
|
|
|
68
82
|
gitops_config: GitopsConfig;
|
|
69
83
|
local_repos: Array<LocalRepo>;
|
|
70
84
|
}> => {
|
|
71
|
-
const {config, dir, download, log, git_ops, npm_ops, parallel, concurrency} =
|
|
85
|
+
const {config, dir, download, log, git_ops, npm_ops, parallel, concurrency, sync, allow_dirty} =
|
|
86
|
+
options;
|
|
72
87
|
const config_path = resolve(config);
|
|
73
88
|
const gitops_config = await import_gitops_config(config_path);
|
|
74
89
|
|
|
@@ -101,6 +116,8 @@ export const get_gitops_ready = async (
|
|
|
101
116
|
npm_ops,
|
|
102
117
|
parallel,
|
|
103
118
|
concurrency,
|
|
119
|
+
sync,
|
|
120
|
+
allow_dirty,
|
|
104
121
|
});
|
|
105
122
|
|
|
106
123
|
return {config_path, repos_dir, gitops_config, local_repos};
|
|
@@ -3,10 +3,13 @@ import {z} from 'zod';
|
|
|
3
3
|
import {styleText as st} from 'node:util';
|
|
4
4
|
|
|
5
5
|
import {get_gitops_ready} from './gitops_task_helpers.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
import {analyze_repos, type DependencyAnalysis} from './graph_validation.js';
|
|
7
|
+
import {
|
|
8
|
+
generate_publishing_plan,
|
|
9
|
+
log_publishing_plan,
|
|
10
|
+
type PublishingPlan,
|
|
11
|
+
} from './publishing_plan.js';
|
|
12
|
+
import {execute_publishing_plan, type PublishingOptions} from './multi_repo_publisher.js';
|
|
10
13
|
import {log_dependency_analysis} from './log_helpers.js';
|
|
11
14
|
import {GITOPS_CONFIG_PATH_DEFAULT} from './gitops_constants.js';
|
|
12
15
|
|
|
@@ -21,6 +24,13 @@ export const Args = z.strictObject({
|
|
|
21
24
|
.meta({description: 'path containing the repos, defaults to the parent of the config dir'})
|
|
22
25
|
.optional(),
|
|
23
26
|
verbose: z.boolean().meta({description: 'show additional details'}).default(false),
|
|
27
|
+
sync: z
|
|
28
|
+
.boolean()
|
|
29
|
+
.meta({
|
|
30
|
+
description:
|
|
31
|
+
'sync repos (switch branch, pull, install) before validating instead of reading the working tree as-is',
|
|
32
|
+
})
|
|
33
|
+
.default(false),
|
|
24
34
|
});
|
|
25
35
|
export type Args = z.infer<typeof Args>;
|
|
26
36
|
|
|
@@ -30,7 +40,7 @@ export const task: Task<Args> = {
|
|
|
30
40
|
summary:
|
|
31
41
|
'validate gitops configuration by running all read-only commands and checking for issues',
|
|
32
42
|
run: async ({args, log}) => {
|
|
33
|
-
const {config, dir, verbose} = args;
|
|
43
|
+
const {config, dir, verbose, sync} = args;
|
|
34
44
|
|
|
35
45
|
log.info(st('cyan', 'Running Gitops Validation Suite'));
|
|
36
46
|
log.info(st('dim', 'This runs all read-only commands and checks for consistency.'));
|
|
@@ -43,30 +53,22 @@ export const task: Task<Args> = {
|
|
|
43
53
|
duration: number;
|
|
44
54
|
warning_details?: Array<string>;
|
|
45
55
|
info_details?: Array<string>;
|
|
46
|
-
analysis?:
|
|
56
|
+
analysis?: DependencyAnalysis;
|
|
47
57
|
}> = [];
|
|
48
58
|
|
|
49
59
|
const start_time = Date.now();
|
|
50
60
|
|
|
51
|
-
// Load repos once (shared by all commands)
|
|
61
|
+
// Load repos once (shared by all commands); read the working tree as-is unless `--sync`
|
|
52
62
|
log.info(st('dim', 'Loading repositories...'));
|
|
53
|
-
const {local_repos} = await get_gitops_ready({config, dir, download: false, log});
|
|
63
|
+
const {local_repos} = await get_gitops_ready({config, dir, download: false, sync, log});
|
|
54
64
|
log.info(st('dim', ` Found ${local_repos.length} local repos`));
|
|
55
65
|
|
|
56
66
|
// 1. Run gitops_analyze
|
|
57
67
|
log.info(st('yellow', 'Running gitops_analyze...'));
|
|
58
68
|
const analyze_start = Date.now();
|
|
59
69
|
try {
|
|
60
|
-
// Build dependency graph and
|
|
61
|
-
const {
|
|
62
|
-
throw_on_prod_cycles: false, // Analyze should report, not throw
|
|
63
|
-
log_cycles: false, // We'll collect our own statistics
|
|
64
|
-
log_order: false,
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Perform additional analysis
|
|
68
|
-
const builder = new DependencyGraphBuilder();
|
|
69
|
-
const analysis = builder.analyze(graph);
|
|
70
|
+
// Build dependency graph and analyze cycles/wildcards (tolerating cycles)
|
|
71
|
+
const {analysis} = analyze_repos(local_repos);
|
|
70
72
|
|
|
71
73
|
const analyze_duration = Date.now() - analyze_start;
|
|
72
74
|
|
|
@@ -113,11 +115,13 @@ export const task: Task<Args> = {
|
|
|
113
115
|
log.error(st('red', ` ✗ gitops_analyze failed: ${error}`));
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
// 2. Run gitops_plan
|
|
118
|
+
// 2. Run gitops_plan (generated once here and reused by the dry run below)
|
|
119
|
+
let publishing_plan: PublishingPlan | undefined;
|
|
117
120
|
log.info(st('yellow', 'Running gitops_plan...'));
|
|
118
121
|
const plan_start = Date.now();
|
|
119
122
|
try {
|
|
120
|
-
const plan = await generate_publishing_plan(local_repos, {
|
|
123
|
+
const plan = await generate_publishing_plan(local_repos, {verbose});
|
|
124
|
+
publishing_plan = plan;
|
|
121
125
|
const plan_duration = Date.now() - plan_start;
|
|
122
126
|
|
|
123
127
|
const warnings = plan.warnings.length;
|
|
@@ -159,11 +163,15 @@ export const task: Task<Args> = {
|
|
|
159
163
|
try {
|
|
160
164
|
const options: PublishingOptions = {
|
|
161
165
|
wetrun: false,
|
|
162
|
-
update_deps: true,
|
|
163
166
|
log: undefined, // Silent for validation
|
|
164
167
|
};
|
|
165
168
|
|
|
166
|
-
|
|
169
|
+
// Reuse the plan from step 2 (regenerate only if that step failed to produce one).
|
|
170
|
+
const result = await execute_publishing_plan(
|
|
171
|
+
local_repos,
|
|
172
|
+
publishing_plan ?? (await generate_publishing_plan(local_repos, {verbose})),
|
|
173
|
+
options,
|
|
174
|
+
);
|
|
167
175
|
const dry_duration = Date.now() - dry_start;
|
|
168
176
|
|
|
169
177
|
// Dry run doesn't have warnings/errors in the same format
|