FORGE is a CI/CD testing framework that orchestrates Kubernetes workloads on OpenShift clusters. It uses a Python DSL to define tasks, apply manifests, and collect artifacts for debugging.
Key paths:
projects/— individual project test harnessesprojects/NAME/orchestration— upper layer of the test harness (config, prepare, test, cleanup)projects/NAME/toolbox— lower layer of the test harness (toolbox of scripts altering the state of the cluster, using the DSL language)projects/NAME/test— unit testing of the test harnessprojects/core/dsl/— shared DSL (task execution, k8s utilities, shell helpers)fournos/gitops/— Tekton pipelines and GitOps base workflowsdocs/— project documentation
NEVER write secrets or sensitive data to artifact directories, logs, or files.
-
Never persist secrets to
ARTIFACT_DIR. Theoc_apply(path, manifest)helper writes the manifest YAML to disk before applying it. If the manifest contains sensitive data (passwords, tokens, pull-secrets, certificates, API keys), it MUST NOT be written to any path underenv.ARTIFACT_DIR. -
Use
oc("apply", "-f", "-", input_text=..., handled_secretly=True)for secrets. Apply secrets via stdin or a dedicated helper that does not persist the payload. Never use the standardoc_apply()for Secret-kind manifests. -
Never log secret values. Do not print, log, or include in error messages: passwords, tokens, bearer credentials, TLS keys, pull-secret
.dockerconfigjson, or anydata:/stringData:field from a Secret. -
Check the
kind:field. Before callingoc_apply(artifact_path, manifest), verify thatmanifest.get("kind") != "Secret". If it is a Secret, use a non-persisting apply path. -
Artifact directories are public. Treat everything under
ARTIFACT_DIRas if it will be published on the internet. Only write non-sensitive operational data (CRs, logs, pod descriptions, events). Pod/Deployment and other resources are safe to store, as they must not contain secrets. See point 9. Kubernetes Secret objects should never be stored in the artifacts. Kubernetes Secret descriptions can be safely stored. -
Use the vault module. Use the project
projects.core.library.vaultmodule to access the secrets, stored in files. Consider all the content of these files as secret. -
Never pass secrets via environment variables For transparency and post-mortem troubleshooting, the environment variables are saved to disk at multiple locations. Do not use environment variables to pass a secret between two components. There might be few exceptions to this rule (MLFlow, AWS), but they must be handled with care.
-
Never pass secrets via Pod environment variables The Pod definition will be stored in public artifacts. Do no use plain text Pod environment variables to pass secrets to the Pod. Instead, use Secret resources and Pod environment variables that reference this secret.
# GOOD: Apply a Secret without persisting to disk
def apply_secret(manifest: dict) -> None:
"""Apply a Secret manifest via stdin — nothing written to disk."""
import yaml
from projects.core.dsl.utils.k8s import oc
oc("apply", "-f", "-", input_text=yaml.safe_dump(manifest, sort_keys=False))
# GOOD: Guard before using oc_apply
def safe_oc_apply(artifact_path, manifest):
if manifest.get("kind") == "Secret":
raise ValueError(
f"Refusing to write Secret '{manifest['metadata']['name']}' to artifacts. "
"Use apply_secret() instead."
)
oc_apply(artifact_path, manifest)# BAD: Writing a pull-secret to artifacts
src_dir = env.ARTIFACT_DIR / "src"
oc_apply(src_dir / "image-pull-secret.yaml", secret_manifest) # LEAKS CREDENTIALS
# BAD: Logging secret content
logger.info(f"Applying secret: {manifest}") # LEAKS if manifest has sensitive data
# BAD: Including tokens in error messages
raise RuntimeError(f"Failed to auth with token {token}") # LEAKS TOKENNever catch-and-warn silently. A logger.warning(...) inside a bare except gets buried in logs and failures go unnoticed for weeks.
-
Raise typed exceptions. Use
ValueError,FileNotFoundError,RuntimeError— notlogger.warning()+return "". A missing config, missing vault secret, or failed version extraction is a real error. -
Register visible notifications for operational failures. Use
ci.add_notification_file(name, message)so failures appear in GitHub notifications, not just in log streams nobody reads. -
Bootstrap edge cases belong in the project, not shared libraries. If a new nightly test has no previous MLflow run yet, the project orchestration should handle that gracefully (e.g., catch the exception and treat it as "no previous version"). Shared/library code should raise — callers decide policy.
# BAD: Exception swallowed, failure invisible
try:
upload_traces(data)
except Exception:
logger.warning("Upload failed; continuing", exc_info=True)
# BAD: Returning empty string instead of raising
if connection is None:
return "" # caller has no idea something broke# GOOD: Raise so caller can decide
if connection is None:
raise ValueError("MLflow connection failed — check vault config")
# GOOD: If truly optional, still make it visible
try:
upload_traces(data)
except Exception:
logger.exception("Trace upload failed")
ci.add_notification_file("trace-upload-failed", "Profiler trace upload failed — check logs")
raiseToolbox scripts are the lower layer — standalone, reusable cluster-state commands. Orchestration is the upper layer — CI phases, config, presets.
-
Toolbox must never import from orchestration. No
from projects.NAME.orchestration.runtime_config import cfgin toolbox code. All inputs come through therun()entrypoint parameters for strong isolation. -
Pass all values explicitly via
run()parameters. Declare typed parameters with defaults in the entrypoint. This makes the command standalone, testable, and reviewable via_meta/metadata.yaml. -
Manifests and templates live with their toolbox command. Put them in
projects/NAME/toolbox/COMMAND_NAME/templates/— keeps the command self-contained.
# BAD: Toolbox importing orchestration config
from projects.llamastack.orchestration.runtime_config import cfg
hpa_cfg = cfg.get_hpa_config()
min_replicas = hpa_cfg.get("min_replicas", 1)# GOOD: All config passed through entrypoint
@entrypoint
def run(*, min_replicas: int = 1, max_replicas: int = 4, memory_target: int = 75):
"""
Configure HPA for the deployment.
Args:
min_replicas: Minimum replica count
max_replicas: Maximum replica count
memory_target: Target memory utilization percentage
"""
return execute_tasks(locals())-
Never use
time.sleep(N)to wait for cluster state changes. Arbitrary sleeps are unreliable — too short in slow environments, wasted time in fast ones. -
Use
@retryto poll for readiness.
# GOOD: Poll until resource is ready
@retry(attempts=60, delay=30, backoff=1.0)
@task
def wait_until_ready(args, ctx):
status = oc_get_json("datasciencecluster", "default-dsc")["status"]["phase"]
return status == "Ready"- Reuse existing wait toolbox scripts.
Before writing a new wait loop, check
projects/rhoai/toolbox/,projects/rhaiis/toolbox/,projects/core/— many wait patterns already exist.
config.project.get_config() returns typed values from YAML. Do not cast.
# BAD: YAML already returns int
replicas = int(config.project.get_config("runtime.replicas"))
# BAD: Custom bool coercion for YAML values
def _as_bool(value):
return value.strip().lower() not in ("false", "0", "no")
# GOOD: Trust YAML types
replicas = config.project.get_config("runtime.replicas")If a config value has the wrong type, fix the YAML — not the code.
Before implementing preset handling, vault init, phase registration, or PR arg parsing —
check what config.init(), ci_base, and core library already provide.
- Preset application: handled by
config.init() - Config overrides: handled by
config.project.apply_config_overrides() /testdirectives: handled at framework level
Duplicating framework behavior leads to drift and subtle bugs.
If you temporarily set an environment variable, restore it in a finally block.
# GOOD: Cleanup in finally
old_value = os.environ.get("MLFLOW_WORKSPACE")
try:
os.environ["MLFLOW_WORKSPACE"] = workspace
# ... do work ...
finally:
if old_value is None:
os.environ.pop("MLFLOW_WORKSPACE", None)
else:
os.environ["MLFLOW_WORKSPACE"] = old_valueOverall, we do not want legacy support. Do not implement legacy fallback, unless explicitly requested by the user.