@mriekki/project-forge 4.7.0 → 4.8.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/generated/ASSEMBLY.json +9 -9
- package/assets/generated/core/core/skills/deploy/SKILL.md +124 -12
- package/assets/generated/core/core/skills/deployment-readiness/SKILL.md +89 -9
- package/assets/generated/deployment/src/project_forge_deployment/app_cli.py +46 -20
- package/assets/generated/deployment/src/project_forge_deployment/app_deployment.py +100 -0
- package/assets/generated/deployment/src/project_forge_deployment/registry_access.py +135 -5
- package/assets/generated/deployment/src/project_forge_deployment/registry_cli.py +33 -4
- package/package.json +1 -1
package/assets/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.8.0",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"components": {
|
|
15
15
|
"core": {
|
|
16
|
-
"revision": "
|
|
16
|
+
"revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
|
|
17
17
|
"version": "4.0.0",
|
|
18
18
|
"distribution": "greenfield-workspace",
|
|
19
19
|
"descriptor": "assets/core/distribution.json",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"canonicalDescriptor": "distributions/greenfield-workspace.json"
|
|
22
22
|
},
|
|
23
23
|
"deployment": {
|
|
24
|
-
"revision": "
|
|
24
|
+
"revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
|
|
25
25
|
"distribution": "greenfield-project",
|
|
26
26
|
"descriptor": "assets/deployment/distribution.json",
|
|
27
27
|
"sourceRoot": "assets/generated/deployment",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.8.0",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
|
-
"revision": "
|
|
7
|
+
"revision": "a78c2482f4f559ead8efbab6c25b4b05496db735",
|
|
8
8
|
"version": "4.0.0",
|
|
9
9
|
"distribution": "greenfield-workspace",
|
|
10
10
|
"files": [
|
|
@@ -116,12 +116,12 @@
|
|
|
116
116
|
{
|
|
117
117
|
"source": "core/skills/deployment-readiness/SKILL.md",
|
|
118
118
|
"destination": ".github/skills/deployment-readiness/SKILL.md",
|
|
119
|
-
"sha256": "
|
|
119
|
+
"sha256": "30202702cc368c872e62ce731e8ffe84a796aae243b287e3e670c8c8e1cd6531"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"source": "core/skills/deploy/SKILL.md",
|
|
123
123
|
"destination": ".github/skills/deploy/SKILL.md",
|
|
124
|
-
"sha256": "
|
|
124
|
+
"sha256": "f5d40e549ca958cb51606ced7ca482651d9a436327b6070a9a94171f22401700"
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
"source": "core/skills/tear-down/SKILL.md",
|
|
@@ -326,7 +326,7 @@
|
|
|
326
326
|
]
|
|
327
327
|
},
|
|
328
328
|
"deployment": {
|
|
329
|
-
"revision": "
|
|
329
|
+
"revision": "aaf5d24c897b3d89b3d1791c79bed4979414ca40",
|
|
330
330
|
"distribution": "greenfield-project",
|
|
331
331
|
"files": [
|
|
332
332
|
{
|
|
@@ -357,12 +357,12 @@
|
|
|
357
357
|
{
|
|
358
358
|
"source": "src/project_forge_deployment/app_cli.py",
|
|
359
359
|
"destination": "tools/project-forge-deployment/project_forge_deployment/app_cli.py",
|
|
360
|
-
"sha256": "
|
|
360
|
+
"sha256": "f27704c80b1eff45888bf56745bcf8755f2fad88851147600af7056953757825"
|
|
361
361
|
},
|
|
362
362
|
{
|
|
363
363
|
"source": "src/project_forge_deployment/app_deployment.py",
|
|
364
364
|
"destination": "tools/project-forge-deployment/project_forge_deployment/app_deployment.py",
|
|
365
|
-
"sha256": "
|
|
365
|
+
"sha256": "606d6a4793ac8c8a95da4cd4c61792c69f02ad86900be1944d2ed570dd71e73e"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"source": "src/project_forge_deployment/app_runtime.py",
|
|
@@ -502,12 +502,12 @@
|
|
|
502
502
|
{
|
|
503
503
|
"source": "src/project_forge_deployment/registry_access.py",
|
|
504
504
|
"destination": "tools/project-forge-deployment/project_forge_deployment/registry_access.py",
|
|
505
|
-
"sha256": "
|
|
505
|
+
"sha256": "1f582f1b5da656579625748b13c779f6d67ed6a6049f0ebb104ce8c3f9185a4b"
|
|
506
506
|
},
|
|
507
507
|
{
|
|
508
508
|
"source": "src/project_forge_deployment/registry_cli.py",
|
|
509
509
|
"destination": "tools/project-forge-deployment/project_forge_deployment/registry_cli.py",
|
|
510
|
-
"sha256": "
|
|
510
|
+
"sha256": "2ce977fa570580407c04a3e59d0a524f128af420b7ee20fd9706e6452378b7b3"
|
|
511
511
|
},
|
|
512
512
|
{
|
|
513
513
|
"source": "src/project_forge_deployment/route_registry.py",
|
|
@@ -44,7 +44,7 @@ Read only what is needed, including when present:
|
|
|
44
44
|
- `PROJECT_FORGE_BASELINE.json`;
|
|
45
45
|
- `DEPLOYMENT_BASELINE.json`;
|
|
46
46
|
- project topology and approved deployment/release requirements;
|
|
47
|
-
- `/deployment-readiness` evidence, including deterministic application isolation evidence when supported;
|
|
47
|
+
- `/deployment-readiness` evidence, including first-application outcome assessment and deterministic application isolation evidence when supported;
|
|
48
48
|
- fresh setup-time environment-proof evidence for this workstation;
|
|
49
49
|
- established application deployment facts.
|
|
50
50
|
|
|
@@ -62,7 +62,27 @@ Proof does not establish the real application's image/release, persistence/migra
|
|
|
62
62
|
|
|
63
63
|
A prior readiness isolation fingerprint is review evidence only. It does not authorize deployment and does not replace the deterministic deployment planner's current isolation revalidation.
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
For a first real deployment, do not collapse a target/platform `READY` result into application deployability. Confirm that readiness established an actual artifact build/publication/acquisition path when that path is material. If artifact identity or acquisition is still hypothetical, return to the missing prerequisite rather than requesting the application write gate.
|
|
66
|
+
|
|
67
|
+
## 3. Preserve execution and credential context
|
|
68
|
+
|
|
69
|
+
When installed tooling exposes structured capability evidence, preserve its semantics exactly in the deployment assessment and preview. In particular, retain fields equivalent to:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
executionLocation
|
|
73
|
+
credentialContext
|
|
74
|
+
capabilityProved
|
|
75
|
+
capabilityAttempted
|
|
76
|
+
doesNotProve
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A registry manifest check executed on a remote target proves only the capability reported for that target/context. Never describe it as workstation registry access. A workstation registry check proves only the workstation capability it reports. Neither implies Actions registry transport or package-metadata API access.
|
|
80
|
+
|
|
81
|
+
Do not infer provider-specific token scopes or error meaning from generic registry results. Provider-specific package metadata/publication behavior belongs to repository/provider tooling and its own evidence.
|
|
82
|
+
|
|
83
|
+
When a new credential is genuinely required, explain its purpose, execution context, storage location, retrievability, and rotation impact before asking the developer to create it. Credential values remain in the provider/local secret surface and never enter chat.
|
|
84
|
+
|
|
85
|
+
## 4. First real deployment asks only unresolved application questions
|
|
66
86
|
|
|
67
87
|
The first real `/deploy <environment>` after environment proof may need a small application-specific completion step. Ask only material unknowns not already answered by project evidence, such as:
|
|
68
88
|
|
|
@@ -79,7 +99,7 @@ When a bounded choice is useful, follow `INTERACTIVE_SESSION_GUIDE.md`: use host
|
|
|
79
99
|
|
|
80
100
|
Once these facts are durably established, later deployments should be routine rather than repeating onboarding.
|
|
81
101
|
|
|
82
|
-
##
|
|
102
|
+
## 5. Placement determines launcher
|
|
83
103
|
|
|
84
104
|
### Workstation container
|
|
85
105
|
|
|
@@ -114,7 +134,7 @@ Use only an installed repository/catalog provider adapter. If none exists, stop
|
|
|
114
134
|
|
|
115
135
|
Report `DEPLOYMENT NOT BOUND YET` and route to `/setup <environment>` or `/deployment-readiness <environment>`.
|
|
116
136
|
|
|
117
|
-
##
|
|
137
|
+
## 6. Preserve custom Production classification
|
|
118
138
|
|
|
119
139
|
Pass the exact configured environment name to the deterministic launcher.
|
|
120
140
|
|
|
@@ -126,7 +146,7 @@ When resolved purpose/classification is Production, use the installed tool's exp
|
|
|
126
146
|
|
|
127
147
|
Inspect installed help/version evidence. If the installed tool cannot preserve Production classification for a custom name, stop rather than downgrading safety.
|
|
128
148
|
|
|
129
|
-
##
|
|
149
|
+
## 7. Build the deterministic plan
|
|
130
150
|
|
|
131
151
|
Use repository-provided tooling to construct/inspect the exact application deployment plan. Supported operations may include:
|
|
132
152
|
|
|
@@ -149,7 +169,44 @@ The deterministic deployment tool owns the final current-state recheck immediate
|
|
|
149
169
|
|
|
150
170
|
Do not smuggle infrastructure provisioning, target/provider rebinding, destructive restore, or environment tear-down into the application plan.
|
|
151
171
|
|
|
152
|
-
|
|
172
|
+
### Remote runtime + route rollout preview
|
|
173
|
+
|
|
174
|
+
When the installed Deployment Kit exposes a deterministic read-only `app rollout` capability and the selected remote deployment has an intended route, prefer that capability for the final pre-write preview rather than independently narrating runtime and route consequences.
|
|
175
|
+
|
|
176
|
+
A current installed contract may expose a command shape similar to:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
project-forge-deploy app rollout <target>
|
|
180
|
+
--owner-repo <owner/repository>
|
|
181
|
+
--environment <exact environment>
|
|
182
|
+
--app-slug <application slug>
|
|
183
|
+
--container-name <container>
|
|
184
|
+
--runtime-port <port>
|
|
185
|
+
--persistence-mode <none|directory>
|
|
186
|
+
--image <immutable digest>
|
|
187
|
+
--health-path <path>
|
|
188
|
+
--run-as <uid[:gid]>
|
|
189
|
+
[--route-hostname <hostname> ...]
|
|
190
|
+
[--production]
|
|
191
|
+
--json
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Inspect installed help/version rather than assuming this example exactly matches every package version.
|
|
195
|
+
|
|
196
|
+
Treat the returned staged rollout as **read-only review evidence**. Preserve:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
runtime plan + isolation evidence
|
|
200
|
+
route plan + route-inventory evidence
|
|
201
|
+
separate stage authorizations
|
|
202
|
+
endpoint state before runtime
|
|
203
|
+
after-runtime / before-route endpoint state
|
|
204
|
+
after-route verification state
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
If the installed package lacks this composite preview, retain the existing safe runtime and route planners and state their sequence explicitly. Do not invent a combined write command.
|
|
208
|
+
|
|
209
|
+
## 8. Preserve exact authorization contracts
|
|
153
210
|
|
|
154
211
|
Core consequence labels do not replace stricter installed-tool phrases.
|
|
155
212
|
|
|
@@ -158,17 +215,20 @@ The standard Deployment Kit may use exact phrases such as:
|
|
|
158
215
|
```text
|
|
159
216
|
[APP DEPLOY WRITE]
|
|
160
217
|
[PRODUCTION APP DEPLOY]
|
|
218
|
+
[ROUTE WRITE]
|
|
161
219
|
```
|
|
162
220
|
|
|
163
221
|
For Production-class environments also show `[PRODUCTION WRITE]` consequence visibility where Core policy calls for it.
|
|
164
222
|
|
|
165
|
-
Resolve the exact phrase from the installed tool/version. Do not assume an example phrase when the installed contract differs.
|
|
223
|
+
Resolve the exact phrase from the installed tool/version. For a staged rollout, resolve it separately for each stage. Do not assume an example phrase when the installed contract differs.
|
|
224
|
+
|
|
225
|
+
Authorization applies only to the exact environment, resolved classification, placement/target/provider, artifact, reviewed plan/stage, current isolation evidence and route evidence, and evidence shown. Material changes require renewed review/authorization.
|
|
166
226
|
|
|
167
|
-
|
|
227
|
+
A runtime authorization never authorizes the route stage. A route authorization never retroactively authorizes a changed runtime artifact. If the rollout contains both stages, preserve the separate gates even though the Builder reviewed one coherent outcome.
|
|
168
228
|
|
|
169
|
-
##
|
|
229
|
+
## 9. Preview and execute only the approved contract
|
|
170
230
|
|
|
171
|
-
Before mutation, show concise non-secret evidence such as exact environment, purpose, placement, target/provider when applicable, proof freshness, immutable application artifact, persistence/migration, exposure/route, application isolation state/fingerprint when exposed, shared failure-domain note when relevant, planned
|
|
231
|
+
Before mutation, show concise non-secret evidence such as exact environment, purpose, placement, target/provider when applicable, proof freshness, immutable application artifact, persistence/migration, exposure/route, application isolation state/fingerprint when exposed, shared failure-domain note when relevant, planned stages, endpoint state at each stage, and the exact required authorization phrase for the next consequence only.
|
|
172
232
|
|
|
173
233
|
After authorization, invoke only the repository-provided deterministic launcher. Never redirect to another environment/host/provider, weaken trust/exposure, force through lock contention, or invent recovery writes.
|
|
174
234
|
|
|
@@ -176,7 +236,37 @@ The deterministic launcher must revalidate current isolation immediately before
|
|
|
176
236
|
|
|
177
237
|
Existing per-app/per-environment locks are normal concurrency protection when another workstation is deploying the same environment.
|
|
178
238
|
|
|
179
|
-
|
|
239
|
+
### Runtime succeeded but route is pending
|
|
240
|
+
|
|
241
|
+
A healthy application runtime is a valid intermediate outcome. When a requested route has not yet been registered, say this explicitly before showing the route gate:
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
APPLICATION RUNTIME READY
|
|
245
|
+
Expected endpoint: <hostname/URL>
|
|
246
|
+
Endpoint state: NOT ACTIVE — ROUTE PENDING
|
|
247
|
+
Do not test this URL yet; the route write has not been authorized/applied.
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Then, if the reviewed route plan is still current, present the route consequence directly. Do not require the developer to invoke `/deploy` or another slash command merely to rediscover the already reviewed route stage.
|
|
251
|
+
|
|
252
|
+
The runtime success does not consume or imply route authorization. If route evidence changed, rebuild/review the route plan first.
|
|
253
|
+
|
|
254
|
+
## 10. Resume the established outcome without navigation ceremony
|
|
255
|
+
|
|
256
|
+
Within the same conversation, if a bounded engineering/delivery correction or external prerequisite completes and the developer's requested deployment outcome remains unambiguous, automatically resume safe **read-only** reconstruction of readiness/deployment evidence from the repository, GitHub, installed tools, and runtime state.
|
|
257
|
+
|
|
258
|
+
Do not require the developer to type `/deliver`, `/deployment-readiness`, or `/deploy` merely as navigation to rediscover known state after that bounded action completes.
|
|
259
|
+
|
|
260
|
+
This continuation:
|
|
261
|
+
|
|
262
|
+
- never repeats a completed write;
|
|
263
|
+
- never reuses exhausted registry/Production/route/merge/migration/destructive authority;
|
|
264
|
+
- stops when the artifact, plan, target, consequence, or other material evidence changed;
|
|
265
|
+
- does not create a parallel persistent workflow database.
|
|
266
|
+
|
|
267
|
+
If `/deploy` is invoked again after an already validated deployment, first reconstruct current deployment evidence. If the same validated artifact/runtime/route remains current and no new deployment outcome is requested, report the existing validated state rather than proposing or performing a redundant redeployment.
|
|
268
|
+
|
|
269
|
+
## 11. Validate live evidence
|
|
180
270
|
|
|
181
271
|
After execution, use repository-provided inventory/health/route/smoke/evidence commands when available.
|
|
182
272
|
|
|
@@ -191,10 +281,32 @@ Artifact: <exact immutable identity>
|
|
|
191
281
|
Artifact delivery: <workstation-transfer|remote-registry|local-runtime|provider>
|
|
192
282
|
Endpoint/Route: READY / LOOPBACK / NOT APPLICABLE
|
|
193
283
|
Application health: READY
|
|
284
|
+
Workstation/browser verification: PASSED / NOT APPLICABLE / PENDING
|
|
194
285
|
Smoke: PASSED / NOT REQUIRED
|
|
195
286
|
Deployment evidence: RECORDED / VERIFIED
|
|
196
287
|
```
|
|
197
288
|
|
|
198
|
-
Do not report success merely because a container/process started when stronger health evidence is required.
|
|
289
|
+
Do not report success merely because a container/process started when stronger health evidence is required. Do not report a route as user-accessible merely because registration returned; perform the repository-supported endpoint/workstation/browser verification when the requested outcome depends on it.
|
|
199
290
|
|
|
200
291
|
A setup-time disposable environment proof is not real application deployment evidence. A successful application deployment never authorizes a later Production write or destructive `/tear-down`.
|
|
292
|
+
|
|
293
|
+
Keep human access confirmation distinct from automated browser/health evidence. The developer saying they can reach the endpoint is useful observed human evidence; it is not automatically formal work-item acceptance.
|
|
294
|
+
|
|
295
|
+
## 12. Reconcile the durable outcome without asserting acceptance
|
|
296
|
+
|
|
297
|
+
After `DEPLOYMENT VALIDATED`, inspect the repository's configured work-item/operating authority. When an existing current-status or deployment-result section is stale and the observed deployment outcome is material, offer a bounded reconciliation containing only supported non-secret facts such as:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
exact environment
|
|
301
|
+
release/source/artifact identity
|
|
302
|
+
artifact delivery method
|
|
303
|
+
runtime health result
|
|
304
|
+
route/endpoint
|
|
305
|
+
workstation/browser validation
|
|
306
|
+
explicit human access confirmation, when actually stated
|
|
307
|
+
known limitations and evidence boundaries
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Preserve historical approval records. Do not silently rewrite product requirements, broaden the milestone, mark formal acceptance, close the work item, or infer acceptance from browser accessibility.
|
|
311
|
+
|
|
312
|
+
If the human authorizes the bounded documentation update, use the repository's normal work-item/delivery workflow for that edit. Documentation reconciliation is not a reason to redeploy and does not create new Production write authority.
|
|
@@ -78,7 +78,78 @@ It does not prove final application artifact identity, persistence/migration beh
|
|
|
78
78
|
|
|
79
79
|
If topology, target binding, host trust, platform state, or another material fact changes, treat the proof as stale and reverify the affected prerequisite.
|
|
80
80
|
|
|
81
|
-
## 3.
|
|
81
|
+
## 3. First-application outcome assessment
|
|
82
|
+
|
|
83
|
+
For the first real application deployment to an environment, or whenever the developer's stated goal is a usable endpoint rather than only machine readiness, keep that requested outcome visible and assess the whole material path before declaring the application deploy-ready.
|
|
84
|
+
|
|
85
|
+
Inspect available non-secret repository, package/tool, workstation, target, and GitHub/CI evidence before asking for facts. Do not require a release workflow to fail merely to discover a prerequisite that can be identified read-only.
|
|
86
|
+
|
|
87
|
+
Build a compact prerequisite matrix with rows such as:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
requested outcome / expected endpoint
|
|
91
|
+
workstation architecture
|
|
92
|
+
required artifact/target architecture
|
|
93
|
+
immutable artifact or release identity policy
|
|
94
|
+
artifact destination / publication state when relevant
|
|
95
|
+
credential capability + execution location for each required operation
|
|
96
|
+
workstation artifact acquisition capability
|
|
97
|
+
target delivery method
|
|
98
|
+
current application isolation / footprint
|
|
99
|
+
runtime health contract
|
|
100
|
+
route / exposure mechanism
|
|
101
|
+
final workstation/browser verification
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
For each material row distinguish:
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
evidence source
|
|
108
|
+
execution location / credential context
|
|
109
|
+
PROVEN / UNKNOWN / BLOCKED / NOT APPLICABLE
|
|
110
|
+
missing fact or prerequisite
|
|
111
|
+
exact consequential gate, if any
|
|
112
|
+
effect on the requested endpoint
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Architecture is a portability fact, not an operating-system stereotype. For example, an ARM64 workstation targeting a Linux/x86-64 environment must surface the required artifact platform explicitly; do not label that generically as a macOS defect.
|
|
116
|
+
|
|
117
|
+
Do not report `ENVIRONMENT READY TO DEPLOY` for the first real application while its actual artifact build/publication/acquisition path is still hypothetical. Target/platform readiness and application-deployment readiness are different dimensions.
|
|
118
|
+
|
|
119
|
+
Keep these evidence dimensions separate in status language:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
static/review correctness
|
|
123
|
+
live external-contract confidence
|
|
124
|
+
credential capability in the exact execution context
|
|
125
|
+
immutable artifact provenance
|
|
126
|
+
environment/runtime readiness
|
|
127
|
+
route state
|
|
128
|
+
workstation/browser validation
|
|
129
|
+
human access confirmation
|
|
130
|
+
formal work-item acceptance
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A green result in one dimension never proves another.
|
|
134
|
+
|
|
135
|
+
### Credential capability and lifecycle
|
|
136
|
+
|
|
137
|
+
Reason from **operation + execution context**, not from the existence of a generic token or successful access somewhere else. When installed tooling exposes structured registry capability evidence, preserve fields such as execution location, credential context, capability proved, and `doesNotProve` rather than paraphrasing a target result as workstation access.
|
|
138
|
+
|
|
139
|
+
Before asking the developer to create or rotate a credential, explain non-secret lifecycle facts first:
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
purpose / capability
|
|
143
|
+
machine, workflow, or process where it is used
|
|
144
|
+
where the credential will be stored
|
|
145
|
+
whether its value can be retrieved again
|
|
146
|
+
rotation/regeneration impact on existing consumers
|
|
147
|
+
value must remain in the local/provider secret surface, never chat
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Do not assume registry transport access proves package-metadata API access, that target registry access proves workstation access, or that a workstation login proves an Actions workflow capability. Provider-specific token types, scopes, and error-body strings belong to installed/provider-owned adapters or Project evidence, not generic Core invention.
|
|
151
|
+
|
|
152
|
+
## 4. Placement branches
|
|
82
153
|
|
|
83
154
|
### A. Workstation placement
|
|
84
155
|
|
|
@@ -188,7 +259,7 @@ DEPLOYMENT INTENTIONALLY DEFERRED
|
|
|
188
259
|
|
|
189
260
|
Do not mark workspace readiness failed. Route to `/setup <environment>` or `/setup remote` if the developer later wants to bind it.
|
|
190
261
|
|
|
191
|
-
##
|
|
262
|
+
## 5. Shared failure domains / colocated environments
|
|
192
263
|
|
|
193
264
|
Multiple logical environments may share physical failure domains, but they remain independently targetable and independently authorized. If environments share physical failure, state it explicitly.
|
|
194
265
|
|
|
@@ -196,7 +267,7 @@ A clear application isolation preflight means the current Deployment Kit-managed
|
|
|
196
267
|
|
|
197
268
|
Colocation does not demonstrate host-level Production isolation. Never infer readiness for one logical environment from another merely because they share hardware.
|
|
198
269
|
|
|
199
|
-
##
|
|
270
|
+
## 6. State-driven cadence
|
|
200
271
|
|
|
201
272
|
Normal cadence is:
|
|
202
273
|
|
|
@@ -205,6 +276,7 @@ select exact project-defined environment
|
|
|
205
276
|
→ read current topology
|
|
206
277
|
→ reuse fresh environment-proof evidence when valid
|
|
207
278
|
→ rediscover placement-relevant local state
|
|
279
|
+
→ for first real deployment, assess the complete artifact/credential/delivery/route/endpoint path
|
|
208
280
|
→ verify current application isolation when the installed adapter supports it
|
|
209
281
|
→ verify/repair only missing prerequisites under exact gates
|
|
210
282
|
→ ENVIRONMENT READY TO DEPLOY
|
|
@@ -213,7 +285,9 @@ select exact project-defined environment
|
|
|
213
285
|
|
|
214
286
|
For remote placement, reuse valid existing target access rather than creating duplicate credentials simply because another Project/environment uses the same machine.
|
|
215
287
|
|
|
216
|
-
|
|
288
|
+
When a bounded engineering/delivery correction completes in the same conversation and the developer's deployment outcome is still clear, automatically resume safe read-only readiness reconstruction from current repository/tool evidence. Do not require the developer to type `/deployment-readiness` again merely as navigation. This continuation does not reuse an exhausted write authorization and does not authorize a new registry, Production, route, migration, merge, or other consequential write.
|
|
289
|
+
|
|
290
|
+
## 7. Read-only status
|
|
217
291
|
|
|
218
292
|
`/deployment-readiness status [environment]` performs no remote writes. Without a specific environment, summarize all known environments from project topology, including Placement and deferred state where evidenced.
|
|
219
293
|
|
|
@@ -229,7 +303,7 @@ Archive demo deferred NOT APPLICABLE DEFERRED
|
|
|
229
303
|
|
|
230
304
|
Do not manufacture target/provider facts for deferred environments.
|
|
231
305
|
|
|
232
|
-
##
|
|
306
|
+
## 8. Bounded readiness writes
|
|
233
307
|
|
|
234
308
|
A missing prerequisite is not authorization to change it. Use the repository/tool's exact authorization phrase for key/trust/platform/provider mutations.
|
|
235
309
|
|
|
@@ -237,21 +311,23 @@ Do not hide the real application deployment inside readiness. Readiness may rely
|
|
|
237
311
|
|
|
238
312
|
The application isolation preflight itself is read-only and does not authorize later deployment, route, persistence, migration, Production, or destructive writes.
|
|
239
313
|
|
|
240
|
-
##
|
|
314
|
+
## 9. Pre-deploy versus post-deploy evidence
|
|
241
315
|
|
|
242
316
|
Readiness asks whether `/deploy <name>` can safely perform the real application deployment now.
|
|
243
317
|
|
|
244
|
-
Pre-deploy evidence may include topology, adapter availability, binding, local/remote identity and access, target platform, fresh proof evidence, artifact policy, deterministic application isolation/collision evidence, route/port availability, persistence/backup/migration prerequisites, lock/recovery capability, and Production-specific safeguards.
|
|
318
|
+
Pre-deploy evidence may include topology, adapter availability, binding, local/remote identity and access, target platform, fresh proof evidence, artifact policy, actual artifact acquisition/publication capability when material, deterministic application isolation/collision evidence, route/port availability, persistence/backup/migration prerequisites, lock/recovery capability, and Production-specific safeguards.
|
|
245
319
|
|
|
246
320
|
Do not require first-deployment live route, deployed-release identity, application health, or smoke evidence before the application has ever been deployed. Those are post-deploy validation evidence owned by `/deploy`.
|
|
247
321
|
|
|
248
|
-
|
|
322
|
+
The intended endpoint may still be known before deployment. When the route does not yet exist, label it explicitly `NOT ACTIVE — ROUTE PENDING`; do not invite the developer to test it as though readiness had made it reachable.
|
|
323
|
+
|
|
324
|
+
## 10. Production policy
|
|
249
325
|
|
|
250
326
|
Production is logical policy, not hardware count and not the literal name `production`. A named environment such as `prod-east` with purpose `production` must retain Production readiness and later deployment gates.
|
|
251
327
|
|
|
252
328
|
Readiness never authorizes Production application deployment.
|
|
253
329
|
|
|
254
|
-
##
|
|
330
|
+
## 11. Final handoff
|
|
255
331
|
|
|
256
332
|
When pre-deploy requirements are satisfied:
|
|
257
333
|
|
|
@@ -263,10 +339,14 @@ Purpose: <purpose>
|
|
|
263
339
|
Placement: <...>
|
|
264
340
|
Binding: VERIFIED / LOCAL / provider-specific
|
|
265
341
|
This-workstation environment proof: PROVEN / NOT REQUIRED / NOT AVAILABLE
|
|
342
|
+
Artifact path: PROVEN / NOT APPLICABLE
|
|
343
|
+
Credential contexts: PROVEN / NOT APPLICABLE
|
|
266
344
|
Application isolation: CLEAR / NOT APPLICABLE
|
|
267
345
|
Isolation evidence: <evidenceSha256 when exposed by installed tooling>
|
|
268
346
|
Platform prerequisites: READY
|
|
269
347
|
Application-specific prerequisites: READY / <remaining items>
|
|
348
|
+
Expected endpoint: <URL/route when known>
|
|
349
|
+
Endpoint state: NOT ACTIVE — ROUTE PENDING / LOOPBACK PENDING / provider-specific
|
|
270
350
|
Security blockers: NONE
|
|
271
351
|
|
|
272
352
|
POST-DEPLOY VALIDATION PENDING
|
|
@@ -12,6 +12,7 @@ from .app_deployment import (
|
|
|
12
12
|
apply_app_deploy,
|
|
13
13
|
apply_app_deploy_from_workstation,
|
|
14
14
|
plan_app_deploy,
|
|
15
|
+
plan_app_rollout,
|
|
15
16
|
)
|
|
16
17
|
from .app_runtime import (
|
|
17
18
|
APP_DEPLOY_WRITE_PHRASE,
|
|
@@ -85,6 +86,29 @@ def _add_isolation_footprint(parser: argparse.ArgumentParser) -> None:
|
|
|
85
86
|
)
|
|
86
87
|
|
|
87
88
|
|
|
89
|
+
def _add_runtime_contract(parser: argparse.ArgumentParser) -> None:
|
|
90
|
+
parser.add_argument("--image", required=True)
|
|
91
|
+
parser.add_argument("--health-path", required=True)
|
|
92
|
+
parser.add_argument("--run-as", required=True)
|
|
93
|
+
parser.add_argument("--data-container-path")
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--env",
|
|
96
|
+
action="append",
|
|
97
|
+
default=[],
|
|
98
|
+
help="Non-secret runtime environment value in NAME=value form; repeat as needed",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--image-source",
|
|
102
|
+
choices=("remote-registry", "workstation"),
|
|
103
|
+
default="remote-registry",
|
|
104
|
+
help=(
|
|
105
|
+
"remote-registry preserves the existing target-side pull path; workstation streams an exact local image "
|
|
106
|
+
"over verified SSH so the target does not need registry credentials"
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument("--json", action="store_true")
|
|
110
|
+
|
|
111
|
+
|
|
88
112
|
def build_parser() -> argparse.ArgumentParser:
|
|
89
113
|
parser = argparse.ArgumentParser(prog="project-forge-deploy app")
|
|
90
114
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
@@ -108,31 +132,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
108
132
|
)
|
|
109
133
|
isolation.add_argument("--json", action="store_true")
|
|
110
134
|
|
|
135
|
+
rollout = sub.add_parser(
|
|
136
|
+
"rollout",
|
|
137
|
+
help="Preview application runtime and owned route as one read-only staged rollout",
|
|
138
|
+
)
|
|
139
|
+
_add_isolation_footprint(rollout)
|
|
140
|
+
_add_runtime_contract(rollout)
|
|
141
|
+
|
|
111
142
|
deploy = sub.add_parser(
|
|
112
143
|
"deploy",
|
|
113
144
|
help="Deploy one immutable application runtime without modifying routes",
|
|
114
145
|
)
|
|
115
146
|
_add_isolation_footprint(deploy)
|
|
116
|
-
deploy
|
|
117
|
-
deploy.add_argument("--health-path", required=True)
|
|
118
|
-
deploy.add_argument("--run-as", required=True)
|
|
119
|
-
deploy.add_argument("--data-container-path")
|
|
120
|
-
deploy.add_argument(
|
|
121
|
-
"--env",
|
|
122
|
-
action="append",
|
|
123
|
-
default=[],
|
|
124
|
-
help="Non-secret runtime environment value in NAME=value form; repeat as needed",
|
|
125
|
-
)
|
|
126
|
-
deploy.add_argument(
|
|
127
|
-
"--image-source",
|
|
128
|
-
choices=("remote-registry", "workstation"),
|
|
129
|
-
default="remote-registry",
|
|
130
|
-
help=(
|
|
131
|
-
"remote-registry preserves the existing target-side pull path; workstation streams an exact local image "
|
|
132
|
-
"over verified SSH so the target does not need registry credentials"
|
|
133
|
-
),
|
|
134
|
-
)
|
|
135
|
-
deploy.add_argument("--json", action="store_true")
|
|
147
|
+
_add_runtime_contract(deploy)
|
|
136
148
|
deploy.add_argument("--yes", action="store_true")
|
|
137
149
|
return parser
|
|
138
150
|
|
|
@@ -175,6 +187,20 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
175
187
|
environment_variables=_parse_env(args.env),
|
|
176
188
|
)
|
|
177
189
|
assert_app_spec_matches_persistence(args.name, spec)
|
|
190
|
+
|
|
191
|
+
if args.command == "rollout":
|
|
192
|
+
rollout_plan = plan_app_rollout(
|
|
193
|
+
args.name,
|
|
194
|
+
spec,
|
|
195
|
+
production=production_classified,
|
|
196
|
+
database_file=args.database_file,
|
|
197
|
+
route_hostnames=args.route_hostname,
|
|
198
|
+
)
|
|
199
|
+
rollout_plan["imageSource"] = args.image_source
|
|
200
|
+
rollout_plan["targetRegistryCredentialRequired"] = args.image_source == "remote-registry"
|
|
201
|
+
_print(rollout_plan, as_json=args.json)
|
|
202
|
+
return 0
|
|
203
|
+
|
|
178
204
|
plan = plan_app_deploy(
|
|
179
205
|
args.name,
|
|
180
206
|
spec,
|
|
@@ -10,15 +10,19 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Any, Iterable
|
|
12
12
|
|
|
13
|
+
from .app_runtime import PRODUCTION_APP_DEPLOY_WRITE_PHRASE
|
|
13
14
|
from .app_runtime import apply_app_deploy as _apply_runtime_deploy
|
|
15
|
+
from .app_runtime import deploy_write_phrase
|
|
14
16
|
from .app_runtime import plan_app_deploy as _plan_runtime_deploy
|
|
15
17
|
from .app_runtime_workstation import apply_app_deploy_with_preloaded_image
|
|
16
18
|
from .environment_isolation import environment_isolation_preflight
|
|
17
19
|
from .errors import DeploymentKitError
|
|
18
20
|
from .image_transfer import transfer_image
|
|
21
|
+
from .route_registry import ROUTE_WRITE_PHRASE, plan_route_registration
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
ISOLATION_REVIEW_KEY = "isolationReview"
|
|
25
|
+
ROLLOUT_SCHEMA_VERSION = 1
|
|
22
26
|
|
|
23
27
|
|
|
24
28
|
def app_isolation_check(
|
|
@@ -139,6 +143,100 @@ def plan_app_deploy(
|
|
|
139
143
|
return plan
|
|
140
144
|
|
|
141
145
|
|
|
146
|
+
def _endpoint_states(route_plan: dict[str, Any] | None) -> dict[str, str]:
|
|
147
|
+
if route_plan is None:
|
|
148
|
+
return {
|
|
149
|
+
"beforeRuntime": "NO_ROUTE_REQUESTED",
|
|
150
|
+
"afterRuntimeBeforeRoute": "NO_ROUTE_REQUESTED",
|
|
151
|
+
"afterRoute": "NO_ROUTE_REQUESTED",
|
|
152
|
+
}
|
|
153
|
+
if route_plan["changeType"] == "create":
|
|
154
|
+
before = "NOT_ACTIVE_ROUTE_ABSENT"
|
|
155
|
+
after_runtime = "NOT_ACTIVE_ROUTE_PENDING"
|
|
156
|
+
else:
|
|
157
|
+
before = "ROUTE_PRESENT_RUNTIME_STATE_NOT_VALIDATED"
|
|
158
|
+
after_runtime = "ROUTE_PRESENT_ROUTE_CHANGE_PENDING"
|
|
159
|
+
return {
|
|
160
|
+
"beforeRuntime": before,
|
|
161
|
+
"afterRuntimeBeforeRoute": after_runtime,
|
|
162
|
+
"afterRoute": "ROUTE_REGISTERED_BROWSER_VERIFICATION_PENDING",
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def plan_app_rollout(
|
|
167
|
+
target_name: str,
|
|
168
|
+
spec: dict[str, Any],
|
|
169
|
+
*,
|
|
170
|
+
production: bool = False,
|
|
171
|
+
database_file: str | None = None,
|
|
172
|
+
route_hostnames: Iterable[str] | None = None,
|
|
173
|
+
) -> dict[str, Any]:
|
|
174
|
+
"""Compose read-only runtime and route plans into one staged rollout preview."""
|
|
175
|
+
|
|
176
|
+
hosts = list(route_hostnames or [])
|
|
177
|
+
app_plan = plan_app_deploy(
|
|
178
|
+
target_name,
|
|
179
|
+
spec,
|
|
180
|
+
production=production,
|
|
181
|
+
database_file=database_file,
|
|
182
|
+
route_hostnames=hosts,
|
|
183
|
+
)
|
|
184
|
+
route_plan = None
|
|
185
|
+
if hosts:
|
|
186
|
+
route_plan = plan_route_registration(
|
|
187
|
+
target_name,
|
|
188
|
+
owner_repo=spec["ownerRepo"],
|
|
189
|
+
environment=spec["environment"],
|
|
190
|
+
app_slug=spec["appSlug"],
|
|
191
|
+
upstream_container=spec["containerName"],
|
|
192
|
+
upstream_port=spec["runtimePort"],
|
|
193
|
+
hostnames=hosts,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
runtime_authorization = (
|
|
197
|
+
PRODUCTION_APP_DEPLOY_WRITE_PHRASE
|
|
198
|
+
if production or spec["environment"] == "production"
|
|
199
|
+
else deploy_write_phrase(app_plan)
|
|
200
|
+
)
|
|
201
|
+
stages = [
|
|
202
|
+
{
|
|
203
|
+
"stage": "runtime",
|
|
204
|
+
"action": "deploy-application-runtime",
|
|
205
|
+
"requiredAuthorization": runtime_authorization,
|
|
206
|
+
"mutated": False,
|
|
207
|
+
}
|
|
208
|
+
]
|
|
209
|
+
if route_plan is not None:
|
|
210
|
+
stages.append(
|
|
211
|
+
{
|
|
212
|
+
"stage": "route",
|
|
213
|
+
"action": "register-application-route",
|
|
214
|
+
"requiredAuthorization": ROUTE_WRITE_PHRASE,
|
|
215
|
+
"mutated": False,
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
runtime_view = {key: value for key, value in app_plan.items() if key != "spec"}
|
|
220
|
+
return {
|
|
221
|
+
"schemaVersion": ROLLOUT_SCHEMA_VERSION,
|
|
222
|
+
"targetName": target_name,
|
|
223
|
+
"environment": spec["environment"],
|
|
224
|
+
"production": bool(production or spec["environment"] == "production"),
|
|
225
|
+
"image": spec["image"],
|
|
226
|
+
"runtimePlan": runtime_view,
|
|
227
|
+
"routePlan": route_plan,
|
|
228
|
+
"stages": stages,
|
|
229
|
+
"separateAuthorizationsRequired": route_plan is not None,
|
|
230
|
+
"endpointState": _endpoint_states(route_plan),
|
|
231
|
+
"postRouteVerification": (
|
|
232
|
+
["application-health", "route-http", "workstation-browser"]
|
|
233
|
+
if route_plan is not None
|
|
234
|
+
else ["application-health"]
|
|
235
|
+
),
|
|
236
|
+
"mutated": False,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
142
240
|
def revalidate_app_isolation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
143
241
|
"""Recheck the reviewed application footprint immediately before mutation."""
|
|
144
242
|
|
|
@@ -186,9 +284,11 @@ def apply_app_deploy_from_workstation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
186
284
|
|
|
187
285
|
__all__ = [
|
|
188
286
|
"ISOLATION_REVIEW_KEY",
|
|
287
|
+
"ROLLOUT_SCHEMA_VERSION",
|
|
189
288
|
"app_isolation_check",
|
|
190
289
|
"apply_app_deploy",
|
|
191
290
|
"apply_app_deploy_from_workstation",
|
|
192
291
|
"plan_app_deploy",
|
|
292
|
+
"plan_app_rollout",
|
|
193
293
|
"revalidate_app_isolation",
|
|
194
294
|
]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
"""Read-only exact-image registry
|
|
1
|
+
"""Read-only exact-image registry capability verification by execution context."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
import shlex
|
|
7
8
|
import subprocess
|
|
@@ -13,6 +14,17 @@ from .ssh import ssh_args, verify_access
|
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
_IMAGE_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/:+-]*@sha256:[0-9a-f]{64}$")
|
|
17
|
+
_REMOTE_DOES_NOT_PROVE = [
|
|
18
|
+
"workstation-registry-access",
|
|
19
|
+
"workstation-image-pull",
|
|
20
|
+
"actions-registry-transport",
|
|
21
|
+
"package-metadata-api-access",
|
|
22
|
+
]
|
|
23
|
+
_WORKSTATION_DOES_NOT_PROVE = [
|
|
24
|
+
"remote-target-registry-access",
|
|
25
|
+
"actions-registry-transport",
|
|
26
|
+
"package-metadata-api-access",
|
|
27
|
+
]
|
|
16
28
|
|
|
17
29
|
_REMOTE_SCRIPT = r'''#!/usr/bin/env bash
|
|
18
30
|
set -Eeuo pipefail
|
|
@@ -61,6 +73,14 @@ printf 'ACCESS\t%s\n' "$credential_state"
|
|
|
61
73
|
'''
|
|
62
74
|
|
|
63
75
|
|
|
76
|
+
class RegistryCapabilityError(DeploymentKitError):
|
|
77
|
+
"""Fail-closed registry capability error with safe structured evidence."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, message: str, result: dict[str, Any]) -> None:
|
|
80
|
+
super().__init__(message)
|
|
81
|
+
self.result = result
|
|
82
|
+
|
|
83
|
+
|
|
64
84
|
def normalize_registry_image(value: str) -> str:
|
|
65
85
|
if not isinstance(value, str) or _IMAGE_RE.fullmatch(value) is None:
|
|
66
86
|
raise DeploymentKitError(
|
|
@@ -83,13 +103,55 @@ def _parse_access(output: str) -> str:
|
|
|
83
103
|
return parts[1]
|
|
84
104
|
|
|
85
105
|
|
|
106
|
+
def _remote_failure_result(
|
|
107
|
+
*,
|
|
108
|
+
name: str,
|
|
109
|
+
image: str,
|
|
110
|
+
address: str,
|
|
111
|
+
returncode: int,
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
failure_codes = {
|
|
114
|
+
42: "docker-cli-unavailable",
|
|
115
|
+
43: "credential-config-unsafe",
|
|
116
|
+
44: "manifest-access-failed",
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
"status": "failed",
|
|
120
|
+
"executionLocation": "remote-target",
|
|
121
|
+
"targetName": name,
|
|
122
|
+
"verifiedAddress": address,
|
|
123
|
+
"capabilityAttempted": "registry-manifest-read",
|
|
124
|
+
"image": image,
|
|
125
|
+
"failureCode": failure_codes.get(returncode, "remote-command-failed"),
|
|
126
|
+
"doesNotProve": list(_REMOTE_DOES_NOT_PROVE),
|
|
127
|
+
"mutated": False,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _workstation_failure_result(*, image: str, failure_code: str) -> dict[str, Any]:
|
|
132
|
+
return {
|
|
133
|
+
"status": "failed",
|
|
134
|
+
"executionLocation": "workstation",
|
|
135
|
+
"credentialContext": "workstation-docker-client",
|
|
136
|
+
"dockerConfigSource": "explicit-environment" if os.environ.get("DOCKER_CONFIG") else "default",
|
|
137
|
+
"capabilityAttempted": "registry-manifest-read",
|
|
138
|
+
"image": image,
|
|
139
|
+
"failureCode": failure_code,
|
|
140
|
+
"doesNotProve": list(_WORKSTATION_DOES_NOT_PROVE),
|
|
141
|
+
"mutated": False,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
86
145
|
def verify_registry_access(name: str, image: str) -> dict[str, Any]:
|
|
146
|
+
"""Verify exact-image manifest access from the named remote target."""
|
|
147
|
+
|
|
87
148
|
exact_image = normalize_registry_image(image)
|
|
88
149
|
profile = load_profile(name)
|
|
89
150
|
access = verify_access(profile)
|
|
151
|
+
address = access["address"]
|
|
90
152
|
command = ssh_args(
|
|
91
153
|
profile,
|
|
92
|
-
|
|
154
|
+
address,
|
|
93
155
|
command="bash -s -- " + shlex.quote(exact_image),
|
|
94
156
|
)
|
|
95
157
|
completed = subprocess.run(
|
|
@@ -99,16 +161,84 @@ def verify_registry_access(name: str, image: str) -> dict[str, Any]:
|
|
|
99
161
|
capture_output=True,
|
|
100
162
|
)
|
|
101
163
|
stdout = completed.stdout.decode("utf-8", errors="replace")
|
|
102
|
-
stderr = completed.stderr.decode("utf-8", errors="replace")
|
|
103
164
|
if completed.returncode != 0:
|
|
104
|
-
|
|
105
|
-
|
|
165
|
+
result = _remote_failure_result(
|
|
166
|
+
name=name,
|
|
167
|
+
image=exact_image,
|
|
168
|
+
address=address,
|
|
169
|
+
returncode=completed.returncode,
|
|
170
|
+
)
|
|
171
|
+
raise RegistryCapabilityError(
|
|
172
|
+
f"Remote-target registry capability check failed ({result['failureCode']}).",
|
|
173
|
+
result,
|
|
174
|
+
)
|
|
106
175
|
|
|
107
176
|
credential_state = _parse_access(stdout)
|
|
108
177
|
return {
|
|
178
|
+
"status": "verified",
|
|
179
|
+
"executionLocation": "remote-target",
|
|
109
180
|
"targetName": name,
|
|
181
|
+
"verifiedAddress": address,
|
|
182
|
+
"credentialContext": (
|
|
183
|
+
"target-docker-config" if credential_state == "protected" else "target-anonymous"
|
|
184
|
+
),
|
|
185
|
+
"capabilityProved": "registry-manifest-read",
|
|
110
186
|
"image": exact_image,
|
|
111
187
|
"manifestAccess": "verified",
|
|
112
188
|
"credentialConfig": credential_state,
|
|
189
|
+
"doesNotProve": list(_REMOTE_DOES_NOT_PROVE),
|
|
113
190
|
"mutated": False,
|
|
114
191
|
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def verify_workstation_registry_access(image: str) -> dict[str, Any]:
|
|
195
|
+
"""Verify exact-image manifest access from this workstation without pulling."""
|
|
196
|
+
|
|
197
|
+
exact_image = normalize_registry_image(image)
|
|
198
|
+
command = ["docker", "manifest", "inspect", exact_image]
|
|
199
|
+
try:
|
|
200
|
+
completed = subprocess.run(
|
|
201
|
+
command,
|
|
202
|
+
check=False,
|
|
203
|
+
capture_output=True,
|
|
204
|
+
)
|
|
205
|
+
except FileNotFoundError as error:
|
|
206
|
+
result = _workstation_failure_result(
|
|
207
|
+
image=exact_image,
|
|
208
|
+
failure_code="docker-cli-unavailable",
|
|
209
|
+
)
|
|
210
|
+
raise RegistryCapabilityError(
|
|
211
|
+
"Workstation registry capability check failed (docker-cli-unavailable).",
|
|
212
|
+
result,
|
|
213
|
+
) from error
|
|
214
|
+
|
|
215
|
+
if completed.returncode != 0:
|
|
216
|
+
result = _workstation_failure_result(
|
|
217
|
+
image=exact_image,
|
|
218
|
+
failure_code="manifest-access-failed",
|
|
219
|
+
)
|
|
220
|
+
raise RegistryCapabilityError(
|
|
221
|
+
"Workstation registry capability check failed (manifest-access-failed).",
|
|
222
|
+
result,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
"status": "verified",
|
|
227
|
+
"executionLocation": "workstation",
|
|
228
|
+
"credentialContext": "workstation-docker-client",
|
|
229
|
+
"dockerConfigSource": "explicit-environment" if os.environ.get("DOCKER_CONFIG") else "default",
|
|
230
|
+
"capabilityProved": "registry-manifest-read",
|
|
231
|
+
"image": exact_image,
|
|
232
|
+
"manifestAccess": "verified",
|
|
233
|
+
"doesNotProve": list(_WORKSTATION_DOES_NOT_PROVE),
|
|
234
|
+
"mutated": False,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
__all__ = [
|
|
239
|
+
"RegistryCapabilityError",
|
|
240
|
+
"build_remote_script",
|
|
241
|
+
"normalize_registry_image",
|
|
242
|
+
"verify_registry_access",
|
|
243
|
+
"verify_workstation_registry_access",
|
|
244
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CLI for read-only registry
|
|
1
|
+
"""CLI for read-only registry capability verification."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -8,7 +8,11 @@ import sys
|
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
10
|
from .errors import DeploymentKitError
|
|
11
|
-
from .registry_access import
|
|
11
|
+
from .registry_access import (
|
|
12
|
+
RegistryCapabilityError,
|
|
13
|
+
verify_registry_access,
|
|
14
|
+
verify_workstation_registry_access,
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
def _print(value: Any, *, as_json: bool) -> None:
|
|
@@ -28,11 +32,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
28
32
|
|
|
29
33
|
verify = sub.add_parser(
|
|
30
34
|
"verify",
|
|
31
|
-
help=
|
|
35
|
+
help=(
|
|
36
|
+
"Verify exact-image registry manifest access on the named remote target without pulling "
|
|
37
|
+
"or modifying target state"
|
|
38
|
+
),
|
|
32
39
|
)
|
|
33
|
-
verify.add_argument("name")
|
|
40
|
+
verify.add_argument("name", help="Named remote target profile")
|
|
34
41
|
verify.add_argument("--image", required=True)
|
|
35
42
|
verify.add_argument("--json", action="store_true")
|
|
43
|
+
|
|
44
|
+
verify_workstation = sub.add_parser(
|
|
45
|
+
"verify-workstation",
|
|
46
|
+
help=(
|
|
47
|
+
"Verify exact-image registry manifest access from this workstation without pulling "
|
|
48
|
+
"or modifying target state"
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
verify_workstation.add_argument("--image", required=True)
|
|
52
|
+
verify_workstation.add_argument("--json", action="store_true")
|
|
36
53
|
return parser
|
|
37
54
|
|
|
38
55
|
|
|
@@ -45,6 +62,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
45
62
|
as_json=args.json,
|
|
46
63
|
)
|
|
47
64
|
return 0
|
|
65
|
+
if args.command == "verify-workstation":
|
|
66
|
+
_print(
|
|
67
|
+
verify_workstation_registry_access(args.image),
|
|
68
|
+
as_json=args.json,
|
|
69
|
+
)
|
|
70
|
+
return 0
|
|
71
|
+
except RegistryCapabilityError as error:
|
|
72
|
+
if getattr(args, "json", False):
|
|
73
|
+
_print(error.result, as_json=True)
|
|
74
|
+
else:
|
|
75
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
|
76
|
+
return 2
|
|
48
77
|
except DeploymentKitError as error:
|
|
49
78
|
print(f"ERROR: {error}", file=sys.stderr)
|
|
50
79
|
return 2
|