@mriekki/project-forge 4.0.0 → 4.2.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/assets/components.json +3 -3
- package/assets/deployment/distribution.json +2 -0
- package/assets/generated/ASSEMBLY.json +25 -15
- package/assets/generated/core/core/instructions/project-forge-workflow.instructions.md +3 -1
- package/assets/generated/core/core/skills/project-forge-update/SKILL.md +3 -3
- package/assets/generated/core/core/skills/setup/SKILL.md +61 -0
- package/assets/generated/core/core/skills/upstream-fix/SKILL.md +137 -18
- package/assets/generated/deployment/docs/DEVELOPER_ENVIRONMENTS.md +52 -12
- package/assets/generated/deployment/docs/ENVIRONMENT_PROOF.md +82 -6
- package/assets/generated/deployment/src/project_forge_deployment/app_runtime_workstation.py +103 -42
- package/assets/generated/deployment/src/project_forge_deployment/environment_cli.py +41 -4
- package/assets/generated/deployment/src/project_forge_deployment/environment_probe.py +159 -79
- package/assets/generated/deployment/src/project_forge_deployment/environment_setup.py +323 -0
- package/assets/generated/deployment/src/project_forge_deployment/home_lab.py +22 -1
- package/assets/generated/deployment/src/project_forge_deployment/image_transfer.py +51 -11
- package/assets/generated/deployment/src/project_forge_deployment/proof_receipt.py +183 -0
- package/assets/generated/deployment/src/project_forge_deployment/ssh.py +277 -23
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Status: **CORE WORKSTATION/TARGET MECHANICS IMPLEMENTED; AUTOMATED HOST PACKAGE PREPARATION REMAINS INCREMENTAL**
|
|
4
4
|
|
|
5
|
-
This document is the canonical developer-environment model for Project
|
|
5
|
+
This document is the canonical developer-environment model for Project Forge. It describes the developer-facing progression from a generated repository to local development, optional Staging/Production capability, additional workstations, and additional remote boxes.
|
|
6
6
|
|
|
7
7
|
The happy path intentionally hides Bootstrap/package/control-plane plumbing. Developers should see meaningful milestones and actionable setup, while the underlying repositories retain exact provenance and safety evidence.
|
|
8
8
|
|
|
@@ -44,6 +44,37 @@ The normal conversational entry point is:
|
|
|
44
44
|
|
|
45
45
|
The first local success target is `LOCAL WORKSPACE READY`. Remote access must not block it unless the Project explicitly requires a remote dependency for local development.
|
|
46
46
|
|
|
47
|
+
For remote onboarding, conversational `/setup remote` or `/setup <environment>` should consume one deterministic read-only setup state machine rather than manually reconstructing low-level steps:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
project-forge-deploy environment setup status <target> --json
|
|
51
|
+
project-forge-deploy environment setup plan <target> --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When target access and the host platform are ready, proof context can be included without performing a write:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
project-forge-deploy environment setup plan <target> \
|
|
58
|
+
--owner-repo <owner/repo> \
|
|
59
|
+
--environment <logical-environment> \
|
|
60
|
+
--json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Add `--production` only when that logical environment is Production-class. Setup status/plan never authorizes a write; they report the existing authorization phrase and exact next deterministic operation when a write is required.
|
|
64
|
+
|
|
65
|
+
Readiness is deliberately separated into:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
WORKSPACE READY
|
|
69
|
+
TARGET DEFINITION READY
|
|
70
|
+
TARGET ACCESS READY
|
|
71
|
+
HOST PLATFORM READY
|
|
72
|
+
DEPLOYMENT PATH PROVEN
|
|
73
|
+
APPLICATION DEPLOYED
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The last state is not part of setup. A successful disposable proof does not deploy the Project application.
|
|
77
|
+
|
|
47
78
|
## Progress and next action
|
|
48
79
|
|
|
49
80
|
Use `/progress` to answer what is complete, what is blocked/deferred, who owns the next action, and local/Staging/Production capability state.
|
|
@@ -101,6 +132,8 @@ An empty profile with `targets: []` is valid. A local-only developer never needs
|
|
|
101
132
|
|
|
102
133
|
`profile sync` fetches and validates the GitHub profile, updates the validated local cache, and with `--apply` creates only missing non-conflicting local target definitions. Existing differing local definitions are never overwritten.
|
|
103
134
|
|
|
135
|
+
Portable identity deliberately excludes workstation-local `addressOverride`, identity-file, and trust-file state. A separately verified temporary IP therefore does not make the same portable host definition a different target.
|
|
136
|
+
|
|
104
137
|
`profile publish` merges new non-conflicting local target definitions into the private profile and fails on conflicts rather than silently replacing remote definitions.
|
|
105
138
|
|
|
106
139
|
### Workstation-local state
|
|
@@ -116,6 +149,8 @@ Each workstation owns its own:
|
|
|
116
149
|
|
|
117
150
|
Private SSH keys are never synchronized between computers. A Mark Windows key, Mark Mac key, Kyle Mac key, and Daniel Windows key are distinct identities even when they can reach the same remote server.
|
|
118
151
|
|
|
152
|
+
The deterministic doctor resolves and reports the actual SSH executable selected from the workstation environment; all Deployment Kit SSH operations use that resolved toolchain rather than asking the developer to mix shell-specific SSH clients manually.
|
|
153
|
+
|
|
119
154
|
## Home Lab remote onboarding
|
|
120
155
|
|
|
121
156
|
The implemented Home Lab path is password-assisted bootstrap followed by normal strict key-only operation.
|
|
@@ -164,7 +199,7 @@ The first Project on a computer may install/verify required local tools and Dock
|
|
|
164
199
|
|
|
165
200
|
### Remote Ubuntu target
|
|
166
201
|
|
|
167
|
-
Target access can
|
|
202
|
+
Target access can be established from the local workstation, but full automatic Ubuntu package preparation is still an incremental capability. The desired host progression remains:
|
|
168
203
|
|
|
169
204
|
```text
|
|
170
205
|
Ubuntu/sudo verification
|
|
@@ -175,7 +210,7 @@ Ubuntu/sudo verification
|
|
|
175
210
|
→ TARGET READY
|
|
176
211
|
```
|
|
177
212
|
|
|
178
|
-
The current platform doctor/install contract still requires host prerequisites to exist before shared-platform installation. Until a reviewed host-package bootstrap is added, `/setup remote`
|
|
213
|
+
The current platform doctor/install contract still requires host prerequisites to exist before shared-platform installation. Until a reviewed host-package bootstrap is added, `/setup remote` reports missing host packages as an actionable preparation gap rather than pretending they were installed. The deterministic setup plan keeps this as a distinct `HOST PLATFORM READY` blocker.
|
|
179
214
|
|
|
180
215
|
Healthy existing Docker/Project Forge installations are adopted/verified, not recreated. Ambiguous/incompatible existing state fails closed rather than being overwritten.
|
|
181
216
|
|
|
@@ -188,13 +223,16 @@ The implemented workstation-mediated artifact path is:
|
|
|
188
223
|
```text
|
|
189
224
|
local GitHub/GHCR authentication when needed
|
|
190
225
|
→ acquire/build exact immutable image locally
|
|
191
|
-
→
|
|
226
|
+
→ preserve registry RepoDigest as declared/release identity
|
|
227
|
+
→ resolve local Docker config/image ID
|
|
192
228
|
→ stream Docker image over verified SSH
|
|
193
229
|
→ remote docker load
|
|
194
|
-
→ verify
|
|
195
|
-
→ deploy/health-check
|
|
230
|
+
→ verify the same config/image ID remotely
|
|
231
|
+
→ deploy/health-check by that verified loaded image ID
|
|
196
232
|
```
|
|
197
233
|
|
|
234
|
+
Docker `save`/`load` does not guarantee that a registry RepoDigest remains inspectable as a remote RepoDigest after load. Project Forge therefore uses two explicit identities: registry digest for reviewed release provenance, and Docker config/image ID for transport verification and the preloaded runtime reference.
|
|
235
|
+
|
|
198
236
|
Remote Home Lab boxes therefore do not need a developer's persistent GitHub PAT for the normal workstation-mediated deployment path.
|
|
199
237
|
|
|
200
238
|
Application deployment selects this with:
|
|
@@ -203,7 +241,7 @@ Application deployment selects this with:
|
|
|
203
241
|
--image-source workstation
|
|
204
242
|
```
|
|
205
243
|
|
|
206
|
-
The legacy `--image-source remote-registry` path remains available
|
|
244
|
+
The legacy `--image-source remote-registry` path remains available for explicit target-side registry use.
|
|
207
245
|
|
|
208
246
|
A credential needed to acquire a private image is entered only through the local Docker/GitHub credential boundary. Never paste it into chat or put it on a command line.
|
|
209
247
|
|
|
@@ -275,7 +313,7 @@ When GitHub cannot be reached, profile sync can use the validated last-known-goo
|
|
|
275
313
|
|
|
276
314
|
## Migration for existing developers
|
|
277
315
|
|
|
278
|
-
Existing
|
|
316
|
+
Existing developer environments should migrate fully to this model rather than remain permanently dual-mode.
|
|
279
317
|
|
|
280
318
|
The migration contract is:
|
|
281
319
|
|
|
@@ -284,8 +322,8 @@ inventory existing workstation/targets
|
|
|
284
322
|
→ ensure private personal project-forge-profile + validated cache
|
|
285
323
|
→ adopt healthy existing Docker/SSH/trust where valid
|
|
286
324
|
→ establish workstation-mediated artifact delivery
|
|
287
|
-
→ prove
|
|
288
|
-
→ verify Production access/readiness without deploying
|
|
325
|
+
→ prove a non-Production environment end-to-end
|
|
326
|
+
→ verify Production access/readiness without deploying when Production is configured
|
|
289
327
|
→ remove/archive superseded legacy configuration only after verification
|
|
290
328
|
```
|
|
291
329
|
|
|
@@ -325,11 +363,13 @@ The intended small human surface is:
|
|
|
325
363
|
|
|
326
364
|
```text
|
|
327
365
|
/setup
|
|
366
|
+
/setup local
|
|
367
|
+
/setup remote
|
|
368
|
+
/setup <environment>
|
|
328
369
|
/progress
|
|
329
370
|
/next-step
|
|
330
|
-
/developer-env-mode status|local|remote
|
|
331
371
|
/deployment-readiness <environment>
|
|
332
372
|
/deploy <environment>
|
|
333
373
|
```
|
|
334
374
|
|
|
335
|
-
|
|
375
|
+
`/setup` is the single setup front door; the local/remote/environment forms select its scope rather than introducing a separate developer-environment command. `project-forge-deploy environment ...` commands are deterministic implementation/fallback surfaces and can be invoked directly when desired.
|
|
@@ -13,15 +13,24 @@ this workstation
|
|
|
13
13
|
→ verified target/profile access
|
|
14
14
|
→ ready shared host platform
|
|
15
15
|
→ pull small stable probe image locally
|
|
16
|
-
→ resolve exact immutable RepoDigest
|
|
17
|
-
→
|
|
18
|
-
→
|
|
16
|
+
→ resolve exact immutable registry RepoDigest
|
|
17
|
+
→ resolve local Docker config/image ID
|
|
18
|
+
→ transfer image over verified SSH
|
|
19
|
+
→ verify the same config/image ID after remote docker load
|
|
20
|
+
→ deploy with the managed application runtime using that verified loaded image ID
|
|
19
21
|
→ verify HTTP health
|
|
20
22
|
→ tear down the reserved probe runtime
|
|
21
|
-
→ verify cleanup evidence
|
|
23
|
+
→ verify cleanup and image-cache evidence
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
The current default probe image tag is `nginx:alpine`. The tag is never treated as
|
|
26
|
+
The current default probe image tag is `nginx:alpine`. The mutable tag is never treated as deployed identity.
|
|
27
|
+
|
|
28
|
+
Project Forge deliberately records two exact identities because Docker `save`/`load` does not guarantee that a registry RepoDigest remains inspectable under the same repository reference after load:
|
|
29
|
+
|
|
30
|
+
- **registry digest** — immutable reviewed artifact/release identity (`repository@sha256:...`);
|
|
31
|
+
- **Docker config/image ID** — content identity used to verify the workstation-to-target transfer and to run the preloaded image (`sha256:...`).
|
|
32
|
+
|
|
33
|
+
The registry digest remains in the deployment spec and release/proof evidence. The loaded runtime is verified and launched by the matching Docker config/image ID. A target that already has that exact image ID can reuse it without retransferring the image.
|
|
25
34
|
|
|
26
35
|
The probe uses:
|
|
27
36
|
|
|
@@ -33,6 +42,22 @@ The probe uses:
|
|
|
33
42
|
- the existing per-app/per-environment deployment lock;
|
|
34
43
|
- automatic cleanup through the existing scoped tear-down machinery.
|
|
35
44
|
|
|
45
|
+
## Setup orchestration
|
|
46
|
+
|
|
47
|
+
Before running proof commands directly, the read-only remote setup orchestrator can show whether target access and the host platform are actually ready:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
project-forge-deploy environment setup status <target> --json
|
|
51
|
+
project-forge-deploy environment setup plan <target> \
|
|
52
|
+
--owner-repo <owner/repo> \
|
|
53
|
+
--environment <name> \
|
|
54
|
+
--json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
These commands never perform the proof or authorize a write. They make `TARGET ACCESS READY`, `HOST PLATFORM READY`, and `DEPLOYMENT PATH PROVEN` separate states.
|
|
58
|
+
|
|
59
|
+
When owner/repository and logical environment context are supplied, setup planning also checks the current workstation for a still-applicable prior proof receipt before recommending another proof run.
|
|
60
|
+
|
|
36
61
|
## Commands
|
|
37
62
|
|
|
38
63
|
Read-only plan:
|
|
@@ -78,17 +103,68 @@ Production-class proof:
|
|
|
78
103
|
|
|
79
104
|
A successful proof never authorizes the later real application deployment. Production proof never authorizes a Production application deployment.
|
|
80
105
|
|
|
106
|
+
## Terminal evidence
|
|
107
|
+
|
|
108
|
+
A proof run returns a terminal result even when the operational path fails after the run begins. The result distinguishes planning from observed execution and includes, when available:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
preflight
|
|
112
|
+
registryDigest
|
|
113
|
+
localImageId
|
|
114
|
+
transferState
|
|
115
|
+
remoteImageId
|
|
116
|
+
runtimeState
|
|
117
|
+
httpHealth
|
|
118
|
+
cleanupState
|
|
119
|
+
imageCacheState
|
|
120
|
+
proof
|
|
121
|
+
mutated
|
|
122
|
+
finalMutationState
|
|
123
|
+
error
|
|
124
|
+
cleanupError
|
|
125
|
+
receipt
|
|
126
|
+
receiptError
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
A read-only plan does not claim `mutated: false` as though it were the eventual run result. The run reports whether transfer/runtime mutation actually occurred and whether cleanup completed.
|
|
130
|
+
|
|
131
|
+
A failed proof exits nonzero even when structured terminal evidence was produced successfully.
|
|
132
|
+
|
|
81
133
|
## Cleanup
|
|
82
134
|
|
|
83
135
|
The proof attempts cleanup even when deployment raises after mutation begins.
|
|
84
136
|
|
|
85
137
|
Successful completion requires terminal cleanup evidence (`removed` or already `absent`) for the reserved probe runtime. If deployment succeeds but cleanup cannot be verified, the command fails rather than reporting a clean proof.
|
|
86
138
|
|
|
139
|
+
A loaded Docker image may remain cached after runtime cleanup. That state is reported explicitly (for example `runtime-clean-image-cached`) rather than being presented as an unmodified host. Cached image content is not application runtime or persistent application data.
|
|
140
|
+
|
|
87
141
|
The probe must not delete another Project, another application, another logical environment, or shared host platform state.
|
|
88
142
|
|
|
143
|
+
## Workstation-local proof receipt
|
|
144
|
+
|
|
145
|
+
Every terminal proof run attempts to write a compact workstation-local proof receipt under the user's Project Forge evidence directory. The receipt is diagnostic/readiness evidence only; it is not deployment authorization and is not repository content.
|
|
146
|
+
|
|
147
|
+
The receipt records only non-secret evidence needed to decide whether the prior proof is still applicable, including:
|
|
148
|
+
|
|
149
|
+
- Project owner/repository;
|
|
150
|
+
- exact logical environment;
|
|
151
|
+
- logical target name and Production classification;
|
|
152
|
+
- Deployment Kit/proof contract version;
|
|
153
|
+
- proof/cleanup/final mutation state;
|
|
154
|
+
- immutable probe artifact identities when available;
|
|
155
|
+
- a hash-based signature of the current target profile, configured workstation identity fingerprint, and target trust file.
|
|
156
|
+
|
|
157
|
+
It never stores private keys, passwords, tokens, known-host contents, or application secrets.
|
|
158
|
+
|
|
159
|
+
`environment setup plan` treats the receipt as `PROVEN` only when the latest receipt passed with terminal cleanup and still matches the current Project/environment/target context and current workstation target identity/trust. A target/profile/trust change, Production-classification change, proof-contract change, or Deployment Kit version change makes the evidence stale and causes a new proof to be planned.
|
|
160
|
+
|
|
161
|
+
A **new chat or new assistant session alone does not invalidate proof**. Evidence validity is based on machine/project/environment facts, not conversation history.
|
|
162
|
+
|
|
163
|
+
A failed proof may also leave a receipt, but that receipt is explicitly `not-proven` and is never promoted to readiness.
|
|
164
|
+
|
|
89
165
|
## Relationship to `/setup` and `/deploy`
|
|
90
166
|
|
|
91
|
-
`/setup` may run this proof after the developer selects
|
|
167
|
+
`/setup remote` or `/setup <environment>` may use deterministic setup status/plan first, then run this proof after the developer selects the named remote environment and the relevant write authorization is granted.
|
|
92
168
|
|
|
93
169
|
A later first real `/deploy <environment>` can reuse fresh proof evidence for target access/platform/transport mechanics and ask only unresolved application-specific questions such as persistence, health, route/exposure, and immutable application artifact policy.
|
|
94
170
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import re
|
|
5
6
|
import subprocess
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -19,48 +20,42 @@ from .profiles import load_profile
|
|
|
19
20
|
from .ssh import ssh_args, verify_access
|
|
20
21
|
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
_IMAGE_ID_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _runtime_image_id(value: str) -> str:
|
|
27
|
+
if not isinstance(value, str) or _IMAGE_ID_RE.fullmatch(value) is None:
|
|
28
|
+
raise DeploymentKitError("Preloaded runtime image ID must be sha256:<64 lowercase hex>.")
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _preloaded_script(spec: dict[str, Any], runtime_image_id: str) -> str:
|
|
33
|
+
runtime_image = _runtime_image_id(runtime_image_id)
|
|
23
34
|
script = build_remote_script(spec)
|
|
24
35
|
registry_block = " decode_desired\n configure_registry_auth\n"
|
|
25
36
|
if registry_block not in script:
|
|
26
37
|
raise DeploymentKitError("Application runtime template no longer contains the expected registry-auth hook.")
|
|
27
38
|
script = script.replace(registry_block, " decode_desired\n", 1)
|
|
39
|
+
|
|
28
40
|
pull = ' docker pull "$D_IMAGE" >/dev/null\n'
|
|
29
|
-
replacement =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
replacement = (
|
|
42
|
+
f' if ! docker image inspect "{runtime_image}" >/dev/null 2>&1; then\n'
|
|
43
|
+
' echo "Exact application image ID was not preloaded by the workstation-mediated deployment path." >&2\n'
|
|
44
|
+
" exit 40\n"
|
|
45
|
+
" fi\n"
|
|
46
|
+
)
|
|
34
47
|
if pull not in script:
|
|
35
48
|
raise DeploymentKitError("Application runtime template no longer contains the expected image-pull hook.")
|
|
36
|
-
|
|
37
|
-
|
|
49
|
+
script = script.replace(pull, replacement, 1)
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
) -> None:
|
|
45
|
-
command = ssh_args(
|
|
46
|
-
profile,
|
|
47
|
-
address,
|
|
48
|
-
command="sudo -n bash -s -- deploy " + (expected_spec_sha or "NONE") + " " + profile["user"],
|
|
49
|
-
)
|
|
50
|
-
completed = subprocess.run(
|
|
51
|
-
command,
|
|
52
|
-
input=_preloaded_script(spec).encode("utf-8"),
|
|
53
|
-
check=False,
|
|
54
|
-
capture_output=True,
|
|
55
|
-
)
|
|
56
|
-
if completed.returncode != 0:
|
|
57
|
-
stdout = completed.stdout.decode("utf-8", errors="replace")
|
|
58
|
-
stderr = completed.stderr.decode("utf-8", errors="replace")
|
|
59
|
-
detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
|
|
60
|
-
raise DeploymentKitError(f"Workstation-mediated remote application deployment failed: {detail}")
|
|
51
|
+
runtime_hook = ' docker_args+=("$D_IMAGE")\n'
|
|
52
|
+
if runtime_hook not in script:
|
|
53
|
+
raise DeploymentKitError("Application runtime template no longer contains the expected runtime-image hook.")
|
|
54
|
+
script = script.replace(runtime_hook, f' docker_args+=("{runtime_image}")\n', 1)
|
|
55
|
+
return script
|
|
61
56
|
|
|
62
57
|
|
|
63
|
-
def
|
|
58
|
+
def _validate_plan(plan: dict[str, Any]) -> None:
|
|
64
59
|
required = {
|
|
65
60
|
"targetName",
|
|
66
61
|
"action",
|
|
@@ -87,19 +82,67 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
87
82
|
if not isinstance(plan, dict) or set(plan) != required or plan["action"] != "deploy":
|
|
88
83
|
raise DeploymentKitError("Malformed application deployment plan.")
|
|
89
84
|
|
|
90
|
-
current = app_inventory(plan["targetName"])
|
|
91
|
-
if _inventory_snapshot(current) != plan["inventorySnapshot"]:
|
|
92
|
-
raise DeploymentKitError(
|
|
93
|
-
"Application inventory changed after the plan was reviewed; create and authorize a fresh plan."
|
|
94
|
-
)
|
|
95
|
-
platform = platform_status(plan["targetName"])
|
|
96
|
-
if platform["state"] != "ready":
|
|
97
|
-
raise DeploymentKitError("Shared host platform is no longer ready; application deployment is blocked.")
|
|
98
85
|
|
|
99
|
-
|
|
86
|
+
def _validate_transfer(plan: dict[str, Any], transfer: dict[str, Any]) -> str:
|
|
87
|
+
required = {
|
|
88
|
+
"targetName",
|
|
89
|
+
"image",
|
|
90
|
+
"registryDigest",
|
|
91
|
+
"localImageId",
|
|
92
|
+
"remoteImageId",
|
|
93
|
+
"runtimeImage",
|
|
94
|
+
"status",
|
|
95
|
+
"mutated",
|
|
96
|
+
}
|
|
97
|
+
if not isinstance(transfer, dict) or set(transfer) != required:
|
|
98
|
+
raise DeploymentKitError("Malformed workstation image-transfer evidence.")
|
|
99
|
+
if transfer["targetName"] != plan["targetName"]:
|
|
100
|
+
raise DeploymentKitError("Image-transfer target does not match the deployment plan.")
|
|
101
|
+
if transfer["registryDigest"] != plan["image"] or transfer["image"] != plan["image"]:
|
|
102
|
+
raise DeploymentKitError("Image-transfer registry digest does not match the deployment plan.")
|
|
103
|
+
local_id = _runtime_image_id(transfer["localImageId"])
|
|
104
|
+
remote_id = _runtime_image_id(transfer["remoteImageId"])
|
|
105
|
+
runtime_id = _runtime_image_id(transfer["runtimeImage"])
|
|
106
|
+
if not (local_id == remote_id == runtime_id):
|
|
107
|
+
raise DeploymentKitError("Workstation and remote Docker image IDs do not match.")
|
|
108
|
+
if transfer["status"] not in {"already-present", "transferred"} or not isinstance(transfer["mutated"], bool):
|
|
109
|
+
raise DeploymentKitError("Image-transfer evidence has an invalid terminal state.")
|
|
110
|
+
return runtime_id
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _run_preloaded_deploy(
|
|
114
|
+
profile: dict[str, Any],
|
|
115
|
+
address: str,
|
|
116
|
+
spec: dict[str, Any],
|
|
117
|
+
expected_spec_sha: str,
|
|
118
|
+
runtime_image_id: str,
|
|
119
|
+
) -> None:
|
|
120
|
+
command = ssh_args(
|
|
121
|
+
profile,
|
|
122
|
+
address,
|
|
123
|
+
command="sudo -n bash -s -- deploy " + (expected_spec_sha or "NONE") + " " + profile["user"],
|
|
124
|
+
)
|
|
125
|
+
completed = subprocess.run(
|
|
126
|
+
command,
|
|
127
|
+
input=_preloaded_script(spec, runtime_image_id).encode("utf-8"),
|
|
128
|
+
check=False,
|
|
129
|
+
capture_output=True,
|
|
130
|
+
)
|
|
131
|
+
if completed.returncode != 0:
|
|
132
|
+
stdout = completed.stdout.decode("utf-8", errors="replace")
|
|
133
|
+
stderr = completed.stderr.decode("utf-8", errors="replace")
|
|
134
|
+
detail = stderr.strip() or stdout.strip() or f"exit {completed.returncode}"
|
|
135
|
+
raise DeploymentKitError(f"Workstation-mediated remote application deployment failed: {detail}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def apply_app_deploy_with_preloaded_image(
|
|
139
|
+
plan: dict[str, Any], transfer: dict[str, Any]
|
|
140
|
+
) -> dict[str, Any]:
|
|
141
|
+
"""Apply a reviewed deployment plan using already-verified transfer evidence."""
|
|
142
|
+
|
|
143
|
+
_validate_plan(plan)
|
|
144
|
+
runtime_image_id = _validate_transfer(plan, transfer)
|
|
100
145
|
|
|
101
|
-
# Image transfer can take time. Re-check the material app/platform evidence before
|
|
102
|
-
# changing runtime state; a changed plan must never ride through on stale approval.
|
|
103
146
|
current = app_inventory(plan["targetName"])
|
|
104
147
|
if _inventory_snapshot(current) != plan["inventorySnapshot"]:
|
|
105
148
|
raise DeploymentKitError(
|
|
@@ -116,6 +159,7 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
116
159
|
evidence["address"],
|
|
117
160
|
plan["spec"],
|
|
118
161
|
plan["expectedSpecSha256"] or "",
|
|
162
|
+
runtime_image_id,
|
|
119
163
|
)
|
|
120
164
|
|
|
121
165
|
final = app_inventory(plan["targetName"])
|
|
@@ -132,5 +176,22 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
132
176
|
raise DeploymentKitError("Application deployment completed but runtime readiness is not healthy.")
|
|
133
177
|
final["mutated"] = True
|
|
134
178
|
final["imageSource"] = "workstation-transfer"
|
|
179
|
+
final["imageTransfer"] = transfer
|
|
135
180
|
final["attachesSharedNetwork"] = NETWORK_NAME
|
|
136
181
|
return final
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
185
|
+
_validate_plan(plan)
|
|
186
|
+
|
|
187
|
+
current = app_inventory(plan["targetName"])
|
|
188
|
+
if _inventory_snapshot(current) != plan["inventorySnapshot"]:
|
|
189
|
+
raise DeploymentKitError(
|
|
190
|
+
"Application inventory changed after the plan was reviewed; create and authorize a fresh plan."
|
|
191
|
+
)
|
|
192
|
+
platform = platform_status(plan["targetName"])
|
|
193
|
+
if platform["state"] != "ready":
|
|
194
|
+
raise DeploymentKitError("Shared host platform is no longer ready; application deployment is blocked.")
|
|
195
|
+
|
|
196
|
+
transfer = transfer_image(plan["targetName"], plan["image"])
|
|
197
|
+
return apply_app_deploy_with_preloaded_image(plan, transfer)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CLI for portable developer environment definitions and
|
|
1
|
+
"""CLI for portable developer environment definitions and remote setup orchestration."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -15,6 +15,7 @@ from .environment_probe import (
|
|
|
15
15
|
plan_environment_proof,
|
|
16
16
|
run_environment_proof,
|
|
17
17
|
)
|
|
18
|
+
from .environment_setup import remote_setup_plan, remote_setup_status
|
|
18
19
|
from .errors import DeploymentKitError
|
|
19
20
|
from .home_lab import HOME_LAB_BOOTSTRAP_PHRASE, apply_home_lab_bootstrap, plan_home_lab_bootstrap
|
|
20
21
|
from .image_transfer import transfer_image, verify_local_image
|
|
@@ -59,6 +60,25 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
59
60
|
parser = argparse.ArgumentParser(prog="project-forge-deploy environment")
|
|
60
61
|
groups = parser.add_subparsers(dest="group", required=True)
|
|
61
62
|
|
|
63
|
+
setup = groups.add_parser(
|
|
64
|
+
"setup",
|
|
65
|
+
help="Read-only remote setup status/plan orchestration across target access, host platform, and proof readiness",
|
|
66
|
+
)
|
|
67
|
+
setup_sub = setup.add_subparsers(dest="command", required=True)
|
|
68
|
+
setup_status = setup_sub.add_parser("status", help="Show one terminal readiness model for a remote target")
|
|
69
|
+
setup_status.add_argument("name")
|
|
70
|
+
setup_status.add_argument("--json", action="store_true")
|
|
71
|
+
setup_plan = setup_sub.add_parser("plan", help="Show the exact next deterministic remote setup action")
|
|
72
|
+
setup_plan.add_argument("name")
|
|
73
|
+
setup_plan.add_argument("--owner-repo")
|
|
74
|
+
setup_plan.add_argument("--environment")
|
|
75
|
+
setup_plan.add_argument(
|
|
76
|
+
"--production",
|
|
77
|
+
action="store_true",
|
|
78
|
+
help="Classify the optional proof environment as Production",
|
|
79
|
+
)
|
|
80
|
+
setup_plan.add_argument("--json", action="store_true")
|
|
81
|
+
|
|
62
82
|
profile = groups.add_parser("profile", help="Personal/portable non-secret target profile operations")
|
|
63
83
|
profile_sub = profile.add_subparsers(dest="command", required=True)
|
|
64
84
|
|
|
@@ -178,6 +198,20 @@ def _home_lab_plan(args: argparse.Namespace) -> dict[str, Any]:
|
|
|
178
198
|
def main(argv: list[str] | None = None) -> int:
|
|
179
199
|
args = build_parser().parse_args(argv)
|
|
180
200
|
try:
|
|
201
|
+
if args.group == "setup" and args.command == "status":
|
|
202
|
+
_print(remote_setup_status(args.name), as_json=args.json)
|
|
203
|
+
return 0
|
|
204
|
+
if args.group == "setup" and args.command == "plan":
|
|
205
|
+
_print(
|
|
206
|
+
remote_setup_plan(
|
|
207
|
+
args.name,
|
|
208
|
+
owner_repo=args.owner_repo,
|
|
209
|
+
environment=args.environment,
|
|
210
|
+
production=args.production,
|
|
211
|
+
),
|
|
212
|
+
as_json=args.json,
|
|
213
|
+
)
|
|
214
|
+
return 0
|
|
181
215
|
if args.group == "profile" and args.command == "status":
|
|
182
216
|
_print(repository_status(), as_json=args.json)
|
|
183
217
|
return 0
|
|
@@ -243,16 +277,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
243
277
|
production=args.production,
|
|
244
278
|
probe_image=args.probe_image,
|
|
245
279
|
)
|
|
246
|
-
_print(plan, as_json=args.json if args.command == "plan" else False)
|
|
247
280
|
if args.command == "plan":
|
|
281
|
+
_print(plan, as_json=args.json)
|
|
248
282
|
return 0
|
|
283
|
+
if not args.json:
|
|
284
|
+
_print(plan, as_json=False)
|
|
249
285
|
_confirm(
|
|
250
286
|
args,
|
|
251
287
|
plan["requiredAuthorization"],
|
|
252
288
|
f"run a disposable deployment proof for {plan['environment']} on target {plan['targetName']}",
|
|
253
289
|
)
|
|
254
|
-
|
|
255
|
-
|
|
290
|
+
result = run_environment_proof(plan)
|
|
291
|
+
_print(result, as_json=args.json)
|
|
292
|
+
return 0 if result.get("proof") == "passed" else 2
|
|
256
293
|
raise DeploymentKitError("Unsupported environment command.")
|
|
257
294
|
except DeploymentKitError as error:
|
|
258
295
|
print(f"ERROR: {error}", file=sys.stderr)
|