From 024eaa9dae12d7d6c3730c28ecd9745fa975bc48 Mon Sep 17 00:00:00 2001 From: strtgbb <146047128+strtgbb@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:13:27 -0400 Subject: [PATCH 1/5] Cache git submodules in S3 so 26.3 builders skip anonymous GitHub clones. Config Workflow stores a content-addressed `.git/modules` archive; jobs with `needs_submodules` restore it instead of cloning every pin from GitHub. Co-authored-by: Cursor --- ci/defs/job_configs.py | 2 + ci/jobs/build_clickhouse.py | 4 + ci/jobs/fast_test.py | 2 + ci/praktika/digest.py | 14 ++- ci/praktika/job.py | 3 + ci/praktika/native_jobs.py | 110 +++++++++++++++++- ci/praktika/runner.py | 41 +++++++ ci/praktika/runtime.py | 2 + ci/praktika/s3.py | 15 ++- ci/praktika/settings.py | 8 ++ ci/settings/altinity_overrides.py | 3 + ci/settings/settings.py | 1 + .../test_s3_head_object_optional_fields.py | 40 +++++++ 13 files changed, 238 insertions(+), 7 deletions(-) create mode 100644 ci/tests/test_s3_head_object_optional_fields.py diff --git a/ci/defs/job_configs.py b/ci/defs/job_configs.py index 998d2f76da6d..b0455b190d02 100644 --- a/ci/defs/job_configs.py +++ b/ci/defs/job_configs.py @@ -76,6 +76,7 @@ run_in_docker=BINARY_DOCKER_COMMAND, timeout=3600 * 4, digest_config=build_digest_config, + needs_submodules=True, ) common_ft_job_config = Job.Config( @@ -193,6 +194,7 @@ class JobConfigs: run_in_docker="altinityinfra/fasttest+--network=host+--volume=./ci/tmp/var/lib/clickhouse:/var/lib/clickhouse+--volume=./ci/tmp/etc/clickhouse-client:/etc/clickhouse-client+--volume=./ci/tmp/etc/clickhouse-server:/etc/clickhouse-server+--volume=./ci/tmp/var/log:/var/log+--volume=.:/ClickHouse+--env=AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID+--env=AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY", digest_config=fast_test_digest_config, result_name_for_cidb="Tests", + needs_submodules=True, ) darwin_fast_test_jobs = Job.Config( name="Darwin fast test", diff --git a/ci/jobs/build_clickhouse.py b/ci/jobs/build_clickhouse.py index 4f48fadfb4f3..ffd591a3db13 100644 --- a/ci/jobs/build_clickhouse.py +++ b/ci/jobs/build_clickhouse.py @@ -176,6 +176,10 @@ def do_checkout(): f"mkdir -p {build_dir} && git submodule sync && git submodule init" ) + if os.path.isdir(".git/modules/contrib") and os.listdir( + ".git/modules/contrib" + ): + print("Submodule cache detected, populating working trees from cache") res = res and Shell.check( "contrib/update-submodules.sh --max-procs 10", retries=3, diff --git a/ci/jobs/fast_test.py b/ci/jobs/fast_test.py index 90dae093da32..4a26ca75e87b 100644 --- a/ci/jobs/fast_test.py +++ b/ci/jobs/fast_test.py @@ -68,6 +68,8 @@ def clone_submodules(): res = Shell.check("git submodule sync", verbose=True, strict=True) res = res and Shell.check("git submodule init", verbose=True, strict=True) + if os.path.isdir(".git/modules/contrib") and os.listdir(".git/modules/contrib"): + print("Submodule cache detected, populating working trees from cache") res = res and Shell.check( # NOTE: max-procs was 10 before, increased to 20 to speed up checkout. # Roll back to 10 if this starts hitting GitHub rate limits. diff --git a/ci/praktika/digest.py b/ci/praktika/digest.py index 62c37353090e..431d53d7e1e6 100644 --- a/ci/praktika/digest.py +++ b/ci/praktika/digest.py @@ -64,10 +64,7 @@ def calc_job_digest(self, job_config: Job.Config, docker_digests, artifact_confi for i, file_path in enumerate(included_files): hash_md5 = self._calc_file_digest(file_path, hash_md5) if config.with_git_submodules: - submodules_shas = Shell.get_output( - "git submodule | awk '{print $1}' | sed 's/^[+-]//'", verbose=True - ) - hash_md5.update(submodules_shas.encode()) + hash_md5.update(Digest.get_submodule_shas().encode()) digest = hash_md5.hexdigest()[: Settings.CACHE_DIGEST_LEN] self.digest_cache[cache_key] = digest @@ -141,6 +138,15 @@ def calc_docker_digest( return hash_md5.hexdigest()[: Settings.CACHE_DIGEST_LEN] + @staticmethod + def get_submodule_shas(): + return ( + Shell.get_output( + "git submodule | awk '{print $1}' | sed 's/^[+-]//'", verbose=True + ) + or "" + ) + @staticmethod def _calc_file_digest(file_path, hash_md5): # Resolve file path if it's a symbolic link diff --git a/ci/praktika/job.py b/ci/praktika/job.py index 8cc9642f6bac..e37700a21299 100644 --- a/ci/praktika/job.py +++ b/ci/praktika/job.py @@ -85,6 +85,9 @@ class Config: # List of commands to call upon job completion post_hooks: List[str] = field(default_factory=list) + # If True, runner.py restores the submodule cache from S3 before the job starts + needs_submodules: bool = False + def parametrize(self, *param_sets: "Job.ParamSet"): res = [] for param_set in param_sets: diff --git a/ci/praktika/native_jobs.py b/ci/praktika/native_jobs.py index f6010e17c8d8..14cba0b176aa 100644 --- a/ci/praktika/native_jobs.py +++ b/ci/praktika/native_jobs.py @@ -1,4 +1,5 @@ import dataclasses +import hashlib import json import os import platform @@ -38,6 +39,102 @@ def _GH_Auth(workflow, force=False): GHAuth.auth(app_id=app_id, app_key=pem) +def _submodule_auth_env() -> dict: + env = os.environ.copy() + if not Settings.ENABLE_SUBMODULE_CLONE_AUTH: + return env + token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") or "" + if not token: + token = Shell.get_output("gh auth token") or "" + if not token: + print("WARNING: no GH token available, submodule clones run anonymously") + return env + env["GIT_CONFIG_COUNT"] = "1" + env["GIT_CONFIG_KEY_0"] = ( + f"url.https://x-access-token:{token}@github.com/.insteadOf" + ) + env["GIT_CONFIG_VALUE_0"] = "https://github.com/" + return env + + +def _prepare_submodule_cache(workflow_config: RunConfig) -> Result: + """Compute a content-addressed hash of submodule SHAs and ensure a cache + archive exists in S3. Stores the hash in workflow_config so that downstream + jobs with needs_submodules=True can restore it.""" + stop_watch = Utils.Stopwatch() + info = "" + try: + submodule_shas = Digest.get_submodule_shas() + if not submodule_shas: + print("WARNING: No submodules found, skipping submodule cache") + return Result.create_from( + name="Submodule Cache", + status=Result.Status.SUCCESS, + stopwatch=stop_watch, + info="No submodules", + ) + + cache_hash = hashlib.sha256(submodule_shas.encode()).hexdigest()[:16] + s3_path = f"{Settings.CACHE_S3_PATH}/submodules/{cache_hash}.tar.zst" + print(f"Submodule cache hash: {cache_hash}") + + if S3.head_object(s3_path): + print(f"Submodule cache hit: {s3_path}") + info = f"cache hit: {cache_hash}" + else: + print(f"Submodule cache miss, creating: {s3_path}") + Shell.check("git submodule sync", verbose=True, strict=True) + Shell.check("git submodule init", verbose=True, strict=True) + Shell.check( + "git submodule update --depth=1 --single-branch --jobs 64", + verbose=True, + strict=True, + retries=3, + env=_submodule_auth_env(), + ) + archive_path = f"{Settings.TEMP_DIR}/submodules_{cache_hash}.tar.zst" + Shell.check( + f"tar -cf - .git/modules | zstd -c -T0 > {archive_path}", + verbose=True, + strict=True, + ) + # Write-once conditional create (If-None-Match: *) instead of an + # unconditional overwrite. The object is content-addressed by the + # submodule SHAs, so it never legitimately changes. + created = S3.put( + s3_path=s3_path, + local_path=archive_path, + if_none_matched=True, + no_strict=True, + ) + Shell.check(f"rm -f {archive_path}") + if not created and not S3.head_object(s3_path): + raise RuntimeError(f"failed to upload submodule cache {s3_path}") + info = ( + f"cache miss, created: {cache_hash}" + if created + else f"cache miss, created concurrently: {cache_hash}" + ) + + workflow_config.submodule_cache_hash = cache_hash + workflow_config.dump() + status = Result.Status.SUCCESS + except Exception as e: + print(f"ERROR: Submodule cache failed: {e}") + traceback.print_exc() + info = f"{e}\n{traceback.format_exc()}" + # Do not continue with an empty submodule_cache_hash. Builds would + # skip the restore and clone the same pins themselves. + status = Result.Status.FAILED + + return Result.create_from( + name="Submodule Cache", + status=status, + stopwatch=stop_watch, + info=info, + ) + + _workflow_config_job = Job.Config( name=Settings.CI_CONFIG_JOB_NAME, runs_on=Settings.CI_CONFIG_RUNS_ON, @@ -50,7 +147,9 @@ def _GH_Auth(workflow, force=False): else None ), command=f"{Settings.PYTHON_INTERPRETER} -m praktika.native_jobs '{Settings.CI_CONFIG_JOB_NAME}'", - timeout=600, + # On a submodule-cache miss this job clones all submodules + # (see _prepare_submodule_cache), which can take well over 10 minutes. + timeout=1800, ) _docker_build_manifest_job = Job.Config( @@ -360,6 +459,7 @@ def _check_db(workflow): cache_artifacts={}, cache_jobs={}, filtered_jobs={}, + submodule_cache_hash="", custom_data={}, ).dump() @@ -605,6 +705,14 @@ def check_affected_jobs(): ) ) + if ( + results[-1].is_ok() + and workflow.enable_cache + and Settings.ENABLE_SUBMODULE_CACHE + ): + result = _prepare_submodule_cache(workflow_config) + results.append(result) + if workflow.enable_slack_feed: if env.PR_NUMBER: commit_authors = set() diff --git a/ci/praktika/runner.py b/ci/praktika/runner.py index 91f74511a8ad..336a5f086dc8 100644 --- a/ci/praktika/runner.py +++ b/ci/praktika/runner.py @@ -69,6 +69,7 @@ def generate_local_run_environment(workflow, job, pr=None, sha=None, branch=None cache_artifacts={}, cache_jobs={}, filtered_jobs={}, + submodule_cache_hash="", custom_data={}, ) # Extract repository name from git remote (format: owner/repo) @@ -335,8 +336,48 @@ def _pre_run(self, workflow, job, local_run=False): if fallback_artifact.compress_zst: Utils.decompress_file(Path(Settings.INPUT_DIR) / fallback_path) + if not local_run and job.needs_submodules and Settings.ENABLE_SUBMODULE_CACHE: + self._restore_submodule_cache() + return 0 + @staticmethod + def _restore_submodule_cache(): + try: + wf_config = RunConfig.from_workflow_data() + cache_hash = wf_config.submodule_cache_hash + if not cache_hash: + print( + "NOTE: No submodule cache hash in workflow config, skipping restore" + ) + return + s3_path = f"{Settings.CACHE_S3_PATH}/submodules/{cache_hash}.tar.zst" + local_archive = f"{Settings.TEMP_DIR}/submodules_{cache_hash}.tar.zst" + print(f"Restoring submodule cache: {s3_path}") + S3.copy_file_from_s3( + s3_path=s3_path, local_path=local_archive, no_strict=True + ) + if Path(local_archive).exists(): + if Shell.check( + f"zstd -d {local_archive} --stdout | tar -xf - -C .", + verbose=True, + ): + Shell.check(f"rm -f {local_archive}") + print("Submodule cache restored successfully") + else: + print("WARNING: Submodule cache extraction failed, cleaning up") + Shell.check("rm -rf .git/modules") + Shell.check(f"rm -f {local_archive}") + else: + print( + "WARNING: Submodule cache download failed, will clone from GitHub" + ) + except Exception as e: + print( + f"WARNING: Submodule cache restore failed: {e}, will clone from GitHub" + ) + traceback.print_exc() + def _run( self, workflow, diff --git a/ci/praktika/runtime.py b/ci/praktika/runtime.py index a4c95b83ca62..5800dec8076a 100644 --- a/ci/praktika/runtime.py +++ b/ci/praktika/runtime.py @@ -20,6 +20,7 @@ class RunConfig(MetaClasses.Serializable): cache_jobs: Dict[str, Cache.CacheRecord] filtered_jobs: Dict[str, str] sha: str + submodule_cache_hash: str custom_data: Dict[str, Any] @classmethod @@ -36,6 +37,7 @@ def from_dict(cls, obj): for job_name, cache_jobs in cache_jobs.items(): cache_jobs_deserialized[job_name] = Cache.CacheRecord.from_dict(cache_jobs) obj["cache_jobs"] = cache_artifacts_deserialized + obj.setdefault("submodule_cache_hash", "") return RunConfig(**obj) @classmethod diff --git a/ci/praktika/s3.py b/ci/praktika/s3.py index 6f185cde1246..8f5a9ed95222 100644 --- a/ci/praktika/s3.py +++ b/ci/praktika/s3.py @@ -439,8 +439,19 @@ def head_object(cls, s3_path): ) if not output: return None - else: - return cls.Object(**json.loads(output)) + data = json.loads(output) + # HeadObject omits optional fields (Expiration) unless a lifecycle Expire + # rule applies, and emits extra keys Object does not declare. + return cls.Object( + AcceptRanges=data.get("AcceptRanges", ""), + Expiration=data.get("Expiration", ""), + LastModified=data.get("LastModified", ""), + ContentLength=data.get("ContentLength", 0), + ETag=data.get("ETag", ""), + ContentType=data.get("ContentType", ""), + ServerSideEncryption=data.get("ServerSideEncryption", ""), + Metadata=data.get("Metadata") or {}, + ) @classmethod def delete(cls, s3_path): diff --git a/ci/praktika/settings.py b/ci/praktika/settings.py index d9929dd36d72..4eeb9b8ca3ff 100644 --- a/ci/praktika/settings.py +++ b/ci/praktika/settings.py @@ -75,6 +75,12 @@ class _Settings: ###################################### # CI Cache settings # ###################################### + # If enabled, Config Workflow creates a content-addressed .git/modules/ archive + # in S3. Jobs with needs_submodules=True download it instead of cloning from GitHub. + ENABLE_SUBMODULE_CACHE: bool = False + # If enabled, submodule clones rewrite github.com URLs with the ambient + # GITHUB_TOKEN / gh auth token; otherwise they run anonymously. + ENABLE_SUBMODULE_CLONE_AUTH: bool = False CACHE_VERSION: int = 1 CACHE_DIGEST_LEN: int = 20 CACHE_S3_PATH: str = "" @@ -171,6 +177,8 @@ class _Settings: "CI_DB_READ_USER", "CI_DB_READ_URL", "TEST_FAILURE_PATTERNS", + "ENABLE_SUBMODULE_CACHE", + "ENABLE_SUBMODULE_CLONE_AUTH", ] diff --git a/ci/settings/altinity_overrides.py b/ci/settings/altinity_overrides.py index c365b25ad191..d8152070b9e6 100644 --- a/ci/settings/altinity_overrides.py +++ b/ci/settings/altinity_overrides.py @@ -48,6 +48,9 @@ class RunnerLabels: CI_DB_READ_URL: str = os.environ.get("CLICKHOUSE_TEST_STAT_URL", "") + "play" USE_CUSTOM_GH_AUTH = False +# NOTE (strtgbb): anonymous submodule fetches of non-tip pins get refused +# after the burst of ~150 clones; send the ambient gh token instead. +ENABLE_SUBMODULE_CLONE_AUTH = True # SECRET_GH_APP_ID: str = "woolenwolf_gh_app.clickhouse-app-id" # SECRET_GH_APP_PEM_KEY: str = "woolenwolf_gh_app.clickhouse-app-key" diff --git a/ci/settings/settings.py b/ci/settings/settings.py index e64910636d97..0640e9f70283 100644 --- a/ci/settings/settings.py +++ b/ci/settings/settings.py @@ -23,6 +23,7 @@ class RunnerLabels: DOCKER_BUILD_AMD_RUNS_ON = RunnerLabels.STYLE_CHECK_AMD CACHE_S3_PATH = f"{S3_BUCKET_NAME}/ci_ch_cache" +ENABLE_SUBMODULE_CACHE = True HTML_S3_PATH = S3_REPORT_BUCKET_NAME S3_BUCKET_TO_HTTP_ENDPOINT = { S3_BUCKET_NAME: S3_BUCKET_HTTP_ENDPOINT, diff --git a/ci/tests/test_s3_head_object_optional_fields.py b/ci/tests/test_s3_head_object_optional_fields.py new file mode 100644 index 000000000000..0d519807f923 --- /dev/null +++ b/ci/tests/test_s3_head_object_optional_fields.py @@ -0,0 +1,40 @@ +""" +S3.head_object must tolerate HeadObject JSON that omits optional AWS fields. + +aws s3api head-object only emits Expiration when a lifecycle Expire rule +applies. Unpacking the raw JSON into S3.Object then raises TypeError and +Config Workflow leaves submodule_cache_hash empty. +""" + +import json +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../..")) + +from ci.praktika import s3 as s3_module +from ci.praktika.s3 import S3 + +# Shape of a successful HeadObject on an untagged cache archive: no Expiration, +# plus keys Object does not declare. +HEAD_WITHOUT_EXPIRATION = { + "AcceptRanges": "bytes", + "LastModified": "Wed, 02 Sep 2026 14:33:49 GMT", + "ContentLength": 123, + "ETag": '"abc"', + "ContentType": "application/zstd", + "Metadata": {}, + "StorageClass": "STANDARD", +} + + +def test_head_object_without_expiration_is_a_hit(monkeypatch): + monkeypatch.setattr( + s3_module.Shell, + "get_output", + lambda *a, **kw: json.dumps(HEAD_WITHOUT_EXPIRATION), + ) + obj = S3.head_object("altinity-build-artifacts/ci_ch_cache/submodules/hash.tar.zst") + assert obj is not None + assert obj.ContentLength == 123 + assert obj.Expiration == "" From 79e6ff4aa5676c5f53cad1f806c19a7c7f280e46 Mon Sep 17 00:00:00 2001 From: strtgbb <146047128+strtgbb@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:54:32 -0400 Subject: [PATCH 2/5] Bring stable-26.3 CI in line with 26.8: report, Fast Builds, and split regression suites. Co-authored-by: Cursor --- .github/PULL_REQUEST_TEMPLATE.md | 1 + .../actions/create_workflow_report/action.yml | 7 +- .../ci_run_report.html.jinja | 176 +++- .../create_workflow_report.py | 817 ++++++++++++++++-- .../test_prs_in_release.py | 440 ++++++++++ .../test_report_queries.py | 109 ++- .../workflow_report_hook.sh | 8 +- .../{release_builds.yml => fast_builds.yml} | 44 +- .github/workflows/regression.yml | 59 +- .../{release_builds.py => fast_builds.py} | 2 +- docker/keeper/Dockerfile | 2 +- 11 files changed, 1503 insertions(+), 162 deletions(-) create mode 100644 .github/actions/create_workflow_report/test_prs_in_release.py rename .github/workflows/{release_builds.yml => fast_builds.yml} (96%) rename ci/workflows/{release_builds.py => fast_builds.py} (99%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cb091ef1dbe7..aa535652d6fe 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -42,6 +42,7 @@ - [ ] ClickHouse Keeper (1h) - [x] Iceberg (2h) - [ ] LDAP (1h) +- [ ] OAuth (5m) - [x] Parquet (1.5h) - [ ] RBAC (1.5h) - [ ] SSL Server (1h) diff --git a/.github/actions/create_workflow_report/action.yml b/.github/actions/create_workflow_report/action.yml index 5e0f517c35a0..d8ff00a1df26 100644 --- a/.github/actions/create_workflow_report/action.yml +++ b/.github/actions/create_workflow_report/action.yml @@ -27,10 +27,13 @@ runs: FINAL: ${{ inputs.final }} shell: bash run: | - pip install clickhouse-driver==0.2.8 numpy==1.26.4 pandas==2.0.3 jinja2==3.1.5 + # Newer runners have these deps preinstalled; only install on Ubuntu 22. + if [[ -f /etc/os-release ]] && . /etc/os-release && [[ "$VERSION_ID" == "22.04" ]]; then + pip install clickhouse-driver==0.2.8 numpy==1.26.4 pandas==2.0.3 jinja2==3.1.5 + fi CMD="python3 .github/actions/create_workflow_report/create_workflow_report.py" - ARGS="--actions-run-url $ACTIONS_RUN_URL --known-fails tests/broken_tests.yaml --cves --pr-number $PR_NUMBER" + ARGS="--actions-run-url $ACTIONS_RUN_URL --known-fails --cves --pr-number $PR_NUMBER" set +e -x if [[ "$FINAL" == "false" ]]; then diff --git a/.github/actions/create_workflow_report/ci_run_report.html.jinja b/.github/actions/create_workflow_report/ci_run_report.html.jinja index 4c94465a16c6..48ae95cefba1 100644 --- a/.github/actions/create_workflow_report/ci_run_report.html.jinja +++ b/.github/actions/create_workflow_report/ci_run_report.html.jinja @@ -13,10 +13,11 @@ :root { --altinity-background: #000D45; --altinity-accent: #189DCF; - --altinity-highlight: #FFC600; + --altinity-link-hover: #FFC600; --altinity-gray: #6c757d; --altinity-light-gray: #f8f9fa; --altinity-white: #ffffff; + --altinity-hover: color-mix(in srgb, var(--altinity-accent) 12%, var(--altinity-white)); } /* Body and heading fonts */ @@ -45,7 +46,7 @@ /* General table styling */ table { - min-width: min(900px, 98vw); + min-width: min(900px, 100%); margin: 1rem 0; border-collapse: collapse; background-color: var(--altinity-white); @@ -107,7 +108,7 @@ /* Table body row styling */ tr:hover { - background-color: var(--altinity-light-gray); + background-color: var(--altinity-hover); } /* Table cell styling */ @@ -123,9 +124,72 @@ } a:hover { - color: var(--altinity-highlight); + color: var(--altinity-link-hover); text-decoration: underline; } + + /* Tab navigation: wrapping strip; each button is an open-bottom tab shape */ + .tab { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + padding: 0.4rem; + border-radius: 0.75rem; + width: fit-content; + max-width: 100%; + margin: 1.5rem 0; + background: var(--altinity-white); + } + + .tab button.tablinks { + appearance: none; + box-sizing: border-box; + border: 1px solid var(--altinity-accent); + border-bottom: 0; + border-radius: 0.5rem 0.5rem 0 0; + background: var(--altinity-white); + padding: 0.55rem 0.95rem; + font: inherit; + cursor: pointer; + white-space: nowrap; + color: var(--altinity-background); + transition: background 0.12s, color 0.12s; + } + + .tab button.tablinks:hover { + background: var(--altinity-hover); + } + + .tab button.tablinks.active { + background: var(--altinity-hover); + box-shadow: inset 0 2px 0 var(--altinity-accent); + color: var(--altinity-accent); + font-weight: 600; + } + + .tabcontent { + display: none; + padding: 0; + overflow-x: auto; + animation: fadeEffect 0.3s; + } + + .tabcontent table { + min-width: 0; + } + + .tabcontent > *:first-child { + margin-top: 0; + } + + .tabcontent.active { + display: block; + } + + @keyframes fadeEffect { + from { opacity: 0; } + to { opacity: 1; } + } {{ title }} This is a preview. The workflow is not yet finished.

{% endif %} -

Table of Contents

- - - {%- if pr_number != 0 -%} -

New Fails in PR

-

Compared with base sha {{ base_sha }}

- {{ new_fails_html }} +
+ {%- if pr_number == 0 %}{% endif %} + {%- if pr_number != 0 %}{% endif %} + + + + + + +
+ + {%- if pr_number == 0 %} +
+ {%- if prs_in_release_search_url %} +

See the PRs on GitHub

+ {%- endif %} + {{ prs_in_release_html }} +
{%- endif %} -

CI Jobs Status

- {{ ci_jobs_status_html }} + {%- if pr_number != 0 %} +
+

Compared with base sha {{ base_sha }}

+ {{ new_fails_html }} +
+ {%- endif %} + +
+ {{ ci_jobs_status_html }} +
-

Checks Errors

- {{ checks_errors_html }} +
+ {{ checks_errors_html }} +
-

Checks New Fails

- {{ checks_fails_html }} +
+ {{ checks_fails_html }} +
-

Regression New Fails

- {{ regression_fails_html }} +
+ {{ regression_fails_html }} +
-

Docker Images CVEs

- {{ docker_images_cves_html }} +
+ {{ docker_images_cves_html }} +
-

Checks Known Fails

-

- Fail reason conventions:
- KNOWN - Accepted fail and fix is not planned
- INVESTIGATE - We don't know why it fails
- NEEDSFIX - Investigation done and a fix is needed to make it pass
-

- {{ checks_known_fails_html }} +
+

+ Fail reason conventions:
+ KNOWN - Accepted fail and fix is not planned
+ INVESTIGATE - We don't know why it fails
+ NEEDSFIX - Investigation done and a fix is needed to make it pass
+

+ {{ checks_known_fails_html }} +
+ +