mirror of
https://github.com/Such-Software/adaptive-multi-protocol-gateway.git
synced 2026-10-17 07:31:32 +02:00
Add health deploy apply stage
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Adaptive Multi-Protocol Gateway
|
||||
|
||||
> Status: draft | Updated 2026-07-06 | Applies to: AMPG contributors and operators
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: AMPG contributors and operators
|
||||
|
||||
Adaptive Multi-Protocol Gateway, or AMPG, turns one canonical site into transport-appropriate
|
||||
published versions for clearnet web, Tor onion services, I2P, Gemini, IPFS, and
|
||||
@@ -63,6 +63,7 @@ python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --pro
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage supervisor --dry-run --profile vps-full
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage start --dry-run --profile vps-full
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage addresses --dry-run --profile vps-full
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage health --dry-run --profile vps-full
|
||||
```
|
||||
|
||||
`init site` writes a readable `gateway.toml` with selected transports enabled and common
|
||||
@@ -74,6 +75,8 @@ managed-daemon config into AMPG-owned state. `deploy apply --stage supervisor` i
|
||||
approved supervisor files after state exists. `deploy apply --stage start` starts
|
||||
AMPG-owned services after state and supervisor files are present. `deploy apply --stage
|
||||
addresses` captures daemon-written public addresses into the address registry.
|
||||
`deploy apply --stage health` verifies published fixture URLs through the selected
|
||||
transport.
|
||||
|
||||
## Interaction tiers
|
||||
|
||||
@@ -145,6 +148,8 @@ python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage star
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage start --profile mobile-i2p --yes
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage addresses --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage addresses --profile mobile-i2p --yes
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage health --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage health --profile mobile-i2p --yes
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build --profile tor-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml build --protocol tor --protocol i2p
|
||||
@@ -213,6 +218,10 @@ checks.
|
||||
be recorded. The live form requires `--yes`; it writes captured addresses to
|
||||
`gateway.state_dir` and leaves health verification for the next stage.
|
||||
|
||||
`deploy apply --stage health --dry-run` shows published fixture checks after addresses
|
||||
are configured or captured. The live form requires `--yes`; it runs the transport check
|
||||
commands and reports pass/fail without changing config or state.
|
||||
|
||||
`dns plan --free-domain-hints` prints optional community subdomain services that may help
|
||||
new users get a clearnet name without buying a domain. These are third-party registries;
|
||||
operators still need to review current terms, availability, content rules, and DNS
|
||||
@@ -242,4 +251,5 @@ Explicit `--protocol` and `--platform` flags override the profile when present.
|
||||
- [ ] Run `ampg deploy apply --stage supervisor --dry-run`; confirm approved service files.
|
||||
- [ ] Run `ampg deploy apply --stage start --dry-run`; confirm service-manager commands.
|
||||
- [ ] Run `ampg deploy apply --stage addresses --dry-run`; confirm captured transport addresses.
|
||||
- [ ] Run `ampg deploy apply --stage health --dry-run`; confirm published health checks.
|
||||
- [ ] Run live apply after the preflight gate is ready.
|
||||
|
||||
+135
@@ -14,6 +14,7 @@ from .addresses import (
|
||||
write_captured_address_results,
|
||||
)
|
||||
from .config import GatewayConfig
|
||||
from .health import HealthCheck, health_plan
|
||||
from .install_plan import (
|
||||
InstallStateCopy,
|
||||
InstallSupervisorAction,
|
||||
@@ -87,6 +88,21 @@ class AddressApplyResult:
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HealthApplyResult:
|
||||
site_id: str
|
||||
protocol: str
|
||||
route: str
|
||||
transport: str
|
||||
profile: str
|
||||
url: str
|
||||
status: str
|
||||
mode: str
|
||||
command: tuple[str, ...]
|
||||
return_code: int | None
|
||||
message: str
|
||||
|
||||
|
||||
CommandRunner = Callable[[tuple[str, ...]], CommandRunResult]
|
||||
|
||||
|
||||
@@ -281,6 +297,59 @@ def apply_addresses(
|
||||
return [_address_result(result, dry_run=dry_run) for result in captures]
|
||||
|
||||
|
||||
def apply_health(
|
||||
config: GatewayConfig,
|
||||
*,
|
||||
dry_run: bool,
|
||||
command_runner: CommandRunner | None = None,
|
||||
) -> list[HealthApplyResult]:
|
||||
checks = health_plan(config)
|
||||
runner = command_runner or _run_command
|
||||
results: list[HealthApplyResult] = []
|
||||
for check in checks:
|
||||
command = _health_command(check)
|
||||
validation = _validate_health_check(check, command=command, dry_run=dry_run)
|
||||
if validation.status == "blocked":
|
||||
results.append(validation)
|
||||
continue
|
||||
if dry_run:
|
||||
results.append(
|
||||
_health_result(
|
||||
check,
|
||||
command=command,
|
||||
status="planned",
|
||||
mode="dry-run",
|
||||
return_code=None,
|
||||
message="would run published health check",
|
||||
)
|
||||
)
|
||||
continue
|
||||
run = runner(command)
|
||||
if run.return_code != 0:
|
||||
results.append(
|
||||
_health_result(
|
||||
check,
|
||||
command=command,
|
||||
status="blocked",
|
||||
mode="live",
|
||||
return_code=run.return_code,
|
||||
message=_command_failure_message(run),
|
||||
)
|
||||
)
|
||||
continue
|
||||
results.append(
|
||||
_health_result(
|
||||
check,
|
||||
command=command,
|
||||
status="passed",
|
||||
mode="live",
|
||||
return_code=run.return_code,
|
||||
message="published health check passed",
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _validate_state_copy(
|
||||
config: GatewayConfig,
|
||||
copy: InstallStateCopy,
|
||||
@@ -480,6 +549,41 @@ def _validate_start_action(
|
||||
)
|
||||
|
||||
|
||||
def _validate_health_check(
|
||||
check: HealthCheck,
|
||||
*,
|
||||
command: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
) -> HealthApplyResult:
|
||||
mode = "dry-run" if dry_run else "live"
|
||||
if check.status != "planned":
|
||||
return _health_result(
|
||||
check,
|
||||
command=command,
|
||||
status="blocked",
|
||||
mode=mode,
|
||||
return_code=None,
|
||||
message=check.message,
|
||||
)
|
||||
if not command:
|
||||
return _health_result(
|
||||
check,
|
||||
command=command,
|
||||
status="blocked",
|
||||
mode=mode,
|
||||
return_code=None,
|
||||
message="health check command is unavailable",
|
||||
)
|
||||
return _health_result(
|
||||
check,
|
||||
command=command,
|
||||
status="ready",
|
||||
mode=mode,
|
||||
return_code=None,
|
||||
message="health check passed safety checks",
|
||||
)
|
||||
|
||||
|
||||
def _address_result(
|
||||
result: AddressCaptureResult,
|
||||
*,
|
||||
@@ -510,6 +614,30 @@ def _address_result(
|
||||
)
|
||||
|
||||
|
||||
def _health_result(
|
||||
check: HealthCheck,
|
||||
*,
|
||||
command: tuple[str, ...],
|
||||
status: str,
|
||||
mode: str,
|
||||
return_code: int | None,
|
||||
message: str,
|
||||
) -> HealthApplyResult:
|
||||
return HealthApplyResult(
|
||||
site_id=check.site_id,
|
||||
protocol=check.protocol,
|
||||
route=check.route,
|
||||
transport=check.transport,
|
||||
profile=check.profile,
|
||||
url=check.url,
|
||||
status=status,
|
||||
mode=mode,
|
||||
command=command,
|
||||
return_code=return_code,
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
def _result(
|
||||
copy: InstallStateCopy,
|
||||
*,
|
||||
@@ -603,6 +731,13 @@ def start_command(
|
||||
return ()
|
||||
|
||||
|
||||
def _health_command(check: HealthCheck) -> tuple[str, ...]:
|
||||
try:
|
||||
return tuple(shlex.split(check.command))
|
||||
except ValueError:
|
||||
return ()
|
||||
|
||||
|
||||
def _termux_prefix(state_root: Path) -> Path:
|
||||
if len(state_root.parts) >= 3 and state_root.parts[-3:] == ("var", "lib", "ampg"):
|
||||
return state_root.parents[2]
|
||||
|
||||
+38
-1
@@ -23,10 +23,12 @@ from .addresses import (
|
||||
)
|
||||
from .apply import (
|
||||
AddressApplyResult,
|
||||
HealthApplyResult,
|
||||
StateApplyResult,
|
||||
StartApplyResult,
|
||||
SupervisorApplyResult,
|
||||
apply_addresses,
|
||||
apply_health,
|
||||
apply_state,
|
||||
apply_start,
|
||||
apply_supervisor,
|
||||
@@ -162,7 +164,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
_add_target_selection(deploy_apply_parser)
|
||||
deploy_apply_parser.add_argument(
|
||||
"--stage",
|
||||
choices=("state", "supervisor", "start", "addresses"),
|
||||
choices=("state", "supervisor", "start", "addresses", "health"),
|
||||
required=True,
|
||||
help="Deployment stage to apply.",
|
||||
)
|
||||
@@ -664,6 +666,23 @@ def _cmd_deploy(config, args) -> int:
|
||||
f"registry=\"{_quote(str(address_registry_path(config)))}\""
|
||||
)
|
||||
return 1 if blocked else 0
|
||||
if args.stage == "health":
|
||||
results = apply_health(config, dry_run=args.dry_run)
|
||||
for result in results:
|
||||
_print_health_apply_result(result)
|
||||
blocked = [result for result in results if result.status == "blocked"]
|
||||
passed = [result for result in results if result.status == "passed"]
|
||||
planned = [result for result in results if result.status == "planned"]
|
||||
print(
|
||||
"AMPG_DEPLOY_APPLY_SUMMARY "
|
||||
f"stage=health "
|
||||
f"mode={'dry-run' if args.dry_run else 'live'} "
|
||||
f"results={len(results)} "
|
||||
f"planned={len(planned)} "
|
||||
f"passed={len(passed)} "
|
||||
f"blocked={len(blocked)}"
|
||||
)
|
||||
return 1 if blocked else 0
|
||||
return 1
|
||||
|
||||
|
||||
@@ -1202,6 +1221,24 @@ def _print_address_apply_result(result: AddressApplyResult) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _print_health_apply_result(result: HealthApplyResult) -> None:
|
||||
return_code = "-" if result.return_code is None else str(result.return_code)
|
||||
print(
|
||||
"AMPG_DEPLOY_HEALTH "
|
||||
f"site={result.site_id} "
|
||||
f"protocol={result.protocol} "
|
||||
f"route=\"{_quote(result.route)}\" "
|
||||
f"transport={result.transport} "
|
||||
f"profile={result.profile} "
|
||||
f"mode={result.mode} "
|
||||
f"url=\"{_quote(result.url)}\" "
|
||||
f"status={result.status} "
|
||||
f"return_code={return_code} "
|
||||
f"command=\"{_quote(format_command(result.command))}\" "
|
||||
f"message=\"{_quote(result.message)}\""
|
||||
)
|
||||
|
||||
|
||||
def _print_dns_record(record: DNSRecordPlan) -> None:
|
||||
print(
|
||||
"AMPG_DNS_RECORD "
|
||||
|
||||
@@ -16,6 +16,7 @@ from .approvals import (
|
||||
)
|
||||
from .config import GatewayConfig
|
||||
from .dns import dns_plan
|
||||
from .health import health_plan
|
||||
from .install_plan import (
|
||||
install_artifacts,
|
||||
install_plan,
|
||||
@@ -95,6 +96,7 @@ def deploy_plan(
|
||||
daemon_probe=daemon_probe,
|
||||
),
|
||||
_address_step(config, command_context),
|
||||
_health_step(config, command_context),
|
||||
)
|
||||
status = _overall_status(steps)
|
||||
return DeployPlan(
|
||||
@@ -279,6 +281,38 @@ def _address_step(config: GatewayConfig, commands: _CommandContext) -> DeploySte
|
||||
return DeployStep("addresses", "ready", commands.command("addresses list"), "addresses are known")
|
||||
|
||||
|
||||
def _health_step(config: GatewayConfig, commands: _CommandContext) -> DeployStep:
|
||||
checks = health_plan(config)
|
||||
if not checks:
|
||||
return DeployStep("health", "ready", "-", "no published health checks are needed")
|
||||
blocked = [check for check in checks if check.status == "blocked"]
|
||||
review = [check for check in checks if check.status == "review"]
|
||||
planned = [check for check in checks if check.status == "planned"]
|
||||
if blocked:
|
||||
status = "todo" if _missing_outputs(config) else "blocked"
|
||||
return DeployStep(
|
||||
"health",
|
||||
status,
|
||||
commands.command("health-plan"),
|
||||
f"fix {len(blocked)} health check blocker(s)",
|
||||
)
|
||||
if review:
|
||||
return DeployStep(
|
||||
"health",
|
||||
"review",
|
||||
commands.command("deploy apply --stage addresses --dry-run"),
|
||||
f"resolve {len(review)} health check address review item(s)",
|
||||
)
|
||||
if planned:
|
||||
return DeployStep(
|
||||
"health",
|
||||
"todo",
|
||||
commands.command("deploy apply --stage health --dry-run"),
|
||||
f"run {len(planned)} published health check(s)",
|
||||
)
|
||||
return DeployStep("health", "ready", commands.command("health-plan"), "health checks are ready")
|
||||
|
||||
|
||||
def _apply_preflight_step(
|
||||
config: GatewayConfig,
|
||||
commands: _CommandContext,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Architecture
|
||||
|
||||
> Status: draft | Updated 2026-07-05 | Applies to: AMPG core
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: AMPG core
|
||||
|
||||
AMPG is a compiler plus an ingress manager. The compiler turns one site into a
|
||||
protocol-neutral content graph, then renders protocol-specific outputs. The ingress
|
||||
@@ -146,6 +146,10 @@ remain separate stages.
|
||||
transport has produced them. It writes only the AMPG address registry; health
|
||||
verification remains a separate stage.
|
||||
|
||||
`ampg deploy apply --stage health` runs published fixture checks through the selected
|
||||
transport after addresses are configured or captured. It reports pass/fail and does not
|
||||
mutate deployment state.
|
||||
|
||||
The dry-run command is intentionally stricter than `doctor`: missing generated output is
|
||||
blocked during activation, because AMPG must not point a transport at an unbuilt output
|
||||
root.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Daemon Management
|
||||
|
||||
> Status: draft | Updated 2026-07-06 | Applies to: protocol adapters
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: protocol adapters
|
||||
|
||||
AMPG can use daemons that already exist on a server or create AMPG-owned daemons when
|
||||
the selected protocol is not running. This keeps simple deployments simple while avoiding
|
||||
@@ -64,6 +64,8 @@ python3 -m ampg --config gateway.toml deploy apply --stage start --dry-run --pro
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage start --profile mobile-i2p --yes
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage addresses --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage addresses --profile mobile-i2p --yes
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage health --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config gateway.toml deploy apply --stage health --profile mobile-i2p --yes
|
||||
python3 -m ampg --config gateway.toml doctor --platform android-termux
|
||||
```
|
||||
|
||||
@@ -108,6 +110,8 @@ configured, captured, derived, or placeholder. `deploy apply --stage addresses`
|
||||
`health-plan` prints fixture-based checks to run after services start. Published checks
|
||||
target real transport URLs; preview checks target local loopback preview endpoints.
|
||||
Missing output blocks the plan, while placeholder transport addresses are review items.
|
||||
`deploy apply --stage health` runs the published checks after addresses are configured or
|
||||
captured.
|
||||
|
||||
`approvals list` prints generated artifact candidates and their digest status. `approve`
|
||||
requires `--all` or a `--kind` filter, then records approvals only for artifacts that
|
||||
@@ -146,6 +150,11 @@ files, capture addresses, or run health checks.
|
||||
Live mode requires `--yes` and writes captured transport addresses under
|
||||
`gateway.state_dir`. It does not rewrite daemon config or run health checks.
|
||||
|
||||
`deploy apply --stage health` is the fifth live apply stage. Dry-run mode prints
|
||||
`AMPG_DEPLOY_HEALTH` rows with published fixture URLs and transport commands. Live mode
|
||||
requires `--yes`, runs those commands, and reports pass/fail. It does not change daemon
|
||||
config, service state, or address registries.
|
||||
|
||||
`--protocol` scopes operational commands to selected enabled protocols. A clearnet
|
||||
`adopt` failure will block a full activation run, but it will not block
|
||||
`apply --dry-run --protocol i2p` because clearnet is outside that selected run.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Fixture Manifests
|
||||
|
||||
> Status: draft | Updated 2026-07-06 | Applies to: AMPG and AMPB compatibility checks
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: AMPG and AMPB compatibility checks
|
||||
|
||||
AMPG writes deterministic fixture manifests so AMPB can verify that generated site
|
||||
variants route to the expected browser transport and profile.
|
||||
@@ -38,6 +38,7 @@ python3 -m ampg --config examples/wownero.gateway.toml addresses capture --profi
|
||||
python3 -m ampg --config examples/wownero.gateway.toml preview manifest
|
||||
python3 -m ampg --config examples/wownero.gateway.toml health-plan
|
||||
python3 -m ampg --config examples/wownero.gateway.toml health-plan --mode preview
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage health --dry-run --profile mobile-i2p
|
||||
```
|
||||
|
||||
`build` emits the manifest after writing protocol outputs. `manifest` rewrites only the
|
||||
@@ -56,6 +57,8 @@ real Tor, I2P, Gemini, or other transport daemons are installed or adopted.
|
||||
|
||||
`health-plan` consumes the same fixture contract. Published mode prints checks for real
|
||||
transport URLs. Preview mode prints loopback checks against generated preview URLs.
|
||||
`deploy apply --stage health` runs the published checks after transport addresses are
|
||||
configured or captured.
|
||||
|
||||
## Address Status
|
||||
|
||||
@@ -66,8 +69,9 @@ verify transport selection without contacting the network.
|
||||
When a managed or adopted daemon writes an actual address, run
|
||||
`deploy apply --stage addresses --dry-run` before recording it. Captured addresses are
|
||||
stored under `gateway.state_dir` and replace placeholders in fixture manifests and
|
||||
published health plans. Operators can also use `addresses capture` for a direct write or
|
||||
`addresses set` to store an address manually.
|
||||
published health plans. Operators can then run `deploy apply --stage health --dry-run`
|
||||
to review the exact transport checks before live verification. Operators can also use
|
||||
`addresses capture` for a direct write or `addresses set` to store an address manually.
|
||||
|
||||
## Interaction Policy
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Wownero Integration
|
||||
|
||||
> Status: draft | Updated 2026-07-05 | Applies to: Wownero static site publishing
|
||||
> Status: draft | Updated 2026-07-07 | Applies to: Wownero static site publishing
|
||||
|
||||
The Wownero integration exercises AMPG against a real public static site. The source is
|
||||
the existing `wownero.org` website checked out next to this repository as
|
||||
@@ -141,8 +141,9 @@ daemon_policy = "auto"
|
||||
- `ampg build` writes a fixture manifest that AMPB can check.
|
||||
- `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live
|
||||
transport daemons are installed or adopted.
|
||||
- `ampg deploy apply --stage state`, `--stage supervisor`, `--stage start`, and
|
||||
`--stage addresses` stage Wownero transport publication without mixing concerns.
|
||||
- `ampg deploy apply --stage state`, `--stage supervisor`, `--stage start`,
|
||||
`--stage addresses`, and `--stage health` stage Wownero transport publication without
|
||||
mixing concerns.
|
||||
- Tor output contains no `<script>` tags or inline event handlers.
|
||||
- Generated Gemini output has readable headings, paragraphs, and links.
|
||||
- Generated Micron output fits terminal-first browsing.
|
||||
@@ -168,6 +169,7 @@ python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage stat
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage start --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage addresses --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage health --dry-run --profile mobile-i2p
|
||||
python3 -m ampg --config examples/i2p-only.gateway.toml plan
|
||||
python3 -m ampg --config examples/i2p-only.gateway.toml build
|
||||
```
|
||||
@@ -182,3 +184,4 @@ python3 -m ampg --config examples/i2p-only.gateway.toml build
|
||||
- [ ] Enable I2P and Reticulum daemon management only after static outputs are stable.
|
||||
- [ ] Start only AMPG-owned Wownero transport services after reviewed files are applied.
|
||||
- [ ] Capture Wownero transport addresses before published health checks.
|
||||
- [ ] Run published health checks through each selected transport.
|
||||
|
||||
+118
-1
@@ -5,7 +5,13 @@ import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from ampg.addresses import address_registry_path
|
||||
from ampg.apply import CommandRunResult, apply_start, apply_state, apply_supervisor
|
||||
from ampg.apply import (
|
||||
CommandRunResult,
|
||||
apply_health,
|
||||
apply_start,
|
||||
apply_state,
|
||||
apply_supervisor,
|
||||
)
|
||||
from ampg.cli import main
|
||||
from ampg.config import load_config
|
||||
from ampg.platforms import PlatformProvider
|
||||
@@ -566,6 +572,117 @@ class DeployApplyTest(unittest.TestCase):
|
||||
self.assertIn("status=written", output)
|
||||
self.assertIn("http://captureexample.b32.i2p/", registry_text)
|
||||
|
||||
def test_deploy_apply_health_blocks_placeholder_addresses(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
|
||||
status, output, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"health",
|
||||
"--dry-run",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(1, status)
|
||||
self.assertEqual("", error)
|
||||
self.assertIn("AMPG_DEPLOY_HEALTH", output)
|
||||
self.assertIn("status=blocked", output)
|
||||
self.assertIn("placeholder address", output)
|
||||
|
||||
def test_deploy_apply_health_dry_run_after_address_capture(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
_write_i2p_address_file(root)
|
||||
status, _, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"addresses",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
"--yes",
|
||||
]
|
||||
)
|
||||
if status != 0:
|
||||
raise AssertionError(error)
|
||||
|
||||
status, output, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"health",
|
||||
"--dry-run",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, status, error)
|
||||
self.assertIn("AMPG_DEPLOY_HEALTH", output)
|
||||
self.assertIn("status=planned", output)
|
||||
self.assertIn("curl --proxy http://127.0.0.1:4444 -fsSL", output)
|
||||
self.assertIn("http://captureexample.b32.i2p/", output)
|
||||
|
||||
def test_deploy_apply_health_live_invokes_runner(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
config_path = _init_i2p_config(root)
|
||||
_write_i2p_address_file(root)
|
||||
status, _, error = _run_cli(
|
||||
[
|
||||
"--config",
|
||||
str(config_path),
|
||||
"deploy",
|
||||
"apply",
|
||||
"--stage",
|
||||
"addresses",
|
||||
"--profile",
|
||||
"mobile-i2p",
|
||||
"--yes",
|
||||
]
|
||||
)
|
||||
if status != 0:
|
||||
raise AssertionError(error)
|
||||
config = load_config(config_path)
|
||||
commands: list[tuple[str, ...]] = []
|
||||
|
||||
def runner(command: tuple[str, ...]) -> CommandRunResult:
|
||||
commands.append(command)
|
||||
return CommandRunResult(return_code=0)
|
||||
|
||||
results = apply_health(
|
||||
config,
|
||||
dry_run=False,
|
||||
command_runner=runner,
|
||||
)
|
||||
|
||||
self.assertTrue(all(result.status == "passed" for result in results))
|
||||
self.assertIn(
|
||||
(
|
||||
"curl",
|
||||
"--proxy",
|
||||
"http://127.0.0.1:4444",
|
||||
"-fsSL",
|
||||
"http://captureexample.b32.i2p/",
|
||||
),
|
||||
commands,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -61,6 +61,7 @@ class DeployPlanTest(unittest.TestCase):
|
||||
self.assertIn("AMPG_DEPLOY_STEP stage=build status=todo", output)
|
||||
self.assertIn("AMPG_DEPLOY_STEP stage=dns status=skipped", output)
|
||||
self.assertIn("AMPG_DEPLOY_STEP stage=artifacts status=todo", output)
|
||||
self.assertIn("AMPG_DEPLOY_STEP stage=health status=todo", output)
|
||||
self.assertIn("AMPG_DEPLOY_NEXT step=1 stage=build", output)
|
||||
self.assertIn("build --profile mobile-i2p", output)
|
||||
self.assertIn("deploy apply --stage addresses --dry-run --profile mobile-i2p", output)
|
||||
@@ -137,6 +138,7 @@ class DeployPlanTest(unittest.TestCase):
|
||||
self.assertEqual(0, status, error)
|
||||
self.assertIn("AMPG_DEPLOY_STEP stage=build status=ready", output)
|
||||
self.assertIn("AMPG_DEPLOY_STEP stage=artifacts status=todo", output)
|
||||
self.assertIn("AMPG_DEPLOY_STEP stage=health status=review", output)
|
||||
|
||||
def test_deploy_plan_guides_staged_apply_after_approval(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
|
||||
Reference in New Issue
Block a user