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/removal-plan.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
//
|
|
2
|
-
// uninstall(
|
|
1
|
+
// Removal-target detector - works out which files the wizard installed.
|
|
2
|
+
// Shared base of uninstall (item checklist) and purge (hidden dev mode); it deletes nothing.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// (a)
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// baseline
|
|
13
|
-
//
|
|
14
|
-
// (version.yml
|
|
4
|
+
// Principle: the union of (a) files whose name exactly matches one in the current payload and (b) installed
|
|
5
|
+
// workflow files that start with the wizard-managed marker (MANAGED_WORKFLOW_MARKER) is the removal target.
|
|
6
|
+
// (a) alone missed files installed by an older version whose payload filename was later renamed or deleted
|
|
7
|
+
// (the original bug). (b) alone would regress by missing every existing install made before the marker
|
|
8
|
+
// existed (whose filenames still match the current payload) - so both are used together as a union. The marker
|
|
9
|
+
// ships only in payload templates released after that fix, so what path (b) newly catches is only
|
|
10
|
+
// "renamed or deleted files that carry the marker".
|
|
11
|
+
// However, the marker follows a workflow the user copied and merely renamed. So (b) accepts only filenames
|
|
12
|
+
// recorded in the baseline, and only for old installs without a baseline falls back to the PROJECT-* prefix.
|
|
13
|
+
// User-made workflows, version.yml, README and .gitignore are not targets
|
|
14
|
+
// (version.yml is the user's version data - an artifact, not a removal target).
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// This file used to be src/commands/revert.js. The revert mode was removed as a subset of uninstall,
|
|
17
|
+
// and only the detection logic remained, moved to core rather than commands.
|
|
18
18
|
import { join, isAbsolute } from "node:path";
|
|
19
19
|
import { existsSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
20
20
|
import { PATHS } from "./paths.js";
|
|
@@ -23,12 +23,12 @@ import { LOG_DIR } from "./logger.js";
|
|
|
23
23
|
import { payloadWorkflowNames } from "./deploy-style.js";
|
|
24
24
|
import { SCRIPT_NAMES } from "./copy/simple.js";
|
|
25
25
|
|
|
26
|
-
// payload/workflows/**/*.yaml
|
|
26
|
+
// Fixed marker planted on the first line of payload/workflows/**/*.yaml - changing this value breaks matching with past installs.
|
|
27
27
|
export const MANAGED_WORKFLOW_MARKER = "# project-auto-wizard:managed-workflow";
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
// .bak/.template.yaml
|
|
31
|
-
//
|
|
29
|
+
// Set of filenames starting with the managed marker in the installed workflows directory (flat layout).
|
|
30
|
+
// .bak/.template.yaml backups are verbatim copies of the original text and keep the marker,
|
|
31
|
+
// so they are recognized the same way without inferring from extension or filename.
|
|
32
32
|
function markedWorkflowNames(wfDir) {
|
|
33
33
|
const names = new Set();
|
|
34
34
|
if (!existsSync(wfDir)) return names;
|
|
@@ -40,8 +40,8 @@ function markedWorkflowNames(wfDir) {
|
|
|
40
40
|
return names;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
44
|
-
// recorded:
|
|
43
|
+
// Whether a marked file was installed by the wizard - .bak/.template.yaml backups are judged by the original filename.
|
|
44
|
+
// recorded: set of filenames in the baseline, null when there is no baseline.
|
|
45
45
|
function isInstalledName(name, recorded) {
|
|
46
46
|
if (!recorded) return name.startsWith("PROJECT-");
|
|
47
47
|
const stem = name.endsWith(".bak") ? name.slice(0, -".bak".length) : name;
|
|
@@ -53,18 +53,18 @@ function isInstalledName(name, recorded) {
|
|
|
53
53
|
return candidates.some((c) => recorded.has(c));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
56
|
+
// Backup-derived files created by conflict handling/cleanup (.bak/.template.yaml) - what the auto-added .gitignore entries hide.
|
|
57
|
+
// If these files remain after a removal that keeps the workflows, the .gitignore entries must stay too, or they show up in git status.
|
|
58
58
|
export const backupArtifacts = (names) => names.filter((n) => n.endsWith(".bak") || n.endsWith(".template.yaml"));
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// Pure function that deletes nothing - shared by uninstall/purge/--dry-run.
|
|
61
61
|
export function planRemoval(payloadRoot, targetRoot = ".") {
|
|
62
62
|
const removedWf = new Set();
|
|
63
63
|
const removedScripts = [];
|
|
64
64
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
65
65
|
const baseline = readBaseline(targetRoot);
|
|
66
66
|
if (existsSync(wfDir)) {
|
|
67
|
-
// (a)
|
|
67
|
+
// (a) Files whose name matches the current payload - marker or not (prevents regressing existing installs).
|
|
68
68
|
for (const name of payloadWorkflowNames(payloadRoot)) {
|
|
69
69
|
const p = join(wfDir, name);
|
|
70
70
|
if (existsSync(p)) removedWf.add(name);
|
|
@@ -72,7 +72,7 @@ export function planRemoval(payloadRoot, targetRoot = ".") {
|
|
|
72
72
|
if (existsSync(join(wfDir, templateName))) removedWf.add(templateName);
|
|
73
73
|
if (existsSync(p + ".bak")) removedWf.add(name + ".bak");
|
|
74
74
|
}
|
|
75
|
-
// (b)
|
|
75
|
+
// (b) Files starting with the managed marker - also recognizes files renamed or deleted in the payload.
|
|
76
76
|
const recorded = baseline ? new Set(Object.keys(baseline.files)) : null;
|
|
77
77
|
for (const name of markedWorkflowNames(wfDir)) {
|
|
78
78
|
if (isInstalledName(name, recorded)) removedWf.add(name);
|
|
@@ -81,24 +81,24 @@ export function planRemoval(payloadRoot, targetRoot = ".") {
|
|
|
81
81
|
for (const s of SCRIPT_NAMES) {
|
|
82
82
|
if (existsSync(join(targetRoot, PATHS.scriptsDir, s))) removedScripts.push(s);
|
|
83
83
|
}
|
|
84
|
-
// Flutter
|
|
84
|
+
// Flutter app files (Fastfile etc.) only if the wizard newly created them and the content is unchanged - a file with filled-in values belongs to the user.
|
|
85
85
|
const appFiles = [];
|
|
86
86
|
for (const [rel, hash] of Object.entries(baseline?.appFiles || {})) {
|
|
87
|
-
if (rel.split(/[\\/]/).includes("..") || isAbsolute(rel)) continue; //
|
|
87
|
+
if (rel.split(/[\\/]/).includes("..") || isAbsolute(rel)) continue; // paths outside the repo are not trusted
|
|
88
88
|
const p = join(targetRoot, rel);
|
|
89
89
|
if (existsSync(p) && appFileHash(readFileSync(p, "utf8")) === hash) appFiles.push(rel);
|
|
90
90
|
}
|
|
91
|
-
// baseline
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// "
|
|
91
|
+
// The baseline is an internal state file made by the wizard - it must disappear along with the installed files.
|
|
92
|
+
// If left, the next install would mistake everything for "files installed before and deleted by the user" and classify all as removed.
|
|
93
|
+
// If only the run logs (.wizard/logs) remain, the whole folder goes too - a log folder left after all
|
|
94
|
+
// installed files are gone is not a "complete removal".
|
|
95
95
|
const baselineDirs = existsSync(join(targetRoot, BASELINE_PATH)) || existsSync(join(targetRoot, LOG_DIR)) ? [BASELINE_DIR] : [];
|
|
96
96
|
return { workflows: [...removedWf], scripts: removedScripts, appFiles, baseline: baselineDirs };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
//
|
|
100
|
-
// payload
|
|
101
|
-
// baseline
|
|
99
|
+
// Old workflows to clean up on update - files the wizard installed (baseline record + managed marker) that are not in the payload now.
|
|
100
|
+
// They were renamed or dropped in the payload, so leaving them keeps running with the old triggers and procedure.
|
|
101
|
+
// Files not in the baseline are not targets - they may be workflows the user made by copying a wizard file.
|
|
102
102
|
export function findStaleWorkflows(payloadRoot, targetRoot = ".", baseline = null) {
|
|
103
103
|
if (!baseline?.files) return [];
|
|
104
104
|
const current = payloadWorkflowNames(payloadRoot);
|
|
@@ -107,8 +107,8 @@ export function findStaleWorkflows(payloadRoot, targetRoot = ".", baseline = nul
|
|
|
107
107
|
.sort();
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
110
|
+
// Same rule as deploy-style cleanup - untouched files (installed hash matches) are deleted, touched files are moved to .bak
|
|
111
|
+
// to keep the content while switching off the trigger. With dryRun only the verdict is made and no file is touched (for the --dry-run preview).
|
|
112
112
|
export function cleanupStaleWorkflows(targetRoot, names, baseline, { dryRun = false } = {}) {
|
|
113
113
|
const wfDir = join(targetRoot, PATHS.workflowsDir);
|
|
114
114
|
const removed = [];
|
package/src/core/types.js
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
import { flutterHooks } from "./flutter-hooks.js";
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// (
|
|
3
|
+
// Project type registry: keeps per-type knowledge (detection markers, detection order, version and build-number files) in one place.
|
|
4
|
+
// The type list, marker map and detection chain are all built from this table, so a new type starts with one more line here.
|
|
5
|
+
// (Writing version files at release time is handled separately by payload/scripts/version_manager.py.)
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// id CLI
|
|
9
|
-
// markers
|
|
10
|
-
// detectBy
|
|
11
|
-
// "package": package.json
|
|
12
|
-
// "package-fallback": package.json
|
|
13
|
-
//
|
|
14
|
-
// detectOrder
|
|
15
|
-
// versionSources
|
|
16
|
-
// buildNumberSource
|
|
17
|
-
// singleServerCd
|
|
18
|
-
// hooks
|
|
19
|
-
// workflowFilter(context)
|
|
7
|
+
// Fields:
|
|
8
|
+
// id Type name used in the CLI and version.yml. Array order is also the display order of --help and interactive choices.
|
|
9
|
+
// markers Evidence files for the type. [0] is the representative file, and monorepo path search prioritizes in this order.
|
|
10
|
+
// detectBy Auto-detection method - "markers": detected when any of markers exists
|
|
11
|
+
// "package": detected by a package.json dependency key (packageDep)
|
|
12
|
+
// "package-fallback": package.json exists but none of the above matched
|
|
13
|
+
// absent: not auto-detected (basic is the result when nothing was detected).
|
|
14
|
+
// detectOrder Evaluation order within the same detectBy. The earlier rule wins (an expo app also has a react-native dependency).
|
|
15
|
+
// versionSources File keys to read the version from at install (sources in detect.js); read first in this order for the primary type.
|
|
16
|
+
// buildNumberSource File key to read the mobile build number from (detectBuildNumberFromFiles in detect.js).
|
|
17
|
+
// singleServerCd The workflow file name when there is exactly one server-deploy CD with no deploy-style variants.
|
|
18
|
+
// hooks Type-specific behavior. A missing hook means "nothing to do", so common code calls only the existing ones through hooksFor().
|
|
19
|
+
// workflowFilter(context) Filter for the type's root workflow files (filename -> bool) or null
|
|
20
20
|
// cleanupWorkflows(dir, installed, context, baseline, opts)
|
|
21
|
-
//
|
|
21
|
+
// Cleanup of workflows dropped from the selection -> { removed, backedUp }
|
|
22
22
|
// planAppFiles / copyAppFiles(context, payloadRoot, targetRoot)
|
|
23
|
-
//
|
|
24
|
-
// appFilesTag
|
|
25
|
-
// statusLabels(options)
|
|
26
|
-
// doctorChecks(cwd, existing, { docs }) doctor
|
|
27
|
-
// resolveOptions({ opts, existing, workflowsDir })
|
|
28
|
-
// (workflowsDir
|
|
29
|
-
// contextDefaults
|
|
23
|
+
// Plan/copy of user-owned app files (created only when missing) -> { created, kept }
|
|
24
|
+
// appFilesTag Category name in the app-file copy log
|
|
25
|
+
// statusLabels(options) String appended to the status options line
|
|
26
|
+
// doctorChecks(cwd, existing, { docs }) Array of doctor diagnostic rows
|
|
27
|
+
// resolveOptions({ opts, existing, workflowsDir }) Type option object decided as CLI options, then saved values, then defaults
|
|
28
|
+
// (workflowsDir is passed only when inferring from installed workflows)
|
|
29
|
+
// contextDefaults Undecided default fields of createContext
|
|
30
30
|
// contextFields(options) / optionsFromContext(context)
|
|
31
|
-
//
|
|
32
|
-
// versionOptionsBlock(options) version.yml options
|
|
33
|
-
// savedOptionKeys version.yml
|
|
34
|
-
// cliFlags[{ flag, field, initial, parse(v) }]
|
|
35
|
-
// installNotices(options)
|
|
36
|
-
// logChoices(context)
|
|
37
|
-
// (
|
|
31
|
+
// Conversion between the option object and install-context fields
|
|
32
|
+
// versionOptionsBlock(options) Block appended under version.yml options (6-space indent)
|
|
33
|
+
// savedOptionKeys Saved version.yml key to parseExisting option field
|
|
34
|
+
// cliFlags[{ flag, field, initial, parse(v) }] Type-specific CLI flags (the parsed result goes into opts[field])
|
|
35
|
+
// installNotices(options) Array of warnings shown right after install (empty values ignored)
|
|
36
|
+
// logChoices(context) Array of selected [name, value] pairs to record in the install log
|
|
37
|
+
// (The type-specific interactive question flow still lives in interactive.js.)
|
|
38
38
|
export const TYPES = [
|
|
39
39
|
{
|
|
40
40
|
id: "spring",
|
|
@@ -68,13 +68,13 @@ export const TYPES = [
|
|
|
68
68
|
id: "react-native",
|
|
69
69
|
markers: ["package.json"],
|
|
70
70
|
detectBy: "package", packageDep: "react-native", detectOrder: 2,
|
|
71
|
-
//
|
|
71
|
+
// The native files synced at release time are the reference; package.json is consulted only when they cannot be read.
|
|
72
72
|
versionSources: ["reactNative", "packageJson"],
|
|
73
73
|
buildNumberSource: "androidGradle",
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
|
-
// Expo
|
|
77
|
-
//
|
|
76
|
+
// Expo may use only app.config.ts/js without app.json, as in the latest create-expo-app template.
|
|
77
|
+
// Even with no config file at all, a package.json with an expo dependency is the evidence.
|
|
78
78
|
id: "react-native-expo",
|
|
79
79
|
markers: ["app.json", "app.config.ts", "app.config.js", "package.json"],
|
|
80
80
|
detectBy: "package", packageDep: "expo", detectOrder: 1,
|
|
@@ -94,7 +94,7 @@ export const TYPES = [
|
|
|
94
94
|
versionSources: ["pyproject", "setupPy"],
|
|
95
95
|
},
|
|
96
96
|
{
|
|
97
|
-
//
|
|
97
|
+
// A type with no marker files: the result when nothing was detected, and a type that needs no path.
|
|
98
98
|
id: "basic",
|
|
99
99
|
markers: [],
|
|
100
100
|
},
|
|
@@ -106,15 +106,15 @@ export const TYPES = [
|
|
|
106
106
|
},
|
|
107
107
|
];
|
|
108
108
|
|
|
109
|
-
//
|
|
109
|
+
// Type used when nothing was detected
|
|
110
110
|
export const FALLBACK_TYPE = "basic";
|
|
111
111
|
|
|
112
112
|
const BY_ID = new Map(TYPES.map((t) => [t.id, t]));
|
|
113
113
|
|
|
114
|
-
//
|
|
114
|
+
// Unknown types give undefined; callers decide their own defaults.
|
|
115
115
|
export const typeInfo = (id) => BY_ID.get(id);
|
|
116
116
|
|
|
117
|
-
// types
|
|
117
|
+
// Only those of types that have hook `name`, as [{ id, hook }], keeping the given type order.
|
|
118
118
|
export function hooksFor(types, name) {
|
|
119
119
|
const found = [];
|
|
120
120
|
for (const id of types) {
|
|
@@ -124,31 +124,31 @@ export function hooksFor(types, name) {
|
|
|
124
124
|
return found;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
//
|
|
127
|
+
// Merges into one object the return values of the hook `name` from the types that have it; used when each type contributes different keys, such as options and context fields.
|
|
128
128
|
export function mergeHookResults(types, name, ...args) {
|
|
129
129
|
return Object.assign({}, ...hooksFor(types, name).map(({ hook }) => hook(...args)));
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
//
|
|
132
|
+
// Merges the hook constants declared by all types (contextDefaults, savedOptionKeys, etc.) into one; for the default shape needed before the types are decided.
|
|
133
133
|
export const allHookValues = (name) => Object.assign({}, ...TYPES.map((t) => t.hooks?.[name]));
|
|
134
134
|
|
|
135
|
-
//
|
|
136
|
-
// VALID_TYPES
|
|
135
|
+
// Type names in display order.
|
|
136
|
+
// VALID_TYPES and ALL_TYPES share the same array, so it is frozen to keep a change on one side from polluting the other.
|
|
137
137
|
export const TYPE_IDS = Object.freeze(TYPES.map((t) => t.id));
|
|
138
138
|
|
|
139
139
|
const byDetectOrder = (a, b) => a.detectOrder - b.detectOrder;
|
|
140
140
|
|
|
141
|
-
//
|
|
141
|
+
// Types detected by file existence alone (in detection order)
|
|
142
142
|
export const MARKER_DETECTED_TYPES = TYPES.filter((t) => t.detectBy === "markers").sort(byDetectOrder);
|
|
143
143
|
|
|
144
|
-
// package.json
|
|
144
|
+
// Types told apart by package.json dependencies (in evaluation order)
|
|
145
145
|
export const PACKAGE_DETECTED_TYPES = TYPES.filter((t) => t.detectBy === "package").sort(byDetectOrder);
|
|
146
146
|
|
|
147
|
-
// package.json
|
|
147
|
+
// Type used when package.json exists but has no known framework dependency
|
|
148
148
|
export const PACKAGE_FALLBACK_TYPE = TYPES.find((t) => t.detectBy === "package-fallback").id;
|
|
149
149
|
|
|
150
|
-
//
|
|
150
|
+
// Types that have a mobile build number (version_code)
|
|
151
151
|
export const BUILD_NUMBER_TYPES = new Set(TYPES.filter((t) => t.buildNumberSource).map((t) => t.id));
|
|
152
152
|
|
|
153
|
-
//
|
|
153
|
+
// Workflow file names for which there is exactly one server-deploy CD, with no deploy-style variants
|
|
154
154
|
export const SINGLE_SERVER_CD_FILES = new Set(TYPES.filter((t) => t.singleServerCd).map((t) => t.singleServerCd));
|
package/src/core/verify.js
CHANGED
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Post-install verification - re-reads the installed workflows to check "will this run as-is".
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Why after install rather than before: substitution happens scattered per file and auto tokens
|
|
4
|
+
// depend on resolver results. Looking at the final disk content is the only way to see the same
|
|
5
|
+
// thing that will actually be deployed.
|
|
5
6
|
import { join } from "node:path";
|
|
6
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { t } from "../i18n/index.js";
|
|
7
9
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
+
// Tokens that are not substitution targets - heredoc delimiters inside workflow scripts. They are
|
|
11
|
+
// syntax, not values, so they are excluded from the unsubstituted check. (e.g. cat <<'__WIZARD_FILE_CONTENT_EOF__')
|
|
10
12
|
const SENTINEL_RE = /^__WIZARD_[A-Z0-9_]*__$/;
|
|
11
13
|
const PLACEHOLDER_RE = /__[A-Z][A-Z0-9_]*__/g;
|
|
12
14
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
15
|
+
// Lines dead as comments - they never run, so they are not checked.
|
|
16
|
+
// The template contains whole optional example steps commented out; counting them
|
|
17
|
+
// would tell the user to "register" secrets that are never used.
|
|
16
18
|
const isCommented = (line) => /^\s*#/.test(line);
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// Scan for unsubstituted placeholders.
|
|
21
|
+
// Previously, even when an auto token computation failed (e.g. application.yaml not found, so the path was an empty string),
|
|
22
|
+
// the line was left untouched, and a workflow still containing __APPLICATION_YML_DIR__ ended as an
|
|
23
|
+
// "install success". The problem only surfaced at deploy time, when a directory with that name got created.
|
|
22
24
|
//
|
|
23
|
-
//
|
|
25
|
+
// Returns: [{ filename, line, token, text }] - with the line number so a person can fix it right away.
|
|
24
26
|
export function scanUnsubstituted(workflowsDir, filenames = []) {
|
|
25
27
|
const found = [];
|
|
26
28
|
for (const filename of filenames) {
|
|
@@ -39,25 +41,29 @@ export function scanUnsubstituted(workflowsDir, filenames = []) {
|
|
|
39
41
|
return found;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
// GITHUB_TOKEN
|
|
44
|
+
// GITHUB_TOKEN is injected by Actions automatically, so it is not something the user registers.
|
|
43
45
|
const AUTO_SECRETS = new Set(["GITHUB_TOKEN"]);
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// AI_API_KEY
|
|
47
|
-
// WORKFLOW_PAT
|
|
46
|
+
// Secrets the workflow runs without - the fallback is documented. Mixing them with required ones under
|
|
47
|
+
// "must be registered to work" would make the guidance itself untrustworthy, so they are kept apart.
|
|
48
|
+
// AI_API_KEY -> if absent: Copilot (opt-in, copilot_ai) -> rule-based fallback
|
|
49
|
+
// WORKFLOW_PAT -> if absent: GITHUB_TOKEN
|
|
48
50
|
export const OPTIONAL_SECRETS = new Set(["AI_API_KEY", "WORKFLOW_PAT"]);
|
|
49
51
|
const SECRET_RE = /secrets\.([A-Z][A-Z0-9_]*)/g;
|
|
50
52
|
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
//
|
|
53
|
+
// The "NAME (optional)" note in a workflow header comment - a secret the workflow is written to work without.
|
|
54
|
+
// The payload uses "(optional)"; the legacy Korean marker ("\uC120\uD0DD") is still accepted so workflows installed
|
|
55
|
+
// by older versions keep being classified correctly.
|
|
56
|
+
const OPTIONAL_NOTE_RE = /^\s*#\s*-?\s*([A-Z][A-Z0-9_]*)\s*\((?:\uC120\uD0DD|optional)/i;
|
|
57
|
+
// `secrets.A || secrets.B` - only one of the two is needed.
|
|
54
58
|
const EITHER_RE = /^\s*\|\|\s*secrets\.([A-Z][A-Z0-9_]*)/;
|
|
55
|
-
// `secrets.A || '3000'`, `secrets.A || vars.A`
|
|
59
|
+
// `secrets.A || '3000'`, `secrets.A || vars.A` - there is a default/alternative value.
|
|
56
60
|
const FALLBACK_AFTER_RE = /^\s*\|\|/;
|
|
57
61
|
const FALLBACK_BEFORE_RE = /\|\|\s*$/;
|
|
58
|
-
|
|
62
|
+
// Separator of a "A or B" secret pair. It is shown to the user and also used to split keys, so it follows
|
|
63
|
+
// the current language; evaluated lazily (via toString) because it must not be fixed at import time.
|
|
64
|
+
export const EITHER_SEP = { toString: () => t("core.verify.eitherSep") };
|
|
59
65
|
|
|
60
|
-
//
|
|
66
|
+
// Sorts the secret references in one file into required / either-of-two / optional.
|
|
61
67
|
function classifyFile(content) {
|
|
62
68
|
const lines = content.split(/\r?\n/);
|
|
63
69
|
const noted = new Set();
|
|
@@ -78,7 +84,7 @@ function classifyFile(content) {
|
|
|
78
84
|
const either = rest.match(EITHER_RE);
|
|
79
85
|
if (either) {
|
|
80
86
|
pairs.push([name, either[1]]);
|
|
81
|
-
i++; //
|
|
87
|
+
i++; // the right side of the pair is already bound
|
|
82
88
|
} else if (FALLBACK_AFTER_RE.test(rest) || FALLBACK_BEFORE_RE.test(line.slice(0, m.index))) {
|
|
83
89
|
optional.add(name);
|
|
84
90
|
} else {
|
|
@@ -96,10 +102,10 @@ function classifyFile(content) {
|
|
|
96
102
|
return out;
|
|
97
103
|
}
|
|
98
104
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
105
|
+
// Splits the secrets of the installed workflows into required and optional.
|
|
106
|
+
// Counting every referenced name as required would mix ones with defaults and either-of-two fallback pairs into
|
|
107
|
+
// "must be registered to work", inflating the count and making first-time users register secrets they do not need.
|
|
108
|
+
// Returns: { required: Map<name|"A or B", filenames[]>, optional: Map<same format> }
|
|
103
109
|
export function classifySecrets(workflowsDir, filenames = []) {
|
|
104
110
|
const required = new Map();
|
|
105
111
|
const optional = new Map();
|
|
@@ -117,7 +123,7 @@ export function classifySecrets(workflowsDir, filenames = []) {
|
|
|
117
123
|
for (const k of r.required) if (!skip(k)) add(required, k, filename);
|
|
118
124
|
for (const k of r.optional) if (!skip(k)) add(optional, k, filename);
|
|
119
125
|
}
|
|
120
|
-
//
|
|
126
|
+
// A name that is required on its own in even one file is required - fallback pairs/optional entries containing it are already satisfied, so drop them.
|
|
121
127
|
const requiredNames = new Set([...required.keys()].filter((k) => !k.includes(EITHER_SEP)));
|
|
122
128
|
const coveredByRequired = (k) => k.split(EITHER_SEP).some((n) => requiredNames.has(n));
|
|
123
129
|
for (const k of [...required.keys()]) if (k.includes(EITHER_SEP) && coveredByRequired(k)) required.delete(k);
|
|
@@ -126,23 +132,23 @@ export function classifySecrets(workflowsDir, filenames = []) {
|
|
|
126
132
|
return { required: sorted(required), optional: sorted(optional) };
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
// SERVER_HOST
|
|
132
|
-
//
|
|
135
|
+
// List of GitHub Secrets the installed workflows require.
|
|
136
|
+
// The completion screen only mentioned WORKFLOW_PAT and permissions, so values the deploy workflow really needs
|
|
137
|
+
// (SERVER_HOST, SSH_KEY, ...) were never announced. Right after install the deploy does not run,
|
|
138
|
+
// and that fact was shown nowhere.
|
|
133
139
|
//
|
|
134
|
-
//
|
|
140
|
+
// Returns: Map<secretName or "A or B", string[] filenames that use the secret>
|
|
135
141
|
export function collectRequiredSecrets(workflowsDir, filenames = []) {
|
|
136
142
|
return classifySecrets(workflowsDir, filenames).required;
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
//
|
|
145
|
+
// Secrets the workflow runs without (having a default/alternative value, or marked optional in the header comment).
|
|
140
146
|
export function collectOptionalSecrets(workflowsDir, filenames = []) {
|
|
141
147
|
return classifySecrets(workflowsDir, filenames).optional;
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
//
|
|
145
|
-
//
|
|
150
|
+
// Secrets where only one of two is needed depending on the SSH auth method - narrows the list using the answer the user already gave.
|
|
151
|
+
// Telling the user to "register" secrets that will not be used makes the guidance itself untrusted.
|
|
146
152
|
export function narrowSecretsBySshAuth(secrets, sshAuthMethod) {
|
|
147
153
|
if (!sshAuthMethod) return secrets;
|
|
148
154
|
const drop = sshAuthMethod === "key" ? "SERVER_PASSWORD" : "SSH_KEY";
|