Add address capture deploy apply stage

This commit is contained in:
jwinterm
2026-07-07 05:56:47 -04:00
parent c3d44a5e5b
commit 8f4724089b
12 changed files with 256 additions and 30 deletions
+13 -3
View File
@@ -62,6 +62,7 @@ python3 -m ampg --config gateway.toml apply --dry-run --profile vps-full
python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --profile vps-full python3 -m ampg --config gateway.toml deploy apply --stage state --dry-run --profile vps-full
python3 -m ampg --config gateway.toml deploy apply --stage supervisor --dry-run --profile vps-full 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 start --dry-run --profile vps-full
python3 -m ampg --config gateway.toml deploy apply --stage addresses --dry-run --profile vps-full
``` ```
`init site` writes a readable `gateway.toml` with selected transports enabled and common `init site` writes a readable `gateway.toml` with selected transports enabled and common
@@ -71,7 +72,8 @@ reachability hints. `apply --dry-run` prints the activation sequence and a prefl
verdict without changing services. `deploy apply --stage state` copies approved verdict without changing services. `deploy apply --stage state` copies approved
managed-daemon config into AMPG-owned state. `deploy apply --stage supervisor` installs managed-daemon config into AMPG-owned state. `deploy apply --stage supervisor` installs
approved supervisor files after state exists. `deploy apply --stage start` starts approved supervisor files after state exists. `deploy apply --stage start` starts
AMPG-owned services after state and supervisor files are present. AMPG-owned services after state and supervisor files are present. `deploy apply --stage
addresses` captures daemon-written public addresses into the address registry.
## Interaction tiers ## Interaction tiers
@@ -141,6 +143,8 @@ python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supe
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --profile mobile-i2p --yes python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage supervisor --profile mobile-i2p --yes
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 start --dry-run --profile mobile-i2p
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 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 build 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 --profile tor-i2p
python3 -m ampg --config examples/wownero.gateway.toml build --protocol tor --protocol i2p python3 -m ampg --config examples/wownero.gateway.toml build --protocol tor --protocol i2p
@@ -174,8 +178,9 @@ platform supervisor files, but are not installed or started. Managed daemon conf
runtime state at `gateway.state_dir`. runtime state at `gateway.state_dir`.
Captured transport addresses are written under `gateway.state_dir` by Captured transport addresses are written under `gateway.state_dir` by
`addresses capture` or `addresses set`. Fixture manifests and health plans use explicit `deploy apply --stage addresses`, `addresses capture`, or `addresses set`. Fixture
config first, then captured addresses, then deterministic placeholders. manifests and health plans use explicit config first, then captured addresses, then
deterministic placeholders.
`approvals list` reports whether generated artifact digests are missing, stale, `approvals list` reports whether generated artifact digests are missing, stale,
unapproved, or approved. `approvals approve` records reviewed digests under unapproved, or approved. `approvals approve` records reviewed digests under
@@ -204,6 +209,10 @@ requires `--yes`; it runs only structured platform commands for AMPG-named servi
does not install packages, rewrite config, delete files, capture addresses, or run health does not install packages, rewrite config, delete files, capture addresses, or run health
checks. checks.
`deploy apply --stage addresses --dry-run` shows daemon-written public addresses that can
be recorded. The live form requires `--yes`; it writes captured addresses to
`gateway.state_dir` and leaves health verification for the next stage.
`dns plan --free-domain-hints` prints optional community subdomain services that may help `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; 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 operators still need to review current terms, availability, content rules, and DNS
@@ -232,4 +241,5 @@ Explicit `--protocol` and `--platform` flags override the profile when present.
- [ ] Run `ampg deploy apply --stage state --dry-run`; confirm approved state copies. - [ ] Run `ampg deploy apply --stage state --dry-run`; confirm approved state copies.
- [ ] Run `ampg deploy apply --stage supervisor --dry-run`; confirm approved service files. - [ ] 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 start --dry-run`; confirm service-manager commands.
- [ ] Run `ampg deploy apply --stage addresses --dry-run`; confirm captured transport addresses.
- [ ] Run live apply after the preflight gate is ready. - [ ] Run live apply after the preflight gate is ready.
+24 -10
View File
@@ -185,27 +185,41 @@ def _protocol_specific_configured_url(protocol: ProtocolConfig) -> Any:
return None return None
def capture_addresses(config: GatewayConfig) -> list[AddressCaptureResult]: def capture_address_results(config: GatewayConfig) -> list[AddressCaptureResult]:
records = load_address_registry(config)
results: list[AddressCaptureResult] = [] results: list[AddressCaptureResult] = []
changed = False
for site in config.sites: for site in config.sites:
for protocol in site.protocols.values(): for protocol in site.protocols.values():
if not protocol.enabled: if not protocol.enabled:
continue continue
result = _capture_protocol_address(config, site, protocol) results.append(_capture_protocol_address(config, site, protocol))
results.append(result) return results
if result.status == "captured":
records[(site.id, protocol.name)] = AddressRecord(
site_id=site.id, def write_captured_address_results(
protocol=protocol.name, config: GatewayConfig,
results: list[AddressCaptureResult],
) -> Path | None:
records = load_address_registry(config)
changed = False
for result in results:
if result.status != "captured":
continue
records[(result.site_id, result.protocol)] = AddressRecord(
site_id=result.site_id,
protocol=result.protocol,
url=result.url, url=result.url,
address_status="captured", address_status="captured",
source=result.source, source=result.source,
) )
changed = True changed = True
if changed: if changed:
write_address_registry(config, records) return write_address_registry(config, records)
return None
def capture_addresses(config: GatewayConfig) -> list[AddressCaptureResult]:
results = capture_address_results(config)
write_captured_address_results(config, results)
return results return results
+58
View File
@@ -8,6 +8,11 @@ import shutil
import shlex import shlex
import subprocess import subprocess
from .addresses import (
AddressCaptureResult,
capture_address_results,
write_captured_address_results,
)
from .config import GatewayConfig from .config import GatewayConfig
from .install_plan import ( from .install_plan import (
InstallStateCopy, InstallStateCopy,
@@ -70,6 +75,18 @@ class StartApplyResult:
message: str message: str
@dataclass(frozen=True)
class AddressApplyResult:
site_id: str
protocol: str
status: str
mode: str
url: str
source: str
path: Path | None
message: str
CommandRunner = Callable[[tuple[str, ...]], CommandRunResult] CommandRunner = Callable[[tuple[str, ...]], CommandRunResult]
@@ -253,6 +270,17 @@ def apply_start(
return results return results
def apply_addresses(
config: GatewayConfig,
*,
dry_run: bool,
) -> list[AddressApplyResult]:
captures = capture_address_results(config)
if not dry_run:
write_captured_address_results(config, captures)
return [_address_result(result, dry_run=dry_run) for result in captures]
def _validate_state_copy( def _validate_state_copy(
config: GatewayConfig, config: GatewayConfig,
copy: InstallStateCopy, copy: InstallStateCopy,
@@ -452,6 +480,36 @@ def _validate_start_action(
) )
def _address_result(
result: AddressCaptureResult,
*,
dry_run: bool,
) -> AddressApplyResult:
if result.status == "captured":
status = "planned" if dry_run else "written"
message = (
"would record captured public transport address"
if dry_run
else "recorded captured public transport address"
)
elif result.status == "missing":
status = "blocked"
message = result.message
else:
status = "skipped"
message = result.message
return AddressApplyResult(
site_id=result.site_id,
protocol=result.protocol,
status=status,
mode="dry-run" if dry_run else "live",
url=result.url,
source=result.source,
path=result.path,
message=message,
)
def _result( def _result(
copy: InstallStateCopy, copy: InstallStateCopy,
*, *,
+38 -1
View File
@@ -22,9 +22,11 @@ from .addresses import (
set_address, set_address,
) )
from .apply import ( from .apply import (
AddressApplyResult,
StateApplyResult, StateApplyResult,
StartApplyResult, StartApplyResult,
SupervisorApplyResult, SupervisorApplyResult,
apply_addresses,
apply_state, apply_state,
apply_start, apply_start,
apply_supervisor, apply_supervisor,
@@ -160,7 +162,7 @@ def main(argv: list[str] | None = None) -> int:
_add_target_selection(deploy_apply_parser) _add_target_selection(deploy_apply_parser)
deploy_apply_parser.add_argument( deploy_apply_parser.add_argument(
"--stage", "--stage",
choices=("state", "supervisor", "start"), choices=("state", "supervisor", "start", "addresses"),
required=True, required=True,
help="Deployment stage to apply.", help="Deployment stage to apply.",
) )
@@ -642,6 +644,26 @@ def _cmd_deploy(config, args) -> int:
f"blocked={len(blocked)}" f"blocked={len(blocked)}"
) )
return 1 if blocked else 0 return 1 if blocked else 0
if args.stage == "addresses":
results = apply_addresses(config, dry_run=args.dry_run)
for result in results:
_print_address_apply_result(result)
blocked = [result for result in results if result.status == "blocked"]
written = [result for result in results if result.status == "written"]
planned = [result for result in results if result.status == "planned"]
skipped = [result for result in results if result.status == "skipped"]
print(
"AMPG_DEPLOY_APPLY_SUMMARY "
f"stage=addresses "
f"mode={'dry-run' if args.dry_run else 'live'} "
f"results={len(results)} "
f"planned={len(planned)} "
f"written={len(written)} "
f"skipped={len(skipped)} "
f"blocked={len(blocked)} "
f"registry=\"{_quote(str(address_registry_path(config)))}\""
)
return 1 if blocked else 0
return 1 return 1
@@ -1165,6 +1187,21 @@ def _print_start_apply_result(result: StartApplyResult) -> None:
) )
def _print_address_apply_result(result: AddressApplyResult) -> None:
path = str(result.path) if result.path else "-"
print(
"AMPG_DEPLOY_ADDRESS "
f"site={result.site_id} "
f"protocol={result.protocol} "
f"mode={result.mode} "
f"status={result.status} "
f"url=\"{_quote(result.url)}\" "
f"source=\"{_quote(result.source)}\" "
f"path=\"{_quote(path)}\" "
f"message=\"{_quote(result.message)}\""
)
def _print_dns_record(record: DNSRecordPlan) -> None: def _print_dns_record(record: DNSRecordPlan) -> None:
print( print(
"AMPG_DNS_RECORD " "AMPG_DNS_RECORD "
+2 -2
View File
@@ -82,7 +82,6 @@ def deploy_plan(
daemon_probe=daemon_probe, daemon_probe=daemon_probe,
), ),
_artifact_step(config, command_context, platform_provider=platform_provider), _artifact_step(config, command_context, platform_provider=platform_provider),
_address_step(config, command_context),
_apply_preflight_step( _apply_preflight_step(
config, config,
command_context, command_context,
@@ -95,6 +94,7 @@ def deploy_plan(
platform_provider=platform_provider, platform_provider=platform_provider,
daemon_probe=daemon_probe, daemon_probe=daemon_probe,
), ),
_address_step(config, command_context),
) )
status = _overall_status(steps) status = _overall_status(steps)
return DeployPlan( return DeployPlan(
@@ -273,7 +273,7 @@ def _address_step(config: GatewayConfig, commands: _CommandContext) -> DeploySte
return DeployStep( return DeployStep(
"addresses", "addresses",
"review", "review",
commands.command("addresses capture"), commands.command("deploy apply --stage addresses --dry-run"),
f"capture or set generated transport address(es): {names}", f"capture or set generated transport address(es): {names}",
) )
return DeployStep("addresses", "ready", commands.command("addresses list"), "addresses are known") return DeployStep("addresses", "ready", commands.command("addresses list"), "addresses are known")
+4
View File
@@ -142,6 +142,10 @@ after the state files they reference exist. It does not start or reload services
services after state and supervisor files exist. Address capture and health verification services after state and supervisor files exist. Address capture and health verification
remain separate stages. remain separate stages.
`ampg deploy apply --stage addresses` records daemon-written public addresses after the
transport has produced them. It writes only the AMPG address registry; health
verification remains a separate stage.
The dry-run command is intentionally stricter than `doctor`: missing generated output is 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 blocked during activation, because AMPG must not point a transport at an unbuilt output
root. root.
+10 -3
View File
@@ -62,6 +62,8 @@ python3 -m ampg --config gateway.toml deploy apply --stage supervisor --dry-run
python3 -m ampg --config gateway.toml deploy apply --stage supervisor --profile mobile-i2p --yes python3 -m ampg --config gateway.toml deploy apply --stage supervisor --profile mobile-i2p --yes
python3 -m ampg --config gateway.toml deploy apply --stage start --dry-run --profile mobile-i2p python3 -m ampg --config gateway.toml deploy apply --stage start --dry-run --profile mobile-i2p
python3 -m ampg --config gateway.toml deploy apply --stage start --profile mobile-i2p --yes 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 doctor --platform android-termux python3 -m ampg --config gateway.toml doctor --platform android-termux
``` ```
@@ -99,9 +101,9 @@ daemon-written addresses under `gateway.state_dir`.
which paths are AMPG-owned, daemon-written, required, or sensitive. which paths are AMPG-owned, daemon-written, required, or sensitive.
`addresses list` prints the effective public address for each enabled protocol: `addresses list` prints the effective public address for each enabled protocol:
configured, captured, derived, or placeholder. `addresses capture` reads daemon-written configured, captured, derived, or placeholder. `deploy apply --stage addresses` and
address files from AMPG state or a protocol `address_file` override, then updates the `addresses capture` read daemon-written address files from AMPG state or a protocol
address registry under `state_dir`. `address_file` override, then update the address registry under `state_dir`.
`health-plan` prints fixture-based checks to run after services start. Published checks `health-plan` prints fixture-based checks to run after services start. Published checks
target real transport URLs; preview checks target local loopback preview endpoints. target real transport URLs; preview checks target local loopback preview endpoints.
@@ -139,6 +141,11 @@ requires both state and supervisor files to exist, and runs only structured plat
commands for AMPG-named services. It does not install packages, rewrite config, remove commands for AMPG-named services. It does not install packages, rewrite config, remove
files, capture addresses, or run health checks. files, capture addresses, or run health checks.
`deploy apply --stage addresses` is the fourth live apply stage. Dry-run mode prints
`AMPG_DEPLOY_ADDRESS` rows for configured, skipped, missing, and capturable addresses.
Live mode requires `--yes` and writes captured transport addresses under
`gateway.state_dir`. It does not rewrite daemon config or run health checks.
`--protocol` scopes operational commands to selected enabled protocols. A clearnet `--protocol` scopes operational commands to selected enabled protocols. A clearnet
`adopt` failure will block a full activation run, but it will not block `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. `apply --dry-run --protocol i2p` because clearnet is outside that selected run.
+6 -4
View File
@@ -33,6 +33,7 @@ or deployment notes.
python3 -m ampg --config examples/wownero.gateway.toml build python3 -m ampg --config examples/wownero.gateway.toml build
python3 -m ampg --config examples/wownero.gateway.toml manifest python3 -m ampg --config examples/wownero.gateway.toml manifest
python3 -m ampg --config examples/wownero.gateway.toml addresses list python3 -m ampg --config examples/wownero.gateway.toml addresses list
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 addresses capture --profile mobile-i2p python3 -m ampg --config examples/wownero.gateway.toml addresses capture --profile mobile-i2p
python3 -m ampg --config examples/wownero.gateway.toml preview manifest 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
@@ -62,10 +63,11 @@ Some transports may not have final public addresses when a fixture is generated.
cases AMPG uses route-valid placeholders such as `http://wownero.onion/` so AMPB can still cases AMPG uses route-valid placeholders such as `http://wownero.onion/` so AMPB can still
verify transport selection without contacting the network. verify transport selection without contacting the network.
When a managed or adopted daemon writes an actual address, run `addresses capture`. When a managed or adopted daemon writes an actual address, run
Captured addresses are stored under `gateway.state_dir` and replace placeholders in `deploy apply --stage addresses --dry-run` before recording it. Captured addresses are
fixture manifests and published health plans. Operators can also use `addresses set` to stored under `gateway.state_dir` and replace placeholders in fixture manifests and
store an address manually. published health plans. Operators can also use `addresses capture` for a direct write or
`addresses set` to store an address manually.
## Interaction Policy ## Interaction Policy
+4
View File
@@ -61,3 +61,7 @@ explicit stage.
`deploy apply --stage start` runs the service-manager start commands for AMPG-named `deploy apply --stage start` runs the service-manager start commands for AMPG-named
services only after required state and supervisor files exist. It does not write or services only after required state and supervisor files exist. It does not write or
delete state files. delete state files.
`deploy apply --stage addresses` reads daemon-written address files from the state
contract and writes captured public addresses to `addresses.json` under
`gateway.state_dir`.
+4 -2
View File
@@ -141,8 +141,8 @@ daemon_policy = "auto"
- `ampg build` writes a fixture manifest that AMPB can check. - `ampg build` writes a fixture manifest that AMPB can check.
- `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live - `ampg preview manifest` writes loopback fixture URLs that AMPB can check before live
transport daemons are installed or adopted. transport daemons are installed or adopted.
- `ampg deploy apply --stage state`, `--stage supervisor`, and `--stage start` stage - `ampg deploy apply --stage state`, `--stage supervisor`, `--stage start`, and
Wownero transport services without mixing config writes and service start. `--stage addresses` stage Wownero transport publication without mixing concerns.
- Tor output contains no `<script>` tags or inline event handlers. - Tor output contains no `<script>` tags or inline event handlers.
- Generated Gemini output has readable headings, paragraphs, and links. - Generated Gemini output has readable headings, paragraphs, and links.
- Generated Micron output fits terminal-first browsing. - Generated Micron output fits terminal-first browsing.
@@ -167,6 +167,7 @@ python3 -m ampg --config examples/wownero.gateway.toml approvals approve --profi
python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p python3 -m ampg --config examples/wownero.gateway.toml deploy apply --stage state --dry-run --profile mobile-i2p
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 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 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/i2p-only.gateway.toml plan python3 -m ampg --config examples/i2p-only.gateway.toml plan
python3 -m ampg --config examples/i2p-only.gateway.toml build python3 -m ampg --config examples/i2p-only.gateway.toml build
``` ```
@@ -180,3 +181,4 @@ python3 -m ampg --config examples/i2p-only.gateway.toml build
- [ ] Add Reticulum/Micron conversion fixtures. - [ ] Add Reticulum/Micron conversion fixtures.
- [ ] Enable I2P and Reticulum daemon management only after static outputs are stable. - [ ] Enable I2P and Reticulum daemon management only after static outputs are stable.
- [ ] Start only AMPG-owned Wownero transport services after reviewed files are applied. - [ ] Start only AMPG-owned Wownero transport services after reviewed files are applied.
- [ ] Capture Wownero transport addresses before published health checks.
+87
View File
@@ -4,6 +4,7 @@ import tempfile
import unittest import unittest
from pathlib import Path 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_start, apply_state, apply_supervisor
from ampg.cli import main from ampg.cli import main
from ampg.config import load_config from ampg.config import load_config
@@ -148,6 +149,13 @@ def _run_termux_supervisor_apply(config_path: Path, root: Path) -> None:
raise AssertionError(blocked[0].message) raise AssertionError(blocked[0].message)
def _write_i2p_address_file(root: Path, value: str = "captureexample.b32.i2p") -> Path:
path = root / ".ampg/state/example/i2p/b32.txt"
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(value + "\n", encoding="utf-8")
return path
class DeployApplyTest(unittest.TestCase): class DeployApplyTest(unittest.TestCase):
def test_deploy_apply_state_blocks_unapproved_artifacts(self): def test_deploy_apply_state_blocks_unapproved_artifacts(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
@@ -479,6 +487,85 @@ class DeployApplyTest(unittest.TestCase):
self.assertIn(("sv-enable", "ampg-example-i2p-i2pd"), commands) self.assertIn(("sv-enable", "ampg-example-i2p-i2pd"), commands)
self.assertIn(("sv-enable", "ampg-example-i2p-nginx-loopback"), commands) self.assertIn(("sv-enable", "ampg-example-i2p-nginx-loopback"), commands)
def test_deploy_apply_addresses_blocks_when_address_is_missing(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",
"addresses",
"--dry-run",
"--profile",
"mobile-i2p",
]
)
self.assertEqual(1, status)
self.assertEqual("", error)
self.assertIn("AMPG_DEPLOY_ADDRESS", output)
self.assertIn("status=blocked", output)
self.assertIn("no address file found", output)
def test_deploy_apply_addresses_dry_run_does_not_write_registry(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
config_path = _init_i2p_config(root)
_write_i2p_address_file(root)
config = load_config(config_path)
status, output, error = _run_cli(
[
"--config",
str(config_path),
"deploy",
"apply",
"--stage",
"addresses",
"--dry-run",
"--profile",
"mobile-i2p",
]
)
registry_exists = address_registry_path(config).exists()
self.assertEqual(0, status, error)
self.assertIn("AMPG_DEPLOY_ADDRESS", output)
self.assertIn("status=planned", output)
self.assertIn("http://captureexample.b32.i2p/", output)
self.assertFalse(registry_exists)
def test_deploy_apply_addresses_live_writes_registry(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
config_path = _init_i2p_config(root)
_write_i2p_address_file(root)
config = load_config(config_path)
status, output, error = _run_cli(
[
"--config",
str(config_path),
"deploy",
"apply",
"--stage",
"addresses",
"--profile",
"mobile-i2p",
"--yes",
]
)
registry_text = address_registry_path(config).read_text(encoding="utf-8")
self.assertEqual(0, status, error)
self.assertIn("status=written", output)
self.assertIn("http://captureexample.b32.i2p/", registry_text)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+1
View File
@@ -63,6 +63,7 @@ class DeployPlanTest(unittest.TestCase):
self.assertIn("AMPG_DEPLOY_STEP stage=artifacts status=todo", output) self.assertIn("AMPG_DEPLOY_STEP stage=artifacts status=todo", output)
self.assertIn("AMPG_DEPLOY_NEXT step=1 stage=build", output) self.assertIn("AMPG_DEPLOY_NEXT step=1 stage=build", output)
self.assertIn("build --profile mobile-i2p", output) self.assertIn("build --profile mobile-i2p", output)
self.assertIn("deploy apply --stage addresses --dry-run --profile mobile-i2p", output)
def test_deploy_plan_shows_dns_review_for_clearnet_site(self): def test_deploy_plan_shows_dns_review_for_clearnet_site(self):
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp: