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 998d2f76da6df..b0455b190d023 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 4f48fadfb4f33..ffd591a3db131 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 90dae093da32f..4a26ca75e87b7 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 62c37353090e7..431d53d7e1e66 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 8cc9642f6bac7..e37700a212991 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 f6010e17c8d80..14cba0b176aa7 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 91f74511a8ad6..336a5f086dc8b 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 a4c95b83ca62f..5800dec8076a2 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 6f185cde12469..8f5a9ed95222b 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 d9929dd36d725..4eeb9b8ca3ffb 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 c365b25ad1912..d8152070b9e63 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 e64910636d976..0640e9f70283a 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 0000000000000..0d519807f9236
--- /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 cb091ef1dbe72..aa535652d6fec 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 5e0f517c35a01..d8ff00a1df26a 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 4c94465a16c6d..48ae95cefba13 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 %}PRs in Release ({{ counts.prs_in_release }}) {% endif %}
+ {%- if pr_number != 0 %}New Fails in PR ({{ counts.pr_new_fails }}) {% endif %}
+ CI Jobs Status ({{ counts.jobs_status }})
+ Checks Errors ({{ counts.checks_errors }})
+ Checks New Fails ({{ counts.checks_new_fails }})
+ Regression New Fails ({{ counts.regression_new_fails }})
+ Docker Images CVEs ({{ counts.cves }})
+ Checks Known Fails ({{ counts.checks_known_fails }})
+
+
+ {%- 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 }}
+
+
+