project-auto-wizard 0.15.0 → 0.16.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 +1 -1
- package/README.ko.md +1 -1
- package/README.md +2 -2
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/payload/config/breaking-changes.json +31 -5
- package/payload/config/wizard-prompts.yml +0 -4
- package/payload/scripts/messages.py +162 -26
- package/payload/scripts/version_manager.py +69 -40
- package/payload/version.yml.template +6 -2
- package/payload/workflows/common/PROJECT-COMMON-AI-PR-SUMMARY.yaml +3 -0
- package/payload/workflows/common/PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml +64 -1
- package/payload/workflows/common/PROJECT-COMMON-README-VERSION-UPDATE.yaml +61 -12
- package/payload/workflows/common/PROJECT-COMMON-RELEASE-PUBLISH.yaml +46 -10
- package/payload/workflows/common/PROJECT-COMMON-VERSION-CONTROL.yaml +40 -11
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-FIREBASE-CICD.yaml +24 -8
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml +29 -10
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml +57 -23
- package/payload/workflows/flutter/PROJECT-FLUTTER-ANDROID-TEST-APK.yaml +155 -72
- package/payload/workflows/flutter/PROJECT-FLUTTER-APP-BUILD-TRIGGER.yaml +49 -20
- package/payload/workflows/flutter/PROJECT-FLUTTER-CI.yaml +162 -62
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TEST-TESTFLIGHT.yaml +184 -87
- package/payload/workflows/flutter/PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml +38 -15
- package/payload/workflows/go/PROJECT-GO-CI.yaml +14 -5
- package/payload/workflows/go/PROJECT-GO-PR-PREVIEW.yaml +375 -120
- package/payload/workflows/go/PROJECT-GO-SIMPLE-CICD.yaml +9 -4
- package/payload/workflows/python/PROJECT-PYTHON-CI.yaml +12 -4
- package/payload/workflows/python/PROJECT-PYTHON-PR-PREVIEW.yaml +375 -120
- package/payload/workflows/python/PROJECT-PYTHON-SIMPLE-CICD.yaml +9 -4
- package/payload/workflows/react/PROJECT-REACT-CI.yaml +38 -9
- package/payload/workflows/react/PROJECT-REACT-CICD.yaml +19 -5
- package/payload/workflows/spring/PROJECT-SPRING-CI.yml +71 -35
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml +9 -6
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-NONSTOP-TRAEFIK-CICD.yaml +7 -4
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-PR-PREVIEW.yaml +425 -144
- package/payload/workflows/spring/server-deploy/PROJECT-SPRING-SIMPLE-CICD.yaml +7 -4
- package/src/cli/args.js +32 -22
- package/src/commands/doctor.js +20 -0
- package/src/commands/dry-run.js +9 -1
- package/src/commands/full.js +9 -4
- package/src/commands/interactive.js +29 -30
- package/src/commands/status.js +19 -5
- package/src/context.js +4 -2
- package/src/core/copy/simple.js +31 -0
- package/src/core/copy/workflows.js +2 -2
- package/src/core/deploy-style.js +1 -1
- package/src/core/detect.js +3 -2
- package/src/core/options.js +88 -0
- package/src/core/release-options.js +6 -23
- package/src/core/types.js +15 -8
- package/src/core/version-yml.js +68 -26
- package/src/i18n/catalog/en/cli.js +3 -1
- package/src/i18n/catalog/en/commands.js +15 -0
- package/src/i18n/catalog/en/core2.js +3 -0
- package/src/i18n/catalog/en/ui.js +1 -0
- package/src/i18n/catalog/ko/cli.js +3 -1
- package/src/i18n/catalog/ko/commands.js +15 -0
- package/src/i18n/catalog/ko/core2.js +3 -0
- package/src/i18n/catalog/ko/ui.js +1 -0
- package/src/index.js +7 -3
- package/src/ui/env-plan.js +1 -1
- package/src/ui/prompts.js +9 -5
- package/src/ui/status-cards.js +2 -2
- package/payload/workflows/next/PROJECT-NEXT-CI.yaml +0 -241
- package/payload/workflows/next/PROJECT-NEXT-CICD.yaml +0 -330
|
@@ -132,6 +132,7 @@ jobs:
|
|
|
132
132
|
SSH_KEY: ${{ secrets.SSH_KEY }}
|
|
133
133
|
DOCKERFILE: ${{ env.DOCKERFILE_PATH }}
|
|
134
134
|
run: |
|
|
135
|
+
[ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
|
|
135
136
|
m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
|
|
136
137
|
MISSING=""
|
|
137
138
|
for NAME in DOCKERHUB_USERNAME DOCKERHUB_TOKEN SERVER_HOST SERVER_USER; do
|
|
@@ -172,6 +173,7 @@ jobs:
|
|
|
172
173
|
# write it to a file in the resources directory
|
|
173
174
|
- name: Generate application-prod.yml
|
|
174
175
|
run: |
|
|
176
|
+
[ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
|
|
175
177
|
m() { python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" get "$@"; }
|
|
176
178
|
mkdir -p ${{ env.APPLICATION_YML_DIR }}
|
|
177
179
|
|
|
@@ -187,10 +189,10 @@ jobs:
|
|
|
187
189
|
run: ./gradlew clean build -x test ${{ env.GRADLE_OPTS }}
|
|
188
190
|
|
|
189
191
|
- name: Set up Docker Buildx
|
|
190
|
-
uses: docker/setup-buildx-action@
|
|
192
|
+
uses: docker/setup-buildx-action@v4
|
|
191
193
|
|
|
192
194
|
- name: Log in to DockerHub
|
|
193
|
-
uses: docker/login-action@
|
|
195
|
+
uses: docker/login-action@v4
|
|
194
196
|
with:
|
|
195
197
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
196
198
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
@@ -205,7 +207,7 @@ jobs:
|
|
|
205
207
|
|
|
206
208
|
# Use the branch name as the image tag to manage images per environment
|
|
207
209
|
- name: Build and push Docker image
|
|
208
|
-
uses: docker/build-push-action@
|
|
210
|
+
uses: docker/build-push-action@v7
|
|
209
211
|
with:
|
|
210
212
|
context: ${{ env.PROJECT_PATH }}
|
|
211
213
|
file: ${{ env.PROJECT_PATH }}/${{ env.DOCKERFILE_PATH }}
|
|
@@ -238,6 +240,7 @@ jobs:
|
|
|
238
240
|
|
|
239
241
|
- name: Load messages
|
|
240
242
|
run: |
|
|
243
|
+
[ -f "$GITHUB_WORKSPACE/.github/scripts/messages.py" ] || { echo "::error::messages.py is missing; rerun \`npx project-auto-wizard --mode full --force\`"; exit 1; }
|
|
241
244
|
MSGS="$(python3 "$GITHUB_WORKSPACE/.github/scripts/messages.py" dump wf_spring_deploy. \
|
|
242
245
|
| python3 -c 'import json, shlex, sys; d = json.load(sys.stdin); print("\n".join("M_%s=%s" % (k.replace(".", "_"), shlex.quote(v)) for k, v in d.items()))' \
|
|
243
246
|
| base64 | tr -d '\n')"
|
|
@@ -429,7 +432,7 @@ jobs:
|
|
|
429
432
|
m simple_summary_branch branch="$BRANCH"
|
|
430
433
|
m simple_summary_container name="$CONTAINER_NAME"
|
|
431
434
|
m simple_summary_port port="$PORT"
|
|
432
|
-
m simple_summary_time time="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
435
|
+
m simple_summary_time time="$(date -u '+%Y-%m-%d %H:%M:%S UTC')"
|
|
433
436
|
echo ""
|
|
434
437
|
m simple_access_url url="http://${{ secrets.SERVER_HOST }}:${PORT}"
|
|
435
438
|
|
package/src/cli/args.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
import { VALID_TYPES, VALID_MODES } from "../context.js";
|
|
3
3
|
import { DEPLOY_STYLES, isDeployStyle, NO_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
4
4
|
import { isValidBranchName } from "../core/branches.js";
|
|
5
|
-
import { TYPES } from "../core/types.js";
|
|
5
|
+
import { TYPES, canonicalTypeId } from "../core/types.js";
|
|
6
6
|
import { CliError } from "../core/errors.js";
|
|
7
|
+
import { OPTIONS, defaultContextFields } from "../core/options.js";
|
|
7
8
|
import { normalizePath, isRepoRelativePath } from "../core/paths.js";
|
|
8
9
|
import { t, DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, isSupportedLanguage, normalizeLanguage } from "../i18n/index.js";
|
|
9
10
|
|
|
@@ -11,6 +12,11 @@ import { t, DEFAULT_LANGUAGE, SUPPORTED_LANGUAGES, isSupportedLanguage, normaliz
|
|
|
11
12
|
export { CliError, normalizePath, isRepoRelativePath };
|
|
12
13
|
|
|
13
14
|
// Type-specific CLI flags (type hook cliFlags): flag name -> { field, initial, parse }. Hooks are collected in type-table order.
|
|
15
|
+
// --<flag> / --no-<flag> for every registry option - a new option needs no change here.
|
|
16
|
+
const OPTION_CLI_FLAGS = new Map(OPTIONS.flatMap((option) => [
|
|
17
|
+
[`--${option.flag}`, { option, value: true }],
|
|
18
|
+
[`--no-${option.flag}`, { option, value: false }],
|
|
19
|
+
]));
|
|
14
20
|
export const TYPE_CLI_FLAGS = TYPES.flatMap((t) => t.hooks?.cliFlags ?? []);
|
|
15
21
|
const TYPE_CLI_FLAG_BY_NAME = new Map(TYPE_CLI_FLAGS.map((f) => [f.flag, f]));
|
|
16
22
|
|
|
@@ -22,7 +28,7 @@ const VALUE_FLAGS = new Set([
|
|
|
22
28
|
const SWITCH_FLAGS = new Set([
|
|
23
29
|
"--version", "--help", "--force", "--dry-run", "--purge-readme", "--purge-gitignore", "--purge-version", "--yes", "--allow-dirty",
|
|
24
30
|
"--delete-develop-branch", "--keep-version-yml", "--keep-readme", "--keep-changelog", "--keep-workflows", "--keep-scripts",
|
|
25
|
-
|
|
31
|
+
...OPTIONS.flatMap((o) => [`--${o.flag}`, `--no-${o.flag}`]),
|
|
26
32
|
]);
|
|
27
33
|
|
|
28
34
|
// `--name=value` -> `--name`, `value`, so the parser below only sees the space-separated form.
|
|
@@ -54,8 +60,7 @@ export function parseArgs(argv) {
|
|
|
54
60
|
version: "", // initial version of the target project (--project-version)
|
|
55
61
|
types: [],
|
|
56
62
|
primaryType: "",
|
|
57
|
-
|
|
58
|
-
includeCopilotAi: null, // --copilot / --no-copilot (default false; opt-in because it consumes AI Credits)
|
|
63
|
+
...defaultContextFields(), // --<flag> / --no-<flag> per registry option (null = unset, resolved downstream)
|
|
59
64
|
pathsCsv: "", // raw "flutter=app,react=client" (normalized in the resolve step)
|
|
60
65
|
mainBranch: "", // release branch (--main-branch). empty = detected default branch
|
|
61
66
|
developBranch: "", // development branch (--develop-branch). empty = develop
|
|
@@ -86,6 +91,13 @@ export function parseArgs(argv) {
|
|
|
86
91
|
const a = args.shift();
|
|
87
92
|
const typeFlag = TYPE_CLI_FLAG_BY_NAME.get(a);
|
|
88
93
|
if (typeFlag) { result[typeFlag.field] = typeFlag.parse(args.shift()); continue; }
|
|
94
|
+
const optionFlag = OPTION_CLI_FLAGS.get(a);
|
|
95
|
+
if (optionFlag) {
|
|
96
|
+
const { option, value } = optionFlag;
|
|
97
|
+
// --<flag> and --no-<flag> together are contradictory, so reject instead of letting the last one win
|
|
98
|
+
if (seenFlags.has(`--${value ? "no-" : ""}${option.flag}`)) throw new CliError(t(option.conflictKey));
|
|
99
|
+
seenFlags.add(a); result[option.ctxField] = value; continue;
|
|
100
|
+
}
|
|
89
101
|
switch (a) {
|
|
90
102
|
case "-m": case "--mode":
|
|
91
103
|
result.mode = args.shift() ?? ""; break;
|
|
@@ -108,12 +120,15 @@ export function parseArgs(argv) {
|
|
|
108
120
|
for (let name of csv.split(",")) {
|
|
109
121
|
name = name.replace(/\s/g, "");
|
|
110
122
|
if (name === "") continue;
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
// The alias is resolved here, before validation and dedup (--type react,next is just react).
|
|
124
|
+
// Only registry ids are valid, so an unsupported name is reported as typed.
|
|
125
|
+
const id = canonicalTypeId(name);
|
|
126
|
+
if (seen.has(id)) continue; // dedup
|
|
127
|
+
if (!VALID_TYPES.includes(id)) {
|
|
113
128
|
throw new CliError(t("cli.args.typeUnsupported", { type: name, valid: VALID_TYPES.join(" ") }));
|
|
114
129
|
}
|
|
115
|
-
seen.add(
|
|
116
|
-
types.push(
|
|
130
|
+
seen.add(id);
|
|
131
|
+
types.push(id);
|
|
117
132
|
}
|
|
118
133
|
if (types.length === 0) throw new CliError(t("cli.args.typeEmpty"));
|
|
119
134
|
result.types = types;
|
|
@@ -153,18 +168,6 @@ export function parseArgs(argv) {
|
|
|
153
168
|
}
|
|
154
169
|
result.deployStyle = v; break;
|
|
155
170
|
}
|
|
156
|
-
case "--semver-auto":
|
|
157
|
-
if (seenFlags.has("--no-semver-auto")) throw new CliError(t("cli.args.semverConflict"));
|
|
158
|
-
seenFlags.add("--semver-auto"); result.includeSemverAuto = true; break;
|
|
159
|
-
case "--no-semver-auto":
|
|
160
|
-
if (seenFlags.has("--semver-auto")) throw new CliError(t("cli.args.semverConflict"));
|
|
161
|
-
seenFlags.add("--no-semver-auto"); result.includeSemverAuto = false; break;
|
|
162
|
-
case "--copilot":
|
|
163
|
-
if (seenFlags.has("--no-copilot")) throw new CliError(t("cli.args.copilotConflict"));
|
|
164
|
-
seenFlags.add("--copilot"); result.includeCopilotAi = true; break;
|
|
165
|
-
case "--no-copilot":
|
|
166
|
-
if (seenFlags.has("--copilot")) throw new CliError(t("cli.args.copilotConflict"));
|
|
167
|
-
seenFlags.add("--no-copilot"); result.includeCopilotAi = false; break;
|
|
168
171
|
case "--paths": {
|
|
169
172
|
// Silently falling back to auto-detection would install to paths other than the ones the user thinks they set.
|
|
170
173
|
const v = (args.shift() ?? "").trim();
|
|
@@ -193,20 +196,27 @@ export function parseArgs(argv) {
|
|
|
193
196
|
// "flutter=app,react=client" -> Map<type, normalizedPath>. Validates types (invalid -> throw).
|
|
194
197
|
export function parsePathsCsv(csv) {
|
|
195
198
|
const map = new Map();
|
|
199
|
+
const spelled = new Map(); // type -> name as typed, to tell an alias clash from a plain repeat
|
|
196
200
|
if (!csv) return map;
|
|
197
201
|
for (const pair of csv.split(",")) {
|
|
198
202
|
if (pair.trim() === "") continue;
|
|
199
203
|
const eq = pair.indexOf("=");
|
|
200
|
-
const
|
|
204
|
+
const typeRaw = (eq >= 0 ? pair.slice(0, eq) : pair).replace(/\s/g, "");
|
|
205
|
+
const type = canonicalTypeId(typeRaw);
|
|
201
206
|
const rawPath = eq >= 0 ? pair.slice(eq + 1) : "";
|
|
202
207
|
if (!VALID_TYPES.includes(type)) {
|
|
203
|
-
throw new CliError(t("cli.args.pathsTypeUnsupported", { type }));
|
|
208
|
+
throw new CliError(t("cli.args.pathsTypeUnsupported", { type: typeRaw }));
|
|
204
209
|
}
|
|
205
210
|
const path = normalizePath(rawPath);
|
|
206
211
|
if (!isRepoRelativePath(path)) {
|
|
207
212
|
throw new CliError(t("cli.args.pathsRelativeOnly", { pair: `${type}=${rawPath.trim()}` }));
|
|
208
213
|
}
|
|
214
|
+
// An alias and its canonical name in one list (react=a,next=b) would silently drop a folder; refuse it instead.
|
|
215
|
+
if (map.has(type) && spelled.get(type) !== typeRaw && map.get(type) !== path) {
|
|
216
|
+
throw new CliError(t("cli.args.pathsDuplicateType", { type }));
|
|
217
|
+
}
|
|
209
218
|
map.set(type, path);
|
|
219
|
+
spelled.set(type, typeRaw);
|
|
210
220
|
}
|
|
211
221
|
return map;
|
|
212
222
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -14,6 +14,8 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { A, paint, colorEnabled, visualWidth } from "../ui/ansi.js";
|
|
16
16
|
import { PATHS } from "../core/paths.js";
|
|
17
|
+
import { findMissingScripts } from "../core/copy/simple.js";
|
|
18
|
+
import { resolvePayloadRoot } from "../core/assets.js";
|
|
17
19
|
import { parseExisting } from "../core/version-yml.js";
|
|
18
20
|
import { hooksFor } from "../core/types.js";
|
|
19
21
|
import { t } from "../i18n/index.js";
|
|
@@ -44,6 +46,15 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
44
46
|
// Per-type local file checks (e.g. Flutter store deploy files): only local files are read, so this runs regardless of the gh lookup results.
|
|
45
47
|
if (installed) {
|
|
46
48
|
const existing = parseExisting(readFileSync(join(cwd, PATHS.versionFile), "utf8"));
|
|
49
|
+
const missing = findMissingScripts(resolvePayloadRoot(), cwd);
|
|
50
|
+
if (missing.length) {
|
|
51
|
+
add({
|
|
52
|
+
name: t("cmd.doctor.scripts.name"), label: t("cmd.doctor.scripts.label"), purpose: t("cmd.doctor.scripts.purpose"), status: "WARN",
|
|
53
|
+
value: t("cmd.doctor.scripts.missing", { files: missing.join(", ") }),
|
|
54
|
+
impact: [t("cmd.doctor.scripts.impact")],
|
|
55
|
+
actions: [t("cmd.doctor.scripts.action")],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
47
58
|
for (const { hook } of hooksFor(existing.types, "doctorChecks")) {
|
|
48
59
|
for (const item of hook(cwd, existing, { docs: DOC })) add(item);
|
|
49
60
|
}
|
|
@@ -176,6 +187,15 @@ export function runDoctor(cwd = process.cwd(), { exec = defaultExec } = {}) {
|
|
|
176
187
|
],
|
|
177
188
|
});
|
|
178
189
|
|
|
190
|
+
// release_automerge is on unless version.yml says false (a missing key also means on), so only an explicit false is reported as off.
|
|
191
|
+
const releaseAutomerge = installed ? parseExisting(readFileSync(join(cwd, "version.yml"), "utf8")).options.releaseAutomerge : null;
|
|
192
|
+
add({
|
|
193
|
+
name: t("cmd.doctor.automerge.name"), label: t("cmd.doctor.automerge.name"), purpose: t("cmd.doctor.automerge.purpose"), status: "INFO",
|
|
194
|
+
note: releaseAutomerge === false
|
|
195
|
+
? [t("cmd.doctor.automerge.off"), t("cmd.doctor.automerge.offHow")]
|
|
196
|
+
: [t("cmd.doctor.automerge.on"), t("cmd.doctor.automerge.onHow")],
|
|
197
|
+
});
|
|
198
|
+
|
|
179
199
|
return results;
|
|
180
200
|
}
|
|
181
201
|
|
package/src/commands/dry-run.js
CHANGED
|
@@ -6,7 +6,7 @@ import { PATHS } from "../core/paths.js";
|
|
|
6
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
7
7
|
import { planTypeAppFiles } from "../core/copy/app-files.js";
|
|
8
8
|
import { planUninstall } from "./uninstall.js";
|
|
9
|
-
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps, droppedPathLines } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
12
|
import { planScripts } from "../core/copy/simple.js";
|
|
@@ -33,6 +33,11 @@ function versionYmlPreview(context, payloadRoot, targetRoot) {
|
|
|
33
33
|
return { existed: existingRaw !== null, changed: existingRaw === null || !sameIgnoringTimestamps(existingRaw, wouldBe) };
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function existingDroppedLines(targetRoot, paths) {
|
|
37
|
+
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
38
|
+
return existsSync(vyPath) ? droppedPathLines(parseExisting(readFileSync(vyPath, "utf8")).droppedPaths, paths) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
36
41
|
// mode: "full" | "uninstall". Read-only: writes no file.
|
|
37
42
|
export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
38
43
|
if (mode === "uninstall") {
|
|
@@ -54,6 +59,8 @@ export function planDryRun(mode, context, payloadRoot, targetRoot = ".") {
|
|
|
54
59
|
// Flutter store deploy files (Fastfile, ExportOptions.plist): existing files are kept, not overwritten.
|
|
55
60
|
flutterApp: planTypeAppFiles(context, payloadRoot, targetRoot),
|
|
56
61
|
versionYml: versionYmlPreview(context, payloadRoot, targetRoot),
|
|
62
|
+
// Folders the rewrite would drop from version.yml (same type listed under two names with different folders).
|
|
63
|
+
droppedPathLines: existingDroppedLines(targetRoot, context.paths),
|
|
57
64
|
// Files the real install also changes; scripts that overwrite existing files in particular must be shown in advance.
|
|
58
65
|
scripts: planScripts(payloadRoot, targetRoot),
|
|
59
66
|
readme: planVersionSection(targetRoot),
|
|
@@ -130,6 +137,7 @@ export function printDryRun(plan) {
|
|
|
130
137
|
// For types with a deploy block (spring etc.) say the preview may differ from the real install.
|
|
131
138
|
lines.push(t("cmd.dryRun.versionYml.note"));
|
|
132
139
|
}
|
|
140
|
+
if (plan.droppedPathLines?.length) lines.push(...plan.droppedPathLines.map((l) => `⚠️ ${l}`));
|
|
133
141
|
if (plan.scripts) {
|
|
134
142
|
const mark = { create: "+", overwrite: "~", unchanged: "=" };
|
|
135
143
|
const note = {
|
package/src/commands/full.js
CHANGED
|
@@ -6,7 +6,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
|
6
6
|
import { writeText, findUnwritable } from "../core/fsutil.js";
|
|
7
7
|
import { CliError } from "../core/errors.js";
|
|
8
8
|
import { PATHS } from "../core/paths.js";
|
|
9
|
-
import { renderVersionYml, parseExisting, sameIgnoringTimestamps } from "../core/version-yml.js";
|
|
9
|
+
import { renderVersionYml, parseExisting, sameIgnoringTimestamps, droppedPathLines } from "../core/version-yml.js";
|
|
10
10
|
import { readVersionYmlTemplate } from "../core/assets.js";
|
|
11
11
|
import { existingMarkerInDir } from "../core/paths-resolve.js";
|
|
12
12
|
import { addVersionSectionToReadme, README_STATUS_LABEL } from "../core/copy/readme.js";
|
|
@@ -18,6 +18,7 @@ import { readBaseline, writeBaseline, appFileHash } from "../core/baseline.js";
|
|
|
18
18
|
import { scanUnsubstituted, classifySecrets, narrowSecretsBySshAuth } from "../core/verify.js";
|
|
19
19
|
import { cleanupOtherDeployWorkflows, payloadWorkflowNames, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
20
20
|
import { hooksFor } from "../core/types.js";
|
|
21
|
+
import { OPTIONS } from "../core/options.js";
|
|
21
22
|
import { findStaleWorkflows, cleanupStaleWorkflows } from "../core/removal-plan.js";
|
|
22
23
|
import { log, maskValue } from "../core/logger.js";
|
|
23
24
|
import { t } from "../i18n/index.js";
|
|
@@ -70,7 +71,10 @@ export function runFull(context, payloadRoot, targetRoot = ".", hooks = {}) {
|
|
|
70
71
|
// Preserve unknown top-level fields of an existing version.yml when regenerating it.
|
|
71
72
|
const vyPath = join(targetRoot, PATHS.versionFile);
|
|
72
73
|
const prevVy = existsSync(vyPath) ? readFileSync(vyPath, "utf8") : null;
|
|
73
|
-
const
|
|
74
|
+
const prevParsed = prevVy != null ? parseExisting(prevVy) : null;
|
|
75
|
+
const extraTopLevel = prevParsed ? prevParsed.extraTopLevel : [];
|
|
76
|
+
// Folders folded into one type entry are not managed after this rewrite; leave a trace in the install log.
|
|
77
|
+
for (const line of droppedPathLines(prevParsed?.droppedPaths, paths)) log.warn("version", "paths", line);
|
|
74
78
|
|
|
75
79
|
// 2. Generate version.yml (render payload/version.yml.template; full regeneration)
|
|
76
80
|
// A re-run where only the date lines differ does not rewrite it (idempotent).
|
|
@@ -204,8 +208,9 @@ export function cleanupWorkflows(context, payloadRoot, targetRoot, baseline, { j
|
|
|
204
208
|
// Values picked interactively converge here too, so "why was it installed like this?" can later be traced from the log alone.
|
|
205
209
|
function logChoices(context, types) {
|
|
206
210
|
if (context.deployStyle) log.info("option", "deploy", context.deployStyle);
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
for (const o of OPTIONS) {
|
|
212
|
+
if (context[o.ctxField] != null) log.info("option", o.logLabel, context[o.ctxField] ? "on" : "off");
|
|
213
|
+
}
|
|
209
214
|
for (const { hook } of hooksFor(types, "logChoices")) {
|
|
210
215
|
for (const [name, value] of hook(context)) log.info("option", name, value);
|
|
211
216
|
}
|
|
@@ -6,8 +6,9 @@ import { join } from "node:path";
|
|
|
6
6
|
import { existsSync, readFileSync } from "node:fs";
|
|
7
7
|
import { resolvePayloadRoot, assertPayload, readTemplateVersion } from "../core/assets.js";
|
|
8
8
|
import { detectTypes, detectVersion, detectDefaultBranch, detectRepoName, makeResolvers, detectMarkers } from "../core/detect-fs.js";
|
|
9
|
-
import { parseExisting } from "../core/version-yml.js";
|
|
9
|
+
import { parseExisting, droppedPathLines } from "../core/version-yml.js";
|
|
10
10
|
import { pickReleaseOptions, resolveReleaseOptions } from "../core/release-options.js";
|
|
11
|
+
import { OPTIONS, askableOptions, explicitFromContext } from "../core/options.js";
|
|
11
12
|
import { runBreakingCheck } from "../core/breaking-check.js";
|
|
12
13
|
import { resolveProjectPaths } from "../core/paths-resolve.js";
|
|
13
14
|
import {
|
|
@@ -32,9 +33,6 @@ import { t, getLanguage } from "../i18n/index.js";
|
|
|
32
33
|
|
|
33
34
|
const CANCEL = prompts.CANCEL;
|
|
34
35
|
|
|
35
|
-
// Resolved lazily so the text follows the language chosen at run time.
|
|
36
|
-
const semverAutoQuestion = () => t("interactive.question.semverAuto");
|
|
37
|
-
const copilotAiQuestion = () => t("interactive.question.copilotAi");
|
|
38
36
|
const isCancel = (v) => v === CANCEL || typeof v === "symbol";
|
|
39
37
|
|
|
40
38
|
// io defaults to the real prompts. Tests inject a stub io.
|
|
@@ -100,8 +98,8 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
100
98
|
const repoName = detectRepoName(cwd);
|
|
101
99
|
// Initial values of optional workflows: CLI flags (--copilot etc.) -> options saved in version.yml
|
|
102
100
|
// Values set by a flag skip the question - same priority as non-interactive mode.
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
// optionState: option name -> boolean|null (null = undecided; settled by resolveReleaseOptions below). Driven by the option registry.
|
|
102
|
+
const optionState = pickReleaseOptions(explicitFromContext(baseCtx ?? {}), existing);
|
|
105
103
|
// Server deploy style - not asked again when a saved value (version.yml) exists (same convention as semver_auto).
|
|
106
104
|
let deployStyle = savedDeployStyle(existing);
|
|
107
105
|
const showOptional = mode === "full";
|
|
@@ -157,23 +155,22 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
157
155
|
// Flutter options - env mode -> store deploy targets -> per-platform deploy mode.
|
|
158
156
|
await askFlutterOptions();
|
|
159
157
|
|
|
160
|
-
//
|
|
161
|
-
// In workflows mode version.yml is not written and the answer
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (mode === "full" && includeCopilotAi === null) {
|
|
169
|
-
const y3 = await io.askYesNo(copilotAiQuestion(), false);
|
|
170
|
-
includeCopilotAi = y3 === true;
|
|
158
|
+
// Optional-workflow questions (auto semver bump default ON, Copilot AI summary default No - it consumes AI Credits).
|
|
159
|
+
// Each is skipped when a flag or a saved value exists. In workflows mode version.yml is not written and the answer
|
|
160
|
+
// is meaningless, so they are asked only in full. Options without `ask` in the registry are never asked.
|
|
161
|
+
for (const o of askableOptions()) {
|
|
162
|
+
if (mode === "full" && optionState[o.name] === null) {
|
|
163
|
+
const answer = await io.askYesNo(t(o.ask.questionKey), o.ask.initial);
|
|
164
|
+
optionState[o.name] = answer === true;
|
|
165
|
+
}
|
|
171
166
|
}
|
|
172
167
|
}
|
|
173
168
|
// When a question was actually asked (the full-mode questions above) its answer is respected, and only options that were not asked
|
|
174
169
|
// are filled with defaults - same function as the CLI path (index.js), hence the same rule.
|
|
175
|
-
(
|
|
176
|
-
|
|
170
|
+
// Fill undecided options with defaults; keep the whole result (also the options that are never asked) so a saved
|
|
171
|
+
// value such as release_automerge: false survives an interactive reinstall.
|
|
172
|
+
const settledOptions = resolveReleaseOptions(optionState, existing);
|
|
173
|
+
for (const o of OPTIONS) optionState[o.name] = settledOptions[o.ctxField];
|
|
177
174
|
const showOptionToggles = mode === "full";
|
|
178
175
|
|
|
179
176
|
// Confirm/edit loop - ESC means 'stay' (only an explicit 'No' exits)
|
|
@@ -184,12 +181,12 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
184
181
|
if (io.analysisCard) {
|
|
185
182
|
io.analysisCard({
|
|
186
183
|
mode, modeLabel: modeLabel(mode), types, version, branch, showOptional, paths, flutter, envModeDefault: flutterAsk.envModeDefault,
|
|
187
|
-
options: showOptionToggles ? {
|
|
184
|
+
options: showOptionToggles ? { ...optionState } : null,
|
|
188
185
|
});
|
|
189
186
|
} else {
|
|
190
187
|
io.note?.(summarize({
|
|
191
188
|
mode, types, version, branch, showOptional, flutter, envModeDefault: flutterAsk.envModeDefault,
|
|
192
|
-
options: showOptionToggles ? {
|
|
189
|
+
options: showOptionToggles ? { ...optionState } : null,
|
|
193
190
|
}), t("interactive.summary.title"));
|
|
194
191
|
}
|
|
195
192
|
const choice = await io.confirmProjectMenu();
|
|
@@ -218,13 +215,11 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
218
215
|
}
|
|
219
216
|
} else if (what === "branch") {
|
|
220
217
|
branch = await askBranchName(io, t("interactive.edit.branchPrompt"), branch, isCancel);
|
|
221
|
-
} else if (
|
|
218
|
+
} else if (askableOptions().some((o) => o.name === what)) {
|
|
222
219
|
// A saved value skips the first question, so this is the only place to change a value once decided.
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const y = await io.askYesNo(copilotAiQuestion(), includeCopilotAi);
|
|
227
|
-
if (typeof y === "boolean") includeCopilotAi = y;
|
|
220
|
+
const o = askableOptions().find((x) => x.name === what);
|
|
221
|
+
const y = await io.askYesNo(t(o.ask.questionKey), optionState[o.name]);
|
|
222
|
+
if (typeof y === "boolean") optionState[o.name] = y;
|
|
228
223
|
} else if (FLUTTER_EDIT_ITEMS.has(what)) {
|
|
229
224
|
flutter = await editFlutterOption(io, what, flutter, flutterAsk.envModeDefault);
|
|
230
225
|
}
|
|
@@ -294,6 +289,9 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
294
289
|
for (const ty of types) if (ty !== "basic" && !paths.has(ty)) paths.set(ty, existing?.paths.get(ty) || ".");
|
|
295
290
|
}
|
|
296
291
|
|
|
292
|
+
// Announce merged folders only now: the message must follow the folders actually chosen, not the saved ones.
|
|
293
|
+
if (existing?.droppedPaths?.length) io.note?.(droppedPathLines(existing.droppedPaths, paths).join("\n"), t("core.versionYml.pathMergedTitle"));
|
|
294
|
+
|
|
297
295
|
// In a monorepo the version/build-number files live inside the type folder - detect there now that paths are settled.
|
|
298
296
|
if (versionAutoDetected && [...paths.values()].some((p) => p && p !== ".")) {
|
|
299
297
|
version = detectVersion(cwd, { types, paths, warn: () => {} });
|
|
@@ -320,7 +318,7 @@ export async function runInteractive(baseCtx, { cwd = process.cwd(), payloadRoot
|
|
|
320
318
|
// Even if the saved value is zero-downtime, a single-server deploy is installed when the chosen types lack that style - record the style actually installed.
|
|
321
319
|
deployStyle: resolveDeployStyle({ payload, types, explicit: deployStyle, existing }),
|
|
322
320
|
typeOptions: flutterOptions,
|
|
323
|
-
releaseOptions:
|
|
321
|
+
releaseOptions: Object.fromEntries(OPTIONS.map((o) => [o.ctxField, optionState[o.name]])),
|
|
324
322
|
mode, force: true, version, versionCode, branch, branches, paths,
|
|
325
323
|
repoName, resolvers, envValues, envUseDefaults, now, today,
|
|
326
324
|
language: baseCtx.language ?? existing?.language ?? getLanguage(),
|
|
@@ -446,8 +444,9 @@ function summarize({ mode, types, version, branch, showOptional, flutter, envMod
|
|
|
446
444
|
}
|
|
447
445
|
}
|
|
448
446
|
if (options) {
|
|
449
|
-
|
|
450
|
-
|
|
447
|
+
for (const o of askableOptions()) {
|
|
448
|
+
lines.push(t(o.ask.summaryKey, { value: options[o.name] ? t("interactive.summary.on") : t("interactive.summary.off") }));
|
|
449
|
+
}
|
|
451
450
|
}
|
|
452
451
|
return lines.join("\n");
|
|
453
452
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// status command: read-only install state check. No network access (local file comparison only).
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { parseExisting } from "../core/version-yml.js";
|
|
4
|
+
import { parseExisting, droppedPathLines } from "../core/version-yml.js";
|
|
5
|
+
import { OPTIONS } from "../core/options.js";
|
|
5
6
|
import { planWorkflows } from "../core/copy/workflows.js";
|
|
6
7
|
import { makeResolvers, detectRepoName, detectDefaultBranch } from "../core/detect-fs.js";
|
|
7
8
|
import { PATHS } from "../core/paths.js";
|
|
8
9
|
import { isDeployStyle, DEFAULT_DEPLOY_STYLE } from "../core/deploy-style.js";
|
|
10
|
+
import { findMissingScripts } from "../core/copy/simple.js";
|
|
9
11
|
import { findStaleWorkflows } from "../core/removal-plan.js";
|
|
10
12
|
import { readBaseline } from "../core/baseline.js";
|
|
11
13
|
import { hooksFor, mergeHookResults } from "../core/types.js";
|
|
@@ -56,6 +58,9 @@ export function runStatus(payloadRoot, targetRoot = ".") {
|
|
|
56
58
|
removed: plan.removed.map((f) => f.filename), // deleted by the user and not restored
|
|
57
59
|
},
|
|
58
60
|
staleFiles: stale,
|
|
61
|
+
// Installed scripts the workflows need but the repo no longer has; the workflow comparison above cannot see them.
|
|
62
|
+
missingScripts: findMissingScripts(payloadRoot, targetRoot),
|
|
63
|
+
droppedPaths: existing.droppedPaths,
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -72,12 +77,12 @@ export function printStatus(status) {
|
|
|
72
77
|
if (status.branches) {
|
|
73
78
|
lines.push(t("cmd.status.branchMode", { mode: status.branches.mode, main: status.branches.main, develop: status.branches.develop }));
|
|
74
79
|
}
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
80
|
+
// An unset option is shown with the value it resolves to on this (existing) install, i.e. its legacyDefault.
|
|
81
|
+
const unsetLabel = (o) => t(o.legacyDefault ? "cmd.status.unsetDefaultTrue" : "cmd.status.unsetDefaultFalse");
|
|
82
|
+
const optionLabels = OPTIONS.map((o) => `${o.key}=${status.options[o.name] ?? unsetLabel(o)}`).join(" ");
|
|
78
83
|
const typeLabels = hooksFor(status.types, "statusLabels").map(({ hook }) => hook(status.options)).join("");
|
|
79
84
|
const deployLabel = status.options.deployStyle ? ` deploy_style=${status.options.deployStyle}` : "";
|
|
80
|
-
lines.push(t("cmd.status.options", { value:
|
|
85
|
+
lines.push(t("cmd.status.options", { value: `${optionLabels}${deployLabel}${typeLabels}` }));
|
|
81
86
|
if (status.modifiedFiles.length) {
|
|
82
87
|
lines.push("", t("cmd.status.modified", { n: status.modifiedFiles.length }));
|
|
83
88
|
for (const f of status.modifiedFiles) lines.push(` - ${f}`);
|
|
@@ -90,6 +95,15 @@ export function printStatus(status) {
|
|
|
90
95
|
for (const f of status.staleFiles) lines.push(` - ${f}`);
|
|
91
96
|
}
|
|
92
97
|
|
|
98
|
+
if (status.missingScripts?.length) {
|
|
99
|
+
lines.push("", t("cmd.status.missingScripts", { n: status.missingScripts.length, dir: PATHS.scriptsDir }));
|
|
100
|
+
for (const f of status.missingScripts) lines.push(` - ${f}`);
|
|
101
|
+
lines.push(t("cmd.status.missingScriptsFix"));
|
|
102
|
+
}
|
|
103
|
+
if (status.droppedPaths?.length) {
|
|
104
|
+
lines.push("", ...droppedPathLines(status.droppedPaths).map((l) => `⚠️ ${l}`));
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
// What an update would do. Installs without a baseline are all zeros, so nothing is printed.
|
|
94
108
|
const b = status.buckets || { autoUpdatable: [], localKept: [], conflicts: [], removed: [] };
|
|
95
109
|
if (b.autoUpdatable.length || b.localKept.length || b.conflicts.length || b.removed.length) {
|
package/src/context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Wizard-wide state made explicit as a single object
|
|
2
2
|
import { TYPE_IDS, allHookValues } from "./core/types.js";
|
|
3
|
+
import { defaultContextFields } from "./core/options.js";
|
|
3
4
|
|
|
4
5
|
// --type whitelist: the display order of the type registry (core/types.js).
|
|
5
6
|
export const VALID_TYPES = TYPE_IDS;
|
|
@@ -24,8 +25,9 @@ export function createContext(overrides = {}) {
|
|
|
24
25
|
branch: "",
|
|
25
26
|
branches: null, // { main, develop, mode: "pr-flow"|"trunk-based" } - result of resolveBranchConfig
|
|
26
27
|
paths: new Map(), // type -> path
|
|
27
|
-
includeSemverAuto
|
|
28
|
-
|
|
28
|
+
// Release option fields (includeSemverAuto, includeCopilotAi, ...): null = unset (resolved downstream), true/false = explicit.
|
|
29
|
+
// Fields and defaults come from the option registry (core/options.js).
|
|
30
|
+
...defaultContextFields(),
|
|
29
31
|
// Type-specific option fields; all ignored in projects without that type. Fields and defaults come from the type hook (contextDefaults).
|
|
30
32
|
...allHookValues("contextDefaults"),
|
|
31
33
|
language: "en", // message language (--lang / env / version.yml), default en
|
package/src/core/copy/simple.js
CHANGED
|
@@ -24,6 +24,37 @@ export function planScripts(payloadRoot, targetRoot = ".") {
|
|
|
24
24
|
return out;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// Script -> bundled scripts it imports (`import x` / `from x import ...`), read from the payload so the
|
|
28
|
+
// dependency table cannot drift from the code. Only names in SCRIPT_NAMES count; stdlib imports are ignored.
|
|
29
|
+
function scriptImports(payloadRoot, name) {
|
|
30
|
+
let src = "";
|
|
31
|
+
try { src = readFileSync(join(payloadRoot, PAYLOAD.scriptsDir, name), "utf8"); } catch { return []; }
|
|
32
|
+
const mods = new Set();
|
|
33
|
+
for (const m of src.matchAll(/^[ \t]*(?:from[ \t]+(\w+)[ \t]+import\b|import[ \t]+([\w, \t]+))/gm)) {
|
|
34
|
+
if (m[1]) mods.add(m[1]);
|
|
35
|
+
else for (const x of m[2].split(",")) mods.add(x.trim().split(/\s+/)[0]);
|
|
36
|
+
}
|
|
37
|
+
return SCRIPT_NAMES.filter((n) => n !== name && mods.has(n.replace(/\.py$/, "")));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Bundled scripts the installed workflows call but the repo no longer has (deleted by hand, never copied).
|
|
41
|
+
// Only scripts an installed workflow actually references count: some (truncate_release_notes.py) belong to
|
|
42
|
+
// workflows of one type only. Scripts those import (messages.py, issue_helper.py, ...) are required too,
|
|
43
|
+
// transitively, since the called script fails at import time without them.
|
|
44
|
+
export function findMissingScripts(payloadRoot, targetRoot = ".") {
|
|
45
|
+
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
46
|
+
let text = "";
|
|
47
|
+
try {
|
|
48
|
+
for (const f of readdirSync(wfDir)) if (/\.ya?ml$/.test(f)) text += `${readFileSync(join(wfDir, f), "utf8")}\n`;
|
|
49
|
+
} catch { return []; }
|
|
50
|
+
const required = new Set(SCRIPT_NAMES.filter((n) => text.includes(n)));
|
|
51
|
+
const queue = [...required];
|
|
52
|
+
while (queue.length) {
|
|
53
|
+
for (const dep of scriptImports(payloadRoot, queue.pop())) if (!required.has(dep)) { required.add(dep); queue.push(dep); }
|
|
54
|
+
}
|
|
55
|
+
return SCRIPT_NAMES.filter((n) => required.has(n) && exists(join(payloadRoot, PAYLOAD.scriptsDir, n)) && !exists(join(targetRoot, PATHS.scriptsDir, n)));
|
|
56
|
+
}
|
|
57
|
+
|
|
27
58
|
// Scripts are always overwritten with the payload version (+chmod, meaningless but harmless on Windows).
|
|
28
59
|
// User-edited scripts are overwritten too, so per-file results are returned for the caller to log.
|
|
29
60
|
export function copyScripts(payloadRoot, targetRoot = ".") {
|
|
@@ -48,7 +48,7 @@ export function readSavedDeployValues(targetRoot = ".") {
|
|
|
48
48
|
// and status/dry-run never see different file sets. When flutterStore is not an array (null = undecided,
|
|
49
49
|
// non-interactive default) no store filter is applied. It always returns a function even without filters,
|
|
50
50
|
// because processDir cannot take null.
|
|
51
|
-
// Some types (go
|
|
51
|
+
// Some types (go, python, react) keep server deploy workflows directly in the type root, so the deploy style
|
|
52
52
|
// filter is always applied. `available` (set of payload file names) lets a type without the chosen style fall back
|
|
53
53
|
// to the single server deploy.
|
|
54
54
|
export function buildTypeRootFilter(type, deployStyle, flutterStore, available = null) {
|
|
@@ -64,7 +64,7 @@ export function buildTypeRootFilter(type, deployStyle, flutterStore, available =
|
|
|
64
64
|
export function typeWorkflowSources(type, payloadRoot, { deployStyle, flutterStore = null, available = null }) {
|
|
65
65
|
const [typeDir, serverDeployDir] = typeWorkflowDirs(payloadRoot, type);
|
|
66
66
|
const sources = [];
|
|
67
|
-
// Some types (go
|
|
67
|
+
// Some types (go, python, react) keep server deploy workflows directly in the type root, so filter the root by deploy style too.
|
|
68
68
|
if (exists(typeDir)) sources.push({ srcDir: typeDir, type, filter: buildTypeRootFilter(type, deployStyle, flutterStore, available) });
|
|
69
69
|
// "No deploy" excludes the whole folder
|
|
70
70
|
if (exists(serverDeployDir) && deployStyle !== NO_DEPLOY_STYLE) {
|
package/src/core/deploy-style.js
CHANGED
|
@@ -31,7 +31,7 @@ export const isDeployWorkflow = (filename) => DEPLOY_STYLES.some((s) => filename
|
|
|
31
31
|
|
|
32
32
|
const PREVIEW_SUFFIX = "-PR-PREVIEW.yaml";
|
|
33
33
|
|
|
34
|
-
// All workflows that deploy to a server: CD bodies, single
|
|
34
|
+
// All workflows that deploy to a server: CD bodies, the single CD (react) and PR preview.
|
|
35
35
|
// "No deploy" removes all of them identically for every type. Removing only some would leave server Secret requirements behind.
|
|
36
36
|
export const isServerDeployWorkflow = (filename) =>
|
|
37
37
|
isDeployWorkflow(filename) || SINGLE_SERVER_CD_FILES.has(filename) || filename.endsWith(PREVIEW_SUFFIX);
|
package/src/core/detect.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// package.json classification: compares dependency "keys" exactly. A raw substring match would misdetect an
|
|
7
7
|
// export script or exponential-backoff as expo, a web app using react-native-web as react-native, and
|
|
8
|
-
// "
|
|
8
|
+
// "react" in keywords as react. Input is the raw package.json string. Evaluation order is the registry's detectOrder.
|
|
9
9
|
export function classifyPackageText(raw) {
|
|
10
10
|
let pkg;
|
|
11
11
|
try { pkg = JSON.parse(String(raw || "")); } catch { return PACKAGE_FALLBACK_TYPE; }
|
|
@@ -15,7 +15,8 @@ export function classifyPackageText(raw) {
|
|
|
15
15
|
const d = pkg[field];
|
|
16
16
|
if (d && typeof d === "object") for (const k of Object.keys(d)) deps.add(k);
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
const hasDep = (t) => [t.packageDep, ...(t.extraPackageDeps || [])].some((k) => deps.has(k));
|
|
19
|
+
return PACKAGE_DETECTED_TYPES.find(hasDep)?.id ?? PACKAGE_FALLBACK_TYPE;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
// Convenience: when given a parsed object, re-serialize it and apply the rule above.
|