Files
greptimedb/.github/workflows/query-regression.yml
T
localhost 35ea88a4ef feat(ci): run query regression on ephemeral Aliyun ECS runners (#8937)
* feat(ci): add aliyun ecs ephemeral runner path for query regression

Signed-off-by: paomian <xpaomian@gmail.com>

* fix: improve condition for query-regression job execution in workflow

* feat: update Docker installation to use official repository and add GPG key handling

* Refactor query regression runner setup and configuration

- Removed deprecated PersistentVolumeClaim for build cache.
- Introduced a new bootstrap script for setting up the ECS runner host.
- Deleted obsolete Helm values files for runner configuration.
- Updated the Aliyun ECS runner provisioning script to reflect new cache paths.
- Modified GitHub workflows to use the new Aliyun ECS runner setup.
- Adjusted documentation to clarify the new runner lifecycle and provisioning process.

* fix: enhance runner service management during bootstrap process

* fix: update alibabacloud_tea_openapi dependency version in metadata

* feat: enhance ECS runner scripts with region_id and resource_group_id support

* fix: move containerd content store to data root for improved storage management

* feat: rename query-regression runner to ephemeral-github runner and update related scripts

* fix: update sentinel polling method to use serial console output for improved reliability

* fix: add environment variable checks for Alibaba Cloud access keys in ECS client

* fix: improve error handling in GitHub API requests for better diagnostics

* fix: improve cache disk detection logic for Aliyun ECS instances

* fix: enhance cache disk waiting logic with detailed output and error handling

* fix: update dependency version for alibabacloud_tea_openapi in teardown script

* fix: enhance cache disk waiting logic for better compatibility and clarity

* fix: enhance console output handling and add incremental logging during instance provisioning

* fix: add PATH environment variable for runner jobs in service and provision script

* fix: add machine telemetry sampling and logging during query regression jobs

* fix: update query regression documentation and provision script for cache disk handling

* fix: update SCCACHE_CACHE_SIZE validation to 10G for improved caching efficiency

* fix: remove outdated cache size checks and cleanup logic for fresh system disk runs

* fix: enhance instance deletion logic with region handling and console output export

* fix: add swap file setup and OOM handling for ECS runner to improve stability

* fix: update OOM handling and service restart logic for ECS runner to enhance stability

* fix: increase system disk size to 100 GiB for cold double nightly builds to prevent ENOSPC errors

* fix: increase system disk size to 150 GiB for ECS runner to prevent ENOSPC errors

* fix: add keep_instance option to preserve ECS instance for post-mortem debugging

* fix: disable unattended upgrades to prevent job cancellations during library updates

* fix: reduce system disk size to 40 GiB for ECS runner to prevent ENOSPC errors

* feat: Refactor Aliyun ECS runner provisioning and introduce nightly regression comparison

- Update `aliyun-ecs-runner-provision.py` to remove cache disk handling, simplifying the provisioning process.
- Introduce `query-regression-nightly-refs.py` to resolve and compare SHAs from successful nightly builds.
- Create `query-regression-nightly.yml` workflow to trigger nightly comparisons based on successful builds.
- Enhance `query-regression.yml` to include a `test-tooling` job for validating Python scripts before provisioning.
- Update tests for the new nightly reference selection logic and refactor existing tests to align with the new caching strategy.
- Modify documentation to reflect changes in caching and nightly comparison workflows.

* fix: enhance runner image tool verification with detailed checks

* fix: improve error handling in runner image tool verification

* fix: update tool versions in ECS image and workflow for consistency

* fix: correct typo in error message for unparseable ECS creation time

* fix: update README and workflow files for query regression tests and image hygiene

---------

Signed-off-by: paomian <xpaomian@gmail.com>
2026-08-26 12:11:14 +00:00

882 lines
40 KiB
YAML

name: Query Regression
on:
workflow_call:
inputs:
case:
description: Query perf case path(s) in candidate checkout, all, or heavy
required: false
type: string
default: all
allow_large_fixture:
description: Pass --allow-large-fixture to runner
required: false
type: boolean
default: true
http_timeout:
description: Runner HTTP timeout seconds
required: false
type: string
default: "300"
base_ref:
description: Base ref/sha to build
required: true
type: string
candidate_ref:
description: Candidate ref/sha to build; reusable release calls must provide an immutable full SHA
required: true
type: string
cargo_profile:
description: Cargo profile to build base and candidate binaries with
required: false
type: string
default: nightly
runner:
description: Self-hosted runner label; aliyun-ecs provisions a fresh ECS instance per run
required: false
type: string
default: aliyun-ecs
workflow_dispatch:
inputs:
case:
description: Query perf case path(s) in candidate checkout, all, or heavy
required: true
default: all
base_ref:
description: Base ref/sha to build
required: true
default: main
candidate_ref:
description: Candidate ref/sha to build (empty = current ref)
required: false
default: ""
allow_large_fixture:
description: Pass --allow-large-fixture to runner
type: boolean
default: true
http_timeout:
description: Runner HTTP timeout seconds
required: true
default: "300"
cargo_profile:
description: Cargo profile to build base and candidate binaries with
required: true
type: choice
default: nightly
options:
- nightly
- release
- dev
runner:
description: >-
Self-hosted runner label; aliyun-ecs provisions a fresh ECS instance
per run, any other value is used as a literal runner label
required: true
type: string
default: aliyun-ecs
keep_instance:
description: >-
Debug: keep the ECS instance after the run (skip teardown) so its
runner _diag logs, journal, and telemetry can be inspected; the
janitor still sweeps it after the TTL
required: false
type: boolean
default: false
pull_request:
types: [labeled]
permissions:
contents: read
jobs:
test-tooling:
# Stdlib unittests for the workflow Python (case selection, report
# helpers, nightly SHA picking, and rendered ECS user-data). They do
# not talk to Aliyun or the Actions runner process; running them on
# ubuntu-latest fails fast before any ECS spend. Not gated on the
# regression labels: those labels boot a VM, these tests should not.
# Ordinary PRs also run the same tests from checks.yml, because this
# workflow only starts on `labeled` (or dispatch / workflow_call).
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Test query regression tooling
run: |
python3 tests/perf/test_query_regression_runner_compaction_toctou.py
python3 tests/perf/test_query_regression_runner_otlp_trace_load.py
python3 tests/perf/test_query_regression_summary_otlp.py
python3 tests/perf/test_query_regression_case_selection.py
python3 tests/perf/test_query_regression_nightly_refs.py
python3 tests/perf/test_aliyun_ecs_runner_scripts.py
provision:
# Runs when the aliyun-ecs path is selected: explicitly via the runner
# input, or for PR labels by default (a QUERY_REGRESSION_PR_RUNNER
# repository variable set to another value redirects PRs to that literal
# runner label instead). Uses trusted scripts from the PR base (or the
# dispatched ref), never from candidate code. Waits for test-tooling so
# a broken user-data template does not still create a VM.
needs: [test-tooling]
if: >-
${{ !failure() && !cancelled() &&
((github.event_name != 'pull_request' && inputs.runner == 'aliyun-ecs') ||
(github.event_name == 'pull_request' &&
!github.event.pull_request.draft &&
(github.event.label.name == 'query-regression' ||
github.event.label.name == 'heavy-regression') &&
(vars.QUERY_REGRESSION_PR_RUNNER || 'aliyun-ecs') == 'aliyun-ecs')) }}
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
label: ${{ steps.provision.outputs.label }}
instance_id: ${{ steps.provision.outputs.instance_id }}
runner_name: ${{ steps.provision.outputs.runner_name }}
steps:
- name: Checkout trusted provisioning scripts
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Provision ECS runner
id: provision
env:
ALIBABA_CLOUD_ACCESS_KEY_ID: ${{ secrets.ALICLOUD_ECS_ACCESS_KEY_ID }}
ALIBABA_CLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUD_ECS_ACCESS_KEY_SECRET }}
GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
ALIYUN_ECS_REGION_ID: ${{ vars.ALIYUN_ECS_REGION_ID }}
ALIYUN_ECS_VSWITCH_ID: ${{ vars.ALIYUN_ECS_VSWITCH_ID }}
ALIYUN_ECS_SECURITY_GROUP_ID: ${{ vars.ALIYUN_ECS_SECURITY_GROUP_ID }}
ALIYUN_ECS_INSTANCE_TYPE: ${{ vars.ALIYUN_ECS_INSTANCE_TYPE }}
ALIYUN_ECS_RESOURCE_GROUP_ID: ${{ vars.ALIYUN_ECS_RESOURCE_GROUP_ID }}
QUERY_REGRESSION_ECS_IMAGE_ID: ${{ vars.QUERY_REGRESSION_ECS_IMAGE_ID }}
QUERY_REGRESSION_RUNNER_UID: ${{ vars.QUERY_REGRESSION_RUNNER_UID || '1001' }}
QUERY_REGRESSION_RUNNER_GID: ${{ vars.QUERY_REGRESSION_RUNNER_GID || '1001' }}
run: >-
uv run .github/scripts/aliyun-ecs-runner-provision.py
query-regression:
needs: [provision, test-tooling]
# `!failure() && !cancelled()` both suppresses the implicit success() and
# lets the job run when provision was intentionally skipped because a
# literal runner label was selected; a failed or cancelled provision still
# blocks the run because no ECS runner would be waiting.
if: >-
${{ !failure() && !cancelled() &&
(github.event_name != 'pull_request' ||
(github.event_name == 'pull_request' &&
!github.event.pull_request.draft &&
(github.event.label.name == 'query-regression' ||
github.event.label.name == 'heavy-regression'))) }}
runs-on: >-
${{ needs.provision.outputs.label ||
(github.event_name != 'pull_request' && inputs.runner ||
(vars.QUERY_REGRESSION_PR_RUNNER || 'aliyun-ecs')) }}
timeout-minutes: 180
env:
CARGO_PROFILE: ${{ github.event_name == 'pull_request' && 'nightly' || inputs.cargo_profile }}
CARGO_HOME: /home/runner/.cargo
UV_CACHE_DIR: /home/runner/.cargo/uv-cache
RUSTUP_HOME: /opt/rustup
RUSTUP_TOOLCHAIN: nightly-2026-03-21
RUSTUP_AUTO_INSTALL: "0"
CARGO_TARGET_DIR: /home/runner/query-regression-target
QUERY_REGRESSION_CACHE_META: /home/runner/query-regression-cache-meta
RUSTC_WRAPPER: /usr/local/bin/sccache
SCCACHE_DIR: /home/runner/.cache/sccache
# Caps the local sccache on the system disk next to target and cargo.
SCCACHE_CACHE_SIZE: 10G
CARGO_INCREMENTAL: "0"
RUSTFLAGS: -D warnings -C link-arg=-fuse-ld=mold
QUERY_REGRESSION_CACHE_EPOCH: "1"
BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || inputs.base_ref }}
CANDIDATE_REF: ${{ github.event_name != 'pull_request' && (inputs.candidate_ref || github.ref) || '' }}
EVENT_MERGE_SHA: ${{ github.event_name == 'pull_request' && github.sha || '' }}
EVENT_HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }}
EVENT_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }}
# Runner identity contract. The ECS image uses 1001; a manually prepared
# host may override via repo variables when 1001 is already taken.
EXPECTED_RUNNER_UID: ${{ vars.QUERY_REGRESSION_RUNNER_UID || '1001' }}
EXPECTED_RUNNER_GID: ${{ vars.QUERY_REGRESSION_RUNNER_GID || '1001' }}
steps:
- name: Report provisioned ECS runner
if: ${{ needs.provision.outputs.instance_id != '' }}
shell: bash
run: |
set -euo pipefail
{
printf -- '- ECS instance: `%s` (type `%s`, image `%s`, region `%s`)\n' \
"${{ needs.provision.outputs.instance_id }}" \
"${{ vars.ALIYUN_ECS_INSTANCE_TYPE }}" \
"${{ vars.QUERY_REGRESSION_ECS_IMAGE_ID }}" \
"${{ vars.ALIYUN_ECS_REGION_ID }}"
} >> "${GITHUB_STEP_SUMMARY}"
- name: Start machine telemetry sampler
shell: bash
# The ECS instance is deleted when the run ends, so machine state must
# be captured while the job runs. A background sampler appends load,
# memory, disk, and top-memory processes to a log every 30s; the
# "Dump machine telemetry" step (if: always()) prints and uploads it.
# Note: when the job is *cancelled*, even always() steps are killed —
# telemetry covers failures, not cancellations.
run: |
set -euo pipefail
log="${GITHUB_WORKSPACE}/machine-telemetry.log"
{
echo "== baseline $(date -u +%Y-%m-%dT%H:%M:%SZ) =="
nproc
free -h
df -h / /home/runner
} >> "${log}"
nohup bash -c "
while true; do
{
date -u '+== %Y-%m-%dT%H:%M:%SZ =='
uptime
free -m
df -h / /home/runner
ps -eo pid,comm,%mem,%cpu --sort=-%mem | head -6
} >> '${log}' 2>&1
sleep 30
done
" >/dev/null 2>&1 &
echo "Telemetry sampler started (pid $!), logging to ${log}"
- name: Checkout base source
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
ref: ${{ env.BASE_REF }}
path: src
persist-credentials: false
- name: Resolve immutable candidate and build base
working-directory: src
shell: bash
env:
CALLER_WORKFLOW_REF: ${{ github.workflow_ref }}
DEFINING_WORKFLOW_REF: ${{ job.workflow_ref }}
run: |
set -euo pipefail
fail_closed() {
printf '%s\n' \
"PR changed or merge result unavailable; maintainer must remove and re-add the regression label after reviewing current revision. $1" >&2
exit 1
}
is_full_sha() {
[[ "$1" =~ ^[[:xdigit:]]{40}$ ]]
}
is_reusable_call=false
if [[ "${CALLER_WORKFLOW_REF}" != "${DEFINING_WORKFLOW_REF}" ]]; then
is_reusable_call=true
fi
fetch_with_retry() {
local ref="$1"
local depth="$2"
local attempt=1
local max_attempts=5
local delay
while (( attempt <= max_attempts )); do
if git fetch --no-tags --prune --depth="${depth}" origin "${ref}"; then
return 0
fi
printf 'Git fetch failed for %s (attempt %d/%d).\n' \
"${ref}" "${attempt}" "${max_attempts}" >&2
if (( attempt < max_attempts )); then
delay=$(( 2 ** (attempt - 1) ))
printf 'Retrying git fetch for %s in %d seconds.\n' "${ref}" "${delay}" >&2
sleep "${delay}"
fi
((attempt += 1))
done
return 1
}
initially_checked_out_base_sha="$(git rev-parse --verify HEAD)"
is_full_sha "${initially_checked_out_base_sha}" || {
printf 'Could not resolve initially checked-out BASE_REF commit.\n' >&2
exit 1
}
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
for identity in EVENT_MERGE_SHA EVENT_HEAD_SHA EVENT_BASE_SHA; do
value="${!identity-}"
is_full_sha "${value}" || fail_closed "Missing or invalid ${identity}."
done
if ! fetch_with_retry "${EVENT_MERGE_SHA}" 2; then
fail_closed "Could not fetch event merge SHA ${EVENT_MERGE_SHA}."
fi
fetched_sha="$(git rev-parse --verify FETCH_HEAD 2>/dev/null)" \
|| fail_closed "Fetched event merge SHA could not be resolved."
[[ "${fetched_sha,,}" == "${EVENT_MERGE_SHA,,}" ]] \
|| fail_closed "FETCH_HEAD ${fetched_sha} does not match event merge SHA ${EVENT_MERGE_SHA}."
read -r -a parents <<< "$(git show -s --format=%P FETCH_HEAD)"
(( ${#parents[@]} == 2 )) \
|| fail_closed "Event merge SHA ${EVENT_MERGE_SHA} is not a two-parent merge commit."
head_parent_count=0
for parent in "${parents[@]}"; do
if [[ "${parent,,}" == "${EVENT_HEAD_SHA,,}" ]]; then
((head_parent_count += 1))
else
VERIFIED_BASE_SHA="${parent,,}"
fi
done
(( head_parent_count == 1 )) \
|| fail_closed "Event merge SHA must have exactly one EVENT_HEAD_SHA parent."
is_full_sha "${VERIFIED_BASE_SHA-}" \
|| fail_closed "Event merge SHA has no non-head parent for the base build."
VERIFIED_CANDIDATE_SHA="${fetched_sha,,}"
else
if [[ "${is_reusable_call}" == true ]] && ! is_full_sha "${CANDIDATE_REF}"; then
printf 'Reusable calls require CANDIDATE_REF to be an immutable full SHA, got: %s\n' \
"${CANDIDATE_REF}" >&2
exit 1
fi
if ! fetch_with_retry "${CANDIDATE_REF}" 1; then
printf 'Could not fetch candidate ref %s.\n' "${CANDIDATE_REF}" >&2
exit 1
fi
fetched_sha="$(git rev-parse --verify FETCH_HEAD)"
if { [[ "${is_reusable_call}" == true ]] || is_full_sha "${CANDIDATE_REF}"; } && \
[[ "${fetched_sha,,}" != "${CANDIDATE_REF,,}" ]]; then
printf 'FETCH_HEAD %s does not match immutable candidate SHA %s.\n' \
"${fetched_sha}" "${CANDIDATE_REF}" >&2
exit 1
fi
VERIFIED_CANDIDATE_SHA="${fetched_sha}"
VERIFIED_BASE_SHA="${initially_checked_out_base_sha}"
fi
is_full_sha "${VERIFIED_CANDIDATE_SHA}" || {
printf 'Could not resolve candidate SHA.\n' >&2
exit 1
}
is_full_sha "${VERIFIED_BASE_SHA}" || {
printf 'Could not resolve build base SHA.\n' >&2
exit 1
}
printf 'VERIFIED_CANDIDATE_SHA=%s\n' "${VERIFIED_CANDIDATE_SHA,,}" >> "${GITHUB_ENV}"
printf 'VERIFIED_BASE_SHA=%s\n' "${VERIFIED_BASE_SHA,,}" >> "${GITHUB_ENV}"
printf 'Verified candidate SHA: %s\n' "${VERIFIED_CANDIDATE_SHA,,}"
printf 'Verified build base SHA: %s\n' "${VERIFIED_BASE_SHA,,}"
printf -- '- Verified candidate SHA: `%s`; built base SHA: `%s`\n' \
"${VERIFIED_CANDIDATE_SHA,,}" "${VERIFIED_BASE_SHA,,}" >> "${GITHUB_STEP_SUMMARY}"
git reset --hard "${VERIFIED_BASE_SHA}"
git clean -ffdx
- name: Preserve trusted query regression scripts
working-directory: src
shell: bash
run: |
set -euo pipefail
helper_ref="${VERIFIED_CANDIDATE_SHA}"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
helper_ref="${VERIFIED_BASE_SHA}"
fi
mkdir -p "${GITHUB_WORKSPACE}/query-regression-trusted-scripts"
git show "${helper_ref}:.github/scripts/query-regression-pr-metadata.py" \
> "${GITHUB_WORKSPACE}/query-regression-trusted-scripts/query-regression-pr-metadata.py"
git show "${helper_ref}:.github/scripts/query-regression-summary.py" \
> "${GITHUB_WORKSPACE}/query-regression-trusted-scripts/query-regression-summary.py"
- name: Verify runner image tools
shell: bash
run: |
set -euo pipefail
errors=()
record() {
printf 'FAIL %s\n' "$*"
errors+=("$*")
}
require_eq() {
local name="$1" actual="$2" expected="$3"
printf 'check %s: %s\n' "${name}" "${actual}"
[[ "${actual}" == "${expected}" ]] || record "${name}: expected '${expected}', got '${actual}'"
}
require_match() {
local name="$1" actual="$2" pattern="$3"
printf 'check %s: %s\n' "${name}" "${actual}"
[[ "${actual}" =~ $pattern ]] || record "${name}: expected to match ${pattern}, got '${actual}'"
}
require() {
local name="$1"
shift
printf 'check %s\n' "${name}"
"$@" || record "${name}"
}
capture() {
local out
if out="$("$@" 2>&1)"; then
printf '%s' "${out}"
else
printf '<failed to run: %s>' "$*"
fi
}
printf 'uid=%s gid=%s PATH=%s CARGO_HOME=%s\n' "$(id -u)" "$(id -g)" "${PATH}" "${CARGO_HOME}"
ls -la "${CARGO_HOME}" 2>&1 || printf '(CARGO_HOME missing)\n'
# Image/host hygiene: CARGO_HOME must be empty of config and
# install-state before cargo/rustup run. Those tools create
# .package-cache / bin on first use, so this cannot come after.
cargo_home_dirty=()
for entry in config config.toml credentials credentials.toml bin .crates.toml .crates2.json .global-cache .package-cache; do
if [[ -e "${CARGO_HOME}/${entry}" ]]; then
cargo_home_dirty+=("${entry}")
record "CARGO_HOME must not contain ${entry}"
fi
done
if (( ${#cargo_home_dirty[@]} > 0 )); then
printf 'CARGO_HOME contents:\n' >&2
ls -la "${CARGO_HOME}" >&2 || true
fi
require_eq uid "$(id -u)" "${EXPECTED_RUNNER_UID}"
require_eq gid "$(id -g)" "${EXPECTED_RUNNER_GID}"
require_eq UV_CACHE_DIR "${UV_CACHE_DIR}" "/home/runner/.cargo/uv-cache"
require_eq protoc "$(capture protoc --version)" "libprotoc 3.21.12"
temporary_proto_dir="$(mktemp --directory)"
trap 'rm -rf "${temporary_proto_dir}"' EXIT
require "readable /usr/include/google/protobuf/any.proto" \
test -r /usr/include/google/protobuf/any.proto
require "readable /usr/include/google/protobuf/empty.proto" \
test -r /usr/include/google/protobuf/empty.proto
printf '%s\n' \
'syntax = "proto3";' \
'package smoke;' \
'import "google/protobuf/any.proto";' \
'import "google/protobuf/empty.proto";' \
'message Smoke { google.protobuf.Any any = 1; google.protobuf.Empty empty = 2; }' \
> "${temporary_proto_dir}/smoke.proto"
if protoc --proto_path="${temporary_proto_dir}" --proto_path=/usr/include \
--descriptor_set_out="${temporary_proto_dir}/smoke.pb" \
"${temporary_proto_dir}/smoke.proto"; then
require "protoc smoke descriptor is non-empty" test -s "${temporary_proto_dir}/smoke.pb"
else
record "protoc smoke compile failed"
fi
require_match uv "$(capture uv --version)" '^uv[[:space:]]0\.11\.26([[:space:]]|$)'
require_match mold "$(capture mold --version)" '^mold[[:space:]]2\.40\.4([[:space:]]|$)'
require_eq python3 "$(capture python3 --version)" "Python 3.14.4"
otelgen_path="$(command -v otelgen || true)"
otelgen_version="<unavailable>"
if [[ -n "${otelgen_path}" ]]; then
otelgen_version="$(capture "${otelgen_path}" --version)"
fi
require_eq otelgen_path "${otelgen_path}" "/usr/local/bin/otelgen"
printf 'check otelgen_version: %s\n' "${otelgen_version}"
[[ "${otelgen_version}" == *"863a3f395d062c7322cc1de08a38774b7fdaa6c8"* ]] \
|| record "otelgen_version: expected commit 863a3f395d062c7322cc1de08a38774b7fdaa6c8, got '${otelgen_version}'"
require_match sccache "$(capture sccache --version)" '^sccache[[:space:]]0\.16\.0([[:space:]]|$)'
require_eq rustup_path "$(command -v rustup || true)" "/opt/cargo/bin/rustup"
require_eq cargo_path "$(command -v cargo || true)" "/opt/cargo/bin/cargo"
require_eq rustc_path "$(command -v rustc || true)" "/opt/cargo/bin/rustc"
require_match rustup "$(capture rustup --version)" '^rustup[[:space:]]1\.29\.0([[:space:]]|$)'
require_match cargo "$(capture cargo --version)" \
'^cargo[[:space:]]1\.96\.0-nightly[[:space:]]\(cbb9bb8bd[[:space:]][0-9]{4}-[0-9]{2}-[0-9]{2}\)$'
require_match rustc "$(capture rustc --version)" \
'^rustc[[:space:]]1\.96\.0-nightly[[:space:]]\(ac7f9ec7d[[:space:]][0-9]{4}-[0-9]{2}-[0-9]{2}\)$'
require_match active_toolchain "$(capture rustup show active-toolchain)" \
'^nightly-2026-03-21-x86_64-unknown-linux-gnu([[:space:]]|$)'
require_eq RUSTUP_HOME "${RUSTUP_HOME}" "/opt/rustup"
require_eq RUSTUP_TOOLCHAIN "${RUSTUP_TOOLCHAIN}" "nightly-2026-03-21"
require_eq RUSTUP_AUTO_INSTALL "${RUSTUP_AUTO_INSTALL}" "0"
require "readable /opt/rustup" test -r /opt/rustup
require "executable /opt/rustup" test -x /opt/rustup
require "runner cannot write /opt/rustup" test ! -w /opt/rustup
require "runner cannot write /opt/cargo/bin" test ! -w /opt/cargo/bin
mkdir -p "${CARGO_HOME}/registry" "${CARGO_HOME}/git"
if (( ${#errors[@]} > 0 )); then
printf '\nVerify runner image tools failed (%d checks):\n' "${#errors[@]}" >&2
printf ' - %s\n' "${errors[@]}" >&2
exit 1
fi
- name: Prepare query regression cache
shell: bash
working-directory: src
run: |
set -euo pipefail
readonly EXPECTED_CARGO_HOME="/home/runner/.cargo"
readonly EXPECTED_CARGO_REGISTRY="/home/runner/.cargo/registry"
readonly EXPECTED_CARGO_GIT="/home/runner/.cargo/git"
readonly EXPECTED_UV_CACHE_DIR="/home/runner/.cargo/uv-cache"
readonly EXPECTED_RUSTUP_HOME="/opt/rustup"
readonly EXPECTED_TARGET_DIR="/home/runner/query-regression-target"
readonly EXPECTED_CACHE_META="/home/runner/query-regression-cache-meta"
readonly EXPECTED_SCCACHE_DIR="/home/runner/.cache/sccache"
readonly EXPECTED_RUSTC_WRAPPER="/usr/local/bin/sccache"
readonly RUNNER_IMAGE_DIGEST="sha256:e713b294e23b7e15184e558866c90025e59930033e72c97650dbc7f1ca022d11"
readonly RUNNER_IMAGE_EPOCH="5"
require_expected_root() {
local name="$1"
local actual="$2"
local expected="$3"
[[ "${actual}" == /* && "${actual}" == "${expected}" ]] || {
printf 'Refusing unexpected %s root: %s\n' "${name}" "${actual}" >&2
exit 1
}
}
clear_directory() {
local root="$1"
[[ "${root}" == /* && "${root}" != "/" ]] || {
printf 'Refusing to clear non-absolute or root path: %s\n' "${root}" >&2
exit 1
}
case "${root}" in
"${EXPECTED_TARGET_DIR}"|"${EXPECTED_TARGET_DIR}/debug"|"${EXPECTED_TARGET_DIR}/release"|"${EXPECTED_TARGET_DIR}/nightly") ;;
*)
printf 'Refusing to clear unexpected cache path: %s\n' "${root}" >&2
exit 1
;;
esac
find "${root}" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
}
report_cache_usage() {
du -sh -- "${CARGO_HOME}" "${CARGO_HOME}/registry" "${CARGO_HOME}/git" \
"${RUSTUP_HOME}" "${CARGO_TARGET_DIR}" "${QUERY_REGRESSION_CACHE_META}" "${SCCACHE_DIR}"
df -P "${CARGO_TARGET_DIR}"
df -hP "${CARGO_TARGET_DIR}"
df -Pi "${CARGO_TARGET_DIR}"
}
require_expected_root CARGO_HOME "${CARGO_HOME}" "${EXPECTED_CARGO_HOME}"
require_expected_root CARGO_REGISTRY "${CARGO_HOME}/registry" "${EXPECTED_CARGO_REGISTRY}"
require_expected_root CARGO_GIT "${CARGO_HOME}/git" "${EXPECTED_CARGO_GIT}"
require_expected_root UV_CACHE_DIR "${UV_CACHE_DIR}" "${EXPECTED_UV_CACHE_DIR}"
require_expected_root RUSTUP_HOME "${RUSTUP_HOME}" "${EXPECTED_RUSTUP_HOME}"
require_expected_root CARGO_TARGET_DIR "${CARGO_TARGET_DIR}" "${EXPECTED_TARGET_DIR}"
require_expected_root QUERY_REGRESSION_CACHE_META "${QUERY_REGRESSION_CACHE_META}" "${EXPECTED_CACHE_META}"
require_expected_root SCCACHE_DIR "${SCCACHE_DIR}" "${EXPECTED_SCCACHE_DIR}"
[[ "${RUSTC_WRAPPER}" == "${EXPECTED_RUSTC_WRAPPER}" ]] || {
printf 'Refusing unexpected RUSTC_WRAPPER: %s\n' "${RUSTC_WRAPPER}" >&2
exit 1
}
[[ "${SCCACHE_CACHE_SIZE}" == "10G" ]] || {
printf 'Refusing unexpected SCCACHE_CACHE_SIZE: %s\n' "${SCCACHE_CACHE_SIZE}" >&2
exit 1
}
[[ "${CARGO_INCREMENTAL}" == "0" ]] || {
printf 'Refusing unexpected CARGO_INCREMENTAL: %s\n' "${CARGO_INCREMENTAL}" >&2
exit 1
}
test -r "${RUSTUP_HOME}"
test -x "${RUSTUP_HOME}"
test ! -w "${RUSTUP_HOME}"
mkdir -p "${CARGO_HOME}" "${CARGO_HOME}/registry" "${CARGO_HOME}/git" "${UV_CACHE_DIR}" \
"${CARGO_TARGET_DIR}" "${QUERY_REGRESSION_CACHE_META}" "${SCCACHE_DIR}"
for root in "${CARGO_HOME}" "${CARGO_HOME}/registry" "${CARGO_HOME}/git" \
"${UV_CACHE_DIR}" "${CARGO_TARGET_DIR}" "${QUERY_REGRESSION_CACHE_META}" "${SCCACHE_DIR}"; do
test -w "${root}"
touch "${root}/.query-regression-write-test"
rm -f "${root}/.query-regression-write-test"
done
case "${CARGO_PROFILE}" in
dev) target_profile="debug" ;;
release|nightly) target_profile="${CARGO_PROFILE}" ;;
*) printf 'Unsupported CARGO_PROFILE: %s\n' "${CARGO_PROFILE}" >&2; exit 1 ;;
esac
rustc_vv="$(rustc -vV)"
if cargo -Vv >/dev/null 2>&1; then
cargo_vv="$(cargo -Vv)"
else
cargo_vv="$(cargo -V)"
fi
host_triple="$(printf '%s\n' "${rustc_vv}" | grep '^host: ' | cut -d' ' -f2-)"
cc_version="$(cc --version)"
mold_version="$(mold --version | grep -m1 .)"
protoc_version="$(protoc --version)"
sccache_version="$(sccache --version)"
rustup_version="$(rustup --version)"
active_toolchain="$(rustup show active-toolchain)"
rustup_path="$(command -v rustup)"
cargo_path="$(command -v cargo)"
rustc_path="$(command -v rustc)"
profile_target_dir="${CARGO_TARGET_DIR}/${target_profile}"
marker="${QUERY_REGRESSION_CACHE_META}/target-${target_profile}.abi"
new_marker="$(mktemp "${QUERY_REGRESSION_CACHE_META}/.${target_profile}.abi.XXXXXX")"
{
printf 'cache_epoch=%s\n' "${QUERY_REGRESSION_CACHE_EPOCH}"
printf 'runner_image_digest=%s\n' "${RUNNER_IMAGE_DIGEST}"
printf 'runner_image_epoch=%s\n' "${RUNNER_IMAGE_EPOCH}"
printf 'host_triple=%s\n' "${host_triple}"
printf 'architecture=%s\n' "$(uname -m)"
printf 'cargo_profile=%s\n' "${CARGO_PROFILE}"
printf 'target_profile=%s\n' "${target_profile}"
printf 'RUSTFLAGS=%s\n' "${RUSTFLAGS-}"
printf 'CARGO_ENCODED_RUSTFLAGS=%s\n' "${CARGO_ENCODED_RUSTFLAGS-}"
printf 'CARGO_INCREMENTAL=%s\n' "${CARGO_INCREMENTAL-}"
printf 'RUSTC_WRAPPER=%s\n' "${RUSTC_WRAPPER-}"
printf 'SCCACHE_DIR=%s\n' "${SCCACHE_DIR-}"
printf 'SCCACHE_CACHE_SIZE=%s\n' "${SCCACHE_CACHE_SIZE-}"
printf 'RUSTUP_HOME=%s\n' "${RUSTUP_HOME-}"
printf 'RUSTUP_TOOLCHAIN=%s\n' "${RUSTUP_TOOLCHAIN-}"
printf 'RUSTUP_AUTO_INSTALL=%s\n' "${RUSTUP_AUTO_INSTALL-}"
printf 'PATH=%s\n' "${PATH-}"
printf '%s\n' "rustc_vV=${rustc_vv}"
printf '%s\n' "cargo_vV=${cargo_vv}"
printf '%s\n' "cc_version=${cc_version}"
printf '%s\n' "mold_version=${mold_version}"
printf '%s\n' "protoc_version=${protoc_version}"
printf '%s\n' "sccache_version=${sccache_version}"
printf '%s\n' "rustup_version=${rustup_version}"
printf '%s\n' "active_toolchain=${active_toolchain}"
printf '%s\n' "rustup_path=${rustup_path}"
printf '%s\n' "cargo_path=${cargo_path}"
printf '%s\n' "rustc_path=${rustc_path}"
} > "${new_marker}"
if ! cmp -s "${marker}" "${new_marker}"; then
printf 'Cache ABI marker mismatch; invalidating target profile: %s\n' "${target_profile}"
mkdir -p "${profile_target_dir}"
clear_directory "${profile_target_dir}"
else
printf 'Cache ABI marker match; reusing target profile: %s\n' "${target_profile}"
fi
mv -f "${new_marker}" "${marker}"
report_cache_usage
# Every run starts from a fresh system disk, so size/free-space
# watermarks from the retired retained-disk era are not restored.
# A run that overflows the disk fails the build outright, which the
# telemetry step makes diagnosable.
sccache --start-server
sccache --zero-stats
sccache --show-stats
- name: Build base greptime
working-directory: src
run: |
set -euo pipefail
SECONDS=0
cargo build --profile "${CARGO_PROFILE}" -p cmd --bin greptime
base_build_elapsed="${SECONDS}"
printf 'Base greptime cargo build elapsed: %s seconds\n' "${base_build_elapsed}"
printf -- '- Base greptime cargo build: %s seconds\n' "${base_build_elapsed}" >> "${GITHUB_STEP_SUMMARY}"
sccache --show-stats
sccache --zero-stats
target_dir="${CARGO_PROFILE}"
if [[ "${CARGO_PROFILE}" == "dev" ]]; then
target_dir="debug"
fi
mkdir -p "${GITHUB_WORKSPACE}/query-regression-bins/base"
cp "${CARGO_TARGET_DIR}/${target_dir}/greptime" \
"${GITHUB_WORKSPACE}/query-regression-bins/base/greptime"
- name: Switch source to candidate
working-directory: src
shell: bash
run: |
set -euo pipefail
[[ "${VERIFIED_CANDIDATE_SHA}" =~ ^[[:xdigit:]]{40}$ ]] || {
printf 'Verified candidate SHA is missing or invalid.\n' >&2
exit 1
}
git cat-file -e "${VERIFIED_CANDIDATE_SHA}^{commit}" || {
printf 'Verified candidate SHA is unavailable in the local repository.\n' >&2
exit 1
}
git reset --hard "${VERIFIED_CANDIDATE_SHA}"
git clean -ffdx
- name: Build candidate greptime and query regression helpers
working-directory: src
run: |
set -euo pipefail
SECONDS=0
cargo build --profile "${CARGO_PROFILE}" -p cmd --bin greptime
cargo build --profile "${CARGO_PROFILE}" -p cmd --bin query_perf_fixture --features dev-tools
cargo build --profile "${CARGO_PROFILE}" -p cmd --bin query_regression_runner --features dev-tools
candidate_build_elapsed="${SECONDS}"
printf 'Candidate greptime and query regression helper cargo builds elapsed: %s seconds\n' "${candidate_build_elapsed}"
printf -- '- Candidate greptime and query regression helper cargo builds: %s seconds\n' "${candidate_build_elapsed}" >> "${GITHUB_STEP_SUMMARY}"
sccache --show-stats
target_dir="${CARGO_PROFILE}"
if [[ "${CARGO_PROFILE}" == "dev" ]]; then
target_dir="debug"
fi
mkdir -p "${GITHUB_WORKSPACE}/query-regression-bins/candidate"
cp "${CARGO_TARGET_DIR}/${target_dir}/greptime" \
"${GITHUB_WORKSPACE}/query-regression-bins/candidate/greptime"
cp "${CARGO_TARGET_DIR}/${target_dir}/query_perf_fixture" \
"${GITHUB_WORKSPACE}/query-regression-bins/candidate/query_perf_fixture"
cp "${CARGO_TARGET_DIR}/${target_dir}/query_regression_runner" \
"${GITHUB_WORKSPACE}/query-regression-bins/candidate/query_regression_runner"
- name: Run query regression
id: run
env:
CASE_PATHS: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'heavy-regression' && 'heavy' || 'all') || inputs.case }}
HTTP_TIMEOUT: ${{ github.event_name == 'pull_request' && '300' || inputs.http_timeout }}
ALLOW_LARGE_FIXTURE: ${{ github.event_name == 'pull_request' && 'true' || inputs.allow_large_fixture }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
CASE_NAME: ${{ github.event_name == 'pull_request' && (github.event.label.name == 'heavy-regression' && 'heavy case set' || 'default case set') || inputs.case }}
BASE_BIN: ${{ github.workspace }}/query-regression-bins/base/greptime
CANDIDATE_BIN: ${{ github.workspace }}/query-regression-bins/candidate/greptime
FIXTURE_GENERATOR: ${{ github.workspace }}/query-regression-bins/candidate/query_perf_fixture
OTELGEN_BIN: /usr/local/bin/otelgen
SUMMARY_SCRIPT: ${{ github.event_name == 'pull_request' && 'query-regression-trusted-scripts/query-regression-summary.py' || 'src/.github/scripts/query-regression-summary.py' }}
QUERY_REGRESSION_RUNNER: ${{ github.workspace }}/query-regression-bins/candidate/query_regression_runner
run: >-
uv run --no-project python src/.github/scripts/query-regression-run.py
--base-src src
--candidate-src src
--summary-script "${SUMMARY_SCRIPT}"
- name: Write PR metadata for trusted comment workflow
if: ${{ always() && github.event_name == 'pull_request' }}
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
BASE_REPO: ${{ github.event.pull_request.base.repo.full_name }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: uv run --no-project python query-regression-trusted-scripts/query-regression-pr-metadata.py
- name: Upload query regression artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: query-regression-report
path: |
query-regression-work/**/query-regression-report.json
query-regression-work/**/base/report.json
query-regression-work/**/candidate/report.json
query-regression-work/**/logs/**
query-regression-work/**/otelgen/**
query-regression-summary.md
machine-telemetry.log
if-no-files-found: warn
retention-days: 7
- name: Upload trusted comment artifact
if: ${{ always() && github.event_name == 'pull_request' }}
uses: actions/upload-artifact@v4
with:
name: query-regression-comment
path: |
query-regression-pr.json
query-regression-work/**/query-regression-report.json
if-no-files-found: warn
retention-days: 7
- name: Report cache usage
if: ${{ always() }}
shell: bash
run: |
set -euo pipefail
for root in \
/home/runner/.cargo \
/home/runner/.cargo/registry \
/home/runner/.cargo/git \
/opt/rustup \
/home/runner/query-regression-target \
/home/runner/query-regression-cache-meta \
/home/runner/.cache/sccache; do
if [[ -e "${root}" ]]; then
du -sh -- "${root}"
else
printf 'Missing cache path (report only): %s\n' "${root}"
fi
done
if [[ -e /opt/rustup ]]; then
if [[ -w /opt/rustup ]]; then
printf 'Immutable Rustup path is unexpectedly writable: /opt/rustup\n' >&2
else
printf 'Immutable Rustup path is read-only to runner: /opt/rustup\n'
fi
fi
report_root=/home/runner/query-regression-target
if [[ ! -e "${report_root}" ]]; then
report_root=/home/runner
printf 'Target cache path is missing; reporting its filesystem through %s\n' "${report_root}"
fi
df -P "${report_root}"
df -hP "${report_root}"
df -Pi "${report_root}"
if command -v sccache >/dev/null 2>&1; then
sccache --show-stats || printf 'Unable to show sccache statistics (report only)\n' >&2
else
printf 'sccache is unavailable (report only)\n' >&2
fi
- name: Dump machine telemetry
if: ${{ always() }}
shell: bash
run: |
log="${GITHUB_WORKSPACE}/machine-telemetry.log"
if [[ -f "${log}" ]]; then
echo "::group::Machine telemetry (last 200 lines)"
tail -n 200 "${log}"
echo "::endgroup::"
else
echo "No telemetry log found (sampler never started?)"
fi
echo "::group::dmesg tail (OOM killer records)"
sudo dmesg -T 2>/dev/null | tail -n 50 || dmesg -T 2>/dev/null | tail -n 50 || \
echo "dmesg unavailable without root"
echo "::endgroup::"
- name: Fail on regression failure
if: ${{ steps.run.outputs.status != '0' }}
run: exit 1
teardown:
# Releases the dynamically provisioned ECS runner. Runs even when the
# benchmark job fails or is cancelled; skipped when a literal runner label
# was selected because the provision outputs are empty, or when the
# dispatch set keep_instance to preserve the machine for post-mortem
# debugging (the janitor sweep still reclaims it after the TTL).
if: ${{ always() && needs.provision.outputs.instance_id != '' && !inputs.keep_instance }}
needs: [provision, query-regression]
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout trusted teardown scripts
uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Teardown ECS runner
env:
ALIBABA_CLOUD_ACCESS_KEY_ID: ${{ secrets.ALICLOUD_ECS_ACCESS_KEY_ID }}
ALIBABA_CLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALICLOUD_ECS_ACCESS_KEY_SECRET }}
GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}
ALIYUN_ECS_REGION_ID: ${{ vars.ALIYUN_ECS_REGION_ID }}
QUERY_REGRESSION_ECS_INSTANCE_ID: ${{ needs.provision.outputs.instance_id }}
QUERY_REGRESSION_ECS_RUNNER_NAME: ${{ needs.provision.outputs.runner_name }}
run: >-
uv run .github/scripts/aliyun-ecs-runner-teardown.py