ci: add query-agent SQL regression workflow (#12749)

## Summary

Adds a standalone workflow that runs the 672-query SQL regression suite
on-demand against a live OpenObserve instance. Triggered by
`/query-agent` PR comment or manual `workflow_dispatch`.

## How it works

```
/query-agent PR comment (or manual dispatch)
  │
  ├── Job 1: Triage
  │     - Allowlist gate (same engineering team list as council)
  │     - Resolve PR number from comment or dispatch input
  │     - Fetch PR diff, grep for query-engine paths
  │     - Post decision comment (skip/run/dry-run)
  │
  └── Job 2: Run tests (if not skipped + not dry-run)
        - Build OO debug binary (nightly-2026-05-20, mimalloc)
        - Setup Python 3.11 + rye
        - Run compute_counts.py to regenerate expected values
        - Start OO server with ZO_MAX_FILE_RETENTION_TIME=1
        - Run pytest tests/query_agent/ (excluding test_3_vortex.py)
        - Post pass/fail comment on PR
```

## Details

- **Trigger**: Exact `/query-agent` comment on a PR (not `contains`), or
manual dispatch with `pr_number`
- **Runner**: `ubicloud-standard-4` (matches api-testing.yml)
- **Timeout**: triage 10 min, tests 60 min
- **Concurrency**: one run per PR, cancel-in-progress
- **Artifacts**: server logs + pytest logs uploaded on failure, JUnit
XML always

## Test plan
- [ ] Merge to main first (required for comment triggers to take effect)
- [ ] Comment `/query-agent` on a PR with query-engine changes →
workflow runs
- [ ] Comment `/query-agent` on a PR with docs-only changes → triage
skips
- [ ] Non-allowlist user comments `/query-agent` → no reaction
- [ ] Manual dispatch with `dry_run=true` → triage only, no tests

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Neha P
2026-06-18 11:15:45 +00:00
committed by GitHub
co-authored by Claude Opus 4.7
parent b9e79ed433
commit 10412e91ea
+354
View File
@@ -0,0 +1,354 @@
name: Query Agent (SQL Regression)
# On-demand SQL regression suite for the OpenObserve query engine.
# Runs 672+ sqllogictest-style queries against a live OO instance and reports
# pass/fail on the PR. Triggered by PR comment or manual dispatch.
#
# Two entry points, both PR-bound:
# - a PR comment, EXACTLY:
# /query-agent → FULL run: triage → build → test → report
# - Manual dispatch with pr_number (required; bare number, "#123", or full URL)
#
# ACCESS CONTROL:
# - TRIGGER: only the engineering-team ALLOWLIST can start a run.
# - TEST EXECUTION: only runs when the PR's AUTHOR is also on the allowlist.
#
# IMPORTANT: merged to main before triggers take effect.
on:
workflow_dispatch:
inputs:
pr_number:
description: "PR number or URL (required)"
required: true
dry_run:
description: "Dry-run: triage only, don't run tests"
type: boolean
required: true
default: true
issue_comment:
types: [created]
permissions:
contents: read
pull-requests: write
issues: write
concurrency:
group: query-agent-${{ github.event.issue.number || github.event.inputs.pr_number || github.ref }}
cancel-in-progress: true
env:
# Server config — matches api-testing.yml
ZO_ROOT_USER_EMAIL: root@example.com
ZO_ROOT_USER_PASSWORD: Complexpass#123
ZO_BASE_URL: http://localhost:5080/
ZO_BASE_URL_SC: http://localhost:5080/
ZO_USAGE_REPORTING_ENABLED: true
ZO_USAGE_PUBLISH_INTERVAL: 5
ZO_ALERT_SCHEDULE_INTERVAL: 3
ZO_INGEST_ALLOWED_UPTO: 48
ZO_CREATE_ORG_THROUGH_INGESTION: true
jobs:
# ---------------------------------------------------------------------------
# JOB 1 — Triage. Resolve PR, gate allowlist, check if diff touches
# query-engine paths, post decision comment.
# ---------------------------------------------------------------------------
triage:
if: >
contains(fromJSON('["bjp232004","hengfeiyang","prabhatsharma","uddhavdave","oasisk","haohuaijin","Subhra264","Loaki07","007harshmahajan","chaitanya-sistla","ktx-kirtan","omkarK06","ktx-vaidehi","ktx-abhay","neha00290","ktx-akshay","ktx-sadhna","nikhilsaikethe","YashodhanJoshi1","ktx-riya","mmosarafO2","Shrinath-O2","ktx-mihir"]'), github.actor) &&
(github.event_name == 'workflow_dispatch' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
github.event.comment.body == '/query-agent'))
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
skip: ${{ steps.ctx.outputs.skip }}
pr_number: ${{ steps.resolve.outputs.pr_number }}
author_allowed: ${{ steps.prinfo.outputs.author_allowed }}
author: ${{ steps.prinfo.outputs.author }}
dry_run: ${{ steps.mode.outputs.dry_run }}
steps:
- name: Resolve PR number
id: resolve
uses: actions/github-script@v7
with:
script: |
const raw = context.eventName === 'issue_comment'
? String(context.payload.issue.number)
: String(context.payload.inputs.pr_number || '');
const urlMatch = raw.match(/\/pull\/(\d+)/);
const numMatch = raw.match(/\d+/);
const pr = urlMatch ? urlMatch[1] : (numMatch ? numMatch[0] : '');
if (!pr) core.setFailed(`Could not parse a PR number from input: "${raw}"`);
core.setOutput('pr_number', pr);
- name: Inspect PR — author allowlist gate
id: prinfo
if: steps.resolve.outputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const ALLOW = ["bjp232004","hengfeiyang","prabhatsharma","uddhavdave","oasisk","haohuaijin","Subhra264","Loaki07","007harshmahajan","chaitanya-sistla","ktx-kirtan","omkarK06","ktx-vaidehi","ktx-abhay","neha00290","ktx-akshay","ktx-sadhna","nikhilsaikethe","YashodhanJoshi1","ktx-riya","mmosarafO2","Shrinath-O2","ktx-mihir"].map(s => s.toLowerCase());
const pr = Number('${{ steps.resolve.outputs.pr_number }}');
const { data } = await github.rest.pulls.get({
owner: context.repo.owner, repo: context.repo.repo, pull_number: pr,
});
const author = (data.user && data.user.login) ? data.user.login : '';
const allowed = ALLOW.includes(author.toLowerCase());
core.setOutput('author', author);
core.setOutput('author_allowed', allowed ? 'true' : 'false');
- name: Determine dry-run mode
id: mode
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_DRY: ${{ github.event.inputs.dry_run }}
run: |
DRY=true
if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ "$INPUT_DRY" = "false" ]; then
DRY=false
elif [ "$EVENT_NAME" = "issue_comment" ]; then
DRY=false # /query-agent comment is always a full run
fi
echo "dry_run=$DRY" >> "$GITHUB_OUTPUT"
- name: Acknowledge — react + post in-progress comment
id: ack
if: steps.resolve.outputs.pr_number != '' && steps.prinfo.outputs.author_allowed == 'true'
uses: actions/github-script@v7
with:
script: |
const pr = Number('${{ steps.resolve.outputs.pr_number }}');
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
if (context.eventName === 'issue_comment') {
try {
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: context.payload.comment.id, content: 'eyes',
});
} catch (e) { core.info(`reaction failed: ${e.message}`); }
}
const { data } = await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo, issue_number: pr,
body: `### 🤖 Query Agent — triage in progress…\n\nAnalyzing the change; I'll update this comment with the decision shortly.\n\n[View run](${runUrl})`,
});
core.setOutput('comment_id', data.id);
- uses: actions/checkout@v4
if: steps.prinfo.outputs.author_allowed == 'true'
with:
fetch-depth: 1
persist-credentials: false
- name: Fetch PR diff
if: steps.prinfo.outputs.author_allowed == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.resolve.outputs.pr_number }}
run: |
mkdir -p /tmp/query-agent-ci
gh pr diff "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" > /tmp/query-agent-ci/diff.patch || true
if [ ! -s /tmp/query-agent-ci/diff.patch ]; then
echo "::warning::Empty diff — nothing to triage"
fi
echo "diff bytes: $(wc -c < /tmp/query-agent-ci/diff.patch)"
- name: Triage — check for query-engine changes
id: ctx
if: steps.prinfo.outputs.author_allowed == 'true'
run: |
# Paths that warrant running the SQL regression suite
QUERY_PATHS='src/query/|src/sql/|src/service/search/|src/infra/table/|src/job/files/|tests/test-data/query-agent/|tests/api-testing/tests/query_agent/'
if grep -qE "$QUERY_PATHS" /tmp/query-agent-ci/diff.patch 2>/dev/null; then
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=Query-engine paths detected in diff" >> "$GITHUB_OUTPUT"
else
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "skip_reason=No query-engine paths in diff" >> "$GITHUB_OUTPUT"
fi
- name: Post or update triage comment
if: always() && steps.resolve.outputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const authorAllowed = '${{ steps.prinfo.outputs.author_allowed }}' === 'true';
const skip = '${{ steps.ctx.outputs.skip }}' || 'true';
const skipReason = '${{ steps.ctx.outputs.skip_reason }}' || '';
const dryRun = '${{ steps.mode.outputs.dry_run }}' === 'true';
let body = '### 🤖 Query Agent\n\n';
if (!authorAllowed) {
const a = '${{ steps.prinfo.outputs.author }}';
body += `Skipped — PR author${a ? ` @${a}` : ''} is not on the engineering-team allowlist. Query agent only runs for PRs authored by the team.`;
} else if (skip === 'true') {
body += `- **Skip:** yes — ${skipReason}\n`;
body += `- No SQL regression tests needed for this change.\n`;
} else {
body += `- **Skip:** no — ${skipReason}\n`;
if (dryRun) {
body += `- ⚠️ **Dry-run:** triage only, no tests executed.\n`;
} else {
body += `- ▶️ Running the query agent regression suite…\n`;
}
}
body += `\n[View run](${runUrl})`;
const commentId = '${{ steps.ack.outputs.comment_id }}';
if (commentId) {
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: Number(commentId), body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: Number('${{ steps.resolve.outputs.pr_number }}'), body,
});
}
# ---------------------------------------------------------------------------
# JOB 2 — Run the query agent regression suite against a live OO instance.
# Builds OO from source, runs all 672+ queries, posts pass/fail on the PR.
# ---------------------------------------------------------------------------
run_tests:
needs: triage
if: >
needs.triage.outputs.dry_run == 'false' &&
needs.triage.outputs.skip == 'false' &&
needs.triage.outputs.author_allowed == 'true'
runs-on: ubicloud-standard-4
timeout-minutes: 60
permissions:
contents: read
steps:
- name: Remove unused tools
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- name: Checkout git repo
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Rust Toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: nightly-2026-05-20
- name: Setup Rust Cache
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
shared-key: backend-debug-mimalloc
save-if: false
- name: Install Protoc
run: bash ./.github/protoc.sh
- name: Build OpenObserve debug binary
run: cargo build --features mimalloc
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11.6"
- name: Setup rye
uses: eifinger/setup-rye@v4
with:
enable-cache: true
working-directory: "tests/api-testing/"
version: "0.27.0"
- name: Pin cpython
run: rye pin cpython@3.11.6
working-directory: tests/api-testing/
- name: Rye sync
run: rye sync
working-directory: tests/api-testing/
- name: Regenerate query-agent expected values
run: |
cd ${{ github.workspace }}/tests/test-data/query-agent
${{ github.workspace }}/tests/api-testing/.venv/bin/python compute_counts.py
- name: Run query agent tests
id: qa_tests
run: |
ZO_MAX_FILE_RETENTION_TIME=1 ${{ github.workspace }}/target/debug/openobserve > o2_query_agent.log 2>&1 &
SERVER_PID=$!
echo "server_pid=$SERVER_PID" >> "$GITHUB_OUTPUT"
for i in $(seq 1 60); do
if curl -sf http://localhost:5080/healthz > /dev/null 2>&1; then
echo "Server ready after ${i}s"; break
fi
sleep 1
done
rye run pytest tests/query_agent/ --ignore=tests/query_agent/test_3_vortex.py -v \
--junitxml=junit-query-agent.xml 2>&1 | tee pytest-query-agent.log
echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
working-directory: tests/api-testing/
- name: Kill server
if: always()
run: kill ${{ steps.qa_tests.outputs.server_pid }} || true
- name: Upload server log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: openobserve-query-agent-log
path: tests/api-testing/o2_query_agent.log
if-no-files-found: warn
retention-days: 14
- name: Upload pytest log on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: pytest-query-agent-log
path: tests/api-testing/pytest-query-agent.log
if-no-files-found: warn
retention-days: 14
- name: Upload JUnit report
if: always()
uses: actions/upload-artifact@v4
with:
name: junit-query-agent
path: tests/api-testing/junit-query-agent.xml
if-no-files-found: warn
retention-days: 7
- name: Post results comment
if: always() && needs.triage.outputs.pr_number != ''
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const pr = Number('${{ needs.triage.outputs.pr_number }}');
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const exitCode = '${{ steps.qa_tests.outputs.exit_code }}';
const passed = exitCode === '0';
let body = '### 🤖 Query Agent — Results\n\n';
body += passed ? '✅ **All query agent tests passed.**\n\n' : '❌ **Query agent tests failed.**\n\n';
// Parse pytest summary from log if available
try {
const log = fs.readFileSync('tests/api-testing/pytest-query-agent.log', 'utf8');
const m = log.match(/(\d+) passed.*(\d+) failed.*(\d+) error/);
if (m) {
body += `\`\`\`\n${m[0]}\n\`\`\`\n`;
}
} catch (e) { /* no log to parse */ }
body += `\n[View run](${runUrl})`;
// Find the ack comment and update it, or create new
await github.rest.issues.createComment({
owner: context.repo.owner, repo: context.repo.repo, issue_number: pr, body,
});