@mriekki/project-forge 4.4.0 → 4.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/assets/components.json +3 -3
- package/assets/generated/ASSEMBLY.json +9 -9
- package/assets/generated/core/core/instructions/project-forge-workflow.instructions.md +2 -2
- package/assets/generated/core/core/process/INTERACTIVE_SESSION_GUIDE.md +4 -2
- package/assets/generated/core/core/skills/refine-requirements/SKILL.md +4 -7
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_adapter.py +21 -6
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_cli.py +9 -3
- package/assets/generated/deployment/src/project_forge_deployment/sqlite_import_recovery.py +24 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,15 @@ npx @mriekki/project-forge@latest check
|
|
|
126
126
|
|
|
127
127
|
Managed updates verify baselines and refuse unsafe drift or conflicts before modifying Project Forge-managed files.
|
|
128
128
|
|
|
129
|
+
## Maintainer system reference
|
|
130
|
+
|
|
131
|
+
For cross-repository architecture, ownership, lifecycle, provenance, deployment, support-evidence, and governance questions, start with:
|
|
132
|
+
|
|
133
|
+
- [`docs/system/PROJECT_FORGE_SYSTEM_REQUIREMENTS.md`](docs/system/PROJECT_FORGE_SYSTEM_REQUIREMENTS.md) — normative cross-repository invariants;
|
|
134
|
+
- [`docs/system/PROJECT_FORGE_SYSTEM_DESIGN.md`](docs/system/PROJECT_FORGE_SYSTEM_DESIGN.md) — explanatory system map and pointers to detailed owning-repository contracts.
|
|
135
|
+
|
|
136
|
+
The system reference deliberately does not duplicate current component SHAs, package/catalog versions, support status, or implementation-level CLI/schema detail. Resolve mutable facts from the owning repositories and machine-readable Platform/Bootstrap/consumer provenance.
|
|
137
|
+
|
|
129
138
|
## Maintainer release process
|
|
130
139
|
|
|
131
140
|
Project Forge maintainers use a gated release workflow that resolves accepted component commits, verifies the assembled package, creates the package pull request, verifies CI, and stops at the explicit publication gate.
|
package/assets/components.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.4.
|
|
4
|
+
"version": "4.4.2",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"components": {
|
|
15
15
|
"core": {
|
|
16
16
|
"repository": "mriekki/project-forge-core",
|
|
17
|
-
"commit": "
|
|
17
|
+
"commit": "d106b1834a8c44bdae0b16e4af5d8dab0711a120",
|
|
18
18
|
"version": "4.0.0",
|
|
19
19
|
"distribution": "greenfield-workspace",
|
|
20
20
|
"descriptor": "assets/core/distribution.json",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"deployment": {
|
|
25
25
|
"repository": "mriekki/project-forge-deployment-kit",
|
|
26
|
-
"commit": "
|
|
26
|
+
"commit": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
|
|
27
27
|
"distribution": "greenfield-project",
|
|
28
28
|
"descriptor": "assets/deployment/distribution.json",
|
|
29
29
|
"sourceRoot": "assets/generated/deployment",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.4.
|
|
4
|
+
"version": "4.4.2",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
7
|
"repository": "mriekki/project-forge-core",
|
|
8
|
-
"commit": "
|
|
8
|
+
"commit": "d106b1834a8c44bdae0b16e4af5d8dab0711a120",
|
|
9
9
|
"version": "4.0.0",
|
|
10
10
|
"distribution": "greenfield-workspace",
|
|
11
11
|
"files": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"source": "core/instructions/project-forge-workflow.instructions.md",
|
|
24
24
|
"destination": ".github/instructions/project-forge-workflow.instructions.md",
|
|
25
|
-
"sha256": "
|
|
25
|
+
"sha256": "13379a56c65921756cdae8e1d2d711c3321b9d899b5d96a81dbf51ecaa9ce022"
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
"source": "core/instructions/project-forge-docs.instructions.md",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{
|
|
43
43
|
"source": "core/skills/refine-requirements/SKILL.md",
|
|
44
44
|
"destination": ".github/skills/refine-requirements/SKILL.md",
|
|
45
|
-
"sha256": "
|
|
45
|
+
"sha256": "9e76b9c3700a00af8852928d4e6bf1aeca9a24c6cc8fc9b2f1873e0198b63962"
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"source": "core/skills/start-work/SKILL.md",
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
{
|
|
188
188
|
"source": "core/process/INTERACTIVE_SESSION_GUIDE.md",
|
|
189
189
|
"destination": "docs/process/INTERACTIVE_SESSION_GUIDE.md",
|
|
190
|
-
"sha256": "
|
|
190
|
+
"sha256": "08df5212fc6d08e46d3121dad3d076e05baa71b046a7e360a201b18916b7e574"
|
|
191
191
|
},
|
|
192
192
|
{
|
|
193
193
|
"source": "core/process/COACHING_GUIDE.md",
|
|
@@ -328,7 +328,7 @@
|
|
|
328
328
|
},
|
|
329
329
|
"deployment": {
|
|
330
330
|
"repository": "mriekki/project-forge-deployment-kit",
|
|
331
|
-
"commit": "
|
|
331
|
+
"commit": "880a911bc87e38c74ca4b336c9563a6f026ac3b4",
|
|
332
332
|
"distribution": "greenfield-project",
|
|
333
333
|
"files": [
|
|
334
334
|
{
|
|
@@ -519,7 +519,7 @@
|
|
|
519
519
|
{
|
|
520
520
|
"source": "src/project_forge_deployment/sqlite_adapter.py",
|
|
521
521
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_adapter.py",
|
|
522
|
-
"sha256": "
|
|
522
|
+
"sha256": "011615339f739316fb1350a7d4f1385a841744cf09c66da61c10e46f444c059f"
|
|
523
523
|
},
|
|
524
524
|
{
|
|
525
525
|
"source": "src/project_forge_deployment/sqlite_adapter_remote.py",
|
|
@@ -529,12 +529,12 @@
|
|
|
529
529
|
{
|
|
530
530
|
"source": "src/project_forge_deployment/sqlite_cli.py",
|
|
531
531
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_cli.py",
|
|
532
|
-
"sha256": "
|
|
532
|
+
"sha256": "abee3a7fbef21f1a02b1511f5d050f90d9ef51afcd6714046c25b786568fb833"
|
|
533
533
|
},
|
|
534
534
|
{
|
|
535
535
|
"source": "src/project_forge_deployment/sqlite_import_recovery.py",
|
|
536
536
|
"destination": "tools/project-forge-deployment/project_forge_deployment/sqlite_import_recovery.py",
|
|
537
|
-
"sha256": "
|
|
537
|
+
"sha256": "b047494e403963120d8667e189f20ed6310526870b56adb1f433a9b0154c7636"
|
|
538
538
|
},
|
|
539
539
|
{
|
|
540
540
|
"source": "src/project_forge_deployment/ssh.py",
|
|
@@ -20,9 +20,9 @@ For implementation, establish the repository-appropriate short-lived work branch
|
|
|
20
20
|
|
|
21
21
|
Continuous modes such as `/grill-me`, `/refine-requirements`, `/coach`, `/learning-review`, and `/deployment-readiness` continue until stopped or naturally complete.
|
|
22
22
|
|
|
23
|
-
During interactive questioning, prefer concise choice-first options for genuinely bounded human decisions, including `Other / specify` and `Not sure — recommend one` when appropriate. When the host exposes native structured question controls, use them instead of a plain numbered chat list. In VS Code, use the host's structured question surface such as `vscode_askQuestions` when available: single-select for mutually exclusive decisions/actions and multi-select only when several simultaneous selections are valid. A textual numbered menu is only the fallback when structured choice UI is unavailable.
|
|
23
|
+
During interactive questioning, prefer concise choice-first options for genuinely bounded human decisions, including `Other / specify` and `Not sure — recommend one` when appropriate. When the host exposes native structured question controls, use them instead of a plain numbered chat list. In VS Code, use the host's structured question surface such as `vscode_askQuestions` when available: single-select for mutually exclusive decisions/actions and multi-select only when several simultaneous selections are valid. A textual numbered or lettered menu is only the fallback when structured choice UI is unavailable.
|
|
24
24
|
|
|
25
|
-
Make a recommendation only when current evidence, repository policy, or a clear engineering tradeoff supports one. Mark exactly one supported choice with the host's `recommended` indicator when available and give a brief rationale. If no option is clearly better, do not mark any option recommended. A recommendation is advice, not approval: record only the human's selected/confirmed answer. If the human asks for no recommendation on one decision, honor that for that decision only; later questions may still include evidence-backed recommendations.
|
|
25
|
+
Make a recommendation only when current evidence, repository policy, or a clear engineering tradeoff supports one. Mark exactly one supported choice with the host's `recommended` indicator when available and give a brief rationale. When the host does not expose recommendation metadata, append `[RECOMMENDED]` to exactly one option label and keep the rationale visible. A default/preselected option may match the recommendation, but a default alone must never communicate advice or human approval. If no option is clearly better, do not mark any option recommended and do not use a default that implies a preference. A recommendation is advice, not approval: record only the human's selected/confirmed answer. If the human asks for no recommendation on one decision, honor that for that decision only; later questions may still include evidence-backed recommendations.
|
|
26
26
|
|
|
27
27
|
Keep factual/environment-specific values and questions intended to elicit the human's own framing, reasoning, debugging approach, design rationale, or understanding open-ended unless options are later useful as scaffolding.
|
|
28
28
|
|
|
@@ -51,16 +51,18 @@ A recommendation is optional advice, not a required field and not an accepted de
|
|
|
51
51
|
|
|
52
52
|
- mark **exactly one** choice as `RECOMMENDED` only when current evidence, repository policy, or a clear engineering tradeoff supports it;
|
|
53
53
|
- use the host's `recommended` indicator/metadata when available and add a brief rationale in the choice description or surrounding prompt;
|
|
54
|
+
- when the host does not expose recommendation metadata, append `[RECOMMENDED]` to exactly one option label and keep the concise rationale visible in the prompt or option description;
|
|
55
|
+
- a default/preselected value may match the recommended option, but a default alone must never communicate that an option is recommended or human-approved;
|
|
54
56
|
- do not mark multiple choices recommended;
|
|
55
57
|
- do not invent a preference merely to populate recommendation metadata;
|
|
56
|
-
- when no option is clearly better, present the choices with **no recommendation
|
|
58
|
+
- when no option is clearly better, present the choices with **no recommendation** and do not use a default that implies a preference;
|
|
57
59
|
- if the human asks for no recommendation on a specific decision, omit the recommendation for that question only; later questions may still include evidence-backed recommendations;
|
|
58
60
|
- the human may select a different option or answer free-form;
|
|
59
61
|
- record only the human's selected/confirmed answer as the requirement, action, or decision. Never record the recommendation itself as accepted state.
|
|
60
62
|
|
|
61
63
|
A structured selector is presentation, not extra authorization. Selecting a choice authorizes only the action/session that the workflow says that choice authorizes.
|
|
62
64
|
|
|
63
|
-
When the host has no structured question UI, use a concise textual fallback. A numbered list is acceptable **only as this fallback**; keep the same labels/recommendation/consequence tags and accept the number, label, or a free-form answer. Do not use a plain numbered list as the normal VS Code interaction when native structured choice UI is available.
|
|
65
|
+
When the host has no structured question UI, use a concise textual fallback. A numbered or lettered list is acceptable **only as this fallback**; keep the same labels/recommendation/consequence tags, including `[RECOMMENDED]` when one option is evidence-backed, and accept the number/letter, label, or a free-form answer. Do not use a plain numbered list as the normal VS Code interaction when native structured choice UI is available.
|
|
64
66
|
|
|
65
67
|
Keep questions open-ended when predefined choices would weaken the purpose of the interaction. This includes:
|
|
66
68
|
|
|
@@ -124,9 +124,9 @@ Then present only relevant actions as a **single-select structured choice** when
|
|
|
124
124
|
- `Approve local requirements` `[REQUIREMENTS GATE]` when applicable
|
|
125
125
|
- `Stop` `[NO ACTION]`
|
|
126
126
|
|
|
127
|
-
Make a recommendation only when current requirement evidence or a clear engineering tradeoff supports one. If so, mark exactly one choice with the host's `recommended` indicator
|
|
127
|
+
Make a recommendation only when current requirement evidence or a clear engineering tradeoff supports one. If so, mark exactly one choice with the host's `recommended` indicator when that metadata exists; otherwise append `[RECOMMENDED]` to exactly one option label. Briefly explain why in the option description or surrounding prompt. A default/preselected value may match the recommendation, but a default alone must never communicate advice or approval. If there is no clear basis, show the choices without a recommendation and without a preference-implying default. The selected answer, not the recommendation, controls what the session does or records.
|
|
128
128
|
|
|
129
|
-
If the host has no structured choice UI, present the same relevant choices as a concise numbered text fallback and accept the number, label, or free-form intent. Do not use the
|
|
129
|
+
If the host has no structured choice UI, present the same relevant choices as a concise numbered or lettered text fallback, preserve `[RECOMMENDED]` on the evidence-backed option when applicable, and accept the number/letter, label, or free-form intent. Do not use the text fallback as the normal VS Code path.
|
|
130
130
|
|
|
131
131
|
## 5. Active refinement session
|
|
132
132
|
|
|
@@ -143,7 +143,7 @@ ask one material requirement question
|
|
|
143
143
|
|
|
144
144
|
For bounded requirement decisions such as lifecycle policy, validation behavior, destructive/reversible handling, or a small set of credible alternatives, use the choice-first interaction guidance from `INTERACTIVE_SESSION_GUIDE.md`. Render mutually exclusive alternatives through host-native **single-select** controls when available. Use multi-select only when the requirement genuinely permits multiple simultaneous selections. Keep `Other / specify` or free-text available when the option set may be incomplete. Keep outcome wording, workflow descriptions, acceptance intent, and other human-authored framing open-ended when predefined choices would constrain the requirement.
|
|
145
145
|
|
|
146
|
-
Recommendation behavior follows the shared guide: recommend at most one option, only when evidence/tradeoffs support it; give a concise rationale; never record the recommendation as accepted until the human selects or confirms it; and honor a `no recommendation` request for that question only.
|
|
146
|
+
Recommendation behavior follows the shared guide: recommend at most one option, only when evidence/tradeoffs support it; make the recommendation visibly distinguishable through native metadata or `[RECOMMENDED]`; give a concise rationale; never record the recommendation as accepted until the human selects or confirms it; and honor a `no recommendation` request for that question only.
|
|
147
147
|
|
|
148
148
|
Example of a supported recommendation:
|
|
149
149
|
|
|
@@ -153,11 +153,8 @@ Decision: How should a browser-to-real-SQLite path be proven for a narrowly scop
|
|
|
153
153
|
Choices:
|
|
154
154
|
- Integration test against real SQLite
|
|
155
155
|
- Browser smoke check through the API
|
|
156
|
-
- Both
|
|
156
|
+
- Both [RECOMMENDED] — proves persistence and browser/API paths without implying a broad end-to-end suite
|
|
157
157
|
- Other / specify
|
|
158
|
-
|
|
159
|
-
Recommendation: Both
|
|
160
|
-
Why: one real-SQLite integration test proves the persistence path and one browser smoke check proves the browser/API path without implying a broad end-to-end suite.
|
|
161
158
|
```
|
|
162
159
|
|
|
163
160
|
Treat this as advice for the human to accept, change, or replace. Do not record `Both` as a requirement until the human selects or confirms it.
|
|
@@ -252,9 +252,18 @@ def _reservation_from_request(request: dict[str, Any]) -> dict[str, Any]:
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
|
|
255
|
-
def plan_sqlite_operation(
|
|
255
|
+
def plan_sqlite_operation(
|
|
256
|
+
name: str,
|
|
257
|
+
action: str,
|
|
258
|
+
request: dict[str, Any],
|
|
259
|
+
*,
|
|
260
|
+
production: bool = False,
|
|
261
|
+
) -> dict[str, Any]:
|
|
256
262
|
if action not in {"import", "backup", "migrate", "rollback-check"}:
|
|
257
263
|
raise DeploymentKitError("Unsupported SQLite plan action.")
|
|
264
|
+
if not isinstance(production, bool):
|
|
265
|
+
raise DeploymentKitError("SQLite Production classification must be a boolean.")
|
|
266
|
+
production_classified = production or request["environment"] == "production"
|
|
258
267
|
_require_ready_platform(name)
|
|
259
268
|
inventory = sqlite_inventory(name)
|
|
260
269
|
existing = _find(inventory, request["appSlug"], request["environment"])
|
|
@@ -284,7 +293,8 @@ def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> di
|
|
|
284
293
|
raise DeploymentKitError(f"SQLite {action} requires an existing database.")
|
|
285
294
|
return {
|
|
286
295
|
"targetName": name, "action": action, "ownerRepo": request["ownerRepo"],
|
|
287
|
-
"environment": request["environment"], "
|
|
296
|
+
"environment": request["environment"], "productionClassified": production_classified,
|
|
297
|
+
"appSlug": request["appSlug"], "image": request["image"],
|
|
288
298
|
"databaseFile": spec["databaseFile"], "runAs": spec["runAs"],
|
|
289
299
|
"sourceDatabase": request["sourceDatabase"] if action == "import" else None,
|
|
290
300
|
"firstInstall": bool(request["firstInstall"] if action == "migrate" else False),
|
|
@@ -295,7 +305,9 @@ def plan_sqlite_operation(name: str, action: str, request: dict[str, Any]) -> di
|
|
|
295
305
|
|
|
296
306
|
|
|
297
307
|
def authorization_phrase(plan: dict[str, Any]) -> str:
|
|
298
|
-
production = plan
|
|
308
|
+
production = plan.get("productionClassified")
|
|
309
|
+
if not isinstance(production, bool):
|
|
310
|
+
raise DeploymentKitError("Malformed SQLite operation plan Production classification.")
|
|
299
311
|
mapping = {
|
|
300
312
|
"import": (SQLITE_IMPORT_WRITE_PHRASE, PRODUCTION_SQLITE_IMPORT_WRITE_PHRASE),
|
|
301
313
|
"backup": (SQLITE_BACKUP_WRITE_PHRASE, PRODUCTION_SQLITE_BACKUP_WRITE_PHRASE),
|
|
@@ -307,9 +319,11 @@ def authorization_phrase(plan: dict[str, Any]) -> str:
|
|
|
307
319
|
|
|
308
320
|
|
|
309
321
|
def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
310
|
-
required = {"targetName", "action", "ownerRepo", "environment", "appSlug", "image", "databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup", "inventorySnapshot", "expectedReservationSha256", "spec"}
|
|
311
|
-
if not isinstance(plan, dict) or set(plan) != required:
|
|
322
|
+
required = {"targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image", "databaseFile", "runAs", "sourceDatabase", "firstInstall", "stopsManagedRuntime", "createsVerifiedBackup", "inventorySnapshot", "expectedReservationSha256", "spec"}
|
|
323
|
+
if not isinstance(plan, dict) or set(plan) != required or not isinstance(plan["productionClassified"], bool):
|
|
312
324
|
raise DeploymentKitError("Malformed SQLite operation plan.")
|
|
325
|
+
if plan["environment"] == "production" and not plan["productionClassified"]:
|
|
326
|
+
raise DeploymentKitError("Literal production environment must remain Production-classified.")
|
|
313
327
|
_require_ready_platform(plan["targetName"])
|
|
314
328
|
current = sqlite_inventory(plan["targetName"])
|
|
315
329
|
if _snapshot(current) != plan["inventorySnapshot"]:
|
|
@@ -319,6 +333,7 @@ def apply_sqlite_operation(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
319
333
|
result = sqlite_inventory(plan["targetName"])
|
|
320
334
|
result["mutated"] = True
|
|
321
335
|
result["operation"] = plan["action"]; result["operationOutput"] = output.strip()
|
|
336
|
+
result["productionClassified"] = plan["productionClassified"]
|
|
322
337
|
return result
|
|
323
338
|
|
|
324
339
|
|
|
@@ -332,4 +347,4 @@ def assert_app_spec_matches_persistence(name: str, app_spec: dict[str, Any]) ->
|
|
|
332
347
|
if existing["runAs"] != app_spec["runAs"]:
|
|
333
348
|
raise DeploymentKitError("Application runAs does not match the prepared SQLite persistence reservation.")
|
|
334
349
|
if app_spec.get("persistence", {}).get("mode") != "directory":
|
|
335
|
-
raise DeploymentKitError("Prepared SQLite persistence requires directory application persistence.")
|
|
350
|
+
raise DeploymentKitError("Prepared SQLite persistence requires directory application persistence.")
|
|
@@ -38,6 +38,11 @@ def _common(parser: argparse.ArgumentParser) -> None:
|
|
|
38
38
|
parser.add_argument('name')
|
|
39
39
|
parser.add_argument('--owner-repo', required=True)
|
|
40
40
|
parser.add_argument('--environment', required=True)
|
|
41
|
+
parser.add_argument(
|
|
42
|
+
'--production',
|
|
43
|
+
action='store_true',
|
|
44
|
+
help="Classify this named environment as Production even when its name is not literally 'production'",
|
|
45
|
+
)
|
|
41
46
|
parser.add_argument('--app-slug', required=True)
|
|
42
47
|
parser.add_argument('--image', required=True)
|
|
43
48
|
parser.add_argument('--maintenance-env', action='append', default=[], help='Non-secret image maintenance environment value in NAME=value form; repeat as needed')
|
|
@@ -68,19 +73,20 @@ def main(argv: list[str]|None=None) -> int:
|
|
|
68
73
|
args=build_parser().parse_args(argv)
|
|
69
74
|
try:
|
|
70
75
|
if args.command=='inventory': _print(sqlite_inventory(args.name),as_json=args.json); return 0
|
|
76
|
+
production_classified = args.production or args.environment == 'production'
|
|
71
77
|
request=normalize_sqlite_request(owner_repo=args.owner_repo,environment=args.environment,app_slug=args.app_slug,image=args.image,
|
|
72
78
|
maintenance_environment=_parse_env(args.maintenance_env),run_as=getattr(args,'run_as',None),database_file=getattr(args,'database_file',None),
|
|
73
79
|
source_database=getattr(args,'source_database',None),first_install=getattr(args,'first_install',False))
|
|
74
80
|
if args.command=='recover-import':
|
|
75
|
-
plan=plan_sqlite_import_recovery(args.name,request)
|
|
81
|
+
plan=plan_sqlite_import_recovery(args.name,request,production=production_classified)
|
|
76
82
|
_print({k:v for k,v in plan.items() if k!='request'},as_json=False)
|
|
77
83
|
phrase=recovery_authorization_phrase(plan)
|
|
78
84
|
_confirm(args,phrase,f"recover SQLite import for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
|
|
79
85
|
_print(apply_sqlite_import_recovery(plan),as_json=args.json); return 0
|
|
80
|
-
plan=plan_sqlite_operation(args.name,args.command,request)
|
|
86
|
+
plan=plan_sqlite_operation(args.name,args.command,request,production=production_classified)
|
|
81
87
|
_print({k:v for k,v in plan.items() if k!='spec'},as_json=False)
|
|
82
88
|
phrase=authorization_phrase(plan)
|
|
83
89
|
_confirm(args,phrase,f"run SQLite {args.command} for {plan['appSlug']} ({plan['environment']}) on target {args.name}")
|
|
84
90
|
_print(apply_sqlite_operation(plan),as_json=args.json); return 0
|
|
85
91
|
except DeploymentKitError as error:
|
|
86
|
-
print(f"ERROR: {error}",file=sys.stderr); return 2
|
|
92
|
+
print(f"ERROR: {error}",file=sys.stderr); return 2
|
|
@@ -356,13 +356,22 @@ def _snapshot(inspection: dict[str, Any]) -> str:
|
|
|
356
356
|
return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")).hexdigest()
|
|
357
357
|
|
|
358
358
|
|
|
359
|
-
def plan_sqlite_import_recovery(
|
|
359
|
+
def plan_sqlite_import_recovery(
|
|
360
|
+
name: str,
|
|
361
|
+
request: dict[str, Any],
|
|
362
|
+
*,
|
|
363
|
+
production: bool = False,
|
|
364
|
+
) -> dict[str, Any]:
|
|
365
|
+
if not isinstance(production, bool):
|
|
366
|
+
raise DeploymentKitError("SQLite Production classification must be a boolean.")
|
|
367
|
+
production_classified = production or request["environment"] == "production"
|
|
360
368
|
inspection = inspect_sqlite_import_recovery(name, request)
|
|
361
369
|
return {
|
|
362
370
|
"targetName": name,
|
|
363
371
|
"action": "recover-import",
|
|
364
372
|
"ownerRepo": request["ownerRepo"],
|
|
365
373
|
"environment": request["environment"],
|
|
374
|
+
"productionClassified": production_classified,
|
|
366
375
|
"appSlug": request["appSlug"],
|
|
367
376
|
"image": request["image"],
|
|
368
377
|
"runAs": request["runAs"],
|
|
@@ -381,22 +390,32 @@ def plan_sqlite_import_recovery(name: str, request: dict[str, Any]) -> dict[str,
|
|
|
381
390
|
|
|
382
391
|
|
|
383
392
|
def authorization_phrase(plan: dict[str, Any]) -> str:
|
|
393
|
+
production = plan.get("productionClassified")
|
|
394
|
+
if not isinstance(production, bool):
|
|
395
|
+
raise DeploymentKitError("Malformed SQLite import recovery Production classification.")
|
|
384
396
|
return (
|
|
385
397
|
PRODUCTION_SQLITE_IMPORT_RECOVERY_WRITE_PHRASE
|
|
386
|
-
if
|
|
398
|
+
if production
|
|
387
399
|
else SQLITE_IMPORT_RECOVERY_WRITE_PHRASE
|
|
388
400
|
)
|
|
389
401
|
|
|
390
402
|
|
|
391
403
|
def apply_sqlite_import_recovery(plan: dict[str, Any]) -> dict[str, Any]:
|
|
392
404
|
required = {
|
|
393
|
-
"targetName", "action", "ownerRepo", "environment", "appSlug", "image", "runAs",
|
|
405
|
+
"targetName", "action", "ownerRepo", "environment", "productionClassified", "appSlug", "image", "runAs",
|
|
394
406
|
"databaseFile", "sourceDatabase", "databaseSha256", "sourceSha256", "databaseBytes",
|
|
395
407
|
"stateSnapshot", "overwritesDatabase", "deletesPersistentData", "createsReservation",
|
|
396
408
|
"reverifiesDatabase", "request",
|
|
397
409
|
}
|
|
398
|
-
if
|
|
410
|
+
if (
|
|
411
|
+
not isinstance(plan, dict)
|
|
412
|
+
or set(plan) != required
|
|
413
|
+
or plan["action"] != "recover-import"
|
|
414
|
+
or not isinstance(plan["productionClassified"], bool)
|
|
415
|
+
):
|
|
399
416
|
raise DeploymentKitError("Malformed SQLite import recovery plan.")
|
|
417
|
+
if plan["environment"] == "production" and not plan["productionClassified"]:
|
|
418
|
+
raise DeploymentKitError("Literal production environment must remain Production-classified.")
|
|
400
419
|
current = inspect_sqlite_import_recovery(plan["targetName"], plan["request"])
|
|
401
420
|
if _snapshot(current) != plan["stateSnapshot"]:
|
|
402
421
|
raise DeploymentKitError(
|
|
@@ -434,4 +453,5 @@ def apply_sqlite_import_recovery(plan: dict[str, Any]) -> dict[str, Any]:
|
|
|
434
453
|
inventory["mutated"] = True
|
|
435
454
|
inventory["operation"] = "recover-import"
|
|
436
455
|
inventory["operationOutput"] = output.strip()
|
|
456
|
+
inventory["productionClassified"] = plan["productionClassified"]
|
|
437
457
|
return inventory
|