Files
Max KainovandClaude Opus 4.7 c21bff0676 CI: Prefix runs_on labels with pr- in PR workflow
Walk every job in ci/workflows/pull_request.py and rewrite each runs_on
label with a pr- prefix, leaving self-hosted untouched. Jobs are
deep-copied via job.copy() before mutation so shared JobConfigs
instances used by other workflows are not affected.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-21 10:43:07 +02:00

252 lines
9.0 KiB
Python

import copy
import importlib.util
from pathlib import Path
from typing import List
from praktika import Workflow
from . import Job
from .info import Info
from .settings import Settings
from .utils import Utils
def _is_local_run():
"""Check if running locally. Returns False if can't determine (e.g., during workflow generation)."""
try:
return Info().is_local_run
except Exception:
# During workflow generation, Info can't be initialized - treat as CI mode
return False
def _get_workflows(
name=None,
file=None,
_for_validation_check=False,
_file_names_out=None,
default=False,
) -> List[Workflow.Config]:
"""
Gets user's workflow configs
"""
res = []
if not Path(Settings.WORKFLOWS_DIRECTORY).is_dir():
Utils.raise_with_error(
f"Workflow directory does not exist [{Settings.WORKFLOWS_DIRECTORY}]. cd to the repo's root?"
)
directory = Path(Settings.WORKFLOWS_DIRECTORY)
for py_file in directory.glob("*.py"):
if not default:
if Settings.ENABLED_WORKFLOWS:
if not any(
py_file.name == Path(enabled_wf_file).name
for enabled_wf_file in Settings.ENABLED_WORKFLOWS
):
print(
f"NOTE: Workflow [{py_file.name}] is not enabled in Settings.ENABLED_WORKFLOWS - skip"
)
continue
if Settings.DISABLED_WORKFLOWS:
if any(
py_file.name == Path(disabled_wf_file).name
for disabled_wf_file in Settings.DISABLED_WORKFLOWS
):
print(
f"NOTE: Workflow [{py_file.name}] is disabled via Settings.DISABLED_WORKFLOWS - skip"
)
continue
if file and str(file) not in str(py_file):
continue
elif py_file.name != Settings.DEFAULT_LOCAL_TEST_WORKFLOW:
if not _is_local_run():
print(f"Skip [{py_file.name}]")
continue
module_name = py_file.name.removeprefix(".py")
spec = importlib.util.spec_from_file_location(
module_name, f"{Settings.WORKFLOWS_DIRECTORY}/{module_name}"
)
assert spec
foo = importlib.util.module_from_spec(spec)
assert spec.loader
spec.loader.exec_module(foo)
try:
for workflow in foo.WORKFLOWS:
if name:
if name == workflow.name:
print(f"Read workflow [{name}] config from [{module_name}]")
res = [workflow]
break
else:
continue
else:
res += [workflow]
print(
f"Read workflow configs from [{module_name}], workflow name [{workflow.name}]"
)
if isinstance(_file_names_out, list):
_file_names_out.append(py_file.name.removeprefix(".py"))
except Exception as e:
print(
f"WARNING: Failed to add WORKFLOWS config from [{module_name}], exception [{e}]"
)
if not res:
Utils.raise_with_error(f"Failed to find [{name or file or 'any'}] workflow")
if not _for_validation_check:
for wf in res:
# add native jobs
_update_workflow_with_native_jobs(wf)
# fill in artifact properties, e.g. _provided_by
_update_workflow_artifacts(wf)
return res
def _get_infra_config():
"""
Returns the infra config which is imported as: Settings.CLOUD_INFRASTRUCTURE_CONFIG_PATH import CLOUD
"""
from pathlib import Path
config_path = Path(Settings.CLOUD_INFRASTRUCTURE_CONFIG_PATH)
if not config_path.exists():
Utils.raise_with_error(
f"Infrastructure config file does not exist [{Settings.CLOUD_INFRASTRUCTURE_CONFIG_PATH}]"
)
module_name = config_path.stem
spec = importlib.util.spec_from_file_location(module_name, config_path)
if not spec or not spec.loader:
Utils.raise_with_error(
f"Failed to load infrastructure config from [{Settings.CLOUD_INFRASTRUCTURE_CONFIG_PATH}]"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
try:
cloud_config = getattr(module, "CLOUD")
cloud_config._settings = Settings
print(
f"Loaded infrastructure config from [{Settings.CLOUD_INFRASTRUCTURE_CONFIG_PATH}]"
)
return cloud_config
except AttributeError:
Utils.raise_with_error(
f"CLOUD variable not found in [{Settings.CLOUD_INFRASTRUCTURE_CONFIG_PATH}]"
)
return None
def _get_artifact_to_providing_job_map(workflow):
artifact_job = {}
for job in workflow.jobs:
for artifact_name in job.provides:
artifact_job[artifact_name] = job.name
return artifact_job
def _update_workflow_artifacts(workflow):
artifact_job = _get_artifact_to_providing_job_map(workflow)
for artifact in workflow.artifacts:
if artifact.name in artifact_job:
artifact._provided_by = artifact_job[artifact.name]
def _update_workflow_with_native_jobs(workflow):
if workflow.dockers and not workflow.disable_dockers_build:
from .native_jobs import (
_docker_build_amd_linux_job,
_docker_build_arm_linux_job,
_docker_build_manifest_job,
)
workflow.jobs = [copy.deepcopy(j) for j in workflow.jobs]
docker_job_names = []
docker_digest_config = Job.CacheDigestConfig()
for docker_config in workflow.dockers:
docker_digest_config.include_paths.append(docker_config.path)
if not Settings.ENABLE_MULTIPLATFORM_DOCKER_IN_ONE_JOB:
aux_job = copy.deepcopy(_docker_build_amd_linux_job)
if not _is_local_run():
print(f"Enable praktika job [{aux_job.name}] for [{workflow.name}]")
if workflow.enable_cache:
if not _is_local_run():
print(f"Add automatic digest config for [{aux_job.name}] job")
aux_job.digest_config = docker_digest_config
workflow.jobs.insert(len(docker_job_names), aux_job)
docker_job_names.append(aux_job.name)
aux_job = copy.deepcopy(_docker_build_arm_linux_job)
if not _is_local_run():
print(f"Enable praktika job [{aux_job.name}] for [{workflow.name}]")
if workflow.enable_cache:
if not _is_local_run():
print(f"Add automatic digest config for [{aux_job.name}] job")
aux_job.digest_config = docker_digest_config
workflow.jobs.insert(len(docker_job_names), aux_job)
docker_job_names.append(aux_job.name)
if (
workflow.enable_dockers_manifest_merge
or Settings.ENABLE_MULTIPLATFORM_DOCKER_IN_ONE_JOB
):
aux_job = copy.deepcopy(_docker_build_manifest_job)
if not _is_local_run():
print(f"Enable praktika job [{aux_job.name}] for [{workflow.name}]")
if workflow.enable_cache:
if not _is_local_run():
print(f"Add automatic digest config for [{aux_job.name}] job")
aux_job.digest_config = docker_digest_config
aux_job.requires = copy.deepcopy(docker_job_names)
workflow.jobs.insert(len(docker_job_names), aux_job)
docker_job_names.append(aux_job.name)
assert docker_job_names, "Docker job names are empty, BUG?"
for job in workflow.jobs[len(docker_job_names) :]:
job.run_after.extend(docker_job_names)
if workflow._enabled_workflow_config():
from .native_jobs import _workflow_config_job
if not _is_local_run():
print(
f"Enable native job [{_workflow_config_job.name}] for [{workflow.name}]"
)
aux_job = copy.deepcopy(_workflow_config_job)
workflow.jobs.insert(0, aux_job)
for job in workflow.jobs[1:]:
job.run_after.append(aux_job.name)
if (
workflow.enable_merge_ready_status
or workflow.post_hooks
or workflow.enable_automerge
or workflow.enable_cidb # to write cpu and storage usage summary into cidb
):
from .native_jobs import _final_job
if not _is_local_run():
print(f"Enable native job [{_final_job.name}] for [{workflow.name}]")
aux_job = copy.deepcopy(_final_job)
for job in workflow.jobs:
aux_job.run_after.append(job.name)
workflow.jobs.append(aux_job)
if workflow.runs_on_label_prefix:
prefix = workflow.runs_on_label_prefix
for job in workflow.jobs:
if not job.runs_on:
continue
job.runs_on = [
label if label == "self-hosted" else f"{prefix}{label}"
for label in job.runs_on
]