@wpmoo/toolkit 0.9.29 → 0.9.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -2
- package/dist/approval-ledger.js +74 -0
- package/dist/cli-routes/doctor.js +20 -0
- package/dist/cli-routes/options.js +36 -0
- package/dist/cli-routes/reset.js +11 -0
- package/dist/cli-routes/source.js +112 -0
- package/dist/cli.js +23 -169
- package/dist/cockpit/command-registry.js +9 -3
- package/dist/cockpit/daily-prompts.js +33 -6
- package/dist/cockpit/menu.js +12 -7
- package/dist/cockpit/module-browser.js +79 -2
- package/dist/daily-actions.js +55 -12
- package/dist/databases.js +223 -36
- package/dist/doctor.js +129 -15
- package/dist/github.js +11 -2
- package/dist/help.js +6 -4
- package/dist/module-actions.js +23 -2
- package/dist/module-manifest.js +6 -0
- package/dist/module-quality.js +98 -0
- package/dist/postgres-diagnostics.js +27 -0
- package/dist/repo-url.js +4 -7
- package/dist/safe-reset.js +21 -12
- package/dist/source-manifest.js +2 -2
- package/dist/templates.js +149 -17
- package/docs/1-0-readiness.md +34 -10
- package/docs/command-reference.md +19 -1
- package/docs/generated-environment-verification.md +23 -2
- package/docs/handoff.md +14 -2
- package/docs/lifecycle-recipes.md +6 -1
- package/docs/troubleshooting.md +29 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { listEnvironmentDatabases, normalizeDatabaseListResult, } from '../databases.js';
|
|
1
|
+
import { listEnvironmentDatabases, findDatabaseSnapshots, normalizeDatabaseListResult, } from '../databases.js';
|
|
2
2
|
import { listModulesInSourceRepo } from '../module-actions.js';
|
|
3
3
|
import { listModuleRepos } from '../repo-actions.js';
|
|
4
4
|
import { listSources } from '../source-actions.js';
|
|
@@ -6,6 +6,7 @@ import { handlePromptCancel, menuPromptMessage, } from '../menu-navigation.js';
|
|
|
6
6
|
import { isPromptCancel, selectPrompt, textPrompt } from '../prompts/index.js';
|
|
7
7
|
const manualModuleValue = '__wpmoo_manual_module_entry__';
|
|
8
8
|
const manualDatabaseValue = '__wpmoo_manual_database_entry__';
|
|
9
|
+
const manualSnapshotValue = '__wpmoo_manual_snapshot_entry__';
|
|
9
10
|
function defaultCancelHandler(value, action) {
|
|
10
11
|
handlePromptCancel(isPromptCancel(value), action);
|
|
11
12
|
}
|
|
@@ -199,11 +200,37 @@ export async function collectDailyActionArgs(command, cwd, promptDepsArg = {}) {
|
|
|
199
200
|
return [db, snapshotName];
|
|
200
201
|
}
|
|
201
202
|
if (command === 'restore-snapshot') {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
const snapshots = findDatabaseSnapshots(cwd).snapshots;
|
|
204
|
+
let snapshotName;
|
|
205
|
+
let fallbackDatabase = 'devel';
|
|
206
|
+
if (snapshots.length > 0) {
|
|
207
|
+
const selected = await deps.select({
|
|
208
|
+
message: menuPromptMessage('Snapshot', 'back'),
|
|
209
|
+
options: [
|
|
210
|
+
...snapshots.map((snapshot) => ({ value: snapshot.name, label: snapshot.name })),
|
|
211
|
+
{ value: manualSnapshotValue, label: 'Manual entry' },
|
|
212
|
+
],
|
|
213
|
+
initialValue: snapshots[0].name,
|
|
214
|
+
});
|
|
215
|
+
deps.handleCancel(selected, 'back');
|
|
216
|
+
if (selected !== manualSnapshotValue) {
|
|
217
|
+
snapshotName = String(selected);
|
|
218
|
+
fallbackDatabase = snapshots.find((snapshot) => snapshot.name === snapshotName)?.databaseName ?? fallbackDatabase;
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
snapshotName = requiredString(await deps.text({
|
|
222
|
+
message: menuPromptMessage('Snapshot name', 'back'),
|
|
223
|
+
validate: (value) => (value.trim() ? undefined : 'Enter the snapshot name.'),
|
|
224
|
+
}), 'Snapshot name is required.', deps);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
snapshotName = requiredString(await deps.text({
|
|
229
|
+
message: menuPromptMessage('Snapshot name', 'back'),
|
|
230
|
+
validate: (value) => (value.trim() ? undefined : 'Enter the snapshot name.'),
|
|
231
|
+
}), 'Snapshot name is required.', deps);
|
|
232
|
+
}
|
|
233
|
+
const db = await databaseArg(cwd, deps, 'Odoo database', fallbackDatabase);
|
|
207
234
|
return [snapshotName, db];
|
|
208
235
|
}
|
|
209
236
|
return [];
|
package/dist/cockpit/menu.js
CHANGED
|
@@ -35,6 +35,7 @@ function commandName(command) {
|
|
|
35
35
|
}
|
|
36
36
|
const disabledReasonNextStep = {
|
|
37
37
|
'No modules found.': 'Next: choose "Add module" first.',
|
|
38
|
+
'No snapshots found.': 'Next: choose "Create snapshot" first.',
|
|
38
39
|
'Services stopped.': 'Next: choose "Start services" first.',
|
|
39
40
|
'Already running.': 'Next: choose "Stop services" or "Restart services".',
|
|
40
41
|
'Docker not running.': 'Next: start Docker, then choose "Start services".',
|
|
@@ -66,10 +67,14 @@ function moduleDisabledReason(command, moduleCount) {
|
|
|
66
67
|
function sourceRepoDisabledReason(command, sourceRepoCount) {
|
|
67
68
|
return sourceRepoCount === 0 && command.id === 'add-module' ? 'No source repos found.' : undefined;
|
|
68
69
|
}
|
|
69
|
-
function
|
|
70
|
+
function snapshotDisabledReason(command, snapshotCount) {
|
|
71
|
+
return snapshotCount === 0 && command.id === 'restore-snapshot' ? 'No snapshots found.' : undefined;
|
|
72
|
+
}
|
|
73
|
+
function disabledReason(command, serviceStatus, moduleCount, sourceRepoCount, snapshotCount) {
|
|
70
74
|
return (serviceDisabledReason(command, serviceStatus) ??
|
|
71
75
|
moduleDisabledReason(command, moduleCount) ??
|
|
72
|
-
sourceRepoDisabledReason(command, sourceRepoCount)
|
|
76
|
+
sourceRepoDisabledReason(command, sourceRepoCount) ??
|
|
77
|
+
snapshotDisabledReason(command, snapshotCount));
|
|
73
78
|
}
|
|
74
79
|
function commandDisabledValue(reason) {
|
|
75
80
|
if (!reason) {
|
|
@@ -77,7 +82,7 @@ function commandDisabledValue(reason) {
|
|
|
77
82
|
}
|
|
78
83
|
return reason;
|
|
79
84
|
}
|
|
80
|
-
function categoryChoices(category, index, serviceStatus, moduleCount, sourceRepoCount) {
|
|
85
|
+
function categoryChoices(category, index, serviceStatus, moduleCount, sourceRepoCount, snapshotCount) {
|
|
81
86
|
const choices = [
|
|
82
87
|
promptSeparator(categoryHeading(category)),
|
|
83
88
|
...topLevelCommands
|
|
@@ -87,7 +92,7 @@ function categoryChoices(category, index, serviceStatus, moduleCount, sourceRepo
|
|
|
87
92
|
value: command,
|
|
88
93
|
name: commandName(command),
|
|
89
94
|
short: command.label,
|
|
90
|
-
disabled: commandDisabledValue(disabledReason(command, serviceStatus, moduleCount, sourceRepoCount)),
|
|
95
|
+
disabled: commandDisabledValue(disabledReason(command, serviceStatus, moduleCount, sourceRepoCount, snapshotCount)),
|
|
91
96
|
};
|
|
92
97
|
}),
|
|
93
98
|
];
|
|
@@ -120,8 +125,8 @@ function menuDeps(deps = {}) {
|
|
|
120
125
|
function isCockpitCommand(value) {
|
|
121
126
|
return typeof value === 'object' && value !== null && 'id' in value && 'slashAlias' in value;
|
|
122
127
|
}
|
|
123
|
-
function topLevelChoices(serviceStatus, moduleCount, sourceRepoCount) {
|
|
124
|
-
return topLevelCategoryOrder.flatMap((category, index) => categoryChoices(category, index, serviceStatus, moduleCount, sourceRepoCount));
|
|
128
|
+
function topLevelChoices(serviceStatus, moduleCount, sourceRepoCount, snapshotCount) {
|
|
129
|
+
return topLevelCategoryOrder.flatMap((category, index) => categoryChoices(category, index, serviceStatus, moduleCount, sourceRepoCount, snapshotCount));
|
|
125
130
|
}
|
|
126
131
|
function defaultCommand(serviceStatus) {
|
|
127
132
|
if (serviceStatus?.kind === 'running') {
|
|
@@ -134,7 +139,7 @@ function defaultCommand(serviceStatus) {
|
|
|
134
139
|
}
|
|
135
140
|
export async function selectCockpitTopLevelMenu(options = {}) {
|
|
136
141
|
const deps = menuDeps(options);
|
|
137
|
-
const choices = topLevelChoices(options.serviceStatus, options.moduleCount, options.sourceRepoCount);
|
|
142
|
+
const choices = topLevelChoices(options.serviceStatus, options.moduleCount, options.sourceRepoCount, options.snapshotCount);
|
|
138
143
|
const cancelAction = 'back';
|
|
139
144
|
const selected = await deps.select({
|
|
140
145
|
message: '',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { styleText } from 'node:util';
|
|
2
2
|
import { listModulesInEnvironment, } from '../module-actions.js';
|
|
3
3
|
import { handlePromptCancel, } from '../menu-navigation.js';
|
|
4
|
-
import { isPromptCancel, promptSeparator, selectPrompt, } from '../prompts/index.js';
|
|
4
|
+
import { isPromptCancel, promptSeparator, searchPrompt, selectPrompt, } from '../prompts/index.js';
|
|
5
5
|
const sourceTypeLabels = {
|
|
6
6
|
private: 'Private',
|
|
7
7
|
oca: 'OCA',
|
|
@@ -10,6 +10,7 @@ const sourceTypeLabels = {
|
|
|
10
10
|
const sourceTypeOrder = ['private', 'oca', 'external'];
|
|
11
11
|
const minimumPageSize = 8;
|
|
12
12
|
const reservedRows = 7;
|
|
13
|
+
const searchableModuleThreshold = 20;
|
|
13
14
|
function rgb(red, green, blue, value) {
|
|
14
15
|
return `\u001B[38;2;${red};${green};${blue}m${value}\u001B[39m`;
|
|
15
16
|
}
|
|
@@ -51,6 +52,7 @@ function defaultCancelHandler(value, action) {
|
|
|
51
52
|
function deps(options = {}) {
|
|
52
53
|
return {
|
|
53
54
|
select: options.select ?? ((selectOptions) => selectPrompt(selectOptions)),
|
|
55
|
+
search: options.search ?? ((searchOptions) => searchPrompt(searchOptions)),
|
|
54
56
|
handleCancel: options.handleCancel ?? defaultCancelHandler,
|
|
55
57
|
};
|
|
56
58
|
}
|
|
@@ -92,14 +94,89 @@ export function moduleBrowserChoices(modules) {
|
|
|
92
94
|
}
|
|
93
95
|
return choices;
|
|
94
96
|
}
|
|
97
|
+
function normalizeModuleSearchTerm(term) {
|
|
98
|
+
return (term ?? '')
|
|
99
|
+
.trim()
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.replace(/[/:]+/g, ' ')
|
|
102
|
+
.split(/\s+/u)
|
|
103
|
+
.filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
function searchableModuleFields(module) {
|
|
106
|
+
const repoSlugName = module.repoSlug?.split('/').at(-1) ?? '';
|
|
107
|
+
return [
|
|
108
|
+
module.moduleName,
|
|
109
|
+
module.repoPath,
|
|
110
|
+
repoSlugName,
|
|
111
|
+
module.sourceType,
|
|
112
|
+
sourceContext(module),
|
|
113
|
+
].map((value) => value.toLowerCase());
|
|
114
|
+
}
|
|
115
|
+
function moduleSearchScore(module, terms) {
|
|
116
|
+
if (terms.length === 0) {
|
|
117
|
+
return 50;
|
|
118
|
+
}
|
|
119
|
+
const moduleName = module.moduleName.toLowerCase();
|
|
120
|
+
const repoPath = module.repoPath.toLowerCase();
|
|
121
|
+
if (terms.some((term) => moduleName === term)) {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
if (terms.some((term) => moduleName.startsWith(term))) {
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
if (terms.some((term) => repoPath === term)) {
|
|
128
|
+
return 2;
|
|
129
|
+
}
|
|
130
|
+
if (terms.some((term) => repoPath.startsWith(term))) {
|
|
131
|
+
return 3;
|
|
132
|
+
}
|
|
133
|
+
if (terms.some((term) => module.sourceType === term)) {
|
|
134
|
+
return 4;
|
|
135
|
+
}
|
|
136
|
+
return 5;
|
|
137
|
+
}
|
|
138
|
+
export function searchModuleBrowserChoices(modules, term) {
|
|
139
|
+
const terms = normalizeModuleSearchTerm(term);
|
|
140
|
+
const moduleWidth = Math.max(...modules.map((module) => module.moduleName.length), 1);
|
|
141
|
+
return modules
|
|
142
|
+
.filter((module) => {
|
|
143
|
+
if (terms.length === 0) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
const fields = searchableModuleFields(module);
|
|
147
|
+
return terms.every((term) => fields.some((field) => field.includes(term)));
|
|
148
|
+
})
|
|
149
|
+
.sort((left, right) => {
|
|
150
|
+
const score = moduleSearchScore(left, terms) - moduleSearchScore(right, terms);
|
|
151
|
+
return score || left.sourceType.localeCompare(right.sourceType) || left.repoPath.localeCompare(right.repoPath) || left.moduleName.localeCompare(right.moduleName);
|
|
152
|
+
})
|
|
153
|
+
.map((module) => ({
|
|
154
|
+
value: module,
|
|
155
|
+
name: moduleChoiceName(module, moduleWidth),
|
|
156
|
+
description: sourceContext(module),
|
|
157
|
+
short: module.moduleName,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
95
160
|
export async function selectModuleFromBrowser(target, options = {}) {
|
|
96
161
|
const modules = await listModulesInEnvironment(target);
|
|
97
162
|
if (modules.length === 0) {
|
|
98
163
|
return undefined;
|
|
99
164
|
}
|
|
100
|
-
const moduleChoices = moduleBrowserChoices(modules);
|
|
101
165
|
const promptDeps = deps(options);
|
|
102
166
|
const cancelAction = options.cancelAction ?? 'back';
|
|
167
|
+
if (modules.length > searchableModuleThreshold) {
|
|
168
|
+
const selected = await promptDeps.search({
|
|
169
|
+
message: 'Search modules',
|
|
170
|
+
pageSize: pageSize(modules.length),
|
|
171
|
+
source: (term) => searchModuleBrowserChoices(modules, term),
|
|
172
|
+
});
|
|
173
|
+
promptDeps.handleCancel(selected, cancelAction);
|
|
174
|
+
if (typeof selected === 'object' && selected !== null && 'moduleName' in selected) {
|
|
175
|
+
return selected;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const moduleChoices = moduleBrowserChoices(modules);
|
|
103
180
|
const selected = await promptDeps.select({
|
|
104
181
|
message: '',
|
|
105
182
|
choices: moduleChoices,
|
package/dist/daily-actions.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { access } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { readActiveApprovals } from './approval-ledger.js';
|
|
4
5
|
import { appendAuditLog } from './audit-log.js';
|
|
5
6
|
import { readEnvFile, selectedComposeEnvironment } from './compose-layout.js';
|
|
6
|
-
import { defaultDatabaseSnapshotMaxAgeMs, findDatabaseSnapshots, normalizeDatabaseName } from './databases.js';
|
|
7
|
-
import { evaluateDailyActionPolicy, } from './environment-policy.js';
|
|
7
|
+
import { defaultDatabaseSnapshotMaxAgeMs, findDatabaseSnapshots, normalizeDatabaseName, normalizeSnapshotName, restoreSnapshotPreflight, } from './databases.js';
|
|
8
|
+
import { evaluateDailyActionPolicy, parseEnvironmentKind, } from './environment-policy.js';
|
|
8
9
|
import { markerPath } from './environment.js';
|
|
9
10
|
import { scanMigrationRisks } from './migrations.js';
|
|
10
11
|
export const dailyActionCommands = [
|
|
@@ -69,7 +70,7 @@ function usage(command) {
|
|
|
69
70
|
if (command === 'resetdb')
|
|
70
71
|
return 'Usage: wpmoo resetdb [db] [module[,module]]';
|
|
71
72
|
if (command === 'snapshot')
|
|
72
|
-
return 'Usage: wpmoo snapshot [db] [snapshot-name]';
|
|
73
|
+
return 'Usage: wpmoo snapshot [--list] [db] [snapshot-name]';
|
|
73
74
|
if (command === 'restore-snapshot')
|
|
74
75
|
return 'Usage: wpmoo restore-snapshot [--dry-run] <snapshot-name> [db]';
|
|
75
76
|
if (command === 'lint')
|
|
@@ -133,7 +134,8 @@ function restoreSnapshotArgs(argv) {
|
|
|
133
134
|
if (args.length < 1 || args.length > 2 || args.some((arg) => arg.startsWith('-'))) {
|
|
134
135
|
throw new Error(usage('restore-snapshot'));
|
|
135
136
|
}
|
|
136
|
-
const
|
|
137
|
+
const snapshotName = normalizeSnapshotName(args[0]);
|
|
138
|
+
const validatedArgs = args.length === 2 ? validateDatabaseArg([snapshotName, args[1]], 1) : [snapshotName];
|
|
137
139
|
return dryRun ? ['--dry-run', ...validatedArgs] : validatedArgs;
|
|
138
140
|
}
|
|
139
141
|
function testArgs(argv) {
|
|
@@ -180,7 +182,8 @@ function scriptArgs(command, argv) {
|
|
|
180
182
|
}
|
|
181
183
|
if (command === 'snapshot') {
|
|
182
184
|
rejectLeadingHyphenDatabaseArg(argv);
|
|
183
|
-
|
|
185
|
+
const args = validateDatabaseArg(positionalArgs(command, argv, 0, 2), 0);
|
|
186
|
+
return args.length === 2 ? [args[0], normalizeSnapshotName(args[1])] : args;
|
|
184
187
|
}
|
|
185
188
|
if (command === 'restore-snapshot')
|
|
186
189
|
return restoreSnapshotArgs(argv);
|
|
@@ -212,7 +215,7 @@ function envValue(env, key) {
|
|
|
212
215
|
function flagEnabled(env, key) {
|
|
213
216
|
return envValue(env, key) === '1';
|
|
214
217
|
}
|
|
215
|
-
function
|
|
218
|
+
function envApprovedFlags(env) {
|
|
216
219
|
return [
|
|
217
220
|
'WPMOO_ALLOW_DESTRUCTIVE',
|
|
218
221
|
'WPMOO_ALLOW_STAGE_LIFECYCLE',
|
|
@@ -221,6 +224,12 @@ function approvedFlags(env) {
|
|
|
221
224
|
'WPMOO_ALLOW_MIGRATIONS',
|
|
222
225
|
].filter((key) => flagEnabled(env, key));
|
|
223
226
|
}
|
|
227
|
+
function hasApproval(approvals, scope) {
|
|
228
|
+
return approvals.some((approval) => approval.scope === scope);
|
|
229
|
+
}
|
|
230
|
+
function approvalFlagLabels(approvals) {
|
|
231
|
+
return [...new Set(approvals.map((approval) => approval.label))];
|
|
232
|
+
}
|
|
224
233
|
function requiresMigrationApproval(command) {
|
|
225
234
|
return command === 'install' || command === 'update' || command === 'test';
|
|
226
235
|
}
|
|
@@ -230,6 +239,25 @@ function noRecentSnapshotMessage(command, environment) {
|
|
|
230
239
|
function migrationRiskMessage(command, environment) {
|
|
231
240
|
return `Refusing migration-risk command '${command}' in WPMOO_ENV=${environment}. Review detected migration scripts or set WPMOO_ALLOW_MIGRATIONS=1 to run it intentionally.`;
|
|
232
241
|
}
|
|
242
|
+
function restoreSnapshotWarningKind(issue) {
|
|
243
|
+
if (issue === 'missing snapshot dump')
|
|
244
|
+
return 'restore-snapshot-missing-dump';
|
|
245
|
+
if (issue === 'missing snapshot filestore')
|
|
246
|
+
return 'restore-snapshot-missing-filestore';
|
|
247
|
+
if (issue.startsWith('snapshot database mismatch:'))
|
|
248
|
+
return 'restore-snapshot-db-name-mismatch';
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
function restoreSnapshotDryRunPreflight(command, args, cwd) {
|
|
252
|
+
if (command !== 'restore-snapshot' || args[0] !== '--dry-run') {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
const snapshotName = args[1];
|
|
256
|
+
if (!snapshotName) {
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
return restoreSnapshotPreflight(cwd, snapshotName, args[2] ?? 'devel');
|
|
260
|
+
}
|
|
233
261
|
async function auditDailyActionPreview(preview) {
|
|
234
262
|
if (preview.environment !== 'prod' || !preview.auditWorthy) {
|
|
235
263
|
return;
|
|
@@ -250,19 +278,32 @@ export async function dailyActionSafetyPreview(command, argv, cwd = process.cwd(
|
|
|
250
278
|
const args = scriptArgs(command, argv);
|
|
251
279
|
const env = await readEnvFile(cwd);
|
|
252
280
|
const envName = envValue(env, 'WPMOO_ENV') || selectedComposeEnvironment(env);
|
|
281
|
+
const environment = parseEnvironmentKind(envName);
|
|
282
|
+
const approvals = await readActiveApprovals(cwd, { command, environment });
|
|
253
283
|
const policy = evaluateDailyActionPolicy(command, args, {
|
|
254
284
|
envName,
|
|
255
|
-
allowDestructive: envValue(env, 'WPMOO_ALLOW_DESTRUCTIVE'),
|
|
256
|
-
allowStageLifecycle: envValue(env, 'WPMOO_ALLOW_STAGE_LIFECYCLE'),
|
|
257
|
-
allowProdLifecycle: envValue(env, 'WPMOO_ALLOW_PROD_LIFECYCLE'),
|
|
285
|
+
allowDestructive: envValue(env, 'WPMOO_ALLOW_DESTRUCTIVE') || (hasApproval(approvals, 'destructive') ? '1' : undefined),
|
|
286
|
+
allowStageLifecycle: envValue(env, 'WPMOO_ALLOW_STAGE_LIFECYCLE') || (hasApproval(approvals, 'stage-lifecycle') ? '1' : undefined),
|
|
287
|
+
allowProdLifecycle: envValue(env, 'WPMOO_ALLOW_PROD_LIFECYCLE') || (hasApproval(approvals, 'prod-lifecycle') ? '1' : undefined),
|
|
258
288
|
});
|
|
259
289
|
const warnings = [];
|
|
290
|
+
const restoreSnapshot = restoreSnapshotDryRunPreflight(command, args, cwd);
|
|
291
|
+
if (restoreSnapshot) {
|
|
292
|
+
for (const issue of restoreSnapshot.issues) {
|
|
293
|
+
const kind = restoreSnapshotWarningKind(issue);
|
|
294
|
+
if (!kind) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
warnings.push({ kind, message: issue, blocking: false });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
260
300
|
const snapshotRequired = policy.isDestructive && (policy.env === 'stage' || policy.env === 'prod');
|
|
261
301
|
const snapshot = snapshotRequired ? findDatabaseSnapshots(cwd) : undefined;
|
|
262
302
|
const noRecentSnapshot = snapshotRequired &&
|
|
263
303
|
snapshot &&
|
|
264
304
|
(snapshot.newestSnapshotAgeMs === null || snapshot.newestSnapshotAgeMs > defaultDatabaseSnapshotMaxAgeMs) &&
|
|
265
|
-
!flagEnabled(env, 'WPMOO_ALLOW_NO_RECENT_SNAPSHOT')
|
|
305
|
+
!flagEnabled(env, 'WPMOO_ALLOW_NO_RECENT_SNAPSHOT') &&
|
|
306
|
+
!hasApproval(approvals, 'no-recent-snapshot');
|
|
266
307
|
if (noRecentSnapshot) {
|
|
267
308
|
warnings.push({
|
|
268
309
|
kind: 'no-recent-snapshot',
|
|
@@ -274,7 +315,7 @@ export async function dailyActionSafetyPreview(command, argv, cwd = process.cwd(
|
|
|
274
315
|
const migrations = requiresMigrationApproval(command) && (policy.env === 'stage' || policy.env === 'prod')
|
|
275
316
|
? await scanMigrationRisks(cwd)
|
|
276
317
|
: undefined;
|
|
277
|
-
if (migrations?.risk && !flagEnabled(env, 'WPMOO_ALLOW_MIGRATIONS')) {
|
|
318
|
+
if (migrations?.risk && !flagEnabled(env, 'WPMOO_ALLOW_MIGRATIONS') && !hasApproval(approvals, 'migration-risk')) {
|
|
278
319
|
warnings.push({
|
|
279
320
|
kind: 'migration-risk',
|
|
280
321
|
requiredFlag: 'WPMOO_ALLOW_MIGRATIONS',
|
|
@@ -303,8 +344,10 @@ export async function dailyActionSafetyPreview(command, argv, cwd = process.cwd(
|
|
|
303
344
|
snapshotPaths: snapshot.snapshotPaths,
|
|
304
345
|
}
|
|
305
346
|
: undefined,
|
|
347
|
+
restoreSnapshot,
|
|
306
348
|
migrations,
|
|
307
|
-
|
|
349
|
+
approvals,
|
|
350
|
+
approvedFlags: [...envApprovedFlags(env), ...approvalFlagLabels(approvals)],
|
|
308
351
|
};
|
|
309
352
|
}
|
|
310
353
|
export async function dailyActionPlan(command, argv, cwd = process.cwd()) {
|