mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
fix(plane): ax task names leave room for the template suffix
ax names a task's ActorTemplate <task>-tmpl-<8 hex> and Substrate caps names at 63 bytes; validation-46's first task (51 characters) fell back to a missing default template on the live cluster. Task names are capped at 49 with a stable hash suffix when longer. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
46f0492eed
commit
c97e216d45
@@ -23,6 +23,7 @@ State: ``~/ousast-results/plane/<run>/`` (``OUSAST_RESULTS``), polled every ``OU
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import queue
|
||||
@@ -79,8 +80,17 @@ def _read_json(path: Path) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
def _ax_name(*parts: str) -> str:
|
||||
return re.sub(r"[^a-z0-9-]+", "-", "-".join(parts).lower()).strip("-")[:63]
|
||||
# ax names a task's ActorTemplate "<task>-tmpl-<8 hex>" and Substrate caps names at 63 bytes (live, 2026-09-29).
|
||||
TASK_NAME_LIMIT = 63 - len("-tmpl-12345678")
|
||||
|
||||
|
||||
def _ax_name(*parts: str, limit: int = 63) -> str:
|
||||
"""An RFC 1123 label; a name over ``limit`` keeps a readable prefix plus a stable hash, so two never collide."""
|
||||
name = re.sub(r"[^a-z0-9-]+", "-", "-".join(parts).lower()).strip("-")
|
||||
if len(name) <= limit:
|
||||
return name
|
||||
digest = hashlib.sha1(name.encode("utf-8")).hexdigest()[:8]
|
||||
return f"{name[: limit - 9].rstrip('-')}-{digest}"
|
||||
|
||||
|
||||
def load_run(run_manifest: Path) -> tuple[Run, Manifests]:
|
||||
@@ -131,7 +141,7 @@ def render_task(run: Run, entry: RunTask, manifests: Manifests, artifact_url: st
|
||||
inputs are never rendered: ax puts every bound Workspace into one env value, which Substrate caps at 32768
|
||||
characters, so the runner fetches them from the receiver (``OUSAST_INPUTS``) after the start."""
|
||||
task: Task = manifests.tasks[entry.task]
|
||||
ax_name = _ax_name(run.metadata.name, entry.name)
|
||||
ax_name = _ax_name(run.metadata.name, entry.name, limit=TASK_NAME_LIMIT)
|
||||
env = {e.name: e.value for e in task.env}
|
||||
env.update(OUSAST_RUN=run.metadata.name, OUSAST_TASK=entry.name, OUSAST_OUTPUT_DIR=f"{OUTPUT_ROOT}/{entry.name}")
|
||||
env.update(OUSAST_ARTIFACT_URL=artifact_url, **({"OUSAST_ARTIFACT_DIAL": dial} if dial else {}))
|
||||
@@ -293,7 +303,7 @@ def _await(ax: Ax, name: str, delivered: Callable[[], bool], start: Callable[[],
|
||||
def _execute(
|
||||
ax: Ax, egress: Egress, run: Run, entry: RunTask, manifests: Manifests, base: Path, receiver: Receiver, router: Router
|
||||
) -> tuple[str, str]:
|
||||
ax_name, task = _ax_name(run.metadata.name, entry.name), manifests.tasks[entry.task]
|
||||
ax_name, task = _ax_name(run.metadata.name, entry.name, limit=TASK_NAME_LIMIT), manifests.tasks[entry.task]
|
||||
atespace, manifest = task.metadata.atespace or "default", base / entry.name / "task.yaml"
|
||||
manifest.parent.mkdir(parents=True, exist_ok=True)
|
||||
docs = render_task(run, entry, manifests, receiver.url, receiver.dial)
|
||||
@@ -337,7 +347,7 @@ def run(run_manifest: Path, *, workers: int = 1, ax: str = "ax", results_root: P
|
||||
|
||||
def worker(entry: RunTask) -> None:
|
||||
if state.status(entry.name) in ("running", "suspended"):
|
||||
cli.delete(_ax_name(run_spec.metadata.name, entry.name)) # an interrupted attempt; ax must not keep it
|
||||
cli.delete(_ax_name(run_spec.metadata.name, entry.name, limit=TASK_NAME_LIMIT)) # an interrupted attempt; ax must not keep it
|
||||
state.set(entry.name, status="running", started=_now(), finished=None)
|
||||
finished.put((entry.name, *_execute(cli, egress, run_spec, entry, manifests, base, receiver, router)))
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -865,3 +866,14 @@ def test_a_megabyte_producer_artifact_keeps_every_rendered_value_under_substrate
|
||||
assert len(yaml.safe_dump(task)) < SUBSTRATE_ENV_LIMIT, f"{entry.name}: AX_TASK_YAML"
|
||||
assert len(yaml.safe_dump_all(workspaces)) < SUBSTRATE_ENV_LIMIT, f"{entry.name}: AX_WORKSPACES_YAML"
|
||||
assert all(len(yaml.safe_dump(w)) < SUBSTRATE_ENV_LIMIT for w in workspaces), entry.name
|
||||
|
||||
|
||||
def test_ax_task_names_leave_room_for_the_template_suffix() -> None:
|
||||
"""ax names the template "<task>-tmpl-<8 hex>" and Substrate caps names at 63 bytes: a 51-character task name
|
||||
failed on the live cluster (2026-09-29). Long names keep a prefix plus a stable hash; short ones are unchanged."""
|
||||
long = reconciler._ax_name("validation-46", "budibase-mongo-template-nosqli-facts", limit=reconciler.TASK_NAME_LIMIT)
|
||||
assert len(long) + len("-tmpl-1e472705") <= 63 and long.startswith("validation-46-budibase")
|
||||
other = reconciler._ax_name("validation-46", "budibase-mongo-template-nosqli-agree", limit=reconciler.TASK_NAME_LIMIT)
|
||||
assert other != long, "no collision between tasks sharing a long prefix"
|
||||
assert reconciler._ax_name("run", "facts", limit=reconciler.TASK_NAME_LIMIT) == "run-facts"
|
||||
assert re.fullmatch(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?", long)
|
||||
|
||||
Reference in New Issue
Block a user