project-auto-wizard 0.13.3 → 0.15.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.ja.md +2 -0
- package/README.ko.md +2 -0
- package/README.md +3 -1
- package/README.zh-CN.md +2 -0
- 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 +304 -265
- package/payload/scripts/issue_helper.py +54 -50
- package/payload/scripts/messages.py +2585 -0
- package/payload/scripts/truncate_release_notes.py +7 -6
- package/payload/scripts/version_manager.py +110 -107
- package/payload/version.yml.template +2 -1
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +23 -16
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +106 -63
- package/payload/workflows/common/PROJECT-COMMON-ISSUE-HELPER.yaml +17 -15
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +55 -45
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +90 -72
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +59 -45
- 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 +90 -89
- package/src/commands/dry-run.js +64 -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 +102 -36
- 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 +188 -0
- package/src/i18n/catalog/en/copy.js +20 -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 +188 -0
- package/src/i18n/catalog/ko/copy.js +20 -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 +132 -97
- 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 +90 -89
package/src/core/version-yml.js
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
import { DEFAULT_DEPLOY_STYLE } from "./deploy-style.js";
|
|
2
2
|
import { escapeYamlDoubleQuoted } from "./wizard-env.js";
|
|
3
3
|
import { hooksFor, mergeHookResults, allHookValues } from "./types.js";
|
|
4
|
+
import { DEFAULT_LANGUAGE, isSupportedLanguage, normalizeLanguage } from "../i18n/languages.js";
|
|
5
|
+
// Aliased: `t` is used as a local variable name (type, trimmed line) throughout this file.
|
|
6
|
+
import { t as tr } from "../i18n/index.js";
|
|
4
7
|
|
|
5
|
-
// version.yml
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
+
// version.yml parsing and generation (full-regeneration strategy).
|
|
9
|
+
// WARNING: no YAML re-serialization - the comments are data.
|
|
10
|
+
// Single source of truth for the layout = payload/version.yml.template (injected by the caller as templateText).
|
|
8
11
|
|
|
9
|
-
// version.yml.template
|
|
10
|
-
// "project_type"(
|
|
11
|
-
//
|
|
12
|
-
//
|
|
12
|
+
// Top-level keys the version.yml.template knows - any other top-level key is considered added by the user.
|
|
13
|
+
// "project_type" (singular) is a legacy key that is no longer rendered but stays in this set:
|
|
14
|
+
// if removed, the singular line of an existing file would be mistaken for a "field added by the user" and
|
|
15
|
+
// revived on regeneration. It must stay a known key so it is absorbed and disappears on re-integration.
|
|
13
16
|
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
14
|
-
"version", "version_code", "project_types", "project_type", "project_paths", "metadata", "deploy",
|
|
17
|
+
"version", "version_code", "project_types", "project_type", "language", "project_paths", "metadata", "deploy",
|
|
15
18
|
]);
|
|
16
19
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
//
|
|
20
|
-
//
|
|
20
|
+
// Preserve unknown top-level fields (arbitrary fields the user added) verbatim.
|
|
21
|
+
// Everything from each unknown top-level key up to just before the next top-level key is captured
|
|
22
|
+
// as one block (so it stays valid YAML even with nested structure). Unknown sub-keys "inside" blocks this
|
|
23
|
+
// module already knows, such as metadata/project_paths/deploy, are out of scope.
|
|
21
24
|
export function parseExtraTopLevel(content) {
|
|
22
25
|
const blocks = [];
|
|
23
26
|
let current = null;
|
|
24
27
|
for (const line of String(content || "").split("\n")) {
|
|
25
|
-
//
|
|
28
|
+
// Top-level keys may contain hyphens (YAML convention).
|
|
26
29
|
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):/);
|
|
27
30
|
if (m) {
|
|
28
31
|
if (current) blocks.push(current.join("\n"));
|
|
@@ -35,22 +38,22 @@ export function parseExtraTopLevel(content) {
|
|
|
35
38
|
return blocks;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
//
|
|
41
|
+
// Type-specific option key -> returned field (type hook savedOptionKeys). The value is returned as the raw string; validity is up to the type hook's resolveOptions.
|
|
39
42
|
const TYPE_OPTION_KEYS = allHookValues("savedOptionKeys");
|
|
40
43
|
const TYPE_OPTION_LINE = new RegExp(`^\\s+(${Object.keys(TYPE_OPTION_KEYS).join("|")}):\\s*(.+)`);
|
|
41
44
|
|
|
42
|
-
// metadata.template.options
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
45
|
+
// State-machine parse of metadata.template.options.
|
|
46
|
+
// Returns: { semverAuto: bool|null, copilotAi: bool|null, deployStyle: string|null,
|
|
47
|
+
// type-specific option fields (e.g. envMode/flutterStore/androidDeployMode/iosDeployMode): string|null } - null = not written.
|
|
48
|
+
// Other keys such as the old synology/coderabbit ones hit no branch and are naturally ignored (no parse error).
|
|
46
49
|
export function parseTemplateOptions(content) {
|
|
47
50
|
const out = {
|
|
48
51
|
semverAuto: null, copilotAi: null, deployStyle: null,
|
|
49
52
|
...Object.fromEntries(Object.values(TYPE_OPTION_KEYS).map((field) => [field, null])),
|
|
50
53
|
};
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
+
// Value normalization: strip quotes + trim
|
|
55
|
+
// Strip the inline comment (` # ...`) first, then clean quotes and whitespace. For keys taking a string value (deploy_style),
|
|
56
|
+
// without stripping the comment the whole "simple # simple | nginx ..." becomes the value.
|
|
54
57
|
const strip = (s) => String(s).replace(/\s+#.*$/, "").replace(/["']/g, "").trim();
|
|
55
58
|
let inTemplate = false;
|
|
56
59
|
let inOptions = false;
|
|
@@ -76,36 +79,39 @@ export function parseTemplateOptions(content) {
|
|
|
76
79
|
if (v === "false") out.copilotAi = false;
|
|
77
80
|
continue;
|
|
78
81
|
}
|
|
79
|
-
//
|
|
82
|
+
// another key indented 0-4 spaces -> end of the options section
|
|
80
83
|
if (/^\s{0,4}[a-z_]+:/.test(line)) { inOptions = false; inTemplate = false; }
|
|
81
84
|
}
|
|
82
|
-
//
|
|
85
|
+
// top-level key -> end of the template section
|
|
83
86
|
if (inTemplate && /^[a-z_]+:/.test(line)) { inTemplate = false; inOptions = false; }
|
|
84
87
|
}
|
|
85
88
|
return out;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
|
-
//
|
|
91
|
+
// Extract values from an existing version.yml (line-based, avoids false hits on comment lines).
|
|
89
92
|
export function parseExisting(content) {
|
|
90
93
|
const text = String(content || "");
|
|
91
94
|
const line = (re) => {
|
|
92
95
|
for (const l of text.split("\n")) {
|
|
93
|
-
if (l.startsWith("#")) continue; //
|
|
96
|
+
if (l.startsWith("#")) continue; // exclude comments
|
|
94
97
|
const m = l.match(re);
|
|
95
98
|
if (m) return m[1];
|
|
96
99
|
}
|
|
97
100
|
return null;
|
|
98
101
|
};
|
|
99
|
-
// version: "x.y.z" (
|
|
102
|
+
// version: "x.y.z" (digits.digits.digits form only)
|
|
100
103
|
const version = line(/^version:\s*["']?([0-9][0-9.]*)["']?/) || "";
|
|
101
|
-
// version_code: N (
|
|
104
|
+
// version_code: N (positive integer, otherwise 1)
|
|
102
105
|
let versionCode = parseInt(line(/^version_code:\s*([0-9]+)/) || "", 10);
|
|
103
106
|
if (!Number.isInteger(versionCode) || versionCode <= 0) versionCode = 1;
|
|
104
107
|
// project_types: ["a","b"]
|
|
105
108
|
const typesRaw = line(/^project_types:\s*(\[[^\]]*\])/);
|
|
106
109
|
let types = [];
|
|
107
110
|
if (typesRaw) types = [...typesRaw.matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
108
|
-
//
|
|
111
|
+
// language: "en" - only supported values count; a hand-edited unknown value is treated as unset
|
|
112
|
+
const langRaw = normalizeLanguage((line(/^language:\s*(.+)/) || "").replace(/\s+#.*$/, "").replace(/["']/g, ""));
|
|
113
|
+
const language = isSupportedLanguage(langRaw) ? langRaw : null;
|
|
114
|
+
// project_paths block: ` type: "path"`
|
|
109
115
|
const paths = new Map();
|
|
110
116
|
let inPaths = false;
|
|
111
117
|
for (const l of text.split("\n")) {
|
|
@@ -113,10 +119,10 @@ export function parseExisting(content) {
|
|
|
113
119
|
if (inPaths) {
|
|
114
120
|
const m = l.match(/^\s+([a-z-]+):\s*"([^"]*)"/);
|
|
115
121
|
if (m) paths.set(m[1], m[2]);
|
|
116
|
-
else if (/^\S/.test(l)) inPaths = false; //
|
|
122
|
+
else if (/^\S/.test(l)) inPaths = false; // end of indentation -> end of block
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
|
-
// template:
|
|
125
|
+
// version inside the template: block
|
|
120
126
|
let templateVersion = "";
|
|
121
127
|
let inTemplate = false;
|
|
122
128
|
for (const l of text.split("\n")) {
|
|
@@ -127,20 +133,20 @@ export function parseExisting(content) {
|
|
|
127
133
|
if (/^\S/.test(l)) break;
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
|
-
//
|
|
136
|
+
// Optional workflow options (metadata.template.options)
|
|
131
137
|
const options = parseTemplateOptions(text);
|
|
132
|
-
// metadata.template.branches
|
|
138
|
+
// metadata.template.branches - main/develop/mode (to skip re-asking in update mode)
|
|
133
139
|
const branches = parseTemplateBranches(text);
|
|
134
140
|
return {
|
|
135
|
-
version, versionCode, types, paths, templateVersion, options, branches,
|
|
141
|
+
version, versionCode, types, language, paths, templateVersion, options, branches,
|
|
136
142
|
deploy: parseDeployBlock(text), extraTopLevel: parseExtraTopLevel(text),
|
|
137
143
|
};
|
|
138
144
|
}
|
|
139
145
|
|
|
140
|
-
// deploy
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
146
|
+
// Parse the deploy block - re-reads the deploy values answered at install time for use as defaults on re-runs and updates.
|
|
147
|
+
// If it is only written and never read, an untouched file skips substitution so the block disappears, and auto-refresh
|
|
148
|
+
// reverts the user's chosen values to the template defaults.
|
|
149
|
+
// Returns: Map<type, Map<KEY, value>> (values are the raw text with the double-quote escapes written by buildVersionYml undone)
|
|
144
150
|
export function parseDeployBlock(content) {
|
|
145
151
|
const out = new Map();
|
|
146
152
|
let inDeploy = false;
|
|
@@ -149,7 +155,7 @@ export function parseDeployBlock(content) {
|
|
|
149
155
|
const l = raw.replace(/\r$/, "");
|
|
150
156
|
if (/^deploy:/.test(l)) { inDeploy = true; current = null; continue; }
|
|
151
157
|
if (!inDeploy) continue;
|
|
152
|
-
if (/^\S/.test(l)) { inDeploy = false; continue; } //
|
|
158
|
+
if (/^\S/.test(l)) { inDeploy = false; continue; } // next top-level key -> end of block
|
|
153
159
|
const t = l.match(/^ {2}([a-z][a-z-]*):\s*(?:#.*)?$/);
|
|
154
160
|
if (t) { current = new Map(); out.set(t[1], current); continue; }
|
|
155
161
|
const kv = l.match(/^ {4}([A-Za-z_][A-Za-z0-9_]*):\s*"((?:[^"\\]|\\.)*)"/);
|
|
@@ -158,10 +164,10 @@ export function parseDeployBlock(content) {
|
|
|
158
164
|
return out;
|
|
159
165
|
}
|
|
160
166
|
|
|
161
|
-
// metadata.template.branches
|
|
167
|
+
// Parse the metadata.template.branches block. All three must be present to be valid - otherwise null.
|
|
162
168
|
export function parseTemplateBranches(content) {
|
|
163
|
-
//
|
|
164
|
-
//
|
|
169
|
+
// Strip the inline comment (` # ...`) first, then clean quotes and whitespace. For keys taking a string value (deploy_style),
|
|
170
|
+
// without stripping the comment the whole "simple # simple | nginx ..." becomes the value.
|
|
165
171
|
const strip = (s) => String(s).replace(/\s+#.*$/, "").replace(/["']/g, "").trim();
|
|
166
172
|
let inTemplate = false;
|
|
167
173
|
let inBranches = false;
|
|
@@ -183,22 +189,23 @@ export function parseTemplateBranches(content) {
|
|
|
183
189
|
return out.main && out.develop && out.mode ? out : null;
|
|
184
190
|
}
|
|
185
191
|
|
|
186
|
-
// version.yml
|
|
192
|
+
// Generate the whole version.yml - renders payload/version.yml.template.
|
|
187
193
|
// opts: { templateText, version, types:[], paths:Map, pathMarkers?:Map,
|
|
188
194
|
// branch, branches?, versionCode, now, today, templateOptions?, deployValues?,
|
|
189
|
-
// extraTopLevel?:string[],
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
+
// extraTopLevel?:string[], <- preserves unknown top-level fields of the existing version.yml
|
|
196
|
+
// language?:string, <- message language (en|ko, default en)
|
|
197
|
+
// typeOptions?:object } <- input of the option block a type hook (versionOptionsBlock) renders only for its own type
|
|
198
|
+
// templateText = raw text of payload/version.yml.template (readVersionYmlTemplate - required)
|
|
199
|
+
// now = "YYYY-MM-DD HH:MM:SS" (UTC) - injected for determinism / today = "YYYY-MM-DD"
|
|
200
|
+
// branches = { main, develop, mode } (result of resolveBranchConfig. Without it, defaults based on branch)
|
|
201
|
+
// pathMarkers = Map<type, markerFilename> (for project_paths comments)
|
|
195
202
|
// templateOptions = { templateVersion, optionsDate }
|
|
196
203
|
export function buildVersionYml({
|
|
197
204
|
templateText, version, types = [], paths = new Map(), pathMarkers = new Map(),
|
|
198
205
|
branch = "main", branches = null, versionCode = 1, now, today,
|
|
199
|
-
templateOptions = null, deployValues = new Map(), extraTopLevel = [], typeOptions = {},
|
|
206
|
+
templateOptions = null, deployValues = new Map(), extraTopLevel = [], typeOptions = {}, language = DEFAULT_LANGUAGE,
|
|
200
207
|
}) {
|
|
201
|
-
if (!templateText) throw new Error("
|
|
208
|
+
if (!templateText) throw new Error(tr("core.versionYml.error.templateRequired"));
|
|
202
209
|
const typesJson = types.length ? `[${types.map((t) => `"${t}"`).join(", ")}]` : `["basic"]`;
|
|
203
210
|
const b = branches || { main: branch || "main", develop: "develop", mode: "pr-flow" };
|
|
204
211
|
const {
|
|
@@ -206,10 +213,10 @@ export function buildVersionYml({
|
|
|
206
213
|
includeSemverAuto = true, includeCopilotAi = false, deployStyle = "", optionsDate = today,
|
|
207
214
|
} = templateOptions || {};
|
|
208
215
|
|
|
209
|
-
// project_paths
|
|
216
|
+
// project_paths block (full-line token {{PROJECT_PATHS}} - line removed when absent)
|
|
210
217
|
let pathsBlock = "";
|
|
211
218
|
if (paths.size) {
|
|
212
|
-
const rows = [`project_paths: #
|
|
219
|
+
const rows = [`project_paths: # ${tr("core.versionYml.pathsComment")}`];
|
|
213
220
|
for (const [t, p] of paths) {
|
|
214
221
|
const marker = pathMarkers.get(t) || "";
|
|
215
222
|
const pf = p === "." ? marker : (marker ? `${p}/${marker}` : p);
|
|
@@ -218,26 +225,27 @@ export function buildVersionYml({
|
|
|
218
225
|
pathsBlock = rows.join("\n");
|
|
219
226
|
}
|
|
220
227
|
|
|
221
|
-
// deploy
|
|
228
|
+
// deploy block (full-line token {{DEPLOY}} - only types with WF ask values, one blank line before)
|
|
222
229
|
let deployBlock = "";
|
|
223
230
|
const deployTypes = [...deployValues.keys()].filter((t) => deployValues.get(t) && deployValues.get(t).size > 0);
|
|
224
231
|
if (deployTypes.length) {
|
|
225
|
-
const rows = ["",
|
|
232
|
+
const rows = ["", `deploy: # ${tr("core.versionYml.deployComment")}`];
|
|
226
233
|
for (const t of deployTypes) {
|
|
227
234
|
rows.push(` ${t}:`);
|
|
228
|
-
//
|
|
229
|
-
//
|
|
235
|
+
// Reuse the same escape - deploy values arrive by the same path as @wizard ask values, so
|
|
236
|
+
// a stray double quote breaks the YAML just like in setEnvLine (the second site).
|
|
230
237
|
for (const [k, v] of deployValues.get(t)) rows.push(` ${k}: "${escapeYamlDoubleQuoted(v)}"`);
|
|
231
238
|
}
|
|
232
239
|
deployBlock = rows.join("\n");
|
|
233
240
|
}
|
|
234
241
|
|
|
235
|
-
//
|
|
242
|
+
// Type option block (full-line token {{TYPE_OPTIONS}} - only when a type emits a block, otherwise the line is removed)
|
|
236
243
|
const typeOptionsBlock = hooksFor(types, "versionOptionsBlock").map(({ hook }) => hook(typeOptions)).join("\n");
|
|
237
244
|
|
|
238
245
|
const scalars = {
|
|
239
246
|
VERSION: version, VERSION_CODE: String(versionCode),
|
|
240
247
|
PROJECT_TYPES: typesJson,
|
|
248
|
+
LANGUAGE: isSupportedLanguage(language) ? language : DEFAULT_LANGUAGE,
|
|
241
249
|
NOW: now, TODAY: today || optionsDate, DEFAULT_BRANCH: branch,
|
|
242
250
|
TEMPLATE_VERSION: templateVersion,
|
|
243
251
|
MAIN_BRANCH: b.main, DEVELOP_BRANCH: b.develop, BRANCH_MODE: b.mode,
|
|
@@ -252,10 +260,10 @@ export function buildVersionYml({
|
|
|
252
260
|
if (t === "{{PROJECT_PATHS}}") { if (pathsBlock) out.push(pathsBlock); continue; }
|
|
253
261
|
if (t === "{{TYPE_OPTIONS}}") { if (typeOptionsBlock) out.push(typeOptionsBlock); continue; }
|
|
254
262
|
if (t === "{{DEPLOY}}") { if (deployBlock) out.push(deployBlock); continue; }
|
|
255
|
-
if (t.startsWith("deploy_style:") && deployStyle === null) continue; //
|
|
263
|
+
if (t.startsWith("deploy_style:") && deployStyle === null) continue; // types without a server deploy do not record it
|
|
256
264
|
out.push(line.replace(/\{\{([A-Z][A-Z0-9_]*)\}\}/g, (_, name) => {
|
|
257
265
|
if (name in scalars) return scalars[name];
|
|
258
|
-
throw new Error(
|
|
266
|
+
throw new Error(tr("core.versionYml.error.unknownPlaceholder", { name }));
|
|
259
267
|
}));
|
|
260
268
|
}
|
|
261
269
|
let text = out.join("\n");
|
|
@@ -264,33 +272,33 @@ export function buildVersionYml({
|
|
|
264
272
|
text += "\n" + extraTopLevel.join("\n\n");
|
|
265
273
|
}
|
|
266
274
|
if (!text.endsWith("\n")) text += "\n";
|
|
267
|
-
return text.replace(/\n{3,}$/, "\n"); //
|
|
275
|
+
return text.replace(/\n{3,}$/, "\n"); // trim excess trailing blank lines
|
|
268
276
|
}
|
|
269
277
|
|
|
270
|
-
//
|
|
271
|
-
//
|
|
278
|
+
// Whether only the install-time lines (metadata.last_updated etc.) differ - callers use it to skip the write so a
|
|
279
|
+
// re-run with no changes does not rewrite the file every time and dirty the working tree.
|
|
272
280
|
const TIMESTAMP_LINE = /^\s*(last_updated|integration_date|integrated_date|last_update_date):/;
|
|
273
281
|
export function sameIgnoringTimestamps(a, b) {
|
|
274
282
|
const norm = (t) => String(t).replace(/\r\n/g, "\n").split("\n").filter((l) => !TIMESTAMP_LINE.test(l)).join("\n");
|
|
275
283
|
return norm(a) === norm(b);
|
|
276
284
|
}
|
|
277
285
|
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// deployValues
|
|
286
|
+
// Builds the final version.yml from a single context - the real install (full) and the preview (dry-run)
|
|
287
|
+
// use the same function, structurally preventing "the preview differs from the result".
|
|
288
|
+
// deployValues exist only in a real install (the preview performs no substitution, so it is an empty Map).
|
|
281
289
|
export function renderVersionYml(context, templateText, { pathMarkers, deployValues = new Map(), extraTopLevel = [] }) {
|
|
282
290
|
const { version, types = [], paths = new Map(), branch = "main", versionCode = 1,
|
|
283
|
-
now, today, templateVersion = "unknown", branches = null,
|
|
291
|
+
now, today, templateVersion = "unknown", branches = null, language,
|
|
284
292
|
includeSemverAuto, includeCopilotAi, deployStyle } = context;
|
|
285
293
|
return buildVersionYml({
|
|
286
294
|
templateText, version, types, paths, pathMarkers, branch, branches, versionCode, now, today,
|
|
287
|
-
deployValues, extraTopLevel,
|
|
295
|
+
deployValues, extraTopLevel, language,
|
|
288
296
|
typeOptions: mergeHookResults(types, "optionsFromContext", context),
|
|
289
297
|
templateOptions: {
|
|
290
298
|
templateVersion,
|
|
291
299
|
includeSemverAuto: includeSemverAuto !== false,
|
|
292
300
|
includeCopilotAi: includeCopilotAi === true,
|
|
293
|
-
// null =
|
|
301
|
+
// null = a type with no server deploy workflow, so the deploy style is meaningless (record omitted)
|
|
294
302
|
deployStyle: deployStyle === null ? null : (deployStyle || DEFAULT_DEPLOY_STYLE),
|
|
295
303
|
optionsDate: today,
|
|
296
304
|
},
|
package/src/core/wizard-env.js
CHANGED
|
@@ -1,55 +1,55 @@
|
|
|
1
|
-
// @wizard env
|
|
2
|
-
//
|
|
1
|
+
// @wizard env token engine - substitution of the workflow's ask/auto/fallback markers and the unchanged verdict.
|
|
2
|
+
// WARNING: no YAML parsing/re-serialization - line-based string handling (preserving format and comments is the premise of the unchanged verdict).
|
|
3
3
|
|
|
4
|
-
// KEY
|
|
5
|
-
//
|
|
6
|
-
// fallback
|
|
4
|
+
// KEY regex: besides env keys (uppercase) it accepts lowercase keys such as `default:` in a workflow_dispatch input.
|
|
5
|
+
// Only lines carrying a marker are targeted, so widening it does not affect other lines.
|
|
6
|
+
// fallback is a marker that replaces the default literal inside a `KEY: ${{ runtimeValue || 'literal' }}` expression.
|
|
7
7
|
const MARKER_RE = /#\s*@wizard\s+(ask|auto|fallback):(.*)$/;
|
|
8
8
|
const KEY_RE = /^(\s*)([A-Za-z_]+):/;
|
|
9
9
|
const PATHS_ANCHOR_RE = /#\s*@wizard\s+paths-anchor/;
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Parse one line into {indent,key,action,arg}. null when there is no ask/auto/fallback marker.
|
|
12
12
|
export function parseWizardLine(line) {
|
|
13
13
|
const marker = line.match(MARKER_RE);
|
|
14
14
|
if (!marker) return null;
|
|
15
15
|
const km = line.match(KEY_RE);
|
|
16
|
-
if (!km) return null; // KEY:
|
|
16
|
+
if (!km) return null; // not in KEY: form (e.g. a paths-anchor comment) - ignore
|
|
17
17
|
return { indent: km[1], key: km[2], action: marker[1], arg: marker[2].trim() };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// (buildVersionYml, src/core/version-yml.js)
|
|
23
|
-
//
|
|
20
|
+
// Escape for safely placing a value inside a YAML double-quoted string (backslash first - otherwise the
|
|
21
|
+
// double-quote escape added afterwards would be broken). Reused by both the @wizard substitution (setEnvLine)
|
|
22
|
+
// and the version.yml deploy block (buildVersionYml, src/core/version-yml.js) - the same bug
|
|
23
|
+
// occurs at both points.
|
|
24
24
|
export function escapeYamlDoubleQuoted(value) {
|
|
25
25
|
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// `KEY: "..."`
|
|
29
|
-
//
|
|
28
|
+
// Substitute the value inside the quotes of `KEY: "..."` + strip the trailing `# @wizard ...` comment on that line.
|
|
29
|
+
// Works on a single line. An empty value skips substitution (the template default stays).
|
|
30
30
|
export function setEnvLine(line, key, value) {
|
|
31
31
|
if (value === "" || value == null) return line;
|
|
32
|
-
// CRLF
|
|
32
|
+
// CRLF safe: split off the trailing \r, process, then restore (for autocrlf projects)
|
|
33
33
|
const cr = line.endsWith("\r") ? "\r" : "";
|
|
34
34
|
const body = cr ? line.slice(0, -1) : line;
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
35
|
+
// Value substitution: KEY: "old" -> KEY: "value"
|
|
36
|
+
// Single quotes (KEY: 'old') are accepted too - the template mixes both notations, and looking only at
|
|
37
|
+
// double quotes would silently ignore the @wizard marker on single-quoted lines (the same failure shape).
|
|
38
|
+
// The result is unified to double quotes and the value is escaped accordingly.
|
|
39
39
|
const escaped = escapeYamlDoubleQuoted(value);
|
|
40
40
|
let out = body.replace(
|
|
41
41
|
new RegExp(`^(\\s*${key}:\\s*)(["'])(?:(?!\\2).)*\\2`),
|
|
42
42
|
(_m, head) => `${head}"${escaped}"`,
|
|
43
43
|
);
|
|
44
|
-
//
|
|
44
|
+
// Strip the trailing # @wizard ... comment on that line (with the preceding whitespace)
|
|
45
45
|
out = out.replace(/(\S)[^\S\r\n]*#[^\S\r\n]*@wizard[^\S\r\n].*$/, "$1");
|
|
46
46
|
return out + cr;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// `KEY: ${{ a || b || 'literal' }} # @wizard fallback:<token>`
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// value
|
|
49
|
+
// `KEY: ${{ a || b || 'literal' }} # @wizard fallback:<token>` - replaces only the "last single-quoted literal" inside the expression
|
|
50
|
+
// and strips the marker comment. setEnvLine handles only quoted values of the `KEY: "value"` form, so it cannot process GitHub expressions.
|
|
51
|
+
// The literal sits at the very end of the `||` chain (the default when runtime input and repo variables are all empty), so runtime precedence does not change.
|
|
52
|
+
// An empty value leaves the line as is (same convention as setEnvLine - the template default stays).
|
|
53
53
|
const WIZARD_COMMENT_RE = /[^\S\r\n]*#[^\S\r\n]*@wizard[^\S\r\n].*$/;
|
|
54
54
|
const LAST_LITERAL_RE = /^(.*)'[^']*'([^']*)$/;
|
|
55
55
|
export function setFallbackLine(line, value) {
|
|
@@ -57,11 +57,11 @@ export function setFallbackLine(line, value) {
|
|
|
57
57
|
const cr = line.endsWith("\r") ? "\r" : "";
|
|
58
58
|
const expression = (cr ? line.slice(0, -1) : line).replace(WIZARD_COMMENT_RE, "");
|
|
59
59
|
if (!LAST_LITERAL_RE.test(expression)) return line;
|
|
60
|
-
const escaped = String(value).replaceAll("'", "''"); //
|
|
60
|
+
const escaped = String(value).replaceAll("'", "''"); // quote escape for a GitHub expression string literal
|
|
61
61
|
return expression.replace(LAST_LITERAL_RE, (_m, head, tail) => `${head}'${escaped}'${tail}`) + cr;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// resolver
|
|
64
|
+
// resolver - value computation is delegated to the injected resolvers (keeps purity).
|
|
65
65
|
// resolvers: { repo, "spring-app-yml-dir"(type), "spring-app-yml-path"(type), "flutter-root",
|
|
66
66
|
// "project-path"(type), "flutter-env-mode", "android-deploy-mode", "ios-deploy-mode" }
|
|
67
67
|
export function resolveToken(name, type, resolvers = {}) {
|
|
@@ -69,25 +69,25 @@ export function resolveToken(name, type, resolvers = {}) {
|
|
|
69
69
|
return typeof fn === "function" ? (fn(type) ?? "") : "";
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
// __PROJECT_NAME__/__APP_ARTIFACT_NAME__
|
|
73
|
-
// substituteEnv()(
|
|
74
|
-
//
|
|
72
|
+
// Replace the global tokens __PROJECT_NAME__/__APP_ARTIFACT_NAME__ with repoName.
|
|
73
|
+
// Reused by both substituteEnv() (installed file body) and collectAsks() (defaults shown on the wizard screen) -
|
|
74
|
+
// if the two diverge into different logic, display mismatches come back.
|
|
75
75
|
export function replaceProjectTokens(text, repoName) {
|
|
76
76
|
if (!text.includes("__PROJECT_NAME__") && !text.includes("__APP_ARTIFACT_NAME__")) return text;
|
|
77
77
|
return text.replaceAll("__PROJECT_NAME__", repoName).replaceAll("__APP_ARTIFACT_NAME__", repoName);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
// content:
|
|
80
|
+
// Whole-file substitution.
|
|
81
|
+
// content: original workflow text. Returns: the substituted text.
|
|
82
82
|
// opts:
|
|
83
|
-
// type -
|
|
84
|
-
// values - Map<key,value>:
|
|
85
|
-
// useDefaults - true
|
|
86
|
-
// resolvers - resolveToken
|
|
87
|
-
// repoName - __PROJECT_NAME__/__APP_ARTIFACT_NAME__
|
|
88
|
-
// projectPath - paths-anchor
|
|
89
|
-
// savedValues - Map<key,value>: version.yml deploy
|
|
90
|
-
// (
|
|
83
|
+
// type - project type (for resolvers/value lookup)
|
|
84
|
+
// values - Map<key,value>: the user's choices for ask keys (if absent, default = arg or resolver)
|
|
85
|
+
// useDefaults - when true, ask also uses defaults (the premise of the unchanged comparison)
|
|
86
|
+
// resolvers - for resolveToken
|
|
87
|
+
// repoName - substitution value for __PROJECT_NAME__/__APP_ARTIFACT_NAME__
|
|
88
|
+
// projectPath - for paths-anchor substitution (anchor unchanged when '.')
|
|
89
|
+
// savedValues - Map<key,value>: values of this type saved in the version.yml deploy block. Take precedence over ask defaults
|
|
90
|
+
// (so re-runs/auto-refresh do not revert answers given at install time to the template defaults).
|
|
91
91
|
export function substituteEnv(content, opts = {}) {
|
|
92
92
|
const {
|
|
93
93
|
type = "", values = new Map(), useDefaults = true, resolvers = {}, repoName = "", projectPath = ".",
|
|
@@ -95,14 +95,14 @@ export function substituteEnv(content, opts = {}) {
|
|
|
95
95
|
} = opts;
|
|
96
96
|
if (!content.includes("@wizard")) return content;
|
|
97
97
|
|
|
98
|
-
// CRLF
|
|
99
|
-
// (JS
|
|
98
|
+
// CRLF safe: split EOLs, parse/substitute on LF basis, then restore the original EOL style.
|
|
99
|
+
// (JS regex `.` does not match \r, so `(.*)$` marker parsing would fail on CRLF.)
|
|
100
100
|
const usesCRLF = content.includes("\r\n");
|
|
101
101
|
const lines = content.split(/\r?\n/);
|
|
102
102
|
for (let i = 0; i < lines.length; i++) {
|
|
103
|
-
const p = parseWizardLine(lines[i]); //
|
|
103
|
+
const p = parseWizardLine(lines[i]); // line with \r already stripped
|
|
104
104
|
if (!p) continue;
|
|
105
|
-
// fallback
|
|
105
|
+
// fallback changes a literal inside an expression, not a quoted value, so its path differs from ask/auto.
|
|
106
106
|
if (p.action === "fallback") {
|
|
107
107
|
lines[i] = setFallbackLine(lines[i], resolveToken(p.arg, type, resolvers));
|
|
108
108
|
continue;
|
|
@@ -117,17 +117,17 @@ export function substituteEnv(content, opts = {}) {
|
|
|
117
117
|
const chosen = values.get(p.key);
|
|
118
118
|
if (chosen != null && chosen !== "" && !useDefaults) val = chosen;
|
|
119
119
|
else val = def;
|
|
120
|
-
// ask
|
|
120
|
+
// Collect only ask keys (auto is recomputed every time, so it is not saved). For the deploy block.
|
|
121
121
|
if (collectAsks) collectAsks.set(p.key, replaceProjectTokens(val, repoName));
|
|
122
122
|
}
|
|
123
123
|
lines[i] = setEnvLine(lines[i], p.key, val);
|
|
124
124
|
}
|
|
125
125
|
let out = lines.join(usesCRLF ? "\r\n" : "\n");
|
|
126
126
|
|
|
127
|
-
//
|
|
127
|
+
// Remaining global tokens
|
|
128
128
|
out = replaceProjectTokens(out, repoName);
|
|
129
129
|
|
|
130
|
-
// paths-anchor:
|
|
130
|
+
// paths-anchor: unless the path is '.', replace the whole comment line with a paths line
|
|
131
131
|
if (PATHS_ANCHOR_RE.test(out) && projectPath && projectPath !== ".") {
|
|
132
132
|
const eol = out.includes("\r\n") ? "\r\n" : "\n";
|
|
133
133
|
out = out.split(/\r?\n/).map((line) => {
|
|
@@ -141,7 +141,7 @@ export function substituteEnv(content, opts = {}) {
|
|
|
141
141
|
return out;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
// unchanged
|
|
144
|
+
// unchanged verdict: byte-compare the "final form virtually substituted with defaults" of the original against the installed copy.
|
|
145
145
|
export function isUnchanged(templateContent, installedContent, opts = {}) {
|
|
146
146
|
const virtual = substituteEnv(templateContent, { ...opts, useDefaults: true });
|
|
147
147
|
return virtual === installedContent;
|