@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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.10.1",
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": "6b13a6e66218c067771311db1336c73229d23899",
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.1",
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": "6b13a6e66218c067771311db1336c73229d23899",
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": "98c77950df1d0bba36da1244001bdd157251238778e7068be0c6c20d2683d2d4"
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": "eea7a5b4322e0601db36d28cb0529522d6acc37c55dfc9a7b41d91518fe14cd4"
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
- final_inventory = cloudflare_route_inventory(name, scope)
702
- if _find_route(final_inventory, spec["routeId"]) != spec:
703
- raise DeploymentKitError("Public route registration returned without exact host ownership metadata.")
704
- final_config = cloudflare_api.get_tunnel_configuration(api_token, host["accountId"], host["tunnelId"])
705
- if final_config != plan["desiredTunnelConfig"]:
706
- raise DeploymentKitError("Cloudflare tunnel configuration does not match the reviewed route plan after apply.")
707
- final_dns = _exact_dns_record(
708
- cloudflare_api.list_dns_records(api_token, host["zoneId"], name=spec["publicHostname"]),
709
- spec=spec,
710
- tunnel_id=host["tunnelId"],
711
- )
712
- if final_dns is None:
713
- raise DeploymentKitError("Cloudflare DNS record is missing after public-route apply.")
714
- return {
715
- "targetName": name,
716
- "scope": scope,
717
- "route": spec,
718
- "dnsRecordId": str(final_dns["id"]),
719
- "publicUrl": f"https://{spec['publicHostname']}",
720
- "mutated": True,
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. Recovery is entered only for errors that are emitted exclusively
121
- by the final verification phase, so preflight failures can never be turned
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 Exception as original_error:
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.10.1",
3
+ "version": "4.10.3",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {