@fuzdev/fuz_gitops 0.71.0 → 0.73.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 +13 -6
- 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/dependency_graph.js +1 -1
- 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 +3 -2
- package/dist/gitops_publish.task.d.ts.map +1 -1
- package/dist/gitops_publish.task.js +90 -26
- 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 +22 -10
- package/dist/local_repo.d.ts.map +1 -1
- package/dist/local_repo.js +92 -75
- 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 +29 -2
- package/dist/multi_repo_publisher.d.ts.map +1 -1
- package/dist/multi_repo_publisher.js +307 -233
- 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 +1 -19
- 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 +123 -0
- package/dist/publishing_event.d.ts.map +1 -0
- package/dist/publishing_event.js +140 -0
- package/dist/publishing_event_handler.d.ts +42 -0
- package/dist/publishing_event_handler.d.ts.map +1 -0
- package/dist/publishing_event_handler.js +75 -0
- package/dist/publishing_plan.d.ts +1 -2
- package/dist/publishing_plan.d.ts.map +1 -1
- package/dist/publishing_plan.js +18 -1
- 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_ops.d.ts +1 -1
- package/dist/repo_ops.js +1 -1
- 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 +6 -7
- package/src/lib/changeset_generator.ts +10 -19
- package/src/lib/changeset_reader.ts +1 -2
- package/src/lib/dependency_graph.ts +1 -1
- 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 +100 -25
- 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 +114 -83
- package/src/lib/log_helpers.ts +8 -26
- package/src/lib/multi_repo_publisher.ts +387 -269
- package/src/lib/operations.ts +6 -22
- package/src/lib/operations_defaults.ts +1 -19
- 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 +151 -0
- package/src/lib/publishing_event_handler.ts +98 -0
- package/src/lib/publishing_plan.ts +23 -3
- package/src/lib/publishing_plan_helpers.ts +5 -18
- package/src/lib/publishing_plan_logging.ts +19 -39
- package/src/lib/repo_ops.ts +1 -1
- 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
|
@@ -66,7 +66,7 @@ export class DependencyGraph {
|
|
|
66
66
|
repo,
|
|
67
67
|
dependencies: new Map(),
|
|
68
68
|
dependents: new Set(),
|
|
69
|
-
publishable:
|
|
69
|
+
publishable: !library.package_json.private,
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
// Extract dependencies
|
|
@@ -84,6 +84,11 @@ export const git_push_tag = async (
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Returns `true` if the working tree has any changes — staged, unstaged, or
|
|
89
|
+
* untracked (`git status --porcelain`). Broader than `git_list_uncommitted_files`,
|
|
90
|
+
* which reports only tracked working-tree changes relative to HEAD.
|
|
91
|
+
*/
|
|
87
92
|
export const git_has_changes = async (options?: SpawnOptions): Promise<boolean> => {
|
|
88
93
|
const {stdout} = await spawn_out('git', ['status', '--porcelain'], options);
|
|
89
94
|
return stdout ? stdout.trim().length > 0 : false;
|
|
@@ -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,15 +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';
|
|
13
|
+
import {stdout_handler} from './publishing_event_handler.js';
|
|
12
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';
|
|
13
17
|
import {format_and_output, type OutputFormatters} from './output_helpers.js';
|
|
14
18
|
import {GITOPS_CONFIG_PATH_DEFAULT, GITOPS_NPM_WAIT_TIMEOUT_DEFAULT} from './gitops_constants.js';
|
|
15
19
|
|
|
@@ -33,21 +37,31 @@ export const Args = z.strictObject({
|
|
|
33
37
|
.meta({description: 'output format'})
|
|
34
38
|
.default('stdout'),
|
|
35
39
|
deploy: z.boolean().meta({description: 'deploy all repos after publishing'}).default(false),
|
|
36
|
-
plan: z
|
|
37
|
-
'no-plan': z
|
|
40
|
+
plan: z
|
|
38
41
|
.boolean()
|
|
39
|
-
.meta({description: '
|
|
40
|
-
.default(
|
|
42
|
+
.meta({description: 'show the plan and confirm before publishing; --no-plan to skip'})
|
|
43
|
+
.default(true),
|
|
41
44
|
max_wait: z
|
|
42
45
|
.number()
|
|
43
46
|
.meta({description: 'max time to wait for npm propagation in ms'})
|
|
44
47
|
.default(GITOPS_NPM_WAIT_TIMEOUT_DEFAULT),
|
|
45
|
-
|
|
48
|
+
emit_json: z
|
|
46
49
|
.boolean()
|
|
47
|
-
.meta({description: '
|
|
50
|
+
.meta({description: 'stream structured publishing events as JSON-lines to stdout'})
|
|
48
51
|
.default(false),
|
|
49
52
|
outfile: z.string().meta({description: 'write output to file instead of logging'}).optional(),
|
|
50
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),
|
|
51
65
|
});
|
|
52
66
|
export type Args = z.infer<typeof Args>;
|
|
53
67
|
|
|
@@ -65,28 +79,42 @@ export const task: Task<Args> = {
|
|
|
65
79
|
deploy,
|
|
66
80
|
plan,
|
|
67
81
|
max_wait,
|
|
68
|
-
|
|
82
|
+
emit_json,
|
|
69
83
|
outfile,
|
|
70
84
|
verbose,
|
|
85
|
+
sync,
|
|
86
|
+
preview,
|
|
71
87
|
} = args;
|
|
72
88
|
|
|
73
|
-
// 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.
|
|
74
91
|
const {local_repos: repos} = await get_gitops_ready({
|
|
75
92
|
config,
|
|
76
93
|
dir,
|
|
77
94
|
download: false, // Don't download if missing
|
|
95
|
+
sync: sync || wetrun,
|
|
78
96
|
log,
|
|
79
97
|
});
|
|
80
98
|
|
|
81
|
-
//
|
|
82
|
-
|
|
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') {
|
|
83
110
|
log.info(st('cyan', 'Publishing Plan'));
|
|
84
|
-
|
|
85
|
-
|
|
111
|
+
log_publishing_plan(publishing_plan, log, {verbose});
|
|
112
|
+
}
|
|
86
113
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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);
|
|
90
118
|
|
|
91
119
|
// Ask for confirmation
|
|
92
120
|
log.info(st('yellow', '⚠️ This will publish the packages shown above.'));
|
|
@@ -96,17 +124,22 @@ export const task: Task<Args> = {
|
|
|
96
124
|
log.info('Publishing cancelled');
|
|
97
125
|
process.exit(0);
|
|
98
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);
|
|
99
132
|
}
|
|
100
133
|
|
|
101
134
|
// Publishing options
|
|
102
135
|
const options: PublishingOptions = {
|
|
103
136
|
wetrun,
|
|
104
|
-
update_deps: true, // Always update dependencies
|
|
105
137
|
version_strategy: peer_strategy,
|
|
106
138
|
deploy,
|
|
107
139
|
max_wait,
|
|
108
|
-
skip_install,
|
|
109
140
|
log,
|
|
141
|
+
// Live JSON-lines stream when requested; events also surface on the result.
|
|
142
|
+
events: emit_json ? stdout_handler() : undefined,
|
|
110
143
|
};
|
|
111
144
|
|
|
112
145
|
// Execute publishing (may throw on fatal errors like circular dependencies)
|
|
@@ -114,7 +147,7 @@ export const task: Task<Args> = {
|
|
|
114
147
|
let fatal_error: Error | null = null;
|
|
115
148
|
|
|
116
149
|
try {
|
|
117
|
-
result = await
|
|
150
|
+
result = await execute_publishing_plan(repos, publishing_plan, options);
|
|
118
151
|
} catch (error) {
|
|
119
152
|
// Construct a failure result for fatal errors so output can still be generated
|
|
120
153
|
fatal_error = error instanceof Error ? error : new Error(String(error));
|
|
@@ -124,13 +157,17 @@ export const task: Task<Args> = {
|
|
|
124
157
|
// Note: FATAL_ERROR is a placeholder - only fatal_error.message is displayed in output
|
|
125
158
|
failed: [{name: 'FATAL_ERROR', error: fatal_error}],
|
|
126
159
|
duration: 0,
|
|
160
|
+
events: [],
|
|
161
|
+
summary: {total: 0, published: 0, failed: 1, skipped: 0, duration: 0},
|
|
162
|
+
plan_errors: publishing_plan.errors,
|
|
163
|
+
plan_warnings: publishing_plan.warnings,
|
|
127
164
|
};
|
|
128
165
|
}
|
|
129
166
|
|
|
130
167
|
// Format and output result (always runs, even on fatal errors)
|
|
131
|
-
// Note: stdout format is handled by
|
|
168
|
+
// Note: stdout format is handled by the executor's logging
|
|
132
169
|
if (format !== 'stdout') {
|
|
133
|
-
await format_and_output({result, fatal_error}, create_publish_formatters(), {
|
|
170
|
+
await format_and_output({result, fatal_error, preview_steps}, create_publish_formatters(), {
|
|
134
171
|
format,
|
|
135
172
|
outfile,
|
|
136
173
|
log,
|
|
@@ -138,7 +175,7 @@ export const task: Task<Args> = {
|
|
|
138
175
|
}
|
|
139
176
|
|
|
140
177
|
// Exit with error if failed
|
|
141
|
-
if (
|
|
178
|
+
if (publish_run_failed(result, fatal_error)) {
|
|
142
179
|
process.exit(1);
|
|
143
180
|
}
|
|
144
181
|
},
|
|
@@ -147,21 +184,36 @@ export const task: Task<Args> = {
|
|
|
147
184
|
interface PublishResultData {
|
|
148
185
|
result: PublishingResult;
|
|
149
186
|
fatal_error: Error | null;
|
|
187
|
+
preview_steps: Array<PublishStep> | null;
|
|
150
188
|
}
|
|
151
189
|
|
|
152
190
|
const create_publish_formatters = (): OutputFormatters<PublishResultData> => ({
|
|
153
|
-
json: (data) =>
|
|
154
|
-
|
|
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),
|
|
155
198
|
stdout: () => {
|
|
156
|
-
// stdout format is handled by
|
|
199
|
+
// stdout format is handled by the executor's logging
|
|
157
200
|
// This should never be called due to early return in task
|
|
158
201
|
},
|
|
159
202
|
});
|
|
160
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
|
+
|
|
161
212
|
// Format the publishing result as markdown
|
|
162
213
|
const format_result_markdown = (
|
|
163
214
|
result: PublishingResult,
|
|
164
215
|
fatal_error: Error | null,
|
|
216
|
+
preview_steps: Array<PublishStep> | null,
|
|
165
217
|
): Array<string> => {
|
|
166
218
|
const lines: Array<string> = [];
|
|
167
219
|
|
|
@@ -205,6 +257,29 @@ const format_result_markdown = (
|
|
|
205
257
|
}
|
|
206
258
|
}
|
|
207
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
|
+
|
|
208
283
|
return lines;
|
|
209
284
|
};
|
|
210
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};
|