project-auto-wizard 0.13.3 → 0.14.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 +1 -1
- package/bin/project-auto-wizard.js +18 -4
- package/package.json +2 -2
- package/payload/config/breaking-changes.json +28 -14
- package/payload/config/wizard-prompts.yml +102 -46
- package/payload/flutter-app/android/fastlane/Fastfile.playstore +44 -32
- package/payload/flutter-app/ios/ExportOptions.plist +8 -8
- package/payload/flutter-app/ios/fastlane/Fastfile +57 -43
- package/payload/scripts/changelog_manager.py +296 -257
- package/payload/scripts/issue_helper.py +54 -50
- package/payload/scripts/messages.py +2279 -0
- package/payload/scripts/truncate_release_notes.py +7 -6
- package/payload/scripts/version_manager.py +64 -61
- package/payload/version.yml.template +2 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +17 -12
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +59 -34
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +17 -10
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +54 -44
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +24 -17
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +194 -184
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +226 -213
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +46 -42
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +334 -241
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +152 -128
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +246 -188
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +353 -238
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +96 -85
- package/payload/workflows/go/PROJECT-GO-CI.yaml +47 -36
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +945 -508
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +155 -133
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +76 -60
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +97 -77
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +62 -47
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +942 -505
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +148 -126
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +86 -70
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +112 -92
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +112 -82
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +180 -150
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +161 -131
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +970 -549
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +193 -162
- package/src/cli/args.js +98 -52
- package/src/cli/help.js +8 -41
- package/src/commands/doctor.js +85 -89
- package/src/commands/dry-run.js +63 -43
- package/src/commands/full.js +79 -77
- package/src/commands/install-settings.js +16 -15
- package/src/commands/interactive-flutter.js +20 -19
- package/src/commands/interactive.js +135 -132
- package/src/commands/purge.js +47 -42
- package/src/commands/status.js +36 -36
- package/src/commands/uninstall.js +43 -41
- package/src/context.js +12 -11
- package/src/core/assets.js +12 -11
- package/src/core/baseline.js +27 -27
- package/src/core/branches.js +29 -27
- package/src/core/branding.js +7 -6
- package/src/core/breaking-check.js +22 -21
- package/src/core/breaking.js +15 -8
- package/src/core/copy/app-files.js +5 -4
- package/src/core/copy/flutter-app.js +8 -8
- package/src/core/copy/gitignore.js +58 -50
- package/src/core/copy/readme.js +45 -35
- package/src/core/copy/simple.js +16 -15
- package/src/core/copy/workflows.js +131 -124
- package/src/core/deploy-style.js +51 -48
- package/src/core/detect-fs.js +57 -57
- package/src/core/detect.js +67 -65
- package/src/core/errors.js +2 -2
- package/src/core/flutter-doctor.js +17 -16
- package/src/core/flutter-hooks.js +33 -32
- package/src/core/flutter-options.js +34 -33
- package/src/core/fsutil.js +12 -12
- package/src/core/installed-stores.js +4 -4
- package/src/core/logger.js +42 -40
- package/src/core/paths-resolve.js +88 -86
- package/src/core/paths.js +10 -10
- package/src/core/release-options.js +8 -8
- package/src/core/removal-exec.js +14 -13
- package/src/core/removal-plan.js +37 -37
- package/src/core/types.js +47 -47
- package/src/core/verify.js +44 -38
- package/src/core/version-yml.js +76 -68
- package/src/core/wizard-env.js +46 -46
- package/src/core/wizard-labels.js +63 -41
- package/src/i18n/catalog/en/cli.js +50 -0
- package/src/i18n/catalog/en/commands.js +187 -0
- package/src/i18n/catalog/en/copy.js +19 -0
- package/src/i18n/catalog/en/core.js +74 -0
- package/src/i18n/catalog/en/core2.js +15 -0
- package/src/i18n/catalog/en/ui.js +221 -0
- package/src/i18n/catalog/en.js +12 -0
- package/src/i18n/catalog/index.js +5 -0
- package/src/i18n/catalog/ko/cli.js +50 -0
- package/src/i18n/catalog/ko/commands.js +187 -0
- package/src/i18n/catalog/ko/copy.js +19 -0
- package/src/i18n/catalog/ko/core.js +74 -0
- package/src/i18n/catalog/ko/core2.js +15 -0
- package/src/i18n/catalog/ko/ui.js +221 -0
- package/src/i18n/catalog/ko.js +10 -0
- package/src/i18n/index.js +44 -0
- package/src/i18n/languages.js +9 -0
- package/src/index.js +128 -94
- package/src/ui/ansi.js +7 -7
- package/src/ui/banner.js +7 -6
- package/src/ui/env-plan.js +82 -81
- package/src/ui/prompts.js +97 -122
- package/src/ui/readline-engine.js +59 -58
- package/src/ui/status-cards.js +37 -31
- package/src/ui/summary.js +89 -88
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Workflow copy engine: classifies, copies and substitutes in the order common -> per type -> server-deploy.
|
|
2
|
+
// The interactive three-way choice (existing file conflict) is collected by copyWorkflowsInteractive (async)
|
|
3
|
+
// into a decisions Map and passed to the synchronous engine (copyWorkflows) as hooks.decisions; signature and
|
|
4
|
+
// force behavior are unchanged.
|
|
4
5
|
import { join, basename } from "node:path";
|
|
5
6
|
import {
|
|
6
7
|
deployFilter, isDeployWorkflow, activateDeployTrigger, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE, NO_DEPLOY_STYLE,
|
|
@@ -14,39 +15,42 @@ import { substitute } from "../branding.js";
|
|
|
14
15
|
import { sha256, readBaseline } from "../baseline.js";
|
|
15
16
|
import { parseDeployBlock } from "../version-yml.js";
|
|
16
17
|
import { log } from "../logger.js";
|
|
18
|
+
import { t } from "../../i18n/index.js";
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
// classify(unchanged
|
|
20
|
+
// Source text loader: applies {{MAIN_BRANCH}}/{{DEVELOP_BRANCH}} substitution when context.branches is set.
|
|
21
|
+
// classify (unchanged decision) and the real copy must see the same substituted text, or re-runs get false conflicts.
|
|
20
22
|
export function makeSrcText(branches, deployStyle = DEFAULT_DEPLOY_STYLE) {
|
|
21
23
|
return (p) => {
|
|
22
24
|
const raw = readFileSync(p, "utf8");
|
|
23
25
|
const out = branches ? substitute(raw, branches) : raw;
|
|
24
|
-
//
|
|
26
|
+
// The CD for the chosen deploy style is installed with its push trigger enabled, so it never ends up installed but not running.
|
|
25
27
|
return isDeployWorkflow(basename(p)) ? activateDeployTrigger(out) : out;
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
+
// Common workflows not installed in trunk-based mode.
|
|
32
|
+
// There is no release PR flow, so RELEASE-PUBLISH alone absorbs bump -> changelog -> tag -> Release.
|
|
31
33
|
const TRUNK_BASED_EXCLUDED = new Set([
|
|
32
34
|
"PROJECT-COMMON-VERSION-CONTROL.yaml",
|
|
33
35
|
"PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml",
|
|
34
36
|
]);
|
|
35
37
|
|
|
36
|
-
// version.yml deploy
|
|
37
|
-
//
|
|
38
|
+
// Deploy values saved in the version.yml deploy block: Map<type, Map<key,value>>.
|
|
39
|
+
// Install, classify and baseline must use the same values as defaults, or re-runs produce false changes.
|
|
38
40
|
export function readSavedDeployValues(targetRoot = ".") {
|
|
39
41
|
const p = join(targetRoot, PATHS.versionFile);
|
|
40
42
|
if (!existsSync(p)) return new Map();
|
|
41
43
|
return parseDeployBlock(readFileSync(p, "utf8"));
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
//
|
|
45
|
-
// copyWorkflowsForType
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
46
|
+
// File filter applied to a type root directory: combines the deploy style filter and the Flutter store selection.
|
|
47
|
+
// copyWorkflowsForType, surveyWorkflows and planWorkflows must use the same function so install, conflict survey
|
|
48
|
+
// and status/dry-run never see different file sets. When flutterStore is not an array (null = undecided,
|
|
49
|
+
// non-interactive default) no store filter is applied. It always returns a function even without filters,
|
|
50
|
+
// because processDir cannot take null.
|
|
51
|
+
// Some types (go/python, react/next) keep server deploy workflows directly in the type root, so the deploy style
|
|
52
|
+
// filter is always applied. `available` (set of payload file names) lets a type without the chosen style fall back
|
|
53
|
+
// to the single server deploy.
|
|
50
54
|
export function buildTypeRootFilter(type, deployStyle, flutterStore, available = null) {
|
|
51
55
|
const filters = [];
|
|
52
56
|
if (deployStyle) filters.push(deployFilter(deployStyle, available));
|
|
@@ -55,21 +59,21 @@ export function buildTypeRootFilter(type, deployStyle, flutterStore, available =
|
|
|
55
59
|
return (filename) => filters.every((keep) => keep(filename));
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
//
|
|
59
|
-
//
|
|
62
|
+
// Returns the source directories a type installs, in order: type root -> server-deploy.
|
|
63
|
+
// Each item: { srcDir, type, filter }. Folder existence and the "no deploy" exclusion are decided here in one place.
|
|
60
64
|
export function typeWorkflowSources(type, payloadRoot, { deployStyle, flutterStore = null, available = null }) {
|
|
61
65
|
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
62
66
|
const sources = [];
|
|
63
|
-
// go/python
|
|
67
|
+
// Some types (go/python, react/next) keep server deploy workflows directly in the type root, so filter the root by deploy style too.
|
|
64
68
|
if (exists(typeDir)) sources.push({ srcDir: typeDir, type, filter: buildTypeRootFilter(type, deployStyle, flutterStore, available) });
|
|
65
|
-
// "
|
|
69
|
+
// "No deploy" excludes the whole folder
|
|
66
70
|
if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
|
|
67
71
|
sources.push({ srcDir: serverDeployDir, type, filter: deployFilter(deployStyle, available) });
|
|
68
72
|
}
|
|
69
73
|
return sources;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
// common
|
|
76
|
+
// The common source, reflecting the trunk-based exclusion filter. null when the folder is missing.
|
|
73
77
|
export function commonWorkflowSource(context, payloadRoot) {
|
|
74
78
|
const commonDir = join(payloadRoot, PAYLOAD.workflowsDir, "common");
|
|
75
79
|
if (!exists(commonDir)) return null;
|
|
@@ -80,8 +84,8 @@ export function commonWorkflowSource(context, payloadRoot) {
|
|
|
80
84
|
};
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
//
|
|
84
|
-
//
|
|
87
|
+
// Yields the workflow sources to install in engine order (common -> type iteration -> type root -> server-deploy).
|
|
88
|
+
// Copy, conflict survey and preview planning must all use this iteration so they never see different file sets.
|
|
85
89
|
export function* workflowSources(context, payloadRoot, { deployStyle, available }) {
|
|
86
90
|
const { types = [], flutterStore = null } = context;
|
|
87
91
|
const common = commonWorkflowSource(context, payloadRoot);
|
|
@@ -89,8 +93,8 @@ export function* workflowSources(context, payloadRoot, { deployStyle, available
|
|
|
89
93
|
for (const type of types) yield* typeWorkflowSources(type, payloadRoot, { deployStyle, flutterStore, available });
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
//
|
|
93
|
-
// values/useDefaults: env
|
|
96
|
+
// Applies env substitution to one file and updates the target file.
|
|
97
|
+
// values/useDefaults: result of the env plan (promptEnvPlan); when unset, the default path (current force behavior).
|
|
94
98
|
function configureEnv(targetPath, { type, projectPath = ".", repoName = "", resolvers = {}, collectAsks = null, values = new Map(), useDefaults = true, savedValues = null }) {
|
|
95
99
|
const content = readFileSync(targetPath, "utf8");
|
|
96
100
|
if (!content.includes("@wizard")) return;
|
|
@@ -98,28 +102,29 @@ function configureEnv(targetPath, { type, projectPath = ".", repoName = "", reso
|
|
|
98
102
|
writeFileSync(targetPath, out);
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
// payload
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
+
// The payload source as the "final form virtually substituted with the values this run will use": reflects the
|
|
106
|
+
// saved deploy values (defaults if none) and the values answered this time. Using only defaults would make a file
|
|
107
|
+
// installed with non-default values look changed on every re-run, and rewriting it would revert the answered
|
|
108
|
+
// values to defaults.
|
|
109
|
+
// Compared with baseline.rendered to decide "did upstream change".
|
|
105
110
|
function renderVirtual(templateContent, envOpts) {
|
|
106
111
|
return substituteEnv(templateContent, { useDefaults: true, ...envOpts, collectAsks: null });
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
//
|
|
114
|
+
// Classification, relative to the target workflows directory. srcText: source loader with branch substitution
|
|
115
|
+
// applied (makeSrcText).
|
|
110
116
|
//
|
|
111
|
-
// baseline
|
|
112
|
-
//
|
|
113
|
-
// 고를 수 있었다.
|
|
117
|
+
// With a baseline it splits 3-way. Before base existed, even a one-character upstream fix pushed files the user
|
|
118
|
+
// never touched into changed, leaving only "skip everything" or "backup everything" as choices.
|
|
114
119
|
//
|
|
115
|
-
// ours === theirs
|
|
116
|
-
// sha(ours) === base.installed
|
|
117
|
-
// sha(theirs) === base.rendered
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
+
// ours === theirs -> unchanged already current, nothing to do
|
|
121
|
+
// sha(ours) === base.installed -> upstreamOnly user did not modify -> replace without asking
|
|
122
|
+
// sha(theirs) === base.rendered -> localOnly upstream unchanged -> keep without asking
|
|
123
|
+
// otherwise -> changed real conflict -> ask
|
|
124
|
+
// missing on disk but in baseline -> removed user deleted it -> ask before restoring
|
|
120
125
|
//
|
|
121
|
-
// baseline
|
|
122
|
-
//
|
|
126
|
+
// Existing installs without a baseline have no base, so upstreamOnly/localOnly cannot be decided and they fall
|
|
127
|
+
// back to the old unchanged/changed two-way split. The baseline is planted in that run.
|
|
123
128
|
function classify(srcDir, workflowsDir, envOpts, srcText, baseline = null, filter = null) {
|
|
124
129
|
const result = { newFiles: [], unchanged: [], changed: [], upstreamOnly: [], localOnly: [], removed: [] };
|
|
125
130
|
for (const filename of listYamlFiles(srcDir)) {
|
|
@@ -129,8 +134,8 @@ function classify(srcDir, workflowsDir, envOpts, srcText, baseline = null, filte
|
|
|
129
134
|
const base = baseline?.files?.[filename] || null;
|
|
130
135
|
|
|
131
136
|
if (!existsSync(dst)) {
|
|
132
|
-
// baseline
|
|
133
|
-
//
|
|
137
|
+
// In baseline but missing on disk = the user deleted a file we installed.
|
|
138
|
+
// Needing no separate deletion-history file is a by-product of the baseline design.
|
|
134
139
|
if (base) result.removed.push(filename);
|
|
135
140
|
else result.newFiles.push(filename);
|
|
136
141
|
continue;
|
|
@@ -147,12 +152,12 @@ function classify(srcDir, workflowsDir, envOpts, srcText, baseline = null, filte
|
|
|
147
152
|
return result;
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
//
|
|
151
|
-
// filter:
|
|
155
|
+
// Processes one source directory according to the classification. common, per-type and server-deploy share the rules.
|
|
156
|
+
// filter: per-file filter such as the trunk-based exclusion.
|
|
152
157
|
//
|
|
153
|
-
//
|
|
154
|
-
// upstreamOnly
|
|
155
|
-
// localOnly
|
|
158
|
+
// The two automatically handled buckets are the heart of this function:
|
|
159
|
+
// upstreamOnly: the user did not touch it, so just replace it with the latest. No reason to ask.
|
|
160
|
+
// localOnly: upstream is unchanged, so keep the user's edited copy. No reason to ask either.
|
|
156
161
|
function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () => true) {
|
|
157
162
|
const { srcText, baseline, decisions, restoreRemoved, baselineTargets } = ctx;
|
|
158
163
|
const c = classify(srcDir, workflowsDir, envOpts, srcText, baseline);
|
|
@@ -166,7 +171,7 @@ function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () =>
|
|
|
166
171
|
|
|
167
172
|
for (const f of c.localOnly.filter(filter)) {
|
|
168
173
|
counters.skipped++; counters.keptLocal.push(f); track(f, false);
|
|
169
|
-
log.info("copy", "keep-local",
|
|
174
|
+
log.info("copy", "keep-local", t("copy.workflows.log.keepLocal", { f }));
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
for (const f of c.newFiles.filter(filter)) {
|
|
@@ -176,28 +181,28 @@ function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () =>
|
|
|
176
181
|
|
|
177
182
|
for (const f of c.upstreamOnly.filter(filter)) {
|
|
178
183
|
write(f); counters.autoUpdated.push(f);
|
|
179
|
-
log.info("copy", "auto-update",
|
|
184
|
+
log.info("copy", "auto-update", t("copy.workflows.log.autoUpdate", { f }));
|
|
180
185
|
}
|
|
181
186
|
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
187
|
+
// Files the user deleted are not silently revived; they are rewritten only when there is a restore decision.
|
|
188
|
+
// Files not revived are not put into baselineTargets: they are absent on disk so there is nothing to hash, and
|
|
189
|
+
// the existing baseline entry survives the merge so the next run still recognizes them as "deleted files".
|
|
185
190
|
for (const f of c.removed.filter(filter)) {
|
|
186
191
|
if (restoreRemoved.has(f)) {
|
|
187
192
|
write(f); counters.restoredFiles.push(f);
|
|
188
|
-
log.info("copy", "restore",
|
|
193
|
+
log.info("copy", "restore", t("copy.workflows.log.restore", { f }));
|
|
189
194
|
} else {
|
|
190
195
|
counters.removedKept.push(f);
|
|
191
|
-
log.info("copy", "removed-kept",
|
|
196
|
+
log.info("copy", "removed-kept", t("copy.workflows.log.removedKept", { f }));
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
|
|
195
200
|
for (const f of c.changed.filter(filter)) {
|
|
196
201
|
const decision = decisions.get(f);
|
|
197
202
|
applyDecision(decision, srcDir, workflowsDir, f, counters, srcText);
|
|
198
|
-
// 'backup'
|
|
199
|
-
// skip
|
|
200
|
-
// "
|
|
203
|
+
// Only 'backup' rewrites the target file itself; 'template' is a different file name and 'skip' keeps the existing one.
|
|
204
|
+
// skip did not take the upstream change, so rendered stays at the old value: switching it to the new value
|
|
205
|
+
// would classify the file as "upstream unchanged" next run and it could never receive that change.
|
|
201
206
|
const kept = decision !== "backup" && decision !== "template";
|
|
202
207
|
if (kept) counters.conflictKept.push(f);
|
|
203
208
|
track(f, decision === "backup", kept);
|
|
@@ -205,37 +210,37 @@ function processDir(srcDir, workflowsDir, envOpts, ctx, counters, filter = () =>
|
|
|
205
210
|
return c;
|
|
206
211
|
}
|
|
207
212
|
|
|
208
|
-
//
|
|
213
|
+
// Copy engine body (synchronous, existing call sites unchanged).
|
|
209
214
|
// context: { types:[], paths:Map, force, repoName, resolvers,
|
|
210
|
-
// envValues?:Map<key,value>, envUseDefaults?:boolean }
|
|
211
|
-
// flutterStore?:string[]|null }
|
|
212
|
-
// hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'>,
|
|
213
|
-
// restoreRemoved?: Set<filename> }
|
|
214
|
-
//
|
|
215
|
-
//
|
|
215
|
+
// envValues?:Map<key,value>, envUseDefaults?:boolean } <- injection point for the env plan (promptEnvPlan) result
|
|
216
|
+
// flutterStore?:string[]|null } <- Flutter store targets (null = no filter, array = selected platforms only)
|
|
217
|
+
// hooks: { decisions?: Map<filename, 'skip'|'backup'|'template'>, - decisions for real conflicts (changed)
|
|
218
|
+
// restoreRemoved?: Set<filename> } - user-deleted files to restore
|
|
219
|
+
// Unspecified files are 'skip' (current force behavior kept 100%). See copyWorkflowsInteractive for interactive collection.
|
|
220
|
+
// Returns: {copied, skipped, templateAdded, backupAdded, autoUpdated, keptLocal, removedKept, restoredFiles}
|
|
216
221
|
export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
217
222
|
const { types = [], paths = new Map(), repoName = "", resolvers = {}, envValues = new Map(), envUseDefaults = true } = context;
|
|
218
223
|
const decisions = hooks.decisions instanceof Map ? hooks.decisions : new Map();
|
|
219
224
|
const restoreRemoved = hooks.restoreRemoved instanceof Set ? hooks.restoreRemoved : new Set();
|
|
220
225
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
221
226
|
const projectTypesDir = join(payloadRoot, PAYLOAD.workflowsDir);
|
|
222
|
-
if (!exists(projectTypesDir)) throw new Error("
|
|
227
|
+
if (!exists(projectTypesDir)) throw new Error(t("copy.workflows.error.noPayloadWorkflows"));
|
|
223
228
|
|
|
224
229
|
const counters = { copied: 0, skipped: 0, templateAdded: 0, backupAdded: 0 };
|
|
225
|
-
const deployValues = new Map(); // Map<type, Map<key,value
|
|
230
|
+
const deployValues = new Map(); // Map<type, Map<key,value>>: ask values for the deploy block
|
|
226
231
|
counters.deployValues = deployValues;
|
|
227
|
-
counters.copiedFiles = []; //
|
|
228
|
-
counters.unchangedFiles = []; // skip(unchanged)
|
|
229
|
-
counters.autoUpdated = []; //
|
|
230
|
-
counters.keptLocal = []; //
|
|
231
|
-
counters.removedKept = []; //
|
|
232
|
-
counters.restoredFiles = []; //
|
|
233
|
-
counters.conflictKept = []; //
|
|
232
|
+
counters.copiedFiles = []; // file names actually newly written in this run (for printSummary accuracy)
|
|
233
|
+
counters.unchangedFiles = []; // skip (unchanged) targets: the evidence for "why nothing changed" in the log
|
|
234
|
+
counters.autoUpdated = []; // files replaced with the latest without asking (user did not modify)
|
|
235
|
+
counters.keptLocal = []; // files whose user-edited copy was kept without asking (upstream unchanged)
|
|
236
|
+
counters.removedKept = []; // files the user deleted and that were not revived
|
|
237
|
+
counters.restoredFiles = []; // files the user deleted but decided to restore
|
|
238
|
+
counters.conflictKept = []; // files where both sides changed and the existing file was kept (upstream change not applied)
|
|
234
239
|
const deployStyle = context.deployStyle || DEFAULT_DEPLOY_STYLE;
|
|
235
240
|
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
236
241
|
const baseline = readBaseline(targetRoot);
|
|
237
242
|
const baselineTargets = new Map(); // filename -> { srcPath, envOpts, wrote }
|
|
238
|
-
// values/useDefaults
|
|
243
|
+
// values/useDefaults only matter on the substitution path (renderVirtual forces useDefaults:true, so virtual comparison is unaffected)
|
|
239
244
|
const saved = readSavedDeployValues(targetRoot);
|
|
240
245
|
const envOptsFor = (type) => ({
|
|
241
246
|
type, projectPath: paths.get(type) || ".", repoName, resolvers, values: envValues, useDefaults: envUseDefaults,
|
|
@@ -244,15 +249,15 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
244
249
|
const available = payloadWorkflowNames(payloadRoot);
|
|
245
250
|
const dirCtx = { srcText, baseline, decisions, restoreRemoved, baselineTargets, available };
|
|
246
251
|
|
|
247
|
-
// (1) common
|
|
248
|
-
// trunk-based
|
|
252
|
+
// (1) common: same rules as per type (README contract).
|
|
253
|
+
// trunk-based mode does not install VERSION-CONTROL/AUTO-CHANGELOG (RELEASE-PUBLISH alone).
|
|
249
254
|
const commonSource = commonWorkflowSource(context, payloadRoot);
|
|
250
255
|
if (commonSource) {
|
|
251
256
|
const { srcDir: commonDir, filter: notExcluded } = commonSource;
|
|
252
257
|
const c = processDir(commonDir, workflowsDir, envOptsFor("common"), dirCtx, counters, notExcluded);
|
|
253
|
-
// env
|
|
254
|
-
//
|
|
255
|
-
// ISSUE_HELPER_CREATE_BRANCH
|
|
258
|
+
// env substitution: as with per-type folders (copyWorkflowsForType), files decided to be left alone
|
|
259
|
+
// (unchanged/localOnly) are skipped. Common top level had no @wizard markers until now so the missing loop
|
|
260
|
+
// went unnoticed, but from ISSUE_HELPER_CREATE_BRANCH on the values must actually be applied.
|
|
256
261
|
const untouched = [...c.unchanged, ...c.localOnly];
|
|
257
262
|
for (const filename of listYamlFiles(commonDir)) {
|
|
258
263
|
if (!notExcluded(filename)) continue;
|
|
@@ -263,73 +268,74 @@ export function copyWorkflows(context, payloadRoot, targetRoot = ".", hooks = {}
|
|
|
263
268
|
}
|
|
264
269
|
}
|
|
265
270
|
|
|
266
|
-
// (2
|
|
271
|
+
// (2-4) per type
|
|
267
272
|
for (const type of types) {
|
|
268
273
|
const asks = new Map();
|
|
269
274
|
copyWorkflowsForType(type, payloadRoot, workflowsDir, { ...context, deployStyle, envOptsFor, collectAsks: asks, dirCtx }, counters);
|
|
270
275
|
if (asks.size) deployValues.set(type, asks);
|
|
271
276
|
}
|
|
272
277
|
|
|
273
|
-
counters.baselineTargets = baselineTargets; //
|
|
278
|
+
counters.baselineTargets = baselineTargets; // the caller (runFull) records the baseline after env substitution completes
|
|
274
279
|
return counters;
|
|
275
280
|
}
|
|
276
281
|
|
|
277
|
-
// copyWorkflows
|
|
282
|
+
// Call after copyWorkflows has finished including env substitution, so the disk content is the final form.
|
|
278
283
|
// entries: Map<filename, {installed:string|null, rendered:string}>
|
|
279
|
-
// savedDeploy:
|
|
280
|
-
//
|
|
284
|
+
// savedDeploy: values this run wrote to the version.yml deploy block (Map<type, Map<key,value>>). The next run
|
|
285
|
+
// classifies with these as defaults, so rendered must be computed with the same values or a re-run is mistaken
|
|
286
|
+
// for an "upstream change".
|
|
281
287
|
export function computeBaselineEntries(baselineTargets, workflowsDir, srcText, savedDeploy = null) {
|
|
282
288
|
const entries = new Map();
|
|
283
289
|
for (const [filename, info] of baselineTargets) {
|
|
284
290
|
const dst = join(workflowsDir, filename);
|
|
285
291
|
if (!existsSync(dst)) continue;
|
|
286
292
|
const envOpts = savedDeploy ? { ...info.envOpts, savedValues: savedDeploy.get(info.envOpts.type) || null } : info.envOpts;
|
|
287
|
-
// keepRendered:
|
|
293
|
+
// keepRendered: the existing file was kept due to a conflict; pass null so the old rendered stays (writeBaseline merges).
|
|
288
294
|
const rendered = info.keepRendered ? null : sha256(renderVirtual(srcText(info.srcPath), envOpts));
|
|
289
|
-
// installed
|
|
290
|
-
// "
|
|
295
|
+
// installed is filled only for files we wrote this time. Recording a user-edited copy as installed would
|
|
296
|
+
// falsely claim "we wrote this", and the file would be silently overwritten on the next update.
|
|
291
297
|
entries.set(filename, { installed: info.wrote ? sha256(readFileSync(dst, "utf8")) : null, rendered });
|
|
292
298
|
}
|
|
293
299
|
return entries;
|
|
294
300
|
}
|
|
295
301
|
|
|
296
|
-
// changed(
|
|
297
|
-
// 'skip'(
|
|
302
|
+
// Handles one changed file (existing and with different content) according to the decision.
|
|
303
|
+
// 'skip' (default): keep the existing one. 'backup': existing -> .bak, then replace. 'template': keep the existing one + write the new version as .template.yaml.
|
|
298
304
|
function applyDecision(decision, srcDir, workflowsDir, filename, counters, srcText) {
|
|
299
305
|
const src = join(srcDir, filename);
|
|
300
306
|
const dst = join(workflowsDir, filename);
|
|
301
307
|
if (decision === "backup") {
|
|
302
|
-
//
|
|
308
|
+
// Back up the existing file as .bak, then replace it with the new version
|
|
303
309
|
renameSync(dst, dst + ".bak");
|
|
304
310
|
writeText(dst, srcText(src));
|
|
305
311
|
counters.copied++;
|
|
306
312
|
counters.backupAdded++;
|
|
307
313
|
counters.copiedFiles.push(filename);
|
|
308
|
-
log.info("copy", "backup",
|
|
314
|
+
log.info("copy", "backup", t("copy.workflows.log.backup", { filename }));
|
|
309
315
|
return;
|
|
310
316
|
}
|
|
311
317
|
if (decision === "template") {
|
|
312
|
-
// `${
|
|
318
|
+
// `${name}.template.yaml`: strip only .yaml and append (.yml is kept and the suffix is appended after it)
|
|
313
319
|
const templateName = (filename.endsWith(".yaml") ? filename.slice(0, -".yaml".length) : filename) + ".template.yaml";
|
|
314
|
-
writeText(join(workflowsDir, templateName), srcText(src)); //
|
|
320
|
+
writeText(join(workflowsDir, templateName), srcText(src)); // overwrites an existing .template.yaml
|
|
315
321
|
counters.templateAdded++;
|
|
316
322
|
counters.copiedFiles.push(templateName);
|
|
317
|
-
log.info("copy", "template",
|
|
323
|
+
log.info("copy", "template", t("copy.workflows.log.template", { filename, templateName }));
|
|
318
324
|
return;
|
|
319
325
|
}
|
|
320
|
-
counters.skipped++; // 'skip'
|
|
321
|
-
//
|
|
326
|
+
counters.skipped++; // 'skip'/unspecified/ESC -> keep the existing file (force default)
|
|
327
|
+
// Without a decision the user did not choose; it is the --force default. Distinguish so the log stays truthful.
|
|
322
328
|
log.info("copy", "skip", decision
|
|
323
|
-
?
|
|
324
|
-
:
|
|
329
|
+
? t("copy.workflows.log.skipDecided", { filename })
|
|
330
|
+
: t("copy.workflows.log.skipForce", { filename }));
|
|
325
331
|
}
|
|
326
332
|
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// common
|
|
330
|
-
//
|
|
331
|
-
// conflicts
|
|
332
|
-
// removed
|
|
333
|
+
// Interactive pre-survey: picks only what a human must answer.
|
|
334
|
+
// It must use the same classify criteria as the copyWorkflows body so the decisions Map matches the actual
|
|
335
|
+
// processing targets 1:1. common is scanned like per-type too (previously common conflicts were never even asked).
|
|
336
|
+
// Returns: { conflicts: [{filename,type}], removed: [{filename,type}] }
|
|
337
|
+
// conflicts: real conflicts where both sides changed. upstreamOnly/localOnly are handled automatically, so not here.
|
|
338
|
+
// removed: files we installed that the user deleted. Must be asked about before reviving.
|
|
333
339
|
export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
334
340
|
const { paths = new Map(), repoName = "", resolvers = {} } = context;
|
|
335
341
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
@@ -337,14 +343,14 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
337
343
|
const srcText = makeSrcText(context.branches || null, deployStyle);
|
|
338
344
|
const baseline = readBaseline(targetRoot);
|
|
339
345
|
const saved = readSavedDeployValues(targetRoot);
|
|
340
|
-
const conflicts = []; //
|
|
346
|
+
const conflicts = []; // same as the engine processing order (common -> type iteration -> direct children -> server-deploy)
|
|
341
347
|
const removed = [];
|
|
342
348
|
const available = payloadWorkflowNames(payloadRoot);
|
|
343
349
|
const values = context.envValues || new Map();
|
|
344
350
|
const useDefaults = context.envUseDefaults !== false;
|
|
345
351
|
|
|
346
352
|
for (const { srcDir, type, filter } of workflowSources(context, payloadRoot, { deployStyle, available })) {
|
|
347
|
-
//
|
|
353
|
+
// Classify with the same values as the copy engine (including this run's answers) so the decision list matches the actual targets.
|
|
348
354
|
const envOpts = type === "common"
|
|
349
355
|
? { type, projectPath: ".", repoName, resolvers, values, useDefaults }
|
|
350
356
|
: { type, projectPath: paths.get(type) || ".", repoName, resolvers, savedValues: saved.get(type) || null, values, useDefaults };
|
|
@@ -355,20 +361,20 @@ export function surveyWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
355
361
|
return { conflicts, removed };
|
|
356
362
|
}
|
|
357
363
|
|
|
358
|
-
//
|
|
364
|
+
// When only the real conflict list is needed (compatible with existing call sites).
|
|
359
365
|
export function listWorkflowConflicts(context, payloadRoot, targetRoot = ".") {
|
|
360
366
|
return surveyWorkflows(context, payloadRoot, targetRoot).conflicts;
|
|
361
367
|
}
|
|
362
368
|
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
// onConflict
|
|
369
|
+
// Interactive entry point (async): awaits onConflict(filename, type) per conflict to build the decisions Map,
|
|
370
|
+
// then delegates to the synchronous engine. WHY separate: making copyWorkflows async would break existing call
|
|
371
|
+
// sites (runFull) that call it without await; the signature stays unchanged.
|
|
372
|
+
// onConflict return value: 'template' | 'skip' | 'backup' (anything else/unset -> 'skip').
|
|
367
373
|
export async function copyWorkflowsInteractive(context, payloadRoot, targetRoot = ".", { onConflict } = {}) {
|
|
368
374
|
const decisions = new Map();
|
|
369
375
|
if (typeof onConflict === "function") {
|
|
370
376
|
for (const { filename, type } of listWorkflowConflicts(context, payloadRoot, targetRoot)) {
|
|
371
|
-
if (decisions.has(filename)) continue; //
|
|
377
|
+
if (decisions.has(filename)) continue; // file names are unique across types via the PROJECT-{TYPE}- prefix
|
|
372
378
|
decisions.set(filename, await onConflict(filename, type));
|
|
373
379
|
}
|
|
374
380
|
}
|
|
@@ -380,8 +386,8 @@ function copyWorkflowsForType(type, payloadRoot, workflowsDir, ctx, counters) {
|
|
|
380
386
|
const { srcText } = dirCtx;
|
|
381
387
|
const sources = typeWorkflowSources(type, payloadRoot, { deployStyle, flutterStore, available: dirCtx.available });
|
|
382
388
|
const envOpts = envOptsFor(type);
|
|
383
|
-
// env
|
|
384
|
-
//
|
|
389
|
+
// Files excluded from env substitution: re-applying substitution to files decided to be left alone
|
|
390
|
+
// (unchanged/localOnly/kept deletions) would overwrite the user's edited copy.
|
|
385
391
|
const untouched = [];
|
|
386
392
|
|
|
387
393
|
for (const { srcDir, filter } of sources) {
|
|
@@ -389,26 +395,27 @@ function copyWorkflowsForType(type, payloadRoot, workflowsDir, ctx, counters) {
|
|
|
389
395
|
untouched.push(...c.unchanged, ...c.localOnly);
|
|
390
396
|
}
|
|
391
397
|
|
|
392
|
-
// env
|
|
398
|
+
// env substitution: only files that were copied from this type's source directories, exist, and were not decided to be left alone
|
|
393
399
|
for (const { srcDir, filter } of sources) {
|
|
394
400
|
for (const filename of listYamlFiles(srcDir)) {
|
|
395
401
|
const target = join(workflowsDir, filename);
|
|
396
|
-
if (!filter(filename)) continue; //
|
|
397
|
-
if (!existsSync(target)) continue; //
|
|
398
|
-
// unchanged/localOnly
|
|
399
|
-
//
|
|
402
|
+
if (!filter(filename)) continue; // excluded CD, unchosen deploy styles, unchosen store workflows
|
|
403
|
+
if (!existsSync(target)) continue; // skip files that were skipped
|
|
404
|
+
// unchanged/localOnly and kept copies that no longer have substitution markers are not substituted again.
|
|
405
|
+
// Instead collect only the ask values from the saved values (defaults if none) so the deploy block does not
|
|
406
|
+
// disappear; the file itself is left untouched.
|
|
400
407
|
if (untouched.includes(filename) || !readFileSync(target, "utf8").includes("@wizard")) {
|
|
401
408
|
if (collectAsks) substituteEnv(srcText(join(srcDir, filename)), { ...envOpts, useDefaults: true, collectAsks });
|
|
402
409
|
continue;
|
|
403
410
|
}
|
|
404
|
-
configureEnv(target, { ...envOpts, collectAsks }); // env
|
|
411
|
+
configureEnv(target, { ...envOpts, collectAsks }); // includes the env plan values/useDefaults
|
|
405
412
|
}
|
|
406
413
|
}
|
|
407
414
|
}
|
|
408
415
|
|
|
409
|
-
//
|
|
410
|
-
// changed
|
|
411
|
-
//
|
|
416
|
+
// Full workflow classification (common + per type + server-deploy), shared by status/dry-run.
|
|
417
|
+
// Unlike listWorkflowConflicts (which returns only changed), it returns everything including newFiles/unchanged
|
|
418
|
+
// (read-only: writes no files).
|
|
412
419
|
export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
413
420
|
const { paths = new Map(), repoName = "", resolvers = {} } = context;
|
|
414
421
|
const workflowsDir = join(targetRoot, PATHS.workflowsDir);
|
|
@@ -417,7 +424,7 @@ export function planWorkflows(context, payloadRoot, targetRoot = ".") {
|
|
|
417
424
|
const baseline = readBaseline(targetRoot);
|
|
418
425
|
const saved = readSavedDeployValues(targetRoot);
|
|
419
426
|
const available = payloadWorkflowNames(payloadRoot);
|
|
420
|
-
// upstreamOnly/localOnly/removed
|
|
427
|
+
// upstreamOnly/localOnly/removed are filled only when a baseline exists.
|
|
421
428
|
const plan = { newFiles: [], unchanged: [], changed: [], upstreamOnly: [], localOnly: [], removed: [] };
|
|
422
429
|
const BUCKETS = ["newFiles", "unchanged", "changed", "upstreamOnly", "localOnly", "removed"];
|
|
423
430
|
|