@rehearsal-db/core 0.1.0-beta.1
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/BENCHMARKS.md +61 -0
- package/CHANGELOG.md +59 -0
- package/COMPATIBILITY.md +22 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/SECURITY.md +19 -0
- package/SUPPORT.md +15 -0
- package/docs/adapters.md +23 -0
- package/docs/baselines.md +33 -0
- package/docs/commands.md +35 -0
- package/docs/configuration.md +98 -0
- package/docs/getting-started.md +247 -0
- package/docs/glossary.md +33 -0
- package/docs/production-source.md +33 -0
- package/docs/releasing.md +79 -0
- package/docs/sanitization.md +69 -0
- package/docs/security-model.md +40 -0
- package/docs/troubleshooting.md +50 -0
- package/docs/tutorial.md +96 -0
- package/package.json +77 -0
- package/scripts/lib/environment/local_supabase.mjs +197 -0
- package/scripts/lib/rehearsal/baseline_artifact.mjs +536 -0
- package/scripts/lib/rehearsal/baseline_builder.mjs +155 -0
- package/scripts/lib/rehearsal/configuration.d.mts +85 -0
- package/scripts/lib/rehearsal/configuration.mjs +559 -0
- package/scripts/lib/rehearsal/diagnostics.mjs +193 -0
- package/scripts/lib/rehearsal/migration_history.mjs +220 -0
- package/scripts/lib/rehearsal/plan.mjs +587 -0
- package/scripts/lib/rehearsal/process_environment.mjs +64 -0
- package/scripts/lib/rehearsal/runtime_restore.mjs +310 -0
- package/scripts/lib/rehearsal/sanitization_policy.mjs +169 -0
- package/scripts/lib/rehearsal/schema_snapshot.mjs +113 -0
- package/scripts/lib/rehearsal/service_environment.mjs +82 -0
- package/scripts/operations/database/manage_rehearsal_database.mjs +784 -0
- package/scripts/operations/rehearsal/rehearsal_cli.mjs +565 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Doctor says Docker is unavailable
|
|
4
|
+
|
|
5
|
+
Start Docker Desktop or Colima, confirm `docker info`, then rerun `rehearsal doctor`.
|
|
6
|
+
Restarting the computer is rarely necessary.
|
|
7
|
+
|
|
8
|
+
## A port is already in use
|
|
9
|
+
|
|
10
|
+
Choose three unique non-privileged ports in config and mirror them in the dedicated
|
|
11
|
+
Supabase config. Do not stop an unrelated database to make the default ports fit.
|
|
12
|
+
|
|
13
|
+
## Baseline checksum mismatch
|
|
14
|
+
|
|
15
|
+
Do not edit the checksum or manifest. The artifact is no longer the reviewed input.
|
|
16
|
+
Rebuild the generation through the project-owned baseline process.
|
|
17
|
+
|
|
18
|
+
## Historical migration changed
|
|
19
|
+
|
|
20
|
+
Restore the exact represented bytes or intentionally build and review a new baseline.
|
|
21
|
+
Renaming edited history into the candidate suffix does not repair lineage.
|
|
22
|
+
|
|
23
|
+
## Candidate confirmation mismatch
|
|
24
|
+
|
|
25
|
+
Run `rehearsal candidates` again. Review every filename, then confirm the new digest only
|
|
26
|
+
if the exact set is intended.
|
|
27
|
+
|
|
28
|
+
## Migration failed
|
|
29
|
+
|
|
30
|
+
The runtime is untrusted and should be removed automatically. Correct the candidate SQL,
|
|
31
|
+
rerun doctor and candidates, then execute with the new digest.
|
|
32
|
+
|
|
33
|
+
## Application proof failed
|
|
34
|
+
|
|
35
|
+
Run the project proof directly against the generated local environment. Fix the app,
|
|
36
|
+
adapter, fixture expectation, or migration; do not weaken the proof to obtain a pass.
|
|
37
|
+
|
|
38
|
+
## Local authentication fails
|
|
39
|
+
|
|
40
|
+
Confirm the provider is enabled in the dedicated local Supabase config, the allowlisted
|
|
41
|
+
service environment file exists with owner-only permissions, and the provider callback
|
|
42
|
+
is the local Auth callback. Do not paste provider secrets into config or terminal output.
|
|
43
|
+
|
|
44
|
+
## Runtime changes disappeared
|
|
45
|
+
|
|
46
|
+
`reset` deliberately restores the immutable baseline. `stop` should preserve Docker
|
|
47
|
+
state, but runtime removal or a failed migration discards untrusted state.
|
|
48
|
+
|
|
49
|
+
Use `--debug` only after ordinary output is insufficient. Diagnostics are redacted, but
|
|
50
|
+
you should still review output before sharing it publicly.
|
package/docs/tutorial.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Tutorial: rehearse a migration in a new project
|
|
2
|
+
|
|
3
|
+
This walkthrough uses a fictional `widgets` application. It demonstrates the complete
|
|
4
|
+
consumer workflow without importing private or hosted data.
|
|
5
|
+
|
|
6
|
+
## Create the application migration history
|
|
7
|
+
|
|
8
|
+
The project begins with one historical migration:
|
|
9
|
+
|
|
10
|
+
```sql
|
|
11
|
+
create table public.widgets (
|
|
12
|
+
id bigint generated always as identity primary key,
|
|
13
|
+
name text not null
|
|
14
|
+
);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Place it at `supabase/migrations/20260101000000_create_widgets.sql`. Add a second,
|
|
18
|
+
candidate migration:
|
|
19
|
+
|
|
20
|
+
```sql
|
|
21
|
+
alter table public.widgets add column description text;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Place that at `supabase/migrations/20260101000100_add_widget_description.sql`.
|
|
25
|
+
|
|
26
|
+
## Configure the isolated runtime
|
|
27
|
+
|
|
28
|
+
Run `npx rehearsal init --write`, then edit the result. Give the runtime unique local
|
|
29
|
+
ports and a unique project ID. Its `rehearsalConfig` must point at a dedicated, unlinked
|
|
30
|
+
Supabase config. Never reuse a hosted project reference or production environment file.
|
|
31
|
+
|
|
32
|
+
Keep the generated `.rehearsal` directory ignored. It contains local artifacts and
|
|
33
|
+
runtime state, not source code.
|
|
34
|
+
|
|
35
|
+
## Build a synthetic baseline
|
|
36
|
+
|
|
37
|
+
The baseline contains the historical migration bundle, a one-row sanitized data stream,
|
|
38
|
+
and a manifest binding their checksums. Baseline construction is deliberately separate
|
|
39
|
+
from runtime execution: the application owns extraction and policy; the engine accepts
|
|
40
|
+
only a completed, verified artifact.
|
|
41
|
+
|
|
42
|
+
Write the safe rows to `rehearsal/synthetic-data.ndjson` and the exact represented
|
|
43
|
+
statements to `rehearsal/migration-ledger.json`, then run:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx rehearsal baseline create \
|
|
47
|
+
--records=rehearsal/synthetic-data.ndjson \
|
|
48
|
+
--ledger=rehearsal/migration-ledger.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For an executable sample, inspect `tests/fixtures/rehearsal-project` in the repository.
|
|
52
|
+
The maintained fixture proof invokes this public command through the packed npm tarball
|
|
53
|
+
and never contacts a hosted service.
|
|
54
|
+
|
|
55
|
+
## Prove planning is non-mutating
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx rehearsal doctor
|
|
59
|
+
npx rehearsal explain
|
|
60
|
+
npx rehearsal run --dry-run
|
|
61
|
+
npx rehearsal candidates --json
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`explain` and `run --dry-run` return the same execution plan. At this point no local
|
|
65
|
+
database has been restored.
|
|
66
|
+
|
|
67
|
+
## Run and inspect
|
|
68
|
+
|
|
69
|
+
Copy the exact digest from `candidates`:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx rehearsal run --confirm-candidates=<sha256>
|
|
73
|
+
npx rehearsal inspect migrations
|
|
74
|
+
npx rehearsal status
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The historical migration should be `represented_by_baseline`; the description migration
|
|
78
|
+
should be `applied_to_current_runtime`. Test normal creates, updates, and deletes through
|
|
79
|
+
your local application. They affect only this disposable database.
|
|
80
|
+
|
|
81
|
+
## Prove failure behavior
|
|
82
|
+
|
|
83
|
+
Add a timestamped migration containing invalid SQL, rerun `candidates`, and use its new
|
|
84
|
+
digest. The command must fail with `migration_candidate_failure`, remove the untrusted
|
|
85
|
+
runtime, and never produce a successful receipt.
|
|
86
|
+
|
|
87
|
+
Then restore the valid migration set and rerun:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx rehearsal reset
|
|
91
|
+
npx rehearsal verify
|
|
92
|
+
npx rehearsal stop
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
That cycle—plan, confirm exact bytes, run, exercise the app, reset—is the normal Rehearsal
|
|
96
|
+
workflow.
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rehearsal-db/core",
|
|
3
|
+
"version": "0.1.0-beta.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Safely rehearse Supabase migrations against sanitized, production-shaped PostgreSQL data.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Ddupasquier/rehearsal-db.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/Ddupasquier/rehearsal-db/issues"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/Ddupasquier/rehearsal-db#readme",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public",
|
|
16
|
+
"tag": "beta",
|
|
17
|
+
"provenance": true
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=24 <25"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"rehearsal": "scripts/operations/rehearsal/rehearsal_cli.mjs"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./scripts/lib/rehearsal/configuration.d.mts",
|
|
29
|
+
"import": "./scripts/lib/rehearsal/configuration.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./baseline": "./scripts/lib/rehearsal/baseline_artifact.mjs",
|
|
32
|
+
"./baseline-builder": "./scripts/lib/rehearsal/baseline_builder.mjs",
|
|
33
|
+
"./diagnostics": "./scripts/lib/rehearsal/diagnostics.mjs",
|
|
34
|
+
"./migrations": "./scripts/lib/rehearsal/migration_history.mjs",
|
|
35
|
+
"./process-environment": "./scripts/lib/rehearsal/process_environment.mjs",
|
|
36
|
+
"./schema": "./scripts/lib/rehearsal/schema_snapshot.mjs",
|
|
37
|
+
"./service-environment": "./scripts/lib/rehearsal/service_environment.mjs"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"scripts/lib/environment/local_supabase.mjs",
|
|
41
|
+
"scripts/lib/rehearsal",
|
|
42
|
+
"scripts/operations/database/manage_rehearsal_database.mjs",
|
|
43
|
+
"scripts/operations/rehearsal/rehearsal_cli.mjs",
|
|
44
|
+
"docs/*.md",
|
|
45
|
+
"README.md",
|
|
46
|
+
"BENCHMARKS.md",
|
|
47
|
+
"CHANGELOG.md",
|
|
48
|
+
"SECURITY.md",
|
|
49
|
+
"COMPATIBILITY.md",
|
|
50
|
+
"SUPPORT.md",
|
|
51
|
+
"LICENSE"
|
|
52
|
+
],
|
|
53
|
+
"scripts": {
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:unit": "vitest run tests/scripts",
|
|
56
|
+
"test:docs": "vitest run tests/contracts/documentationCommands.test.mjs",
|
|
57
|
+
"test:fixture": "node scripts/operations/rehearsal/prove_independent_fixture.mjs",
|
|
58
|
+
"check": "npm run check:syntax && npm run test && npm run format:check && npm run package:audit",
|
|
59
|
+
"check:syntax": "find scripts tests -type f -name '*.mjs' -exec node --check {} +",
|
|
60
|
+
"format": "prettier --write .",
|
|
61
|
+
"format:check": "prettier --check .",
|
|
62
|
+
"package:audit": "node scripts/operations/rehearsal/audit_package.mjs"
|
|
63
|
+
},
|
|
64
|
+
"keywords": [
|
|
65
|
+
"postgresql",
|
|
66
|
+
"supabase",
|
|
67
|
+
"migration",
|
|
68
|
+
"testing",
|
|
69
|
+
"database"
|
|
70
|
+
],
|
|
71
|
+
"license": "MIT",
|
|
72
|
+
"dependencies": {},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"prettier": "^3.9.6",
|
|
75
|
+
"vitest": "^4.1.10"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Start and inspect local-only Supabase workdirs without inheriting hosted
|
|
3
|
+
* credentials. Do not run directly; this module is reusable script infrastructure.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { createCleanProcessEnvironment } from "../rehearsal/process_environment.mjs";
|
|
8
|
+
|
|
9
|
+
export const parseSupabaseStatusEnvironment = (output) => {
|
|
10
|
+
const values = {};
|
|
11
|
+
for (const line of output.split("\n")) {
|
|
12
|
+
const match = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
|
|
13
|
+
if (!match) continue;
|
|
14
|
+
let value = match[2].trim();
|
|
15
|
+
if (value.startsWith('"') && value.endsWith('"')) value = JSON.parse(value);
|
|
16
|
+
values[match[1]] = value;
|
|
17
|
+
}
|
|
18
|
+
return values;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const runLocalCommand = (
|
|
22
|
+
command,
|
|
23
|
+
args,
|
|
24
|
+
{
|
|
25
|
+
capture = false,
|
|
26
|
+
cwd = process.cwd(),
|
|
27
|
+
input,
|
|
28
|
+
environment = {},
|
|
29
|
+
maxBuffer = 16 * 1024 * 1024,
|
|
30
|
+
} = {},
|
|
31
|
+
) => {
|
|
32
|
+
const shouldPipe = capture || input !== undefined;
|
|
33
|
+
const result = spawnSync(command, args, {
|
|
34
|
+
cwd,
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
env: createCleanProcessEnvironment({ overrides: environment }),
|
|
37
|
+
input,
|
|
38
|
+
maxBuffer,
|
|
39
|
+
stdio: shouldPipe ? ["pipe", "pipe", "pipe"] : "inherit",
|
|
40
|
+
});
|
|
41
|
+
if (result.error) throw result.error;
|
|
42
|
+
if (result.status !== 0) {
|
|
43
|
+
const detail = [result.stdout, result.stderr]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join("\n")
|
|
46
|
+
.trim();
|
|
47
|
+
throw new Error(
|
|
48
|
+
`${command} ${args.join(" ")} failed${detail ? `:\n${detail}` : "."}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return result.stdout ?? "";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const localCommandSucceeds = (
|
|
55
|
+
command,
|
|
56
|
+
args,
|
|
57
|
+
{ cwd = process.cwd(), environment = {} } = {},
|
|
58
|
+
) =>
|
|
59
|
+
spawnSync(command, args, {
|
|
60
|
+
cwd,
|
|
61
|
+
env: createCleanProcessEnvironment({ overrides: environment }),
|
|
62
|
+
stdio: "ignore",
|
|
63
|
+
}).status === 0;
|
|
64
|
+
|
|
65
|
+
export const ensureLocalContainerRuntime = ({ cwd = process.cwd() } = {}) => {
|
|
66
|
+
if (localCommandSucceeds("docker", ["info"], { cwd })) return;
|
|
67
|
+
if (localCommandSucceeds("colima", ["version"], { cwd })) {
|
|
68
|
+
runLocalCommand(
|
|
69
|
+
"colima",
|
|
70
|
+
["start", "--cpu", "4", "--memory", "4", "--disk", "40"],
|
|
71
|
+
{ cwd },
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (!localCommandSucceeds("docker", ["info"], { cwd })) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
"A local Docker-compatible runtime is required. Install Docker or Colima, then rerun the command.",
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const withWorkdir = (args, workdir) =>
|
|
82
|
+
workdir ? [...args, "--workdir", workdir] : args;
|
|
83
|
+
|
|
84
|
+
export const readLocalSupabaseEnvironment = ({
|
|
85
|
+
cwd,
|
|
86
|
+
workdir,
|
|
87
|
+
environment = {},
|
|
88
|
+
} = {}) => {
|
|
89
|
+
const output = runLocalCommand(
|
|
90
|
+
"supabase",
|
|
91
|
+
withWorkdir(["status", "-o", "env"], workdir),
|
|
92
|
+
{ capture: true, cwd, environment },
|
|
93
|
+
);
|
|
94
|
+
const values = parseSupabaseStatusEnvironment(output);
|
|
95
|
+
const localEnvironment = {
|
|
96
|
+
apiUrl: values.API_URL,
|
|
97
|
+
publishableKey: values.PUBLISHABLE_KEY ?? values.ANON_KEY,
|
|
98
|
+
serviceRoleKey: values.SERVICE_ROLE_KEY ?? values.SECRET_KEY,
|
|
99
|
+
studioUrl: values.STUDIO_URL,
|
|
100
|
+
};
|
|
101
|
+
for (const [label, value] of [
|
|
102
|
+
["API URL", localEnvironment.apiUrl],
|
|
103
|
+
["publishable key", localEnvironment.publishableKey],
|
|
104
|
+
["service-role key", localEnvironment.serviceRoleKey],
|
|
105
|
+
]) {
|
|
106
|
+
if (!value) throw new Error(`Local Supabase status omitted its ${label}.`);
|
|
107
|
+
}
|
|
108
|
+
const hostname = new URL(localEnvironment.apiUrl).hostname;
|
|
109
|
+
if (!["127.0.0.1", "::1", "localhost"].includes(hostname)) {
|
|
110
|
+
throw new Error("Refusing to use a non-loopback Supabase status target.");
|
|
111
|
+
}
|
|
112
|
+
return localEnvironment;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const startLocalSupabase = ({
|
|
116
|
+
cwd,
|
|
117
|
+
workdir,
|
|
118
|
+
exclude = [],
|
|
119
|
+
environment = {},
|
|
120
|
+
applyMigrations = true,
|
|
121
|
+
} = {}) => {
|
|
122
|
+
ensureLocalContainerRuntime({ cwd });
|
|
123
|
+
const startArguments = withWorkdir(
|
|
124
|
+
["start", ...(exclude.length ? ["--exclude", exclude.join(",")] : [])],
|
|
125
|
+
workdir,
|
|
126
|
+
);
|
|
127
|
+
runLocalCommand("supabase", startArguments, {
|
|
128
|
+
capture: true,
|
|
129
|
+
cwd,
|
|
130
|
+
environment,
|
|
131
|
+
});
|
|
132
|
+
if (applyMigrations) {
|
|
133
|
+
runLocalCommand(
|
|
134
|
+
"supabase",
|
|
135
|
+
withWorkdir(["migration", "up", "--local"], workdir),
|
|
136
|
+
{ capture: true, cwd, environment },
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return readLocalSupabaseEnvironment({ cwd, workdir, environment });
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const stopLocalSupabase = ({ cwd, workdir, environment = {} } = {}) => {
|
|
143
|
+
if (!localCommandSucceeds("docker", ["info"], { cwd })) return;
|
|
144
|
+
runLocalCommand("supabase", withWorkdir(["stop"], workdir), {
|
|
145
|
+
cwd,
|
|
146
|
+
environment,
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const removeLocalSupabaseProjectResources = ({
|
|
151
|
+
cwd = process.cwd(),
|
|
152
|
+
projectId,
|
|
153
|
+
} = {}) => {
|
|
154
|
+
if (!/^[a-z0-9][a-z0-9-]{1,62}$/u.test(projectId ?? "")) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
"Refusing to remove local Supabase resources without an exact safe project id.",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const label = `com.supabase.cli.project=${projectId}`;
|
|
160
|
+
const list = (resource, format) =>
|
|
161
|
+
runLocalCommand(
|
|
162
|
+
"docker",
|
|
163
|
+
[resource, "ls", "--filter", `label=${label}`, "--format", format],
|
|
164
|
+
{ capture: true, cwd },
|
|
165
|
+
)
|
|
166
|
+
.split("\n")
|
|
167
|
+
.map((value) => value.trim())
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
const containers = runLocalCommand(
|
|
170
|
+
"docker",
|
|
171
|
+
["ps", "--all", "--filter", `label=${label}`, "--format", "{{.ID}}"],
|
|
172
|
+
{ capture: true, cwd },
|
|
173
|
+
)
|
|
174
|
+
.split("\n")
|
|
175
|
+
.map((value) => value.trim())
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
if (containers.length) {
|
|
178
|
+
runLocalCommand("docker", ["rm", "--force", ...containers], {
|
|
179
|
+
capture: true,
|
|
180
|
+
cwd,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const volumes = list("volume", "{{.Name}}");
|
|
184
|
+
if (volumes.length) {
|
|
185
|
+
runLocalCommand("docker", ["volume", "rm", "--force", ...volumes], {
|
|
186
|
+
capture: true,
|
|
187
|
+
cwd,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
const networks = list("network", "{{.ID}}");
|
|
191
|
+
if (networks.length) {
|
|
192
|
+
runLocalCommand("docker", ["network", "rm", ...networks], {
|
|
193
|
+
capture: true,
|
|
194
|
+
cwd,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
};
|