terp-cli 0.31.0__tar.gz → 0.32.0__tar.gz
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.
- {terp_cli-0.31.0 → terp_cli-0.32.0}/PKG-INFO +8 -8
- {terp_cli-0.31.0 → terp_cli-0.32.0}/pyproject.toml +8 -8
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/docker.py +14 -0
- terp_cli-0.32.0/src/terp/cli/docker_masks.py +167 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/verify.py +18 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/version.py +10 -3
- {terp_cli-0.31.0 → terp_cli-0.32.0}/.gitignore +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/__init__.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/_appref.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/_engine.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/_output.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/_subjects.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/access.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/apidocs.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/authz_surface.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/capabilities.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/deploy_safety.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/dev.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/envfile.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/envschema.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/envseams.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/fmt.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/grants.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/jobs.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/leases.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/module_roles.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/openapi.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/outbox.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/ports.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/profiles.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/py.typed +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/routes.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/scaffold.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/schema.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/seed.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/service_accounts.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/smoke.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/users.py +0 -0
- {terp_cli-0.31.0 → terp_cli-0.32.0}/src/terp/cli/workbench.py +0 -0
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: terp-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs.
|
|
5
5
|
Project-URL: Repository, https://github.com/AITT-NL/terp-framework
|
|
6
6
|
Project-URL: Changelog, https://github.com/AITT-NL/terp-framework/blob/main/CHANGELOG.md
|
|
7
7
|
License-Expression: Apache-2.0
|
|
8
8
|
Requires-Python: >=3.13
|
|
9
9
|
Requires-Dist: pyyaml>=6.0
|
|
10
|
-
Requires-Dist: terp-arch==0.
|
|
11
|
-
Requires-Dist: terp-core==0.
|
|
12
|
-
Requires-Dist: terp-migrations==0.
|
|
10
|
+
Requires-Dist: terp-arch==0.32.0
|
|
11
|
+
Requires-Dist: terp-core==0.32.0
|
|
12
|
+
Requires-Dist: terp-migrations==0.32.0
|
|
13
13
|
Provides-Extra: jobs
|
|
14
|
-
Requires-Dist: terp-cap-outbox==0.
|
|
15
|
-
Requires-Dist: terp-cap-scheduler-apscheduler==0.
|
|
14
|
+
Requires-Dist: terp-cap-outbox==0.32.0; extra == 'jobs'
|
|
15
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.32.0; extra == 'jobs'
|
|
16
16
|
Provides-Extra: scheduler
|
|
17
|
-
Requires-Dist: terp-cap-scheduler-apscheduler==0.
|
|
17
|
+
Requires-Dist: terp-cap-scheduler-apscheduler==0.32.0; extra == 'scheduler'
|
|
18
18
|
Provides-Extra: worker
|
|
19
|
-
Requires-Dist: terp-cap-outbox==0.
|
|
19
|
+
Requires-Dist: terp-cap-outbox==0.32.0; extra == 'worker'
|
|
@@ -4,18 +4,18 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "terp-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.32.0"
|
|
8
8
|
description = "Terp command-line tool — inspect, scaffolding, migrations, checks, api-docs."
|
|
9
9
|
requires-python = ">=3.13"
|
|
10
10
|
license = "Apache-2.0"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"terp-core==0.
|
|
12
|
+
"terp-core==0.32.0",
|
|
13
13
|
# `terp migrate` delegates to terp-migrations (lazily imported), keeping Alembic
|
|
14
14
|
# off the path for `terp inspect` / `terp guide` while making migrations work.
|
|
15
|
-
"terp-migrations==0.
|
|
15
|
+
"terp-migrations==0.32.0",
|
|
16
16
|
# `terp guide rules` projects the live rule registry; terp-arch is imported lazily
|
|
17
17
|
# (only when that topic is rendered), so it stays off the common `terp guide` path.
|
|
18
|
-
"terp-arch==0.
|
|
18
|
+
"terp-arch==0.32.0",
|
|
19
19
|
# `terp verify --only env-seams` and `terp smoke` read the compose profiles as data
|
|
20
20
|
# (anchors and `<<:` merge keys resolved), so both answer without a Docker daemon or
|
|
21
21
|
# even the `docker` binary. Declared explicitly rather than leant on: every app
|
|
@@ -29,11 +29,11 @@ dependencies = [
|
|
|
29
29
|
# or a scheduler engine into apps that only use inspect/check/migrate. Deployments can select
|
|
30
30
|
# one process role, while `jobs` is the convenient complete jobs-process bundle.
|
|
31
31
|
[project.optional-dependencies]
|
|
32
|
-
worker = ["terp-cap-outbox==0.
|
|
33
|
-
scheduler = ["terp-cap-scheduler-apscheduler==0.
|
|
32
|
+
worker = ["terp-cap-outbox==0.32.0"]
|
|
33
|
+
scheduler = ["terp-cap-scheduler-apscheduler==0.32.0"]
|
|
34
34
|
jobs = [
|
|
35
|
-
"terp-cap-outbox==0.
|
|
36
|
-
"terp-cap-scheduler-apscheduler==0.
|
|
35
|
+
"terp-cap-outbox==0.32.0",
|
|
36
|
+
"terp-cap-scheduler-apscheduler==0.32.0",
|
|
37
37
|
]
|
|
38
38
|
|
|
39
39
|
[project.scripts]
|
|
@@ -29,6 +29,8 @@ import subprocess
|
|
|
29
29
|
from collections.abc import Callable, Sequence
|
|
30
30
|
|
|
31
31
|
from terp.cli import ports
|
|
32
|
+
from terp.cli._output import emit
|
|
33
|
+
from terp.cli.docker_masks import clear_stale_masks
|
|
32
34
|
|
|
33
35
|
_DEFAULT_COMPOSE = "docker-compose.yml"
|
|
34
36
|
|
|
@@ -229,6 +231,18 @@ def run_docker_dev_command(
|
|
|
229
231
|
_, note = ports.ensure_assigned(path.parent)
|
|
230
232
|
if note:
|
|
231
233
|
print(note)
|
|
234
|
+
# Before `watch` creates anything: a workbench from before 0.30.0 holds its
|
|
235
|
+
# node_modules mask as a volume, which Compose would reattach over the tmpfs the
|
|
236
|
+
# compose file now declares and keep serving old packages from
|
|
237
|
+
# (terp.cli.docker_masks). No docker on PATH is `watch`'s to report, in its own
|
|
238
|
+
# words, not this check's.
|
|
239
|
+
try:
|
|
240
|
+
cleared = clear_stale_masks(path, project_name=project_name, capture=capture or _capture)
|
|
241
|
+
except OSError:
|
|
242
|
+
cleared = ""
|
|
243
|
+
if cleared:
|
|
244
|
+
# Through the CLI's one output seam, and flushed: `watch` holds the terminal next.
|
|
245
|
+
emit(cleared)
|
|
232
246
|
status = (runner or _run)(docker_dev_argv(path, project_name=project_name))
|
|
233
247
|
message = f"docker compose watch exited with status {status}"
|
|
234
248
|
if status == 0:
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""A mask the compose file declares as ``tmpfs`` that a running workbench still holds as a volume.
|
|
2
|
+
|
|
3
|
+
0.30.0 turned the frontend's ``node_modules`` mask from an anonymous volume into a
|
|
4
|
+
``tmpfs``, so that a dependency bump reaches the running dev server. A workbench that
|
|
5
|
+
was already up before the upgrade does not get it. When Compose recreates a container
|
|
6
|
+
it reattaches the old container's anonymous volumes by target path, and that wins over
|
|
7
|
+
the ``tmpfs`` now declared at the same path. The dev server then keeps serving the
|
|
8
|
+
first boot's packages from that volume, with every container healthy and the page
|
|
9
|
+
answering 200. An upgraded-on-paper workbench and an upgraded one look identical.
|
|
10
|
+
|
|
11
|
+
Compose owns that reattachment, so the place to undo it is before ``watch`` creates
|
|
12
|
+
anything. For every service whose mount at a ``tmpfs`` path is still a volume, the
|
|
13
|
+
container is removed, and so is that volume and only that one. Compose then creates
|
|
14
|
+
both fresh, with the ``tmpfs``. The volume held nothing but a copy of the image's
|
|
15
|
+
packages: the mask exists to hide the checkout's ``node_modules``, never to keep one.
|
|
16
|
+
A named volume is never touched, and neither is any other volume of the container.
|
|
17
|
+
|
|
18
|
+
Best effort, like the failure diagnosis next door: a daemon that is not up, a compose
|
|
19
|
+
file Compose cannot read, or a ``ps`` with no containers yet means there is nothing to
|
|
20
|
+
undo, and ``watch`` speaks for itself.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import pathlib
|
|
27
|
+
from collections.abc import Callable, Sequence
|
|
28
|
+
|
|
29
|
+
#: Runs argv, returning ``(exit status, combined output)``.
|
|
30
|
+
Capture = Callable[[Sequence[str]], tuple[int, str]]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _compose(compose_file: pathlib.Path, project_name: str | None) -> list[str]:
|
|
34
|
+
argv = ["docker", "compose", "-f", str(compose_file)]
|
|
35
|
+
if project_name:
|
|
36
|
+
argv += ["-p", project_name]
|
|
37
|
+
return argv
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _json_entries(output: str) -> list[dict]:
|
|
41
|
+
"""A JSON array, one object, or newline-delimited objects, as Compose has printed each."""
|
|
42
|
+
stripped = output.strip()
|
|
43
|
+
if not stripped:
|
|
44
|
+
return []
|
|
45
|
+
try:
|
|
46
|
+
parsed = json.loads(stripped)
|
|
47
|
+
except json.JSONDecodeError:
|
|
48
|
+
entries = []
|
|
49
|
+
for line in stripped.splitlines():
|
|
50
|
+
try:
|
|
51
|
+
entries.append(json.loads(line))
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
continue
|
|
54
|
+
return [entry for entry in entries if isinstance(entry, dict)]
|
|
55
|
+
items = parsed if isinstance(parsed, list) else [parsed]
|
|
56
|
+
return [entry for entry in items if isinstance(entry, dict)]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def tmpfs_targets(config_output: str) -> dict[str, frozenset[str]]:
|
|
60
|
+
"""Per service, the paths ``docker compose config --format json`` declares as ``tmpfs``.
|
|
61
|
+
|
|
62
|
+
Both spellings count: a ``volumes`` entry of ``type: tmpfs``, and the service-level
|
|
63
|
+
``tmpfs:`` list (whose entries may carry ``:options`` after the path).
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
config = json.loads(config_output)
|
|
67
|
+
except json.JSONDecodeError:
|
|
68
|
+
return {}
|
|
69
|
+
services = config.get("services") if isinstance(config, dict) else None
|
|
70
|
+
if not isinstance(services, dict):
|
|
71
|
+
return {}
|
|
72
|
+
found: dict[str, frozenset[str]] = {}
|
|
73
|
+
for name, service in services.items():
|
|
74
|
+
if not isinstance(service, dict):
|
|
75
|
+
continue
|
|
76
|
+
targets = {
|
|
77
|
+
volume["target"]
|
|
78
|
+
for volume in service.get("volumes") or ()
|
|
79
|
+
if isinstance(volume, dict)
|
|
80
|
+
and volume.get("type") == "tmpfs"
|
|
81
|
+
and isinstance(volume.get("target"), str)
|
|
82
|
+
}
|
|
83
|
+
listed = service.get("tmpfs") or ()
|
|
84
|
+
for entry in [listed] if isinstance(listed, str) else listed:
|
|
85
|
+
if isinstance(entry, str) and entry:
|
|
86
|
+
targets.add(entry.split(":", 1)[0])
|
|
87
|
+
if targets:
|
|
88
|
+
found[name] = frozenset(targets)
|
|
89
|
+
return found
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def containers(ps_output: str) -> dict[str, str]:
|
|
93
|
+
"""Service name to container id, from ``docker compose ps --all --format json``."""
|
|
94
|
+
found: dict[str, str] = {}
|
|
95
|
+
for entry in _json_entries(ps_output):
|
|
96
|
+
service, container = entry.get("Service"), entry.get("ID")
|
|
97
|
+
if isinstance(service, str) and isinstance(container, str) and container:
|
|
98
|
+
found.setdefault(service, container)
|
|
99
|
+
return found
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def stale_volumes(inspect_output: str, targets: frozenset[str]) -> tuple[str, ...]:
|
|
103
|
+
"""The volumes ``docker inspect`` shows mounted at a path that should be a ``tmpfs``.
|
|
104
|
+
|
|
105
|
+
Only a volume counts, never a bind: a bind at that path is somebody's deliberate
|
|
106
|
+
choice, and removing it is not this command's to decide.
|
|
107
|
+
"""
|
|
108
|
+
names: list[str] = []
|
|
109
|
+
for container in _json_entries(inspect_output):
|
|
110
|
+
for mount in container.get("Mounts") or ():
|
|
111
|
+
if (
|
|
112
|
+
isinstance(mount, dict)
|
|
113
|
+
and mount.get("Type") == "volume"
|
|
114
|
+
and mount.get("Destination") in targets
|
|
115
|
+
and isinstance(mount.get("Name"), str)
|
|
116
|
+
and mount["Name"] not in names
|
|
117
|
+
):
|
|
118
|
+
names.append(mount["Name"])
|
|
119
|
+
return tuple(names)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def clear_stale_masks(
|
|
123
|
+
compose_file: pathlib.Path,
|
|
124
|
+
*,
|
|
125
|
+
project_name: str | None = None,
|
|
126
|
+
capture: Capture,
|
|
127
|
+
) -> str:
|
|
128
|
+
"""Undo a pre-0.30.0 ``node_modules`` volume before ``watch``; what was done, or ``""``."""
|
|
129
|
+
compose = _compose(compose_file, project_name)
|
|
130
|
+
status, config = capture([*compose, "config", "--format", "json"])
|
|
131
|
+
targets = tmpfs_targets(config) if status == 0 else {}
|
|
132
|
+
if not targets:
|
|
133
|
+
return ""
|
|
134
|
+
status, ps_output = capture([*compose, "ps", "--all", "--format", "json"])
|
|
135
|
+
running = containers(ps_output) if status == 0 else {}
|
|
136
|
+
stale: dict[str, tuple[str, ...]] = {}
|
|
137
|
+
for service, paths in sorted(targets.items()):
|
|
138
|
+
container = running.get(service)
|
|
139
|
+
if container is None:
|
|
140
|
+
continue
|
|
141
|
+
status, inspected = capture(["docker", "inspect", container])
|
|
142
|
+
volumes = stale_volumes(inspected, paths) if status == 0 else ()
|
|
143
|
+
if volumes:
|
|
144
|
+
stale[service] = volumes
|
|
145
|
+
if not stale:
|
|
146
|
+
return ""
|
|
147
|
+
services = sorted(stale)
|
|
148
|
+
volumes = [volume for service in services for volume in stale[service]]
|
|
149
|
+
listing = ", ".join(repr(service) for service in services)
|
|
150
|
+
removed, _ = capture([*compose, "rm", "--stop", "--force", *services])
|
|
151
|
+
if removed == 0:
|
|
152
|
+
removed, _ = capture(["docker", "volume", "rm", *volumes])
|
|
153
|
+
if removed != 0:
|
|
154
|
+
renew = [*compose, "up", "-d", "--no-deps", "--force-recreate", "--renew-anon-volumes"]
|
|
155
|
+
return (
|
|
156
|
+
f"terp docker dev: {listing} still mount(s) a volume where the compose file "
|
|
157
|
+
"declares a tmpfs (a workbench from before 0.30.0), so the dev server would keep "
|
|
158
|
+
"serving its old packages. Removing it failed; once, by hand:\n"
|
|
159
|
+
f" {' '.join([*renew, *services])}\n"
|
|
160
|
+
f" docker volume rm {' '.join(volumes)}"
|
|
161
|
+
)
|
|
162
|
+
return (
|
|
163
|
+
f"terp docker dev: {listing} still mounted a volume where the compose file declares "
|
|
164
|
+
"a tmpfs (a workbench from before 0.30.0), so the dev server kept serving the packages "
|
|
165
|
+
"it first booted with. Removed the container and that volume; this start creates both "
|
|
166
|
+
"fresh."
|
|
167
|
+
)
|
|
@@ -1043,6 +1043,24 @@ def _node_modules_problem(root: pathlib.Path, workspace: str) -> str | None:
|
|
|
1043
1043
|
except (OSError, ValueError):
|
|
1044
1044
|
return None
|
|
1045
1045
|
|
|
1046
|
+
# A directory with none of the lockfile's platform-neutral packages in it is not
|
|
1047
|
+
# a tree for another platform but no tree at all -- an install that never ran,
|
|
1048
|
+
# or a fresh mount point. Calling it "a different platform" sends the reader
|
|
1049
|
+
# looking for a second machine that does not exist. Neutral ones only: every
|
|
1050
|
+
# platform installs those, so not one of them present means nothing was.
|
|
1051
|
+
neutral = [
|
|
1052
|
+
name
|
|
1053
|
+
for name, entry in packages.items()
|
|
1054
|
+
if name.startswith("node_modules/")
|
|
1055
|
+
and isinstance(entry, dict)
|
|
1056
|
+
and not (entry.get("os") or entry.get("cpu") or entry.get("libc"))
|
|
1057
|
+
]
|
|
1058
|
+
if neutral and not any((directory / name).exists() for name in neutral):
|
|
1059
|
+
return (
|
|
1060
|
+
f"{label} is missing (the directory is there, but holds none of the "
|
|
1061
|
+
f"lockfile's packages) — {subject} cannot run.\n Fix: {fix}"
|
|
1062
|
+
)
|
|
1063
|
+
|
|
1046
1064
|
system, arch = _node_platform()
|
|
1047
1065
|
libc = _node_libc(system)
|
|
1048
1066
|
missing = [
|
|
@@ -236,6 +236,7 @@ _APP_OWNED_SCAFFOLD_FILES = (
|
|
|
236
236
|
"control_plane/app_operations.py",
|
|
237
237
|
"environment.schema.json",
|
|
238
238
|
"escape-hatch-budget.json",
|
|
239
|
+
"frontend/escape-hatch-budget.json",
|
|
239
240
|
"frontend/layout-contract.json",
|
|
240
241
|
"frontend/src/house-style.css",
|
|
241
242
|
"frontend/src/routes.gen.d.ts",
|
|
@@ -450,8 +451,11 @@ def _rerender_recipe(target: str, current: str, count: int) -> list[str]:
|
|
|
450
451
|
" and its own diff is much easier to review on its own.",
|
|
451
452
|
" 3. copier update (or the Studio's upgrade flow, which records the",
|
|
452
453
|
" answers file it needs). This rewrites EVERY file the template owns and",
|
|
453
|
-
" writes the terp-* and @terpjs/* pins for you.",
|
|
454
|
-
"
|
|
454
|
+
" writes the terp-* and @terpjs/* pins for you. Its --pretend lists none of",
|
|
455
|
+
" that, so do not judge the update by a dry run: review what it did as its",
|
|
456
|
+
" own diff (git status, git diff) — that is why step 2 cleans the tree.",
|
|
457
|
+
" 4. Resolve what it reports: conflict markers in the file, or a *.rej beside it",
|
|
458
|
+
" when it ran with --conflict rej. Two conflicts are structural, not bad luck,",
|
|
455
459
|
" because the template owns the file and your app also writes to it:",
|
|
456
460
|
" pyproject.toml keep your dependencies, take the terp-* pins.",
|
|
457
461
|
" control_plane/operations.py the capability folding is the template's;",
|
|
@@ -468,7 +472,10 @@ def _rerender_recipe(target: str, current: str, count: int) -> list[str]:
|
|
|
468
472
|
" in while the source is bind-mounted, so correct new code reloads against old",
|
|
469
473
|
" libraries and dies on an import nowhere near its cause. `terp docker dev`",
|
|
470
474
|
" rebuilds on a pyproject.toml change; a plain `docker compose up` does not,",
|
|
471
|
-
" and `terp verify` now refuses the skew either way.",
|
|
475
|
+
" and `terp verify` now refuses the skew either way. Coming from 0.29 or older,",
|
|
476
|
+
" a stack that is already up keeps its old node_modules volume where 0.30.0",
|
|
477
|
+
" declares a tmpfs. `terp docker dev` removes it before it starts; with plain",
|
|
478
|
+
" compose, run `up --force-recreate --renew-anon-volumes` once.",
|
|
472
479
|
"",
|
|
473
480
|
"A green gate proves the upgrade did not break this app. It cannot prove the",
|
|
474
481
|
"release did not change something this app should adopt — step 1 is the only",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|