aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorОлег <150132506+iddqdex@users.noreply.github.com>2025-07-25 23:57:41 +0300
committerGitHub <noreply@github.com>2025-07-25 23:57:41 +0300
commitff32dad4c0a4ac13a90dfb44fdd19a5454cfe13e (patch)
treecb906af035c953af32e06c73e1eebbbd1751d3cd
parent1205e71f0d50577d63276ba84b6ef78df7763903 (diff)
downloadydb-main.tar.gz
add config comporator and workflow for release reports (#21709)HEADmain
-rw-r--r--.github/workflows/compare_configs.yml55
-rw-r--r--ydb/tests/library/compatibility/binaries/downloader/__main__.py8
-rw-r--r--ydb/tests/library/compatibility/configs/comparator/__main__.py132
-rw-r--r--ydb/tests/library/compatibility/configs/comparator/ya.make5
-rw-r--r--ydb/tests/library/compatibility/configs/ya.make69
5 files changed, 236 insertions, 33 deletions
diff --git a/.github/workflows/compare_configs.yml b/.github/workflows/compare_configs.yml
new file mode 100644
index 00000000000..1414401fa0a
--- /dev/null
+++ b/.github/workflows/compare_configs.yml
@@ -0,0 +1,55 @@
+name: Compare ydb configs in branches
+on:
+ schedule:
+ - cron: "0 * * * *" # Every hour
+ workflow_dispatch:
+ inputs:
+ commit_sha:
+ type: string
+ default: ""
+
+defaults:
+ run:
+ shell: bash
+jobs:
+ main:
+ name: Compare configs
+ runs-on: [ self-hosted, auto-provisioned, build-preset-analytic-node]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.commit_sha }}
+ fetch-depth: 1
+ - name: Setup ydb access
+ uses: ./.github/actions/setup_ci_ydb_service_account_key_file_credentials
+ with:
+ ci_ydb_service_account_key_file_credentials: ${{ secrets.CI_YDB_SERVICE_ACCOUNT_KEY_FILE_CREDENTIALS }}
+ - name: Build
+ uses: ./.github/actions/build_and_test_ya
+ with:
+ build_preset: "release"
+ build_target: "ydb/tests/library/compatibility/configs ydb/tests/library/compatibility/configs/comparator"
+ increment: false
+ run_tests: false
+ put_build_results_to_cache: false
+ secs: ${{ format('{{"AWS_KEY_ID":"{0}","AWS_KEY_VALUE":"{1}","REMOTE_CACHE_USERNAME":"{2}","REMOTE_CACHE_PASSWORD":"{3}"}}',
+ secrets.AWS_KEY_ID, secrets.AWS_KEY_VALUE, secrets.REMOTE_CACHE_USERNAME, secrets.REMOTE_CACHE_PASSWORD ) }}
+ vars: ${{ format('{{"AWS_BUCKET":"{0}","AWS_ENDPOINT":"{1}","REMOTE_CACHE_URL":"{2}","TESTMO_URL":"{3}","TESTMO_PROJECT_ID":"{4}"}}',
+ vars.AWS_BUCKET, vars.AWS_ENDPOINT, vars.REMOTE_CACHE_URL_YA, vars.TESTMO_URL, vars.TESTMO_PROJECT_ID ) }}
+ - name: Setup s3cmd
+ uses: ./.github/actions/s3cmd
+ with:
+ s3_bucket: "ydb-builds"
+ s3_endpoint: ${{ vars.AWS_ENDPOINT }}
+ s3_key_id: ${{ secrets.AWS_KEY_ID }}
+ s3_key_secret: ${{ secrets.AWS_KEY_VALUE }}
+
+ - name: Comapare and publish result
+ shell: bash
+ run: |
+ set -xe
+ cd ./ydb/tests/library/compatibility/configs
+ ./comparator/comparator stable-25-1 stable-25-1-1 stable-25-1-2 stable-25-1-3 current >config_diff.html
+ s3cmd sync --follow-symlinks --acl-public --no-progress --stats --no-check-md5 "config_diff.html" "s3://ydb-builds/main/config_diff.html" -d
+
diff --git a/ydb/tests/library/compatibility/binaries/downloader/__main__.py b/ydb/tests/library/compatibility/binaries/downloader/__main__.py
index f5b893b4043..78bee56343e 100644
--- a/ydb/tests/library/compatibility/binaries/downloader/__main__.py
+++ b/ydb/tests/library/compatibility/binaries/downloader/__main__.py
@@ -21,15 +21,15 @@ def main():
s3_bucket = AWS_BUCKET
remote_src = sys.argv[2]
local_dst = sys.argv[3]
- binary_name = sys.argv[4]
+ binary_name = sys.argv[4] if len(sys.argv) > 4 else None
s3_client.download_file(s3_bucket, remote_src, local_dst)
# chmod +x
st = os.stat(local_dst)
os.chmod(local_dst, st.st_mode | stat.S_IEXEC)
-
- with open(local_dst + "-name", "w") as f:
- f.write(binary_name)
+ if binary_name:
+ with open(local_dst + "-name", "w") as f:
+ f.write(binary_name)
elif mode == 'append-version':
local_dst = sys.argv[2]
binary_name = sys.argv[3]
diff --git a/ydb/tests/library/compatibility/configs/comparator/__main__.py b/ydb/tests/library/compatibility/configs/comparator/__main__.py
new file mode 100644
index 00000000000..d0522607fe4
--- /dev/null
+++ b/ydb/tests/library/compatibility/configs/comparator/__main__.py
@@ -0,0 +1,132 @@
+#! /usr/bin/python3
+
+from __future__ import annotations
+import json
+import logging
+import sys
+import os
+from enum import StrEnum
+
+
+class Resolution(StrEnum):
+ NO = ''
+ OK = '#aaffaa'
+ INFO = '#aaffaa'
+ WARNING = '#ffffaa'
+ ERROR = '#ffaaaa'
+
+
+class FieldInfo:
+ def __init__(self, field: dict):
+ self.id = field.get('id')
+ self.value = field.get('default-value')
+
+
+class Differ:
+
+ def __init__(self):
+ self.fields: list[tuple[str, list[FieldInfo]]] = []
+ self.resolutions: list[tuple[str, list[tuple[Resolution, str]]]] = []
+ self.names: list[str] = []
+
+ def load_files(self, names: list[str]):
+ configs = []
+ for name in names:
+ with open(name) as f:
+ configs.append(json.load(f).get('proto'))
+ self.names.append(os.path.basename(name))
+ self._add_fields_dict(configs, [])
+
+ def _add_fields_dict(self, fields: list[dict], path: list[str]):
+ keys = set()
+ for f in fields:
+ if isinstance(f, dict):
+ for key in f.keys():
+ keys.add(key)
+
+ for k in sorted(keys):
+ key_fields = [f.get(k) if isinstance(f, dict) else None for f in fields]
+ self._add_fields(key_fields, path + [k])
+
+ def _add_fields(self, fields: list[dict], path: list[str]):
+ maxlist = 0
+ dicts = []
+ infos = []
+ for f in fields:
+ if f is None:
+ infos.append(None)
+ dicts.append(None)
+ continue
+ info = FieldInfo(f)
+ infos.append(info)
+ dicts.append(info.value if isinstance(info.value, dict) else None)
+ if isinstance(info.value, list):
+ maxlist = max(maxlist, len(info.value))
+ self.fields.append(('.'.join(path), infos))
+ self._add_fields_dict(dicts, path)
+ for i in range(maxlist):
+ index_fields = [info.value[i] if info and isinstance(info.value, list) and len(info.value) > i else None for info in infos]
+ self._add_fields(index_fields, path + [str(i)])
+
+ def compare_two_fields(self, old: FieldInfo, new: FieldInfo, path: str) -> tuple[Resolution, str]:
+ if old is None and new is None:
+ return Resolution.NO, ''
+ if old is None:
+ if isinstance(new.value, dict):
+ value = '{}'
+ elif isinstance(new.value, list):
+ value = '[]'
+ else:
+ value = new.value
+ return Resolution.INFO, f'added <b>{value}</b>'
+ if new is None:
+ return Resolution.ERROR, 'deleted'
+ if old.id != new.id:
+ return Resolution.ERROR, f'id changed<br/>{old.id} -> {new.id}'
+ if type(old.value) is not type(new.value):
+ return Resolution.ERROR, 'type changed'
+ if isinstance(old.value, dict) or isinstance(old.value, list):
+ return Resolution.OK, ''
+ if old.value != new.value:
+ if path.startswith('FeatureFlags.') and isinstance(old.value, bool):
+ if not old.value:
+ return Resolution.INFO, 'FF switched on'
+ else:
+ return Resolution.ERROR, 'FF switched off'
+ return Resolution.WARNING, f'value changed<br/>{old.value} -> {new.value}'
+ return Resolution.OK, ''
+
+ def compare(self):
+ for name, values in self.fields:
+ if len(values) == 0:
+ continue
+ result = [(Resolution.NO if values[0] is None else Resolution.OK, '')]
+ intresting = False
+ for i in range(1, len(values)):
+ result.append(self.compare_two_fields(values[i-1], values[i], name))
+ intresting = intresting or result[-1][0] not in {Resolution.OK, Resolution.NO, Resolution.INFO}
+ if intresting:
+ self.resolutions.append((name, result))
+
+ def print_result(self) -> None:
+ print('<html><body><table border=1 valign="center">')
+ print('<thead style="position: sticky; top: 0; background: white; align: center"><tr><th style="padding-left: 10; padding-right: 10">field</th>')
+ for name in self.names:
+ print(f'<th style="padding-left: 10; padding-right: 10">{name}</th>')
+ print('</tr></thead>')
+ print('<tbody>')
+ for field, result in self.resolutions:
+ print(f'<tr><td style="padding-left: 10; padding-right: 10">{field}</td>')
+ for color, msg in result:
+ print(f'<td align="center" bgcolor="{color}" style="padding-left: 10; padding-right: 10">{msg}</td>')
+ print('</tr>')
+ print('</tbody>')
+ print('</table></body></html>')
+
+
+if __name__ == '__main__':
+ logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
+ differ = Differ()
+ differ.load_files(sys.argv[1:])
+ differ.compare()
+ differ.print_result()
diff --git a/ydb/tests/library/compatibility/configs/comparator/ya.make b/ydb/tests/library/compatibility/configs/comparator/ya.make
new file mode 100644
index 00000000000..e05d90c2108
--- /dev/null
+++ b/ydb/tests/library/compatibility/configs/comparator/ya.make
@@ -0,0 +1,5 @@
+PY3_PROGRAM()
+
+PY_SRCS(__main__.py)
+
+END() \ No newline at end of file
diff --git a/ydb/tests/library/compatibility/configs/ya.make b/ydb/tests/library/compatibility/configs/ya.make
index fb35e0383a3..20852af9316 100644
--- a/ydb/tests/library/compatibility/configs/ya.make
+++ b/ydb/tests/library/compatibility/configs/ya.make
@@ -1,34 +1,45 @@
RECURSE(dump)
+RECURSE(comparator)
-INCLUDE(${ARCADIA_ROOT}/ydb/tests/library/compatibility/versions.inc)
+UNION()
+
+RUN_PROGRAM(
+ ydb/tests/library/compatibility/binaries/downloader download stable-25-1/release/config-meta.json stable-25-1
+ OUT_NOAUTO stable-25-1
+)
+
+RUN_PROGRAM(
+ ydb/tests/library/compatibility/binaries/downloader download stable-25-1-1/release/config-meta.json stable-25-1-1
+ OUT_NOAUTO stable-25-1-1
+)
+
+RUN_PROGRAM(
+ ydb/tests/library/compatibility/binaries/downloader download stable-25-1-2/release/config-meta.json stable-25-1-2
+ OUT_NOAUTO stable-25-1-2
+)
+
+RUN_PROGRAM(
+ ydb/tests/library/compatibility/binaries/downloader download stable-25-1-3/release/config-meta.json stable-25-1-3
+ OUT_NOAUTO stable-25-1-3
+)
-# UNION()
-#
-#
-# RUN_PROGRAM(
-# ydb/tests/library/compatibility/binaries/downloader download $YDB_COMPAT_INTER_REF/release/config-meta.json inter $YDB_COMPAT_INTER_REF
-# OUT_NOAUTO inter inter-name
-# )
-#
# RUN_PROGRAM(
-# ydb/tests/library/compatibility/binaries/downloader download $YDB_COMPAT_INIT_REF/release/config-meta.json init $YDB_COMPAT_INIT_REF
-# OUT_NOAUTO init init-name
+# ydb/tests/library/compatibility/binaries/downloader download prestable-25-2/release/config-meta.json prestable-25-2
+# OUT_NOAUTO prestable-25-2
# )
-#
-# IF(${YDB_COMPAT_TARGET_REF} != "current")
-# RUN_PROGRAM(
-# ydb/tests/library/compatibility/binaries/downloader download $YDB_COMPAT_TARGET_REF/release/config-meta.json target $YDB_COMPAT_TARGET_REF
-# OUT_NOAUTO target target-name
-# )
-# ELSE()
-# RUN_PROGRAM(
-# ydb/tests/library/compatibility/configs/dump/dumper
-# STDOUT_NOAUTO target
-# )
-# RUN(
-# echo current
-# STDOUT_NOAUTO target-name
-# )
-# ENDIF()
-#
-# END()
+
+RUN_PROGRAM(
+ ydb/tests/library/compatibility/binaries/downloader download prestable-25-3/release/config-meta.json prestable-25-3
+ OUT_NOAUTO prestable-25-3
+)
+
+RUN_PROGRAM(
+ ydb/tests/library/compatibility/configs/dump/dumper
+ STDOUT_NOAUTO current
+)
+RUN(
+ echo current
+ STDOUT_NOAUTO current-name
+)
+
+END()