diff --git a/.claude/skills/check-ci/SKILL.md b/.claude/skills/check-ci/SKILL.md new file mode 100644 index 0000000..a00a156 --- /dev/null +++ b/.claude/skills/check-ci/SKILL.md @@ -0,0 +1,50 @@ +--- +name: check-ci +description: Fetch and report CI results for a silk PR. Use when the user asks to investigate, address, or fix CI failures, or refers to a PR without specifying what's broken. +--- + +Always fetch the CI result JSON before reading code or proposing fixes. + +## Determining PR, sha, and workflow name + +**From a Praktika report URL** (e.g. `https://silk-artifacts-eu-north-1.s3.amazonaws.com/json.html?PR=12&sha=abc123&name_0=Pull%20Request%20CI`): +- Extract `PR`, `sha`, `name_0` from query params +- Normalize `name_0`: lowercase + spaces → underscores (e.g. `"Pull Request CI"` → `"pull_request_ci"`) + +**From a bare PR number:** +- Run `gh pr view {PR} --json headRefOid --jq '.headRefOid'` to get the latest sha +- Use `pull_request_ci` as the normalized workflow name + +## Building the JSON URL + +``` +https://silk-artifacts-eu-north-1.s3.amazonaws.com/PRs/{PR}/{sha}/{normalized}/result_{normalized}.json +``` + +Fetch this URL with WebFetch. + +## Result structure + +The JSON is a serialized `praktika.Result`: + +``` +{ + "name": str, + "status": str, # OK | FAIL | ERROR | SKIPPED | UNKNOWN | XFAIL | XPASS | PENDING | RUNNING | DROPPED + "start_time": float?, + "duration": float?, + "info": str, + "results": [...], # nested Result objects, same shape, recursive + "files": [...], + "links": [...], + "ext": { + "labels": [{"name": str, "link": str?, "hint": str?}, ...], + "warnings": [...], + "errors": [...], + "report_url": str?, + ... + } +} +``` + +Walk the `results` tree recursively to find all failing jobs and sub-jobs. Use the `info` field of failing nodes as the primary signal for what went wrong before touching any code. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 028416c..bc3fcce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,6 @@ name: CI on: push: branches: [main] - pull_request: - branches: [main] workflow_dispatch: jobs: diff --git a/.gitignore b/.gitignore index 4148b04..3927231 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .tools/ build/ compile_commands.json +ci/tmp/ diff --git a/ci/infrastructure/projects.py b/ci/infrastructure/projects.py new file mode 100644 index 0000000..cd8c56b --- /dev/null +++ b/ci/infrastructure/projects.py @@ -0,0 +1,259 @@ +from ci.settings.settings import PROJECT_NAME, PROJECT_SLUG, PRAKTIKA_BASE_VENV +from praktika.infrastructure import Components, Storage, VPC +from praktika.infrastructure.cloud import CloudInfrastructure + + +# until published in pip +_PRAKTIKA_WHL = "https://praktika-artifacts-eu-north-1.s3.amazonaws.com/packages/praktika-0.1.3-py3-none-any.whl" +_PRAKTIKA_CONTROLLER_WHL = "https://praktika-artifacts-eu-north-1.s3.amazonaws.com/packages/praktika_controller-0.1.1-py3-none-any.whl" + + +def _runner_user_data(): + return "\n".join( + [ + "#!/usr/bin/env bash", + "set -xeuo pipefail", + "", + "# Force-reinstall praktika so the runner uses the version pinned here", + "# rather than whatever was baked into the AMI at image-build time.", + ( + f"/opt/praktika/base-venvs/{PRAKTIKA_BASE_VENV}/bin/python " + f"-m pip install --force-reinstall {_PRAKTIKA_WHL}" + ), + "# Add any host customization you need above this line.", + "/usr/local/bin/praktika-configure-cloudwatch-agent", + "/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/etc/praktika/amazon-cloudwatch-agent.json -s", + "systemctl enable --now praktika-controller", + "", + ] + ) + + +def _silk_ci_dependencies_component(): + return { + "name": "silk-ci-dependencies", + "platform": "Linux", + "description": "Install Silk CI toolchains and build dependencies", + "commands": [ + "export DEBIAN_FRONTEND=noninteractive", + ( + "wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key " + "> /etc/apt/trusted.gpg.d/apt.llvm.org.asc" + ), + ( + ". /etc/os-release && echo \"deb http://apt.llvm.org/" + "${VERSION_CODENAME}/ llvm-toolchain-${VERSION_CODENAME}-21 main\" " + "> /etc/apt/sources.list.d/llvm-21.list" + ), + ( + "wget -qO- https://apt.kitware.com/keys/" + "kitware-archive-latest.asc > /etc/apt/trusted.gpg.d/kitware.asc" + ), + ( + ". /etc/os-release && echo \"deb https://apt.kitware.com/ubuntu/ " + "${VERSION_CODENAME} main\" > /etc/apt/sources.list.d/kitware.list" + ), + ( + "DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 " + "install -y software-properties-common" + ), + "add-apt-repository -y ppa:ubuntu-toolchain-r/test", + "apt-get -o DPkg::Lock::Timeout=60 update -q", + ( + "DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=60 " + "install -y clang-21 clang-format-21 llvm-21 cmake " + "libstdc++-13-dev ninja-build ccache libboost-dev " + "libdouble-conversion-dev libelf-dev zlib1g-dev" + ), + ], + } + + +def _silk_ci_image_test_component(): + return Components.create_image_test_component( + name="silk-ci-image-test", + description="Validate Silk CI toolchains and build dependencies", + commands=[ + "test -d /opt/praktika/work", + "test -w /opt/praktika/work", + "test -x /usr/bin/clang-21", + "test -x /usr/bin/clang-format-21", + "test -x /usr/bin/cmake", + "test -x /usr/bin/ninja", + "test -x /usr/bin/ccache", + "clang-21 --version", + "clang-format-21 --version", + "cmake --version", + "ninja --version", + "ccache --version", + ( + "for package in llvm-21 libstdc++-13-dev libboost-dev " + "libdouble-conversion-dev libelf-dev zlib1g-dev; do " + "dpkg-query -W -f='${Status}\\n' \"$package\" " + "| grep -qx 'install ok installed'; done" + ), + ], + ) + + +def _praktika_controller_image_test_component(): + controller = "praktika-controller" + start_script = f"/usr/local/bin/${{controller}}-start" + service_unit = f"/etc/systemd/system/${{controller}}.service" + return Components.create_image_test_component( + name="silk-praktika-controller-image-test", + description="Validate Praktika controller runtime and boot wiring", + commands=[ + f"controller={controller}; command -v \"$controller\"", + ( + f"controller={controller}; " + "python3.12 -m pip show \"$controller\"" + ), + f"controller={controller}; test -x {start_script}", + f"controller={controller}; bash -n {start_script}", + f"controller={controller}; test -f {service_unit}", + ( + f"controller={controller}; " + f"grep -qx \"ExecStart={start_script}\" {service_unit}" + ), + ( + f"controller={controller}; " + f"grep -qx \"StandardOutput=append:/var/log/${{controller}}.log\" {service_unit}" + ), + ( + f"controller={controller}; " + f"grep -qx \"StandardError=append:/var/log/${{controller}}.log\" {service_unit}" + ), + ( + "test -x /usr/local/bin/praktika-configure-cloudwatch-agent " + "&& bash -n /usr/local/bin/praktika-configure-cloudwatch-agent" + ), + "test -x /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl", + ], + ) + + +def _silk_ci_image_components(): + return [ + _silk_ci_dependencies_component(), + _silk_ci_image_test_component(), + _praktika_controller_image_test_component(), + ] + + +def _image_builders(): + image_recipe_version = "1.0.7" + prebuilt_venvs = [ + Components.create_praktika_venv_config( + PRAKTIKA_BASE_VENV, + "0.1.3", + ), + ] + return [ + Components.create_ubuntu_image_builder_config( + name="ci-arm64-image", + version=image_recipe_version, + controller_package=_PRAKTIKA_CONTROLLER_WHL, + prebuilt_venvs=prebuilt_venvs, + instance_types=["t4g.small"], + components=_silk_ci_image_components(), + ), + Components.create_ubuntu_image_builder_config( + name="ci-x86_64-image", + version=image_recipe_version, + controller_package=_PRAKTIKA_CONTROLLER_WHL, + prebuilt_venvs=prebuilt_venvs, + instance_types=["t3.small"], + components=_silk_ci_image_components(), + ), + ] + + +_GH_TOKEN_MINTER = Components.GitHubTokenMinter( + repositories=[PROJECT_NAME], +) +_IMAGE_BUILDERS = _image_builders() +_IMAGE_BUILDERS_BY_NAME = {builder.name: builder for builder in _IMAGE_BUILDERS} + +PROJECTS = [ + CloudInfrastructure.Config( + name=PROJECT_NAME, + min_praktika_version="0.1.3", + vpcs=[ + VPC.Config( + subnets=[ + VPC.Subnet(availability_zone="eu-north-1a"), + ], + ) + ], + storages=[ + Storage.Config( + name="artifacts-eu-north-1", + retention_days=30, + public=True, + ), + ], + report_pages=[Components.report_page_config], + image_builders=_IMAGE_BUILDERS, + github_token_minters=[_GH_TOKEN_MINTER], + orchestrator_pool=Components.OrchestratorPool( + instance_type="t4g.small", + scaling=Components.OrchestratorPool.Scaling.Auto, + size=0, + max_size=50, + capacity_reserve=1, + volume_size_gb=100, + image_builder=_IMAGE_BUILDERS_BY_NAME["ci-arm64-image"], + ext={"allowed_push_branches": ["main", "add-praktika-ci-config"]} + ), + runner_pools=[ + Components.RunnerPool( + name="arm-small", + instance_type="t4g.medium", + scaling=Components.RunnerPool.Scaling.Auto, + size=0, + max_size=50, + volume_size_gb=100, + image_builder=_IMAGE_BUILDERS_BY_NAME["ci-arm64-image"], + user_data=_runner_user_data(), + ), + Components.RunnerPool( + name="amd-small", + instance_type="t3.medium", + scaling=Components.RunnerPool.Scaling.Auto, + size=0, + max_size=50, + volume_size_gb=100, + image_builder=_IMAGE_BUILDERS_BY_NAME["ci-x86_64-image"], + user_data=_runner_user_data(), + ), + Components.RunnerPool( + name="arm-medium", + instance_type="c7g.4xlarge", + scaling=Components.RunnerPool.Scaling.Auto, + size=0, + max_size=50, + volume_size_gb=100, + image_builder=_IMAGE_BUILDERS_BY_NAME["ci-arm64-image"], + ), + Components.RunnerPool( + name="arm-large", + instance_type="c7g.8xlarge", + scaling=Components.RunnerPool.Scaling.Auto, + size=0, + max_size=50, + volume_size_gb=100, + image_builder=_IMAGE_BUILDERS_BY_NAME["ci-arm64-image"], + ), + Components.RunnerPool( + name="amd-medium", + instance_type="c7a.4xlarge", + scaling=Components.RunnerPool.Scaling.Auto, + size=0, + max_size=50, + volume_size_gb=100, + image_builder=_IMAGE_BUILDERS_BY_NAME["ci-x86_64-image"], + ), + ], + ) +] diff --git a/ci/jobs/fmt_job.py b/ci/jobs/fmt_job.py new file mode 100644 index 0000000..93245f6 --- /dev/null +++ b/ci/jobs/fmt_job.py @@ -0,0 +1,7 @@ +from praktika.result import Result + +if __name__ == "__main__": + Result.from_commands_run( + name="Check formatting", + command=["./bb fmt --check"], + ).complete_job() diff --git a/ci/jobs/init_submodules.py b/ci/jobs/init_submodules.py new file mode 100644 index 0000000..81010ed --- /dev/null +++ b/ci/jobs/init_submodules.py @@ -0,0 +1,52 @@ +import subprocess + +from praktika.info import Info + + +COMMON_SUBMODULES = [ + "contrib/benchmark", + "contrib/bpftool", + "contrib/cxxopts", + "contrib/googletest", + "contrib/libbacktrace", + "contrib/libbpf", + "contrib/librseq", + "contrib/liburing", +] + +EXTRA_SUBMODULES_BY_BUILD = { + "release": ["contrib/poco", "contrib/jemalloc"], + "tsan": ["contrib/poco"], + "msan": ["contrib/llvm-project"], +} + + +def run(*args): + print("+", " ".join(args), flush=True) + subprocess.run(args, check=True) + + +def checkout_submodules(paths): + run( + "git", + "submodule", + "update", + "--init", + "--no-fetch", + "--depth=1", + "--jobs", + "8", + *paths, + ) + + +if __name__ == "__main__": + job_name = Info().job_name + + run("git", "submodule", "sync") + checkout_submodules(COMMON_SUBMODULES) + + for build, paths in EXTRA_SUBMODULES_BY_BUILD.items(): + if f"({build})" in job_name: + checkout_submodules(paths) + break diff --git a/ci/jobs/test_job.py b/ci/jobs/test_job.py new file mode 100644 index 0000000..97a6582 --- /dev/null +++ b/ci/jobs/test_job.py @@ -0,0 +1,68 @@ +import sys + +from praktika.result import Result + +_CONFIGS = { + "coverage": { + "test": "./bb -b debug test --coverage", + }, + "release": { + "configure": "./bb -b release configure --build-poco --build-jemalloc", + "test": "./bb -b release test", + "bench": "./bb -b release bench", + "perf": "./bb -v -b release perf file net http", + }, + "tsan": { + "configure": "./bb -b release -s thread configure --build-poco", + "test": "./bb -b release -s thread test", + "bench": "./bb -b release -s thread bench", + "perf": "./bb -v -b release -s thread perf file net http", + }, + "asan": { + "test": "./bb -b release -s address test", + }, + "ubsan": { + "test": "./bb -b release -s undefined test", + }, + "msan": { + "test": "./bb -b release -s memory test", + }, +} + +if __name__ == "__main__": + build = sys.argv[1] + config = _CONFIGS[build] + results = [] + + if "configure" in config: + results.append( + Result.from_commands_run( + name="Configure", + command=[config["configure"]], + ) + ) + + results.append( + Result.from_commands_run( + name="Build and test", + command=[config["test"]], + ) + ) + + if "bench" in config: + results.append( + Result.from_commands_run( + name="Bench", + command=[config["bench"]], + ) + ) + + if "perf" in config: + results.append( + Result.from_commands_run( + name="Perf", + command=[config["perf"]], + ) + ) + + Result.create_from(results=results).complete_job() diff --git a/ci/settings/settings.py b/ci/settings/settings.py new file mode 100644 index 0000000..3895e57 --- /dev/null +++ b/ci/settings/settings.py @@ -0,0 +1,31 @@ +class RunnerLabels: + SMALL_ARM = "arm-small" + SMALL_AMD = "amd-small" + MEDIUM_ARM = "arm-medium" + MEDIUM_AMD = "amd-medium" + LARGE_ARM = "arm-large" + + +PROJECT_NAME = "silk" +PROJECT_SLUG = "silk" +MAIN_BRANCH = "main" + +CI_CONFIG_RUNS_ON = [RunnerLabels.SMALL_ARM] + +AWS_REGION = "eu-north-1" +AWS_ACCOUNT_ID = "420943511422" +AWS_PROFILE = "Box" + +S3_ARTIFACT_BUCKET = f"{PROJECT_SLUG}-artifacts-{AWS_REGION}" +S3_REPORT_BUCKET = S3_ARTIFACT_BUCKET +CACHE_S3_PATH = f"{S3_ARTIFACT_BUCKET}/ci_cache" +ENABLE_SUBMODULE_CACHE = True +S3_BUCKET_TO_HTTP_ENDPOINT = { + S3_REPORT_BUCKET: f"{S3_REPORT_BUCKET}.s3.amazonaws.com", +} + +USE_CUSTOM_GH_AUTH = True +GH_AUTH_LAMBDA_NAME = f"{PROJECT_SLUG}-gh-token" +GH_AUTH_LAMBDA_REGION = AWS_REGION +PRAKTIKA_BASE_VENV = "praktika-runtime-0.1.2" + diff --git a/ci/workflows/main_ci.py b/ci/workflows/main_ci.py new file mode 100644 index 0000000..4e6d151 --- /dev/null +++ b/ci/workflows/main_ci.py @@ -0,0 +1,20 @@ +from praktika import Job, Workflow +from ci.settings.settings import RunnerLabels + + +WORKFLOWS = [ + Workflow.Config( + name="Main CI", + event=Workflow.Event.PUSH, + branches=["main", "add-praktika-ci-config"], + jobs=[ + Job.Config( + name="Hello World Test", + runs_on=[RunnerLabels.SMALL_ARM], + command='python3 -c \'print("hello world")\'', + ), + ], + enable_report=True, + enable_exit_code_result=True, + ) +] diff --git a/ci/workflows/pull_request.py b/ci/workflows/pull_request.py new file mode 100644 index 0000000..1f4a21e --- /dev/null +++ b/ci/workflows/pull_request.py @@ -0,0 +1,71 @@ +from praktika import Job, Workflow +from ci.settings.settings import RunnerLabels + +FMT_JOB = Job.Config( + name="Formatting", + runs_on=[RunnerLabels.SMALL_ARM], + command="python3 ./ci/jobs/fmt_job.py", + digest_config=Job.CacheDigestConfig( + include_paths=["src", "include"], + ), +) + +_TEST_DIGEST = Job.CacheDigestConfig( + include_paths=[ + "src", + "include", + "CMakeLists.txt", + "CMakePresets.json", + "bb", + "ci/jobs/init_submodules.py", + ], + with_git_submodules=True, +) + +_CHECKOUT_TEST_SUBMODULES = "python3 ./ci/jobs/init_submodules.py" + +_TEST_ARM = Job.Config( + name="Test ARM", + runs_on=[RunnerLabels.MEDIUM_ARM], + command="python3 ./ci/jobs/test_job.py {PARAMETER}", + needs_submodules=True, + pre_hooks=[_CHECKOUT_TEST_SUBMODULES], + timeout=2 * 3600, + digest_config=_TEST_DIGEST, +) + +_TEST_AMD = Job.Config( + name="Test AMD", + runs_on=[RunnerLabels.MEDIUM_AMD], + command="python3 ./ci/jobs/test_job.py {PARAMETER}", + needs_submodules=True, + pre_hooks=[_CHECKOUT_TEST_SUBMODULES], + timeout=2 * 3600, + digest_config=_TEST_DIGEST, +) + +_BUILD_VARIANTS = [ + Job.ParamSet(parameter="coverage"), + Job.ParamSet(parameter="release"), + Job.ParamSet(parameter="tsan"), + Job.ParamSet(parameter="asan"), + Job.ParamSet(parameter="ubsan"), + Job.ParamSet(parameter="msan"), +] + +WORKFLOWS = [ + Workflow.Config( + name="Pull Request CI", + event=Workflow.Event.PULL_REQUEST, + base_branches=["main"], + jobs=[ + FMT_JOB, + *_TEST_ARM.parametrize(*_BUILD_VARIANTS), + *_TEST_AMD.parametrize(*_BUILD_VARIANTS), + ], + enable_cache=True, + enable_report=True, + enable_gh_summary_comment=True, + enable_exit_code_result=True, + ) +]