@davidvornholt/standards 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/package.json +9 -1
- package/src/cli.ts +1 -1
- package/src/creds-args.ts +9 -0
- package/src/creds-cloudflare-account.ts +2 -2
- package/src/creds-cloudflare-id.ts +10 -0
- package/src/creds-cloudflare-zone.ts +3 -3
- package/src/creds-commands.ts +13 -2
- package/src/creds-naming.ts +13 -0
- package/src/creds-plan-report.ts +87 -0
- package/src/creds-plan-run.ts +2 -11
- package/src/creds-plan-types.ts +19 -0
- package/src/creds-plan-unmanaged.ts +102 -0
- package/src/creds-plan.ts +25 -21
- package/src/creds-revoke.ts +162 -0
- package/src/creds-store-lock-holder.ts +8 -1
- package/src/creds-store-lock.ts +2 -1
- package/src/dev-env-compose.ts +120 -0
- package/src/dev-env-destination-duplicates.ts +10 -10
- package/src/dev-env-destination-gitignore.ts +22 -0
- package/src/dev-env-destination.ts +52 -30
- package/src/dev-env-document.ts +19 -6
- package/src/dev-env-dotenv.ts +12 -3
- package/src/dev-env-reconciliation.ts +170 -0
- package/src/dev-env-secrets.ts +41 -0
- package/src/dev-env-transaction-recovery.ts +74 -11
- package/src/dev-env-transaction.ts +61 -15
- package/src/dev-env.ts +115 -63
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
constants,
|
|
4
|
+
type Dirent,
|
|
5
|
+
existsSync,
|
|
6
|
+
fstatSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { lstat, open } from 'node:fs/promises';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import type { DevEnvRemoval, DevEnvWrite } from './dev-env-destination';
|
|
15
|
+
import { hasDevEnvGeneratedHeader } from './dev-env-dotenv';
|
|
16
|
+
|
|
17
|
+
const WORKSPACE_GROUPS = ['apps', 'packages'] as const;
|
|
18
|
+
|
|
19
|
+
export type DevEnvRemovalPlan = {
|
|
20
|
+
readonly removals: ReadonlyArray<DevEnvRemoval>;
|
|
21
|
+
readonly problems: ReadonlyArray<string>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const message = (error: unknown): string =>
|
|
25
|
+
error instanceof Error ? error.message : String(error);
|
|
26
|
+
|
|
27
|
+
export const devEnvStatOrNull = async (path: string) => {
|
|
28
|
+
try {
|
|
29
|
+
return await lstat(path);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type WorkspaceDiscovery = {
|
|
39
|
+
readonly workspaces: ReadonlyArray<Dirent>;
|
|
40
|
+
readonly problems: ReadonlyArray<string>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const discoverWorkspaces = (
|
|
44
|
+
consumer: string,
|
|
45
|
+
group: (typeof WORKSPACE_GROUPS)[number],
|
|
46
|
+
): WorkspaceDiscovery => {
|
|
47
|
+
const groupPath = join(consumer, group);
|
|
48
|
+
try {
|
|
49
|
+
const stats = lstatSync(groupPath);
|
|
50
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
51
|
+
return { workspaces: [], problems: [] };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
workspaces: readdirSync(groupPath, { withFileTypes: true })
|
|
55
|
+
.filter((workspace) => workspace.isDirectory())
|
|
56
|
+
.sort((left, right) => left.name.localeCompare(right.name)),
|
|
57
|
+
problems: [],
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
61
|
+
return { workspaces: [], problems: [] };
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
workspaces: [],
|
|
65
|
+
problems: [`could not inspect ${group} workspaces: ${message(error)}`],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type RemovalInspection = {
|
|
71
|
+
readonly removal: DevEnvRemoval | null;
|
|
72
|
+
readonly problem: string | null;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const inspectRemoval = (
|
|
76
|
+
consumer: string,
|
|
77
|
+
envRel: string,
|
|
78
|
+
): RemovalInspection => {
|
|
79
|
+
const envPath = join(consumer, envRel);
|
|
80
|
+
let file: number | null = null;
|
|
81
|
+
try {
|
|
82
|
+
file = openSync(
|
|
83
|
+
envPath,
|
|
84
|
+
constants.O_RDONLY + constants.O_NOFOLLOW + constants.O_NONBLOCK,
|
|
85
|
+
);
|
|
86
|
+
const stats = fstatSync(file);
|
|
87
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
88
|
+
return { removal: null, problem: null };
|
|
89
|
+
}
|
|
90
|
+
const owned = hasDevEnvGeneratedHeader(readFileSync(file, 'utf8'));
|
|
91
|
+
return {
|
|
92
|
+
removal: owned
|
|
93
|
+
? {
|
|
94
|
+
rel: envRel,
|
|
95
|
+
identity: { device: stats.dev, inode: stats.ino },
|
|
96
|
+
}
|
|
97
|
+
: null,
|
|
98
|
+
problem: null,
|
|
99
|
+
};
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (
|
|
102
|
+
['ELOOP', 'ENOENT'].includes((error as NodeJS.ErrnoException).code ?? '')
|
|
103
|
+
) {
|
|
104
|
+
return { removal: null, problem: null };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
removal: null,
|
|
108
|
+
problem: `could not inspect ${envRel}: ${message(error)}`,
|
|
109
|
+
};
|
|
110
|
+
} finally {
|
|
111
|
+
if (file !== null) {
|
|
112
|
+
closeSync(file);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const matchesDevEnvRemoval = async (
|
|
118
|
+
path: string,
|
|
119
|
+
removal: DevEnvRemoval,
|
|
120
|
+
): Promise<boolean> => {
|
|
121
|
+
try {
|
|
122
|
+
const file = await open(
|
|
123
|
+
path,
|
|
124
|
+
constants.O_RDONLY + constants.O_NOFOLLOW + constants.O_NONBLOCK,
|
|
125
|
+
);
|
|
126
|
+
try {
|
|
127
|
+
const stats = await file.stat();
|
|
128
|
+
return (
|
|
129
|
+
stats.isFile() &&
|
|
130
|
+
stats.dev === removal.identity.device &&
|
|
131
|
+
stats.ino === removal.identity.inode &&
|
|
132
|
+
hasDevEnvGeneratedHeader(await file.readFile('utf8'))
|
|
133
|
+
);
|
|
134
|
+
} finally {
|
|
135
|
+
await file.close();
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const planDevEnvRemovals = (
|
|
143
|
+
consumer: string,
|
|
144
|
+
writes: ReadonlyArray<DevEnvWrite>,
|
|
145
|
+
): DevEnvRemovalPlan => {
|
|
146
|
+
const plannedWrites = new Set(writes.map((write) => write.rel));
|
|
147
|
+
const removals: Array<DevEnvRemoval> = [];
|
|
148
|
+
const problems: Array<string> = [];
|
|
149
|
+
for (const group of WORKSPACE_GROUPS) {
|
|
150
|
+
const discovery = discoverWorkspaces(consumer, group);
|
|
151
|
+
problems.push(...discovery.problems);
|
|
152
|
+
for (const workspace of discovery.workspaces) {
|
|
153
|
+
const workspaceRel = `${group}/${workspace.name}`;
|
|
154
|
+
const envRel = `${workspaceRel}/.env.local`;
|
|
155
|
+
if (
|
|
156
|
+
!plannedWrites.has(envRel) &&
|
|
157
|
+
existsSync(join(consumer, workspaceRel, 'package.json'))
|
|
158
|
+
) {
|
|
159
|
+
const inspected = inspectRemoval(consumer, envRel);
|
|
160
|
+
if (inspected.removal !== null) {
|
|
161
|
+
removals.push(inspected.removal);
|
|
162
|
+
}
|
|
163
|
+
if (inspected.problem !== null) {
|
|
164
|
+
problems.push(inspected.problem);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { removals, problems };
|
|
170
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { runSops } from './sops-exec';
|
|
4
|
+
|
|
5
|
+
export const DEV_SECRETS_FILE = 'secrets/dev.yaml';
|
|
6
|
+
|
|
7
|
+
const SOPS_ARGS = ['--decrypt', '--output-type', 'json', DEV_SECRETS_FILE];
|
|
8
|
+
|
|
9
|
+
export type DevSecretsResult =
|
|
10
|
+
| { readonly ok: true; readonly value: unknown }
|
|
11
|
+
| { readonly ok: false; readonly problem: string };
|
|
12
|
+
|
|
13
|
+
// sops emits JSON here so the CLI never parses the encrypted YAML itself.
|
|
14
|
+
export const readDevSecrets = (consumer: string): DevSecretsResult => {
|
|
15
|
+
if (!existsSync(join(consumer, DEV_SECRETS_FILE))) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
problem: `${DEV_SECRETS_FILE} not found; create it with \`just secrets edit dev\``,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const result = runSops(SOPS_ARGS, consumer);
|
|
22
|
+
if (result.status !== 0) {
|
|
23
|
+
const detail = result.errorMessage ?? result.stderr.trim();
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
problem: detail
|
|
27
|
+
? `could not decrypt ${DEV_SECRETS_FILE}: ${detail}`
|
|
28
|
+
: `could not decrypt ${DEV_SECRETS_FILE}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return { ok: true, value: JSON.parse(result.stdout) as unknown };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
problem: `could not parse decrypted ${DEV_SECRETS_FILE} as JSON: ${
|
|
37
|
+
error instanceof Error ? error.message : String(error)
|
|
38
|
+
}`,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { link, rm } from 'node:fs/promises';
|
|
2
2
|
import {
|
|
3
3
|
type DevEnvDestination,
|
|
4
4
|
devEnvParentProblem,
|
|
5
5
|
} from './dev-env-destination';
|
|
6
|
+
import { devEnvStatOrNull } from './dev-env-reconciliation';
|
|
6
7
|
|
|
7
8
|
const message = (error: unknown): string =>
|
|
8
9
|
error instanceof Error ? error.message : String(error);
|
|
@@ -11,20 +12,81 @@ const requireParent = async (
|
|
|
11
12
|
destination: DevEnvDestination,
|
|
12
13
|
): Promise<string | null> => devEnvParentProblem(destination);
|
|
13
14
|
|
|
15
|
+
const isWrite = (destination: DevEnvDestination): boolean =>
|
|
16
|
+
'content' in destination.mutation;
|
|
17
|
+
|
|
18
|
+
const rollbackBlockedProblem = (destination: DevEnvDestination): string =>
|
|
19
|
+
`${destination.mutation.rel}: rollback blocked because destination changed`;
|
|
20
|
+
|
|
21
|
+
const restoreBackupNoReplace = async (
|
|
22
|
+
destination: DevEnvDestination,
|
|
23
|
+
): Promise<boolean> => {
|
|
24
|
+
try {
|
|
25
|
+
await link(destination.backup, destination.dest);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
28
|
+
destination.rollbackBlocked = true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await rm(destination.backup);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
destination.rollbackBlocked = true;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
destination.backupCreated = false;
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const restoreClaimedDevEnvRemoval = async (
|
|
44
|
+
destination: DevEnvDestination,
|
|
45
|
+
beforeRestore: () => void | Promise<void> = () => undefined,
|
|
46
|
+
): Promise<void> => {
|
|
47
|
+
const parentProblem = await devEnvParentProblem(destination);
|
|
48
|
+
if (parentProblem !== null) {
|
|
49
|
+
destination.rollbackBlocked = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await beforeRestore();
|
|
53
|
+
await restoreBackupNoReplace(destination);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const removeCommittedDestination = async (
|
|
57
|
+
destination: DevEnvDestination,
|
|
58
|
+
): Promise<string | null> => {
|
|
59
|
+
const parentProblem = await requireParent(destination);
|
|
60
|
+
if (parentProblem !== null) {
|
|
61
|
+
return parentProblem;
|
|
62
|
+
}
|
|
63
|
+
const current = await devEnvStatOrNull(destination.dest);
|
|
64
|
+
if (!isWrite(destination) && current !== null) {
|
|
65
|
+
destination.rollbackBlocked = true;
|
|
66
|
+
return rollbackBlockedProblem(destination);
|
|
67
|
+
}
|
|
68
|
+
if (isWrite(destination)) {
|
|
69
|
+
await rm(destination.dest, { force: true });
|
|
70
|
+
}
|
|
71
|
+
destination.committed = false;
|
|
72
|
+
return null;
|
|
73
|
+
};
|
|
74
|
+
|
|
14
75
|
const rollbackOne = async (
|
|
15
76
|
destination: DevEnvDestination,
|
|
16
77
|
): Promise<ReadonlyArray<string>> => {
|
|
17
78
|
const problems: Array<string> = [];
|
|
79
|
+
if (destination.rollbackBlocked) {
|
|
80
|
+
return [rollbackBlockedProblem(destination)];
|
|
81
|
+
}
|
|
18
82
|
if (destination.committed) {
|
|
19
|
-
const parentProblem = await requireParent(destination);
|
|
20
|
-
if (parentProblem !== null) {
|
|
21
|
-
return [parentProblem];
|
|
22
|
-
}
|
|
23
83
|
try {
|
|
24
|
-
await
|
|
25
|
-
|
|
84
|
+
const problem = await removeCommittedDestination(destination);
|
|
85
|
+
if (problem !== null) {
|
|
86
|
+
return [problem];
|
|
87
|
+
}
|
|
26
88
|
} catch (error) {
|
|
27
|
-
problems.push(`${destination.
|
|
89
|
+
problems.push(`${destination.mutation.rel}: ${message(error)}`);
|
|
28
90
|
}
|
|
29
91
|
}
|
|
30
92
|
if (destination.backupCreated && problems.length === 0) {
|
|
@@ -33,10 +95,11 @@ const rollbackOne = async (
|
|
|
33
95
|
return [parentProblem];
|
|
34
96
|
}
|
|
35
97
|
try {
|
|
36
|
-
await
|
|
37
|
-
|
|
98
|
+
if (!(await restoreBackupNoReplace(destination))) {
|
|
99
|
+
return [rollbackBlockedProblem(destination)];
|
|
100
|
+
}
|
|
38
101
|
} catch (error) {
|
|
39
|
-
problems.push(`${destination.
|
|
102
|
+
problems.push(`${destination.mutation.rel}: ${message(error)}`);
|
|
40
103
|
}
|
|
41
104
|
}
|
|
42
105
|
return problems;
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { link, open, rename } from 'node:fs/promises';
|
|
2
2
|
import {
|
|
3
3
|
type DevEnvDestination,
|
|
4
|
+
type DevEnvMutation,
|
|
5
|
+
type DevEnvRemoval,
|
|
4
6
|
type DevEnvWrite,
|
|
5
7
|
devEnvParentProblem,
|
|
6
|
-
devEnvStatOrNull,
|
|
7
8
|
preflightDevEnvDestinations,
|
|
8
9
|
} from './dev-env-destination';
|
|
10
|
+
import {
|
|
11
|
+
devEnvStatOrNull,
|
|
12
|
+
matchesDevEnvRemoval,
|
|
13
|
+
} from './dev-env-reconciliation';
|
|
9
14
|
import {
|
|
10
15
|
cleanupDevEnvArtifacts,
|
|
16
|
+
restoreClaimedDevEnvRemoval,
|
|
11
17
|
rollbackDevEnvFiles,
|
|
12
18
|
} from './dev-env-transaction-recovery';
|
|
13
19
|
|
|
@@ -16,6 +22,8 @@ const OWNER_ONLY_FILE_MODE = 0o600;
|
|
|
16
22
|
export type DevEnvTransactionHooks = {
|
|
17
23
|
readonly beforeStage?: (index: number) => void | Promise<void>;
|
|
18
24
|
readonly beforeCommit?: (index: number) => void | Promise<void>;
|
|
25
|
+
readonly afterDestinationCheck?: (index: number) => void | Promise<void>;
|
|
26
|
+
readonly beforeRemovalRestore?: (index: number) => void | Promise<void>;
|
|
19
27
|
readonly beforeCleanup?: () => void | Promise<void>;
|
|
20
28
|
};
|
|
21
29
|
|
|
@@ -30,12 +38,18 @@ const requireParent = async (destination: DevEnvDestination): Promise<void> => {
|
|
|
30
38
|
}
|
|
31
39
|
};
|
|
32
40
|
|
|
41
|
+
const isWrite = (mutation: DevEnvMutation): mutation is DevEnvWrite =>
|
|
42
|
+
'content' in mutation;
|
|
43
|
+
|
|
33
44
|
const stage = async (destination: DevEnvDestination): Promise<void> => {
|
|
45
|
+
if (!isWrite(destination.mutation)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
34
48
|
await requireParent(destination);
|
|
35
49
|
const file = await open(destination.temp, 'wx', OWNER_ONLY_FILE_MODE);
|
|
36
50
|
try {
|
|
37
51
|
await file.chmod(OWNER_ONLY_FILE_MODE);
|
|
38
|
-
await file.writeFile(destination.
|
|
52
|
+
await file.writeFile(destination.mutation.content, 'utf8');
|
|
39
53
|
await file.sync();
|
|
40
54
|
} finally {
|
|
41
55
|
await file.close();
|
|
@@ -60,20 +74,52 @@ const stageAll = async (
|
|
|
60
74
|
destinations: ReadonlyArray<DevEnvDestination>,
|
|
61
75
|
hooks: DevEnvTransactionHooks,
|
|
62
76
|
): Promise<void> => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
await previous;
|
|
66
|
-
await hooks.beforeStage?.(index);
|
|
67
|
-
await stage(destination);
|
|
68
|
-
},
|
|
69
|
-
Promise.resolve(),
|
|
77
|
+
const writes = destinations.filter((destination) =>
|
|
78
|
+
isWrite(destination.mutation),
|
|
70
79
|
);
|
|
80
|
+
await writes.reduce<Promise<void>>(async (previous, destination, index) => {
|
|
81
|
+
await previous;
|
|
82
|
+
await hooks.beforeStage?.(index);
|
|
83
|
+
await stage(destination);
|
|
84
|
+
}, Promise.resolve());
|
|
71
85
|
};
|
|
72
86
|
|
|
73
|
-
const
|
|
87
|
+
const commitRemoval = async (
|
|
88
|
+
destination: DevEnvDestination,
|
|
89
|
+
removal: DevEnvRemoval,
|
|
90
|
+
index: number,
|
|
91
|
+
hooks: DevEnvTransactionHooks,
|
|
92
|
+
): Promise<void> => {
|
|
93
|
+
const beforeRestore = () => hooks.beforeRemovalRestore?.(index);
|
|
94
|
+
await requireParent(destination);
|
|
95
|
+
await rename(destination.dest, destination.backup);
|
|
96
|
+
destination.backupCreated = true;
|
|
97
|
+
if (!(await matchesDevEnvRemoval(destination.backup, removal))) {
|
|
98
|
+
await restoreClaimedDevEnvRemoval(destination, beforeRestore);
|
|
99
|
+
throw new Error(`${removal.rel} changed after preflight`);
|
|
100
|
+
}
|
|
101
|
+
const parentProblem = await devEnvParentProblem(destination);
|
|
102
|
+
const replacement = await devEnvStatOrNull(destination.dest);
|
|
103
|
+
if (parentProblem !== null || replacement !== null) {
|
|
104
|
+
await restoreClaimedDevEnvRemoval(destination, beforeRestore);
|
|
105
|
+
throw new Error(`${removal.rel} changed after preflight`);
|
|
106
|
+
}
|
|
107
|
+
destination.committed = true;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const commitOne = async (
|
|
111
|
+
destination: DevEnvDestination,
|
|
112
|
+
index: number,
|
|
113
|
+
hooks: DevEnvTransactionHooks,
|
|
114
|
+
): Promise<void> => {
|
|
74
115
|
await requireParent(destination);
|
|
75
116
|
if (!(await unchanged(destination))) {
|
|
76
|
-
throw new Error(`${destination.
|
|
117
|
+
throw new Error(`${destination.mutation.rel} changed after preflight`);
|
|
118
|
+
}
|
|
119
|
+
await hooks.afterDestinationCheck?.(index);
|
|
120
|
+
if (!isWrite(destination.mutation)) {
|
|
121
|
+
await commitRemoval(destination, destination.mutation, index, hooks);
|
|
122
|
+
return;
|
|
77
123
|
}
|
|
78
124
|
if (destination.previous !== null) {
|
|
79
125
|
await requireParent(destination);
|
|
@@ -93,7 +139,7 @@ const commitAll = async (
|
|
|
93
139
|
async (previous, destination, index) => {
|
|
94
140
|
await previous;
|
|
95
141
|
await hooks.beforeCommit?.(index);
|
|
96
|
-
await commitOne(destination);
|
|
142
|
+
await commitOne(destination, index, hooks);
|
|
97
143
|
},
|
|
98
144
|
Promise.resolve(),
|
|
99
145
|
);
|
|
@@ -121,12 +167,12 @@ const recoverFailure = async (
|
|
|
121
167
|
};
|
|
122
168
|
};
|
|
123
169
|
|
|
124
|
-
export const
|
|
170
|
+
export const applyDevEnvChanges = async (
|
|
125
171
|
consumer: string,
|
|
126
|
-
|
|
172
|
+
mutations: ReadonlyArray<DevEnvMutation>,
|
|
127
173
|
hooks: DevEnvTransactionHooks = {},
|
|
128
174
|
): Promise<DevEnvTransactionResult> => {
|
|
129
|
-
const checked = await preflightDevEnvDestinations(consumer,
|
|
175
|
+
const checked = await preflightDevEnvDestinations(consumer, mutations);
|
|
130
176
|
if (!checked.ok) {
|
|
131
177
|
return checked;
|
|
132
178
|
}
|