@mriekki/project-forge 4.10.1 → 4.10.3
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 +2 -2
- package/assets/generated/ASSEMBLY.json +4 -4
- package/assets/generated/deployment/src/project_forge_deployment/cloudflare_ingress.py +31 -21
- package/assets/generated/deployment/src/project_forge_deployment/cloudflare_route_apply.py +23 -18
- 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.10.
|
|
4
|
+
"version": "4.10.3",
|
|
5
5
|
"defaultProfile": "greenfield-project",
|
|
6
6
|
"profiles": {
|
|
7
7
|
"greenfield-project": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"canonicalDescriptor": "distributions/greenfield-workspace.json"
|
|
22
22
|
},
|
|
23
23
|
"deployment": {
|
|
24
|
-
"revision": "
|
|
24
|
+
"revision": "75c272db383f1803383e8cd4ad4511537c175abe",
|
|
25
25
|
"distribution": "greenfield-project",
|
|
26
26
|
"descriptor": "assets/deployment/distribution.json",
|
|
27
27
|
"sourceRoot": "assets/generated/deployment",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@mriekki/project-forge",
|
|
4
|
-
"version": "4.10.
|
|
4
|
+
"version": "4.10.3",
|
|
5
5
|
"components": {
|
|
6
6
|
"core": {
|
|
7
7
|
"revision": "8796fd8dc22df8b64078e5b460e1008a8c7a5f7d",
|
|
@@ -326,7 +326,7 @@
|
|
|
326
326
|
]
|
|
327
327
|
},
|
|
328
328
|
"deployment": {
|
|
329
|
-
"revision": "
|
|
329
|
+
"revision": "75c272db383f1803383e8cd4ad4511537c175abe",
|
|
330
330
|
"distribution": "greenfield-project",
|
|
331
331
|
"files": [
|
|
332
332
|
{
|
|
@@ -402,7 +402,7 @@
|
|
|
402
402
|
{
|
|
403
403
|
"source": "src/project_forge_deployment/cloudflare_ingress.py",
|
|
404
404
|
"destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_ingress.py",
|
|
405
|
-
"sha256": "
|
|
405
|
+
"sha256": "ca2c80e0b64792f550a08fc51994bdfdac42644c35f23cdf7ae5c50b468109f8"
|
|
406
406
|
},
|
|
407
407
|
{
|
|
408
408
|
"source": "src/project_forge_deployment/cloudflare_ingress_remote.py",
|
|
@@ -412,7 +412,7 @@
|
|
|
412
412
|
{
|
|
413
413
|
"source": "src/project_forge_deployment/cloudflare_route_apply.py",
|
|
414
414
|
"destination": "tools/project-forge-deployment/project_forge_deployment/cloudflare_route_apply.py",
|
|
415
|
-
"sha256": "
|
|
415
|
+
"sha256": "d3fe77a2f61ae281308ad1046ee1e6e5ea460f3605c5cd275cc16cff3e9bb568"
|
|
416
416
|
},
|
|
417
417
|
{
|
|
418
418
|
"source": "src/project_forge_deployment/cli.py",
|
|
@@ -37,6 +37,11 @@ PUBLIC_INGRESS_ROUTE_WRITE_PHRASE = "[PUBLIC INGRESS ROUTE WRITE]"
|
|
|
37
37
|
PUBLIC_INGRESS_ROUTE_REMOVE_PHRASE = "[PUBLIC INGRESS ROUTE REMOVE]"
|
|
38
38
|
CLOUDFLARE_API_TOKEN_ENV = "PROJECT_FORGE_CLOUDFLARE_API_TOKEN"
|
|
39
39
|
|
|
40
|
+
|
|
41
|
+
class CloudflarePostWriteVerificationError(DeploymentKitError):
|
|
42
|
+
"""Final public-route verification failed after mutations completed."""
|
|
43
|
+
|
|
44
|
+
|
|
40
45
|
_SLUG_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
|
|
41
46
|
_REPO_RE = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
42
47
|
_LABEL_RE = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")
|
|
@@ -698,27 +703,32 @@ def apply_public_route(plan: dict[str, Any], *, api_token: str) -> dict[str, Any
|
|
|
698
703
|
pass
|
|
699
704
|
raise
|
|
700
705
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
706
|
+
try:
|
|
707
|
+
final_inventory = cloudflare_route_inventory(name, scope)
|
|
708
|
+
if _find_route(final_inventory, spec["routeId"]) != spec:
|
|
709
|
+
raise DeploymentKitError("Public route registration returned without exact host ownership metadata.")
|
|
710
|
+
final_config = cloudflare_api.get_tunnel_configuration(api_token, host["accountId"], host["tunnelId"])
|
|
711
|
+
if final_config != plan["desiredTunnelConfig"]:
|
|
712
|
+
raise DeploymentKitError("Cloudflare tunnel configuration does not match the reviewed route plan after apply.")
|
|
713
|
+
final_dns = _exact_dns_record(
|
|
714
|
+
cloudflare_api.list_dns_records(api_token, host["zoneId"], name=spec["publicHostname"]),
|
|
715
|
+
spec=spec,
|
|
716
|
+
tunnel_id=host["tunnelId"],
|
|
717
|
+
)
|
|
718
|
+
if final_dns is None:
|
|
719
|
+
raise DeploymentKitError("Cloudflare DNS record is missing after public-route apply.")
|
|
720
|
+
return {
|
|
721
|
+
"targetName": name,
|
|
722
|
+
"scope": scope,
|
|
723
|
+
"route": spec,
|
|
724
|
+
"dnsRecordId": str(final_dns["id"]),
|
|
725
|
+
"publicUrl": f"https://{spec['publicHostname']}",
|
|
726
|
+
"mutated": True,
|
|
727
|
+
}
|
|
728
|
+
except Exception as error:
|
|
729
|
+
raise CloudflarePostWriteVerificationError(
|
|
730
|
+
f"Cloudflare public-route post-write verification failed: {error}"
|
|
731
|
+
) from error
|
|
722
732
|
|
|
723
733
|
|
|
724
734
|
def plan_public_route_removal(
|
|
@@ -17,12 +17,6 @@ from .errors import DeploymentKitError
|
|
|
17
17
|
from .profiles import load_profile
|
|
18
18
|
from .ssh import verify_access
|
|
19
19
|
|
|
20
|
-
_FINAL_VERIFICATION_ERRORS = {
|
|
21
|
-
"Public route registration returned without exact host ownership metadata.",
|
|
22
|
-
"Cloudflare tunnel configuration does not match the reviewed route plan after apply.",
|
|
23
|
-
"Cloudflare DNS record is missing after public-route apply.",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
20
|
|
|
27
21
|
def _reviewed_config_matches(actual: Any, reviewed: Any) -> bool:
|
|
28
22
|
"""Match reviewed config after removing only the observed safe provider default."""
|
|
@@ -113,23 +107,32 @@ def _rollback_final_state(
|
|
|
113
107
|
return errors
|
|
114
108
|
|
|
115
109
|
|
|
110
|
+
def _uncertain_recovery_error(
|
|
111
|
+
original_error: Exception,
|
|
112
|
+
recovery_error: Exception,
|
|
113
|
+
) -> DeploymentKitError:
|
|
114
|
+
return DeploymentKitError(
|
|
115
|
+
"Cloudflare public-route final verification failed, and recovery could not read "
|
|
116
|
+
"the current host/provider state. External state is uncertain and rollback was "
|
|
117
|
+
"not attempted because current ownership could not be verified safely. "
|
|
118
|
+
f"Original verification error: {original_error} "
|
|
119
|
+
f"Recovery read failed: {recovery_error} "
|
|
120
|
+
"Inspect the managed host route, tunnel configuration, and DNS record before retrying."
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
116
124
|
def apply_public_route(plan: dict[str, Any], *, api_token: str) -> dict[str, Any]:
|
|
117
125
|
"""Apply a reviewed public route with bounded post-write recovery.
|
|
118
126
|
|
|
119
127
|
The underlying apply retains all pre-write stale-state checks and its write
|
|
120
|
-
rollback.
|
|
121
|
-
|
|
128
|
+
rollback. Recovery is entered only for the typed error emitted by the
|
|
129
|
+
post-write verification phase, so preflight failures can never be turned
|
|
122
130
|
into success by observing later provider state.
|
|
123
131
|
"""
|
|
124
132
|
try:
|
|
125
133
|
return ingress.apply_public_route(plan, api_token=api_token)
|
|
126
|
-
except
|
|
127
|
-
if (
|
|
128
|
-
not isinstance(original_error, DeploymentKitError)
|
|
129
|
-
or str(original_error) not in _FINAL_VERIFICATION_ERRORS
|
|
130
|
-
or not isinstance(plan, dict)
|
|
131
|
-
or plan.get("action") != "register"
|
|
132
|
-
):
|
|
134
|
+
except ingress.CloudflarePostWriteVerificationError as original_error:
|
|
135
|
+
if not isinstance(plan, dict) or plan.get("action") != "register":
|
|
133
136
|
raise
|
|
134
137
|
required = {
|
|
135
138
|
"targetName", "scope", "route", "previousRoute", "tunnelConfig",
|
|
@@ -140,8 +143,8 @@ def apply_public_route(plan: dict[str, Any], *, api_token: str) -> dict[str, Any
|
|
|
140
143
|
|
|
141
144
|
try:
|
|
142
145
|
state = _read_final_state(plan, api_token=api_token)
|
|
143
|
-
except Exception:
|
|
144
|
-
raise original_error
|
|
146
|
+
except Exception as recovery_error:
|
|
147
|
+
raise _uncertain_recovery_error(original_error, recovery_error) from recovery_error
|
|
145
148
|
|
|
146
149
|
route_ok = state["route"] == plan["route"]
|
|
147
150
|
config_ok = _reviewed_config_matches(state["config"], plan["desiredTunnelConfig"])
|
|
@@ -159,7 +162,9 @@ def apply_public_route(plan: dict[str, Any], *, api_token: str) -> dict[str, Any
|
|
|
159
162
|
rollback_errors = _rollback_final_state(plan, state, api_token=api_token)
|
|
160
163
|
if rollback_errors:
|
|
161
164
|
raise DeploymentKitError(
|
|
162
|
-
"Cloudflare public-route final verification failed and rollback was incomplete
|
|
165
|
+
"Cloudflare public-route final verification failed and rollback was incomplete; "
|
|
166
|
+
"external state may remain changed. "
|
|
163
167
|
+ "; ".join(rollback_errors)
|
|
168
|
+
+ " Inspect the managed host route, tunnel configuration, and DNS record before retrying."
|
|
164
169
|
) from original_error
|
|
165
170
|
raise original_error
|