mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 11:49:28 +00:00
fix(plane): ax field names verbatim; pins and bindings as stripped annotations
Requirement 1.1 (Task/Workspace/Model use only fields ax documents) and
Requirement 1.4 (no ax field repurposed) were violated: manifests accepted
metadata.namespace and a Workspace git `commit`, and the reconciler dumped
every git field into what `ax apply` receives.
Facts from ax's source (pkg/apis/v1alpha1/ax.proto, internal/workspace/setup.go):
- ObjectMeta is `name` and `atespace` (plus server-set creation_timestamp);
no `namespace`, no `annotations`.
- GitRepo is `name`, `repo`, `branch`, `dir`, `depth`; no `commit`.
- The API server decodes with protojson.UnmarshalOptions{}: an unknown field
is an error at `ax apply` (confirmed on the live cluster: a rendered
Workspace+Model applied cleanly; the same Workspace with `commit` was
refused with `unknown field "commit"`).
- ValidateModel checks only metadata; any spec.provider string is accepted.
Changes:
- manifests: metadata takes `name`, `atespace`, `annotations` (our extension,
never sent to ax); git entries take exactly ax's five fields (depth int);
`commit` in spec.git is rejected with a pointer to the new Workspace
annotation `openultrasast.io/git-commits: "<git name>=<40-hex>,..."`,
validated for unknown git names and non-hex shas.
- reconciler: rendered docs carry only ax fields; each bound Workspace's pins
reach the Task as env OUSAST_GIT_PINS = {"<workspace>/<git name>": "<sha>"}.
A test walks every rendered doc against ax's field lists.
- runner: pins come from OUSAST_GIT_PINS; git is fetched as setup.go does
(init/remote/fetch with --depth when depth > 0/checkout FETCH_HEAD, default
branch main, cloneDestination's dir and placeholder-name rules), then the
pin is checked out. A pinned entry without a branch fetches the commit.
- workspaces: emits the annotation, `dir: repo` and `depth: 1` instead of
`commit`, so generated case workspaces keep <path>/repo at depth 1.
- design.md: ax field names verbatim; pins and model binding as stripped
openultrasast.io/* annotations carried to the task by env.
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
e6d5124ffc
commit
18b67cf942
@@ -1,7 +1,11 @@
|
||||
"""Manifests of the model-level service plane.
|
||||
|
||||
``Task``, ``Workspace`` and ``Model`` follow google/ax (``apiVersion: ax.io/v1alpha1``) and carry only the
|
||||
fields ax documents. ``Run`` (``apiVersion: openultrasast.io/v1alpha1``) is this project's kind for the task
|
||||
fields ax documents, under ax's own names (``metadata.atespace``, not ``namespace``). ``metadata.annotations``
|
||||
is this project's extension: ax's ObjectMeta has no such field and its API server rejects unknown fields, so the
|
||||
reconciler strips annotations before anything reaches ``ax apply``. They declare the provider extension and carry
|
||||
the Model binding and the Workspace commit pins (``openultrasast.io/git-commits: "<git name>=<sha>,..."``), which
|
||||
reach the task as env. ``Run`` (``apiVersion: openultrasast.io/v1alpha1``) is this project's kind for the task
|
||||
graph, artifacts and budgets; it holds nothing a task needs to execute. Every schema violation raises
|
||||
:class:`ManifestError`, whose message names the kind, the ``metadata.name`` and the offending field path
|
||||
(``Task/verify: spec.foo is not a field``), before anything runs.
|
||||
@@ -9,6 +13,7 @@ graph, artifacts and budgets; it holds nothing a task needs to execute. Every sc
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -20,6 +25,7 @@ __all__ = [
|
||||
"AX_API_VERSION",
|
||||
"AX_PROVIDERS",
|
||||
"EXTENSION_PROVIDERS",
|
||||
"GIT_COMMITS_ANNOTATION",
|
||||
"PROVIDER_EXTENSION_ANNOTATION",
|
||||
"RUN_API_VERSION",
|
||||
"Budget",
|
||||
@@ -48,6 +54,7 @@ __all__ = [
|
||||
AX_API_VERSION = "ax.io/v1alpha1"
|
||||
RUN_API_VERSION = "openultrasast.io/v1alpha1"
|
||||
PROVIDER_EXTENSION_ANNOTATION = "openultrasast.io/provider-extension"
|
||||
GIT_COMMITS_ANNOTATION = "openultrasast.io/git-commits"
|
||||
AX_PROVIDERS = frozenset({"google", "anthropic"})
|
||||
EXTENSION_PROVIDERS = frozenset({"deepseek"})
|
||||
|
||||
@@ -64,7 +71,7 @@ class ManifestError(ValueError):
|
||||
@dataclass(frozen=True)
|
||||
class Metadata:
|
||||
name: str
|
||||
namespace: str | None = None
|
||||
atespace: str | None = None
|
||||
annotations: Mapping[str, str] = field(default_factory=dict)
|
||||
|
||||
|
||||
@@ -106,10 +113,13 @@ class Task:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GitSource:
|
||||
"""ax's ``GitRepo``: exactly ``name``, ``repo``, ``branch``, ``dir``, ``depth`` (a commit pin is an annotation)."""
|
||||
|
||||
name: str
|
||||
repo: str
|
||||
branch: str | None = None
|
||||
commit: str | None = None
|
||||
dir: str | None = None
|
||||
depth: int | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -138,6 +148,11 @@ class Workspace:
|
||||
mcp: McpSpec | None = None
|
||||
skills: SkillsSpec | None = None
|
||||
|
||||
@property
|
||||
def pins(self) -> dict[str, str]:
|
||||
"""Git entry name -> 40-hex commit, from ``metadata.annotations["openultrasast.io/git-commits"]``."""
|
||||
return _split_pins(self.metadata.annotations.get(GIT_COMMITS_ANNOTATION, ""))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SecretKey:
|
||||
@@ -275,6 +290,12 @@ class _Check:
|
||||
raise self.fail(_join(_join(path, key), str(item_key)), "must map a string to a non-empty string")
|
||||
return value
|
||||
|
||||
def optional_int(self, parent: Mapping[str, Any], path: str, key: str) -> int | None:
|
||||
value = parent.get(key)
|
||||
if value is not None and (isinstance(value, bool) or not isinstance(value, int) or value < 0):
|
||||
raise self.fail(_join(path, key), "must be a non-negative integer")
|
||||
return value
|
||||
|
||||
def boolean(self, parent: Mapping[str, Any], path: str, key: str) -> bool:
|
||||
value = parent.get(key, False)
|
||||
if not isinstance(value, bool):
|
||||
@@ -286,10 +307,10 @@ class _Check:
|
||||
|
||||
|
||||
def _parse_metadata(check: _Check, value: object) -> Metadata:
|
||||
meta = check.mapping(value, "metadata", ("name", "namespace", "annotations"))
|
||||
meta = check.mapping(value, "metadata", ("name", "atespace", "annotations"))
|
||||
return Metadata(
|
||||
name=check.string(meta, "metadata", "name"),
|
||||
namespace=check.optional_string(meta, "metadata", "namespace"),
|
||||
atespace=check.optional_string(meta, "metadata", "atespace"),
|
||||
annotations=check.string_map(meta, "metadata", "annotations"),
|
||||
)
|
||||
|
||||
@@ -329,20 +350,59 @@ def _parse_task(check: _Check, metadata: Metadata, spec: Mapping[str, Any]) -> T
|
||||
)
|
||||
|
||||
|
||||
_SHA = re.compile(r"[0-9a-f]{40}")
|
||||
|
||||
|
||||
def _pin_items(value: str) -> list[str]:
|
||||
return [item for item in (part.strip() for part in value.split(",")) if item]
|
||||
|
||||
|
||||
def _split_pins(value: str) -> dict[str, str]:
|
||||
"""``name=sha,name=sha`` -> ``{name: sha}`` (validated at parse time by :func:`_check_pins`)."""
|
||||
pairs = (item.partition("=") for item in _pin_items(value))
|
||||
return {name.strip(): sha.strip() for name, _, sha in pairs}
|
||||
|
||||
|
||||
def _check_pins(check: _Check, metadata: Metadata, git: tuple[GitSource, ...]) -> None:
|
||||
path = f'metadata.annotations["{GIT_COMMITS_ANNOTATION}"]'
|
||||
names = {g.name for g in git}
|
||||
for item in _pin_items(metadata.annotations.get(GIT_COMMITS_ANNOTATION, "")):
|
||||
name, eq, sha = (part.strip() for part in item.partition("="))
|
||||
if not eq or not name:
|
||||
raise check.fail(path, f'item "{item}" must be "<git name>=<40-hex commit>"')
|
||||
if name not in names:
|
||||
raise check.fail(path, f'"{name}" names no spec.git entry')
|
||||
if not _SHA.fullmatch(sha):
|
||||
raise check.fail(path, f'"{name}={sha}" is not a 40-hex commit')
|
||||
|
||||
|
||||
def _parse_git(check: _Check, spec: Mapping[str, Any]) -> tuple[GitSource, ...]:
|
||||
for index, entry in enumerate(check.sequence(spec, "spec", "git")):
|
||||
if isinstance(entry, dict) and "commit" in entry:
|
||||
raise check.fail(
|
||||
f"spec.git[{index}].commit",
|
||||
f'is not an ax field; pin the commit in metadata.annotations["{GIT_COMMITS_ANNOTATION}"] as "<git name>=<sha>"',
|
||||
)
|
||||
return tuple(
|
||||
GitSource(
|
||||
check.string(g, p, "name"),
|
||||
check.string(g, p, "repo"),
|
||||
check.optional_string(g, p, "branch"),
|
||||
check.optional_string(g, p, "dir"),
|
||||
check.optional_int(g, p, "depth"),
|
||||
)
|
||||
for p, g in check.mappings(spec, "spec", "git", ("name", "repo", "branch", "dir", "depth"))
|
||||
)
|
||||
|
||||
|
||||
def _parse_workspace(check: _Check, metadata: Metadata, spec: Mapping[str, Any]) -> Workspace:
|
||||
mcp = check.optional_mapping(spec, "spec", "mcp", ("registries", "servers"))
|
||||
skills = check.optional_mapping(spec, "spec", "skills", ("registries", "path"))
|
||||
git = _parse_git(check, spec)
|
||||
_check_pins(check, metadata, git)
|
||||
return Workspace(
|
||||
metadata=metadata,
|
||||
git=tuple(
|
||||
GitSource(
|
||||
check.string(g, p, "name"),
|
||||
check.string(g, p, "repo"),
|
||||
check.optional_string(g, p, "branch"),
|
||||
check.optional_string(g, p, "commit"),
|
||||
)
|
||||
for p, g in check.mappings(spec, "spec", "git", ("name", "repo", "branch", "commit"))
|
||||
),
|
||||
git=git,
|
||||
files=tuple(
|
||||
FileEntry(check.string(f, p, "path"), check.string(f, p, "content"))
|
||||
for p, f in check.mappings(spec, "spec", "files", ("path", "content"))
|
||||
|
||||
@@ -6,9 +6,11 @@ file (the Task with its env extended, its Workspaces, one generated Workspace ca
|
||||
``files`` entries, and the bound Model), runs ``ax apply -f``, polls ``ax get task`` until a terminal phase and
|
||||
``ax delete task`` afterwards. The outcome is read from the delivered ``summary.json``, never from the phase alone.
|
||||
|
||||
Model binding: a Task names its Model in ``metadata.annotations["openultrasast.io/model"]`` (ax's ObjectMeta has no
|
||||
annotations, so the rendered Task carries only ``name``/``atespace``); the task receives ``OUSAST_MODEL`` and the
|
||||
Model's ``spec.parameters`` as ``OUSAST_MODEL_PARAMS`` JSON (Req 7.1). Inputs: ``OUSAST_INPUT_<NAME>`` points into a
|
||||
ax's ObjectMeta is ``name``/``atespace`` and its API server rejects unknown fields, so rendered documents carry only
|
||||
ax's fields; ``openultrasast.io/*`` annotations are stripped and reach the task as env. Model binding: a Task names
|
||||
its Model in ``openultrasast.io/model``; the task receives ``OUSAST_MODEL`` and the Model's ``spec.parameters`` as
|
||||
``OUSAST_MODEL_PARAMS`` JSON (Req 7.1). Commit pins: each bound Workspace's ``openultrasast.io/git-commits`` becomes
|
||||
``OUSAST_GIT_PINS`` = ``{"<workspace>/<git name>": "<sha>"}``. Inputs: ``OUSAST_INPUT_<NAME>`` points into a
|
||||
generated Workspace bound at ``/workspace/.ousast-in/<task>`` (ax mounts every workspace under ``/workspace``).
|
||||
Artifacts: the runner POSTs an uncompressed tar of its output directory to ``OUSAST_ARTIFACT_URL`` with
|
||||
``X-Ousast-Run`` and ``X-Ousast-Task`` headers; the receiver listens on ``0.0.0.0`` at ``OUSAST_ARTIFACT_PORT``
|
||||
@@ -97,8 +99,8 @@ def load_run(run_manifest: Path) -> tuple[Run, Manifests]:
|
||||
return next(iter(manifests.runs.values())), manifests
|
||||
|
||||
|
||||
def _doc(kind: str, name: str, spec: dict[str, Any], namespace: str | None = None) -> dict[str, Any]:
|
||||
meta = {"name": name, **({"atespace": namespace} if namespace else {})}
|
||||
def _doc(kind: str, name: str, spec: dict[str, Any], atespace: str | None = None) -> dict[str, Any]:
|
||||
meta = {"name": name, **({"atespace": atespace} if atespace else {})}
|
||||
return {"apiVersion": AX_API_VERSION, "kind": kind, "metadata": meta, "spec": spec}
|
||||
|
||||
|
||||
@@ -116,7 +118,7 @@ def _workspace_doc(ws: Workspace) -> dict[str, Any]:
|
||||
spec["mcp"] = {"registries": list(ws.mcp.registries), "servers": list(ws.mcp.servers)}
|
||||
if ws.skills:
|
||||
spec["skills"] = {"registries": list(ws.skills.registries), **({"path": ws.skills.path} if ws.skills.path else {})}
|
||||
return _doc("Workspace", ws.metadata.name, spec, ws.metadata.namespace)
|
||||
return _doc("Workspace", ws.metadata.name, spec, ws.metadata.atespace)
|
||||
|
||||
|
||||
def _model_doc(model: Model) -> dict[str, Any]:
|
||||
@@ -125,7 +127,7 @@ def _model_doc(model: Model) -> dict[str, Any]:
|
||||
spec["secretKey"] = _fields(model.secret_key)
|
||||
if model.parameters:
|
||||
spec["parameters"] = dict(model.parameters)
|
||||
return _doc("Model", model.metadata.name, spec, model.metadata.namespace)
|
||||
return _doc("Model", model.metadata.name, spec, model.metadata.atespace)
|
||||
|
||||
|
||||
def _input_files(entry: RunTask, base: Path) -> list[dict[str, str]]:
|
||||
@@ -157,8 +159,10 @@ def render_task(run: Run, entry: RunTask, manifests: Manifests, base: Path, arti
|
||||
docs.append(_model_doc(model))
|
||||
bindings = [_fields(b) for b in task.workspaces]
|
||||
docs.extend(_workspace_doc(manifests.workspaces[b.name]) for b in task.workspaces)
|
||||
pins = {f"{b.name}/{git}": sha for b in task.workspaces for git, sha in manifests.workspaces[b.name].pins.items()}
|
||||
env.update({"OUSAST_GIT_PINS": json.dumps(pins, sort_keys=True)} if pins else {})
|
||||
if entry.inputs:
|
||||
docs.append(_doc("Workspace", f"{ax_name}-inputs", {"files": _input_files(entry, base)}, task.metadata.namespace))
|
||||
docs.append(_doc("Workspace", f"{ax_name}-inputs", {"files": _input_files(entry, base)}, task.metadata.atespace))
|
||||
bindings.append({"name": f"{ax_name}-inputs", "path": f"{INPUTS_ROOT}/{entry.name}"})
|
||||
spec: dict[str, Any] = {"command": list(task.command), "env": [{"name": k, "value": v} for k, v in env.items()]}
|
||||
if task.image:
|
||||
@@ -169,7 +173,7 @@ def render_task(run: Run, entry: RunTask, manifests: Manifests, base: Path, arti
|
||||
spec["workspaces"] = bindings
|
||||
if task.debug:
|
||||
spec["debug"] = True
|
||||
return [_doc("Task", ax_name, spec, task.metadata.namespace), *docs]
|
||||
return [_doc("Task", ax_name, spec, task.metadata.atespace), *docs]
|
||||
|
||||
|
||||
class Ax:
|
||||
|
||||
@@ -17,6 +17,7 @@ Environment the runner reads, all optional except the two ax variables:
|
||||
AX_RUNNER_HTTP "0" disables the health server (unit tests only)
|
||||
AX_RUNNER_PORT port of the health server, default 80; "0" picks a free port
|
||||
OUSAST_CASE_CACHE a directory of git checkouts (``benchmarks/independent`` cache layout)
|
||||
OUSAST_GIT_PINS JSON ``{"<workspace>/<git name>": "<40-hex commit>"}`` from the reconciler
|
||||
OUSAST_ARTIFACT_URL, OUSAST_RUN where to deliver, and the run name carried in ``X-Ousast-Run``
|
||||
OUSAST_DELIVERY_BACKOFF seconds before the second delivery attempt, doubled per retry
|
||||
|
||||
@@ -32,7 +33,7 @@ import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
@@ -61,7 +62,9 @@ __all__ = [
|
||||
"RunnerError",
|
||||
"build_tar",
|
||||
"deliver",
|
||||
"clone_destination",
|
||||
"find_cached_checkout",
|
||||
"load_pins",
|
||||
"load_task",
|
||||
"load_workspaces",
|
||||
"main",
|
||||
@@ -77,6 +80,9 @@ DELIVERY_ATTEMPTS = 3
|
||||
TASK_PACKAGE = "openultrasast.plane.tasks"
|
||||
EXIT_BY_STATUS = {"done": 0, "failed": 2, "unfinished": 3}
|
||||
SUMMARY = "summary.json"
|
||||
DEFAULT_BRANCH = "main" # ax internal/workspace/setup.go: defaultBranch
|
||||
_PLACEHOLDER_NAMES = ("", "repo", "origin") # setup.go: names that do not name a checkout directory
|
||||
_SHA = re.compile(r"[0-9a-f]{40}")
|
||||
|
||||
log = logging.getLogger("ousast.plane.runner")
|
||||
|
||||
@@ -221,13 +227,45 @@ def find_cached_checkout(cache: Path, repo: str) -> Path | None:
|
||||
return None
|
||||
|
||||
|
||||
def _pinned_ref(source: GitSource) -> str:
|
||||
return source.commit or source.branch or "HEAD"
|
||||
def load_pins(text: str) -> dict[str, str]:
|
||||
"""Parse ``OUSAST_GIT_PINS``: the reconciler's rendering of the Workspaces' ``openultrasast.io/git-commits``."""
|
||||
if not text.strip():
|
||||
return {}
|
||||
try:
|
||||
pins = json.loads(text)
|
||||
except ValueError as exc:
|
||||
raise RunnerError(f"OUSAST_GIT_PINS is not JSON: {exc}") from exc
|
||||
if not isinstance(pins, dict) or not all(
|
||||
isinstance(k, str) and "/" in k and isinstance(v, str) and _SHA.fullmatch(v) for k, v in pins.items()
|
||||
):
|
||||
raise RunnerError(f'OUSAST_GIT_PINS must map "<workspace>/<git name>" to a 40-hex commit, got {text[:200]!r}')
|
||||
return pins
|
||||
|
||||
|
||||
def _export_from_cache(checkout: Path, source: GitSource, dest: Path) -> None:
|
||||
def repo_dir_name(url: str) -> str:
|
||||
"""setup.go ``RepoDirName``: ``https://github.com/chalk/chalk.git`` -> ``chalk``."""
|
||||
trimmed = url.strip().removesuffix("/").removesuffix(".git")
|
||||
return trimmed[max(trimmed.rfind("/"), trimmed.rfind(":")) + 1 :]
|
||||
|
||||
|
||||
def clone_destination(source: GitSource, root: Path) -> Path:
|
||||
"""setup.go ``cloneDestination``: ``dir`` wins (``.`` the workspace root, absolute as-is, else under the root);
|
||||
otherwise the entry's ``name``, unless that is a placeholder (``repo``, ``origin``), then the URL's last segment."""
|
||||
if source.dir:
|
||||
return root if source.dir == "." else root / source.dir # an absolute dir replaces root in the join
|
||||
name = source.name
|
||||
if name in _PLACEHOLDER_NAMES:
|
||||
name = repo_dir_name(source.repo) or name or "repo"
|
||||
return root / name
|
||||
|
||||
|
||||
def _pinned_ref(source: GitSource, commit: str | None) -> str:
|
||||
return commit or source.branch or DEFAULT_BRANCH
|
||||
|
||||
|
||||
def _export_from_cache(checkout: Path, source: GitSource, commit: str | None, dest: Path) -> None:
|
||||
"""``git archive`` the pinned ref out of the cache into ``dest`` (no ``.git``), as ``evaluate.export`` does."""
|
||||
ref = source.commit or (f"origin/{source.branch}" if source.branch else "HEAD")
|
||||
ref = commit or (f"origin/{source.branch}" if source.branch else "HEAD")
|
||||
done = subprocess.run(["git", "-C", str(checkout), "archive", ref], capture_output=True, check=False)
|
||||
if done.returncode != 0:
|
||||
raise RunnerError(f"git archive {ref} in {checkout} failed: {done.stderr.decode(errors='replace').strip()[:400]}")
|
||||
@@ -241,47 +279,60 @@ def _export_from_cache(checkout: Path, source: GitSource, dest: Path) -> None:
|
||||
raise RunnerError(f"{source.repo}@{ref}: the export from {checkout} is empty")
|
||||
|
||||
|
||||
def _shallow_clone(source: GitSource, dest: Path) -> None:
|
||||
"""Shallow clone at the branch, then make sure the pinned commit is checked out."""
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
args = ["clone", "--quiet", "--depth", "1"]
|
||||
if source.branch:
|
||||
args += ["--branch", source.branch]
|
||||
_git(*args, source.repo, str(dest))
|
||||
if source.commit:
|
||||
present = subprocess.run(["git", "cat-file", "-e", f"{source.commit}^{{commit}}"], cwd=dest, capture_output=True, check=False)
|
||||
def _fetch(source: GitSource, commit: str | None, dest: Path) -> None:
|
||||
"""setup.go ``fetchRepo`` (init, remote, fetch ``--depth`` when ``depth`` > 0, checkout FETCH_HEAD), then the pin.
|
||||
|
||||
A pinned entry without a branch fetches the commit itself rather than ax's default ``main``.
|
||||
"""
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
depth = [f"--depth={source.depth}"] if source.depth else []
|
||||
_git("init", "--quiet", cwd=dest)
|
||||
try:
|
||||
_git("remote", "add", "origin", source.repo, cwd=dest)
|
||||
except RunnerError:
|
||||
_git("remote", "set-url", "origin", source.repo, cwd=dest)
|
||||
_git("fetch", "--quiet", *depth, "origin", source.branch or commit or DEFAULT_BRANCH, cwd=dest)
|
||||
_git("checkout", "--quiet", "-f", "FETCH_HEAD", cwd=dest)
|
||||
if commit:
|
||||
present = subprocess.run(["git", "cat-file", "-e", f"{commit}^{{commit}}"], cwd=dest, capture_output=True, check=False)
|
||||
if present.returncode != 0:
|
||||
_git("fetch", "--quiet", "--depth", "1", "origin", source.commit, cwd=dest)
|
||||
_git("checkout", "--quiet", "--detach", source.commit, cwd=dest)
|
||||
_git("fetch", "--quiet", *depth, "origin", commit, cwd=dest)
|
||||
_git("checkout", "--quiet", "-f", "--detach", commit, cwd=dest)
|
||||
|
||||
|
||||
def _materialise_git(source: GitSource, root: Path, cache: Path | None) -> Path:
|
||||
dest = root / source.name
|
||||
if dest.is_dir() and any(dest.iterdir()):
|
||||
def _materialise_git(source: GitSource, root: Path, cache: Path | None, commit: str | None) -> Path:
|
||||
dest = clone_destination(source, root)
|
||||
resumed = (dest / ".git").exists() if dest == root else dest.is_dir() and any(dest.iterdir())
|
||||
if resumed:
|
||||
log.info("workspace repo %s already present at %s (resumed volume); keeping it", source.repo, dest)
|
||||
return dest
|
||||
checkout = find_cached_checkout(cache, source.repo) if cache is not None else None
|
||||
if checkout is not None:
|
||||
log.info("exporting %s@%s from cache %s to %s", source.repo, _pinned_ref(source), checkout, dest)
|
||||
_export_from_cache(checkout, source, dest)
|
||||
log.info("exporting %s@%s from cache %s to %s", source.repo, _pinned_ref(source, commit), checkout, dest)
|
||||
_export_from_cache(checkout, source, commit, dest)
|
||||
else:
|
||||
log.info("cloning %s@%s to %s", source.repo, _pinned_ref(source), dest)
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
_shallow_clone(source, dest)
|
||||
log.info("fetching %s@%s to %s (depth %s)", source.repo, _pinned_ref(source, commit), dest, source.depth or "full")
|
||||
_fetch(source, commit, dest)
|
||||
return dest
|
||||
|
||||
|
||||
def materialise(task: Task, workspaces: Mapping[str, Workspace], cache: Path | None) -> list[Path]:
|
||||
"""Write every bound workspace at its path (``spec.files`` verbatim, ``spec.git`` at ``<path>/<name>``).
|
||||
def materialise(task: Task, workspaces: Mapping[str, Workspace], cache: Path | None, pins: Mapping[str, str] | None = None) -> list[Path]:
|
||||
"""Write every bound workspace at its path (``spec.files`` verbatim, ``spec.git`` where ax's setup puts it).
|
||||
|
||||
Returns the bound paths in binding order; raises :class:`RunnerError` for a binding without a Workspace.
|
||||
``pins`` (from ``OUSAST_GIT_PINS``) names the commit of ``<workspace>/<git name>``; a pin naming no bound git
|
||||
entry, or a binding without a Workspace, raises :class:`RunnerError` before anything is written.
|
||||
"""
|
||||
pins = dict(pins or {})
|
||||
for binding in task.workspaces:
|
||||
if binding.name not in workspaces:
|
||||
raise RunnerError(f"Task/{task.metadata.name}: workspace {binding.name!r} is bound but not in AX_WORKSPACES_YAML")
|
||||
known = {f"{b.name}/{g.name}" for b in task.workspaces for g in workspaces[b.name].git}
|
||||
unknown = sorted(set(pins) - known)
|
||||
if unknown:
|
||||
raise RunnerError(f"OUSAST_GIT_PINS names no bound git entry: {', '.join(unknown)}")
|
||||
paths: list[Path] = []
|
||||
for binding in task.workspaces:
|
||||
workspace = workspaces.get(binding.name)
|
||||
if workspace is None:
|
||||
raise RunnerError(f"Task/{task.metadata.name}: workspace {binding.name!r} is bound but not in AX_WORKSPACES_YAML")
|
||||
workspace = workspaces[binding.name]
|
||||
root = Path(binding.path)
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
for entry in workspace.files:
|
||||
@@ -289,7 +340,7 @@ def materialise(task: Task, workspaces: Mapping[str, Workspace], cache: Path | N
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(entry.content, encoding="utf-8")
|
||||
for source in workspace.git:
|
||||
_materialise_git(source, root, cache)
|
||||
_materialise_git(source, root, cache, pins.get(f"{binding.name}/{source.name}"))
|
||||
paths.append(root)
|
||||
return paths
|
||||
|
||||
@@ -449,7 +500,8 @@ def main(argv: Sequence[str] | None = None, environ: MutableMapping[str, str] |
|
||||
try:
|
||||
session = _prepare(env)
|
||||
cache_dir = env.get("OUSAST_CASE_CACHE")
|
||||
materialise(session.task, session.workspaces, Path(cache_dir) if cache_dir else None)
|
||||
pins = load_pins(env.get("OUSAST_GIT_PINS", ""))
|
||||
materialise(session.task, session.workspaces, Path(cache_dir) if cache_dir else None, pins)
|
||||
ready.set()
|
||||
log.info("workspaces ready; running task %s: %s", session.task.metadata.name, list(session.task.command))
|
||||
summary = run_task_module(session.task.command, session.output_dir)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""Workspace manifests for the cases of an independent population file (ai-service-plane task 4).
|
||||
|
||||
``ousast plane workspaces <population.toml> [--out DIR]`` writes one ax ``Workspace`` per case pin (vulnerable,
|
||||
fixed, benign base, benign tip) with the case's repository URL and the pinned commit, so a Run binds a case at a
|
||||
pin by name. The layout of a case is that of ``benchmarks/independent/evaluate.py``'s ``pins()``.
|
||||
fixed, benign base, benign tip) with the case's repository URL (checked out at ``<path>/repo``, depth 1) and the
|
||||
pinned commit in the ``openultrasast.io/git-commits`` annotation (ax's ``GitRepo`` has no commit field), so a Run
|
||||
binds a case at a pin by name. The layout of a case is that of ``benchmarks/independent/evaluate.py``'s ``pins()``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,6 +13,7 @@ from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from .manifests import GIT_COMMITS_ANNOTATION
|
||||
from .reconciler import _ax_name, _doc
|
||||
|
||||
__all__ = ["workspaces"]
|
||||
@@ -26,7 +28,8 @@ def workspaces(population: Path, out: Path) -> list[Path]:
|
||||
pins = {"vulnerable": case["vulnerable"], "fixed": case["fixed"], "benign-base": benign["base"], "benign-tip": benign["tip"]}
|
||||
for label, commit in pins.items():
|
||||
name = _ax_name(case["id"], label)
|
||||
doc = _doc("Workspace", name, {"git": [{"name": "repo", "repo": case["repo"], "commit": commit}]})
|
||||
doc = _doc("Workspace", name, {"git": [{"name": "repo", "repo": case["repo"], "dir": "repo", "depth": 1}]})
|
||||
doc["metadata"]["annotations"] = {GIT_COMMITS_ANNOTATION: f"repo={commit}"}
|
||||
text = f"# generated by `ousast plane workspaces` from {population.name}\n" + yaml.safe_dump(doc, sort_keys=False)
|
||||
(out / f"{name}.yaml").write_text(text, encoding="utf-8")
|
||||
written.append(out / f"{name}.yaml")
|
||||
|
||||
Reference in New Issue
Block a user