@mriekki/project-forge 4.4.0 → 4.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/assets/components.json +2 -2
- package/assets/generated/ASSEMBLY.json +5 -5
- 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.1",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.4.
|
|
4
|
+
"version": "4.4.1",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
7
|
"repository": "mriekki/project-forge-core",
|
|
@@ -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",
|
|
@@ -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
|