Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3567fd3
feat(xtest): support platform-embedded otdfctl for migration to monorepo
dmihalcik-virtru Apr 15, 2026
0ea2e5d
fixup ruff format
dmihalcik-virtru Apr 16, 2026
7c20d73
fix(xtest): update remaining otdfctl references for platform monorepo…
dmihalcik-virtru Apr 16, 2026
cd0aec5
fixup pkg.go removes tag prefixes IIRC
dmihalcik-virtru Apr 16, 2026
1f02700
refactor(xtest): consolidate .version and .module-path into single .v…
dmihalcik-virtru Apr 16, 2026
9b9c9d7
feat(sdk-mgr): wire --source option through install artifact command
dmihalcik-virtru Apr 16, 2026
0c1b428
fix(setup-cli-tool): avoid script injection by using env vars for inp…
dmihalcik-virtru Apr 16, 2026
bf739dc
Apply suggestion from @gemini-code-assist[bot]
dmihalcik-virtru Apr 16, 2026
edb5e3a
feat(setup-cli-tool): support multiple platform-source Go versions
dmihalcik-virtru Apr 16, 2026
8f783c3
fix: address PR review findings for platform otdfctl migration
dmihalcik-virtru Apr 16, 2026
1e5fc53
fix(sdk-mgr): accept "standalone" as valid Go source in go_module_path
dmihalcik-virtru Apr 16, 2026
821c02c
docs(xtest): clarify auto mode resolves releases from standalone
dmihalcik-virtru Apr 16, 2026
2901544
fix(setup-cli-tool): require SHA match in auto-detect platform fallback
dmihalcik-virtru Apr 16, 2026
7e99e2d
fix: harden validation and error handling for platform otdfctl migration
dmihalcik-virtru Apr 16, 2026
13385f5
fixup ruff format
dmihalcik-virtru Apr 16, 2026
bfe1119
refactor(xtest): extract composite actions from xtest.yml
dmihalcik-virtru Apr 17, 2026
9cfc673
fix: add OT_ROOT_KEY env and guard fromJson on empty outputs
dmihalcik-virtru Apr 17, 2026
daf28e9
feat(otdf-local): add xtest config YAML and local test runner
dmihalcik-virtru Apr 17, 2026
1084033
fix(xtest): fix CI failures in xtest config generation
dmihalcik-virtru Apr 17, 2026
9cbdd5a
fix(xtest): make config generation non-blocking and add debug output
dmihalcik-virtru Apr 17, 2026
6e13659
fix(xtest): allow resolve to work without local platform checkout
dmihalcik-virtru Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
464 changes: 194 additions & 270 deletions .github/workflows/xtest.yml

Large diffs are not rendered by default.

223 changes: 223 additions & 0 deletions otdf-local/src/otdf_local/ci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
"""CI-specific commands for otdf-local.

These commands adapt the local environment management for GitHub Actions CI,
where the platform is already started by an external action and we only need
to start KAS instances as background processes.
"""

from __future__ import annotations

import os
import sys
from pathlib import Path
from typing import Annotated

import typer

from otdf_local.config.ports import Ports
from otdf_local.config.settings import Settings
from otdf_local.health.waits import WaitTimeoutError, wait_for_health
from otdf_local.services import get_kas_manager
from otdf_local.utils.console import (
print_error,
print_info,
print_success,
print_warning,
)
from otdf_local.utils.yaml import load_yaml, save_yaml, set_nested

ci_app = typer.Typer(
name="ci",
help="CI-specific commands for GitHub Actions workflows.",
no_args_is_help=True,
)


def _emit_github_output(key: str, value: str) -> None:
"""Write a key=value pair to $GITHUB_OUTPUT if available, else print to stdout."""
github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(github_output, "a") as f:
f.write(f"{key}={value}\n")
else:
# Fallback for local testing
print(f"{key}={value}", file=sys.stdout)


def _prepare_kas_template(
settings: Settings, root_key: str | None, ec_tdf_enabled: bool
) -> None:
"""Ensure the KAS template config has the right root key and EC TDF settings.

In CI, the platform config may have a root_key that differs from what
we want for additional KAS instances. This updates the platform config
in-place so that KASService._generate_config reads the correct root_key.
"""
if root_key:
config = load_yaml(settings.platform_config)
set_nested(config, "services.kas.root_key", root_key)
if ec_tdf_enabled:
set_nested(config, "services.kas.preview.ec_tdf_enabled", True)
save_yaml(settings.platform_config, config)


@ci_app.command("start-kas")
def start_kas(
platform_dir: Annotated[
Path,
typer.Option(
"--platform-dir",
help="Path to the platform checkout (must contain opentdf-kas-mode.yaml)",
envvar="OTDF_LOCAL_PLATFORM_DIR",
),
],
root_key: Annotated[
str | None,
typer.Option(
"--root-key",
help="Root key for KAS instances (overrides platform config value)",
envvar="OT_ROOT_KEY",
),
] = None,
ec_tdf_enabled: Annotated[
bool,
typer.Option(
"--ec-tdf-enabled/--no-ec-tdf",
help="Enable EC TDF support",
),
] = True,
key_management: Annotated[
bool,
typer.Option(
"--key-management/--no-key-management",
help="Enable key management on km1/km2 instances",
),
] = False,
log_type: Annotated[
str,
typer.Option(
"--log-type",
help="Log format type (json, text)",
),
] = "json",
health_timeout: Annotated[
int,
typer.Option(
"--health-timeout",
help="Seconds to wait for each KAS instance to become healthy",
),
] = 60,
instances: Annotated[
str | None,
typer.Option(
"--instances",
help="Comma-separated KAS instance names (default: all)",
),
] = None,
) -> None:
"""Start KAS instances for CI and emit GitHub Actions outputs.

Expects the platform to already be running (started by start-up-with-containers).
Starts all 6 KAS instances (alpha, beta, gamma, delta, km1, km2) as background
processes, waits for each to pass health checks, and emits log file paths as
GitHub Actions step outputs.

Output keys (written to $GITHUB_OUTPUT):
kas-alpha-log-file, kas-beta-log-file, kas-gamma-log-file,
kas-delta-log-file, kas-km1-log-file, kas-km2-log-file
"""
platform_dir = platform_dir.resolve()
if not platform_dir.is_dir():
print_error(f"Platform directory does not exist: {platform_dir}")
raise typer.Exit(1)

# Check for required template files
kas_template = platform_dir / "opentdf-kas-mode.yaml"
platform_config = platform_dir / "opentdf-dev.yaml"
Comment on lines +135 to +136
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The filenames opentdf-kas-mode.yaml and opentdf-dev.yaml are hardcoded. It would be more robust to define these as constants at the module level or make them configurable via CLI options, especially if the platform repository structure might change in the future.

if not kas_template.exists():
# Fall back to opentdf.yaml if opentdf-kas-mode.yaml doesn't exist
kas_template_alt = platform_dir / "opentdf.yaml"
if kas_template_alt.exists():
print_info(
f"Using {kas_template_alt} as KAS template (opentdf-kas-mode.yaml not found)"
)
else:
print_error(
f"Neither opentdf-kas-mode.yaml nor opentdf.yaml found in {platform_dir}"
)
raise typer.Exit(1)

if not platform_config.exists():
# Try opentdf.yaml as fallback
platform_config_alt = platform_dir / "opentdf.yaml"
if platform_config_alt.exists():
platform_config = platform_config_alt

# Build settings with CI-specific overrides
# We use a fresh xtest_root derived from this package's location
settings = Settings(
platform_dir=platform_dir,
)
settings.ensure_directories()

# Update root key in platform config if provided
if root_key:
_prepare_kas_template(settings, root_key, ec_tdf_enabled)

# Determine which instances to start
if instances:
kas_names = [n.strip() for n in instances.split(",")]
for name in kas_names:
if name not in Ports.all_kas_names():
print_error(f"Unknown KAS instance: {name}")
raise typer.Exit(1)
else:
kas_names = Ports.all_kas_names()

# Start KAS instances
print_info(f"Starting KAS instances: {', '.join(kas_names)}...")
kas_manager = get_kas_manager(settings)

failed = []
for name in kas_names:
kas = kas_manager.get(name)
if kas is None:
print_error(f"KAS instance {name} not found in manager")
failed.append(name)
continue
if not kas.start():
print_error(f"Failed to start KAS {name}")
failed.append(name)

if failed:
print_error(f"Failed to start: {', '.join(failed)}")
raise typer.Exit(1)

# Wait for health
print_info("Waiting for KAS health checks...")
unhealthy = []
for name in kas_names:
port = Ports.get_kas_port(name)
try:
wait_for_health(
f"http://localhost:{port}/healthz",
timeout=health_timeout,
service_name=f"KAS {name}",
)
except WaitTimeoutError as e:
print_warning(str(e))
unhealthy.append(name)

if unhealthy:
print_error(f"KAS instances failed health check: {', '.join(unhealthy)}")
raise typer.Exit(1)

print_success(f"All {len(kas_names)} KAS instances are healthy")

# Emit outputs
for name in kas_names:
log_path = settings.get_kas_log_path(name)
output_key = f"kas-{name}-log-file"
_emit_github_output(output_key, str(log_path))

print_success("CI KAS startup complete")
5 changes: 5 additions & 0 deletions otdf-local/src/otdf_local/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from rich.live import Live

from otdf_local import __version__
from otdf_local.ci import ci_app
from otdf_local.xtest.cli import xtest_app
from otdf_local.config.ports import Ports
from otdf_local.config.settings import get_settings
from otdf_local.health.waits import WaitTimeoutError, wait_for_health, wait_for_port
Expand Down Expand Up @@ -43,6 +45,9 @@
pretty_exceptions_enable=sys.stderr.isatty(),
)

app.add_typer(ci_app, name="ci")
app.add_typer(xtest_app, name="xtest")


def _show_provision_error(result: ProvisionResult, target: str) -> None:
"""Display provisioning error with stderr details."""
Expand Down
10 changes: 10 additions & 0 deletions otdf-local/src/otdf_local/xtest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""xtest configuration and execution subpackage."""

from otdf_local.xtest.config import Features, TestPhase, XtestConfig, XtestInputs

__all__ = [
"Features",
"TestPhase",
"XtestConfig",
"XtestInputs",
]
Loading
Loading