diff --git a/.github/create_combined_ci_report.py b/.github/create_combined_ci_report.py new file mode 100755 index 000000000000..012348b3e278 --- /dev/null +++ b/.github/create_combined_ci_report.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +import argparse +import os +from pathlib import Path +from itertools import combinations + +import requests +from clickhouse_driver import Client +import boto3 +from botocore.exceptions import NoCredentialsError + +DATABASE_HOST_VAR = "CHECKS_DATABASE_HOST" +DATABASE_USER_VAR = "CHECKS_DATABASE_USER" +DATABASE_PASSWORD_VAR = "CHECKS_DATABASE_PASSWORD" +S3_BUCKET = "altinity-build-artifacts" + + +def get_checks_fails(client: Client, job_url: str): + """ + Get tests that did not succeed for the given job URL. + Exclude checks that have status 'error' as they are counted in get_checks_errors. + """ + columns = ( + "check_status, check_name, test_status, test_name, report_url as results_link" + ) + query = f"""SELECT {columns} FROM `gh-data`.checks + WHERE task_url='{job_url}' + AND test_status in ('FAIL', 'ERROR') + AND check_status!='error' + ORDER BY check_name, test_name + """ + return client.query_dataframe(query) + + +def get_checks_errors(client: Client, job_url: str): + """ + Get checks that have status 'error' for the given job URL. + """ + columns = ( + "check_status, check_name, test_status, test_name, report_url as results_link" + ) + query = f"""SELECT {columns} FROM `gh-data`.checks + WHERE task_url='{job_url}' + AND check_status=='error' + ORDER BY check_name, test_name + """ + return client.query_dataframe(query) + + +def drop_prefix_rows(df, column_to_clean): + """ + Drop rows from the dataframe if: + - the row matches another row completely except for the specified column + - the specified column of that row is a prefix of the same column in another row + """ + to_drop = set() + reference_columns = [col for col in df.columns if col != column_to_clean] + for (i, row_1), (j, row_2) in combinations(df.iterrows(), 2): + if all(row_1[col] == row_2[col] for col in reference_columns): + if row_2[column_to_clean].startswith(row_1[column_to_clean]): + to_drop.add(i) + elif row_1[column_to_clean].startswith(row_2[column_to_clean]): + to_drop.add(j) + return df.drop(to_drop) + + +def get_regression_fails(client: Client, job_url: str): + """ + Get regression tests that did not succeed for the given job URL. + """ + # If you rename the alias for report_url, also update the formatters in format_results_as_html_table + query = f"""SELECT arch, status, test_name, results_link + FROM ( + SELECT + architecture as arch, + test_name, + argMax(result, start_time) AS status, + job_url, + report_url as results_link + FROM `gh-data`.clickhouse_regression_results + GROUP BY architecture, test_name, job_url, report_url, start_time + ORDER BY start_time DESC, length(test_name) DESC + ) + WHERE job_url='{job_url}' + AND status IN ('Fail', 'Error') + """ + df = client.query_dataframe(query) + df = drop_prefix_rows(df, "test_name") + return df + + +def url_to_html_link(url: str) -> str: + if not url: + return "" + text = url.split("/")[-1] + if not text: + text = "results" + return f'{text}' + + +def format_test_name_for_linewrap(text: str) -> str: + """Tweak the test name to improve line wrapping.""" + return text.replace(".py::", "/") + + +def format_results_as_html_table(results) -> str: + if results.empty: + return "" + results.columns = [col.replace("_", " ").title() for col in results.columns] + html = ( + results.to_html( + index=False, + formatters={ + "Results Link": url_to_html_link, + "Test Name": format_test_name_for_linewrap, + }, + escape=False, + ) # tbody/thead tags interfere with the table sorting script + .replace("\n", "") + .replace("\n", "") + .replace("\n", "") + .replace("\n", "") + .replace(' argparse.Namespace: + parser = argparse.ArgumentParser(description="Create a combined CI report.") + parser.add_argument( + "--actions-run-url", required=True, help="URL of the actions run" + ) + parser.add_argument( + "--pr-number", required=True, help="Pull request number for the S3 path" + ) + parser.add_argument( + "--commit-sha", required=True, help="Commit SHA for the S3 path" + ) + parser.add_argument( + "--no-upload", action="store_true", help="Do not upload the report" + ) + parser.add_argument( + "--mark-preview", action="store_true", help="Mark the report as a preview" + ) + return parser.parse_args() + + +def main(): + args = parse_args() + + db_client = Client( + host=os.getenv(DATABASE_HOST_VAR), + user=os.getenv(DATABASE_USER_VAR), + password=os.getenv(DATABASE_PASSWORD_VAR), + port=9440, + secure="y", + verify=False, + settings={"use_numpy": True}, + ) + + s3_path = ( + f"https://s3.amazonaws.com/{S3_BUCKET}/{args.pr_number}/{args.commit_sha}/" + ) + report_destination_url = s3_path + "combined_report.html" + ci_running_report_url = s3_path + "ci_running.html" + + response = requests.get(ci_running_report_url) + if response.status_code == 200: + ci_running_report: str = response.text + else: + print( + f"Failed to download CI running report. Status code: {response.status_code}, Response: {response.text}" + ) + exit(1) + + fail_results = { + "checks_fails": get_checks_fails(db_client, args.actions_run_url), + "checks_errors": get_checks_errors(db_client, args.actions_run_url), + "regression_fails": get_regression_fails(db_client, args.actions_run_url), + } + + combined_report = ( + ci_running_report.replace("ClickHouse CI running for", "Combined CI Report for") + .replace( + "
", + f"""

Table of Contents

+{'

This is a preview. FinishCheck has not completed.

' if args.mark_preview else ""} + + +

CI Jobs Status

+
""", + ) + .replace( + "
", + f""" + +

Checks Fails

+{format_results_as_html_table(fail_results['checks_fails'])} + +

Checks Errors

+{format_results_as_html_table(fail_results['checks_errors'])} + +

Regression Fails

+{format_results_as_html_table(fail_results['regression_fails'])} +""", + ) + ) + report_path = Path("combined_report.html") + report_path.write_text(combined_report, encoding="utf-8") + + if args.no_upload: + print(f"Report saved to {report_path}") + exit(0) + + # Upload the report to S3 + s3_client = boto3.client("s3") + + try: + s3_client.put_object( + Bucket=S3_BUCKET, + Key=f"{args.pr_number}/{args.commit_sha}/combined_report.html", + Body=combined_report, + ContentType="text/html; charset=utf-8", + ) + except NoCredentialsError: + print("Credentials not available for S3 upload.") + + print(report_destination_url) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 73728451316e..0fec14f81a80 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -174,6 +174,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/regression.py @@ -182,9 +183,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} ${{ matrix.SUITE }}" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -225,6 +237,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u alter/regression.py @@ -234,9 +247,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} Alter ${{ matrix.ONLY }} partition" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -277,6 +301,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/benchmark.py @@ -293,9 +318,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} Benchmark ${{ matrix.STORAGE }}" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -332,6 +368,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/regression.py @@ -341,9 +378,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} Clickhouse Keeper SSL" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -383,6 +431,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/regression.py @@ -391,9 +440,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} LDAP ${{ matrix.SUITE }}" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -429,6 +489,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/regression.py @@ -437,9 +498,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} Parquet" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -480,6 +552,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/regression.py @@ -493,9 +566,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} Parquet ${{ matrix.STORAGE }}" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -536,6 +620,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/regression.py @@ -555,9 +640,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} S3 ${{ matrix.STORAGE }}" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: @@ -598,6 +694,7 @@ jobs: - name: Get deb url run: python3 .github/get-deb-url.py --reports-path ${{ env.REPORTS_PATH }} --github-env $GITHUB_ENV - name: Run ${{ env.SUITE }} suite + id: run_suite run: EXITCODE=0; python3 -u ${{ env.SUITE }}/regression.py @@ -613,9 +710,20 @@ jobs: ${{ env.args }} || EXITCODE=$?; .github/add_link_to_logs.sh; exit $EXITCODE + - name: Set Commit Status + if: always() + run: python3 .github/set_builds_status.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JOB_OUTCOME: ${{ steps.run_suite.outcome }} + SUITE_NAME: "Regression ${{ inputs.arch }} Tiered Storage ${{ matrix.STORAGE }}" - name: Create and upload logs if: always() run: .github/create_and_upload_logs.sh 1 + - name: Upload logs to regression results database + if: always() + timeout-minutes: 20 + run: .github/upload_results_to_database.sh 1 - uses: actions/upload-artifact@v4 if: always() with: diff --git a/.github/workflows/release_branches.yml b/.github/workflows/release_branches.yml index 1b7f0a244d5a..36bf370954cb 100644 --- a/.github/workflows/release_branches.yml +++ b/.github/workflows/release_branches.yml @@ -539,7 +539,7 @@ jobs: secrets: inherit with: runner_type: altinity-on-demand, altinity-type-cpx51, altinity-image-x86-app-docker-ce, altinity-setup-regression - commit: 18c56fe46fae827748c557e9ebda0599c11b0a55 + commit: a5b768e5aa5d634f17c5b0c960c1e79580058347 arch: release build_sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} timeout_minutes: 300 @@ -550,7 +550,7 @@ jobs: secrets: inherit with: runner_type: altinity-on-demand, altinity-type-cax41, altinity-image-arm-app-docker-ce, altinity-setup-regression - commit: 18c56fe46fae827748c557e9ebda0599c11b0a55 + commit: a5b768e5aa5d634f17c5b0c960c1e79580058347 arch: aarch64 build_sha: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} timeout_minutes: 300 @@ -626,3 +626,25 @@ jobs: ${{ toJson(needs) }} EOF python3 ./tests/ci/ci_buddy.py --check-wf-status + - name: Create and upload combined report + if: ${{ !cancelled() }} + env: + CHECKS_DATABASE_HOST: ${{ secrets.CHECKS_DATABASE_HOST }} + CHECKS_DATABASE_USER: ${{ secrets.CHECKS_DATABASE_USER }} + CHECKS_DATABASE_PASSWORD: ${{ secrets.CHECKS_DATABASE_PASSWORD }} + COMMIT_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + PR_NUMBER: ${{ github.event.number }} + ACTIONS_RUN_URL: ${{ github.event.repository.html_url }}/actions/runs/${{ github.run_id }} + shell: bash + run: | + pip install clickhouse-driver==0.2.8 numpy==1.26.4 pandas==2.2.0 + + REPORT_LINK=$(python3 .github/create_combined_ci_report.py --pr-number $PR_NUMBER --commit-sha $COMMIT_SHA --actions-run-url $ACTIONS_RUN_URL) + + IS_VALID_URL=$(echo $REPORT_LINK | grep -E '^https?://') + if [[ -n $IS_VALID_URL ]]; then + echo "Combined CI Report: [View Report]($REPORT_LINK)" >> $GITHUB_STEP_SUMMARY + else + echo "Error: $REPORT_LINK" >> $GITHUB_STEP_SUMMARY + exit 1 + fi diff --git a/tests/ci/ci_config.py b/tests/ci/ci_config.py index 5f49d36c8219..f9bc53324714 100644 --- a/tests/ci/ci_config.py +++ b/tests/ci/ci_config.py @@ -403,21 +403,24 @@ class CI: JobNames.INTEGRATION_TEST_ASAN_OLD_ANALYZER: CommonJobConfigs.INTEGRATION_TEST.with_properties( required_builds=[BuildNames.PACKAGE_ASAN], num_batches=6, + timeout=12000, # the job timed out with default value (7200) ), JobNames.INTEGRATION_TEST_TSAN: CommonJobConfigs.INTEGRATION_TEST.with_properties( required_builds=[BuildNames.PACKAGE_TSAN], num_batches=6, - timeout=9000, # the job timed out with default value (7200) + timeout=12000, # the job timed out with default value (7200) ), JobNames.INTEGRATION_TEST_ARM: CommonJobConfigs.INTEGRATION_TEST.with_properties( required_builds=[BuildNames.PACKAGE_AARCH64], num_batches=6, runner_type=Runners.FUNC_TESTER_ARM, + timeout=12000, # the job timed out with default value (7200) ), JobNames.INTEGRATION_TEST: CommonJobConfigs.INTEGRATION_TEST.with_properties( required_builds=[BuildNames.PACKAGE_RELEASE], num_batches=4, #release_only=True, + timeout=12000, # the job timed out with default value (7200) ), JobNames.INTEGRATION_TEST_FLAKY: CommonJobConfigs.INTEGRATION_TEST.with_properties( required_builds=[BuildNames.PACKAGE_ASAN],