feat(gate): add R10/R11 e2e-perf gates and post-merge/nightly workflows

This commit is contained in:
BrunoGaoSZ
2026-02-26 16:27:26 +01:00
parent 2c35a073c1
commit aafd2daf22
19 changed files with 635 additions and 19 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Deploy Quality Dashboard
on:
workflow_run:
workflows: ["Gate Check"]
workflows: ["Gate Check", "Post Merge E2E", "Nightly Regression"]
types: [completed]
workflow_dispatch:
+129
View File
@@ -0,0 +1,129 @@
name: Nightly Regression
on:
schedule:
- cron: "30 3 * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
repository-projects: write
jobs:
nightly_regression:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.PROJECT_TOKEN != '' && secrets.PROJECT_TOKEN || (secrets.PROJECT_V2_TOKEN != '' && secrets.PROJECT_V2_TOKEN || github.token) }}
APP_IMAGE: ${{ vars.APP_IMAGE != '' && vars.APP_IMAGE || 'ghcr.io/brunogao/ljwx-platform-app:latest' }}
BASE_URL: http://localhost:8080
ENABLE_E2E: "1"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate tools
shell: bash
run: |
set -euo pipefail
command -v jq >/dev/null
command -v gh >/dev/null
command -v docker >/dev/null
- name: Start test environment
shell: bash
run: |
set -euo pipefail
bash scripts/setup-test-env.sh up
- name: Run R10 E2E
id: r10
continue-on-error: true
shell: bash
run: |
set -euo pipefail
status="PASS"
if ! bash scripts/gates/gate-e2e.sh; then
status="FAIL"
fi
echo "status=$status" >> "$GITHUB_OUTPUT"
- name: Run R11 Perf baseline
id: r11
continue-on-error: true
shell: bash
run: |
set -euo pipefail
status="PASS"
if ! bash scripts/gates/gate-perf.sh; then
status="FAIL"
fi
echo "status=$status" >> "$GITHUB_OUTPUT"
- name: Build report payload
if: always()
shell: bash
run: |
set -euo pipefail
TMP_DIR=/tmp/ljwx-gate-results
mkdir -p "$TMP_DIR"
bash scripts/gen-rtm.sh || true
bash scripts/gates/gate-report.sh 00 "$TMP_DIR"
bash scripts/gates/gate-summary.sh
- name: Create regression issue on failure
if: always() && (steps.r10.outputs.status == 'FAIL' || steps.r11.outputs.status == 'FAIL')
shell: bash
run: |
set -euo pipefail
run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
gate="R10"
if [[ "${{ steps.r11.outputs.status }}" == "FAIL" ]]; then gate="R11"; fi
title="[Bug][Regression][P0] Nightly ${gate} regression detected"
if [[ "$gate" == "R10" ]]; then
title="[Bug][Regression][P0] Cross-tenant access must return 404 (nightly)"
fi
gh issue create \
--repo "${GITHUB_REPOSITORY}" \
--title "$title" \
--label "type:bugfix" \
--label "priority:P0" \
--label "gate:fail" \
--body "Automated nightly regression detection.\n\n- Run URL: ${run_url}\n- Gate: ${gate}\n- R10 expected: cross-tenant access MUST return HTTP 404\n- R11 mode: baseline observation (no threshold)\n\nPlease triage and link fix PR." || true
- name: Update weekly regression campaign
if: always()
shell: bash
run: |
set -euo pipefail
overall="PASS"
if [[ "${{ steps.r10.outputs.status }}" != "PASS" || "${{ steps.r11.outputs.status }}" != "PASS" ]]; then
overall="FAIL"
fi
bash scripts/regression/regression-weekly.sh --mode nightly --status "$overall" --apply
- name: Upload gate reports for Pages
if: always()
uses: actions/upload-artifact@v4
with:
name: gate-reports
path: |
docs/reports/**
tests/results/**
- name: Teardown test environment
if: always()
shell: bash
run: |
set -euo pipefail
bash scripts/setup-test-env.sh clean || true
- name: Fail workflow on regression
if: always()
shell: bash
run: |
set -euo pipefail
if [[ "${{ steps.r10.outputs.status }}" != "PASS" || "${{ steps.r11.outputs.status }}" != "PASS" ]]; then
echo "Nightly regression detected: R10=${{ steps.r10.outputs.status }} R11=${{ steps.r11.outputs.status }}" >&2
exit 1
fi
+108
View File
@@ -0,0 +1,108 @@
name: Post Merge E2E
on:
push:
branches: [master, main]
workflow_dispatch:
permissions:
contents: read
issues: write
repository-projects: write
jobs:
post_merge_e2e:
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ secrets.PROJECT_TOKEN != '' && secrets.PROJECT_TOKEN || (secrets.PROJECT_V2_TOKEN != '' && secrets.PROJECT_V2_TOKEN || github.token) }}
APP_IMAGE: ${{ vars.APP_IMAGE != '' && vars.APP_IMAGE || 'ghcr.io/brunogao/ljwx-platform-app:latest' }}
BASE_URL: http://localhost:8080
ENABLE_E2E: "1"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate tools
shell: bash
run: |
set -euo pipefail
command -v jq >/dev/null
command -v gh >/dev/null
command -v docker >/dev/null
- name: Start test environment
shell: bash
run: |
set -euo pipefail
bash scripts/setup-test-env.sh up
- name: Run R10 E2E smoke
id: r10
continue-on-error: true
shell: bash
run: |
set -euo pipefail
TMP_DIR=/tmp/ljwx-gate-results
mkdir -p "$TMP_DIR"
rm -f "$TMP_DIR"/R*.json
status="PASS"
if ! bash scripts/gates/gate-e2e.sh; then
status="FAIL"
fi
bash scripts/gen-rtm.sh || true
bash scripts/gates/gate-report.sh 00 "$TMP_DIR"
bash scripts/gates/gate-summary.sh
echo "r10_status=$status" >> "$GITHUB_OUTPUT"
- name: Create regression bug issue on R10 failure
if: always() && steps.r10.outputs.r10_status == 'FAIL'
shell: bash
run: |
set -euo pipefail
run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
gh issue create \
--repo "${GITHUB_REPOSITORY}" \
--title "[Bug][Regression][P0] Cross-tenant access must return 404 (post-merge e2e)" \
--label "type:bugfix" \
--label "priority:P0" \
--label "gate:fail" \
--body "Automated regression detection from post-merge E2E.\n\n- Run URL: ${run_url}\n- Gate: R10\n- Expected: cross-tenant read/update/delete MUST return HTTP 404\n- Actual: one or more checks failed in E2E smoke\n\nPlease triage and link fix PR." || true
- name: Update weekly regression campaign
if: always()
shell: bash
run: |
set -euo pipefail
status="${{ steps.r10.outputs.r10_status }}"
if [[ -z "$status" ]]; then status="FAIL"; fi
bash scripts/regression/regression-weekly.sh --mode post-merge --status "$status" --apply
- name: Upload gate reports for Pages
if: always()
uses: actions/upload-artifact@v4
with:
name: gate-reports
path: |
docs/reports/**
tests/results/**
- name: Teardown test environment
if: always()
shell: bash
run: |
set -euo pipefail
bash scripts/setup-test-env.sh clean || true
- name: Fail workflow when R10 fails
if: always()
shell: bash
run: |
set -euo pipefail
status="${{ steps.r10.outputs.r10_status }}"
if [[ "$status" != "PASS" ]]; then
echo "R10 failed: ${status:-unknown}" >&2
exit 1
fi
+1 -2
View File
@@ -6,8 +6,7 @@ on:
# workflow_run:
# workflows: ["Gate Check"]
# types: [completed]
schedule:
- cron: "0 3 * * *"
# schedule moved to nightly-regression.yml to avoid duplicate campaign comments
permissions:
contents: read
@@ -54,6 +54,7 @@ services:
interval: 10s
timeout: 5s
retries: 30
profiles: ["app"]
volumes:
mysql_test_data:
+4 -4
View File
@@ -67,7 +67,7 @@
<div class="card span-6"><div class="label">Tests Failure Rate</div><div id="kFailRate" class="kpi">-</div></div>
<div class="card span-8">
<h3>Quality Gate Matrix (R01-R09 x Phase)</h3>
<h3>Quality Gate Matrix (R01-R11 x Phase)</h3>
<div class="matrix"><table id="matrixTable"></table></div>
</div>
<div class="card span-4">
@@ -140,7 +140,7 @@
</section>
<script>
const RULES = ["R01","R02","R03","R04","R05","R06","R07","R08","R09"];
const RULES = ["R01","R02","R03","R04","R05","R06","R07","R08","R09","R10","R11"];
const FALLBACK_REPO = { owner: "BrunoGao", name: "ljwx-platform", branch: "master" };
const state = {
@@ -410,10 +410,10 @@
if (tab === "summary") {
if (!pj) body.innerHTML = `<div class="empty">Select a phase.</div>`;
else {
const rows = (pj.rules || []).map(r => `<tr><td>${esc(r.id)}</td><td>${badge(r.status)}</td><td>${esc(r.message || "")}</td></tr>`).join("");
const rows = (pj.rules || []).map(r => `<tr><td>${esc(r.id)}</td><td>${badge(r.status)}</td><td>${esc(r.checks ?? "-")}</td><td>${esc(r.passed ?? "-")}</td><td>${esc(r.failed ?? "-")}</td><td>${esc((r.p95_ms ?? r.avg_ms ?? "-") + "")}</td><td>${esc(r.message || "")}</td></tr>`).join("");
const art = pj.artifacts || {};
body.innerHTML = `
<div class="table-wrap"><table><thead><tr><th>Gate</th><th>Status</th><th>Message</th></tr></thead><tbody>${rows || `<tr><td colspan="3" class="empty">No gate rows</td></tr>`}</tbody></table></div>
<div class="table-wrap"><table><thead><tr><th>Gate</th><th>Status</th><th>Checks</th><th>Passed</th><th>Failed</th><th>p95(ms)</th><th>Message</th></tr></thead><tbody>${rows || `<tr><td colspan="7" class="empty">No gate rows</td></tr>`}</tbody></table></div>
<p class="hint">Surefire: ${art.surefire_xml_index ? `<a class="link" href="./${art.surefire_xml_index}" target="_blank">index.json</a>` : "-"} | ${art.surefire_html ? `<a class="link" href="./${art.surefire_html}" target="_blank">html</a>` : "-"}</p>
<p class="hint">Gate Log: ${art.gate_raw_log ? `<a class="link" href="./${art.gate_raw_log}" target="_blank">open</a>` : "-"}</p>`;
}
+1 -1
View File
@@ -4,7 +4,7 @@
"required": ["phase", "status", "timestamp", "git", "rules", "summary"],
"properties": {
"phase": {"type": "string", "pattern": "^[0-9]{2}$"},
"status": {"type": "string", "enum": ["PASS", "FAIL", "PENDING"]},
"status": {"type": "string", "enum": ["PASS", "FAIL", "PENDING", "SKIP"]},
"timestamp": {"type": "string"},
"git": {
"type": "object",
+57
View File
@@ -0,0 +1,57 @@
# System Testing (R10 / R11)
## Scope
- `R10`: E2E system smoke/regression via k6
- `R11`: performance baseline via k6 (observation stage, no hard threshold)
Cross-tenant contract is fixed:
- Cross-tenant `GET/PUT/DELETE` by id MUST return `404`.
- Do not return `403` for cross-tenant resource probing.
## Local Run
1. Start test environment:
```bash
bash scripts/setup-test-env.sh up
```
2. Run E2E smoke:
```bash
BASE_URL=http://localhost:8080 bash tests/k6-run.sh e2e-01
BASE_URL=http://localhost:8080 bash tests/k6-run.sh e2e-02
```
3. Run performance baseline:
```bash
BASE_URL=http://localhost:8080 bash tests/k6-run.sh perf-baseline
```
4. Stop environment:
```bash
bash scripts/setup-test-env.sh down
```
## Gate Integration
- `scripts/gates/gate-e2e.sh` writes `/tmp/ljwx-gate-results/R10.json`
- `scripts/gates/gate-perf.sh` writes `/tmp/ljwx-gate-results/R11.json`
- `scripts/gates/gate-all.sh` runs `R10` only when `ENABLE_E2E=1`
## CI Strategy
- PR gate: no docker-compose E2E
- Post-merge (`post-merge-e2e.yml`): run `R10` smoke and update weekly campaign
- Nightly (`nightly-regression.yml`): run `R10 + R11`, upload reports, update weekly campaign
## R11 Progressive Policy
1. Observation phase: collect p95/avg only (`R11` pass unless execution error)
2. Alert phase: comment + issue when p95 crosses baseline band
3. Blocking phase: enforce threshold in gate after baseline stabilizes
+24
View File
@@ -39,6 +39,7 @@ if ((10#$PHASE >= 10 && 10#$PHASE <= 19)); then
PROFILE="frontend"
fi
SKIP_HEAVY_GATES="${SKIP_HEAVY_GATES:-false}"
ENABLE_E2E="${ENABLE_E2E:-0}"
echo "╔══════════════════════════════════════════════════╗"
echo "║ LJWX Gate — Full Check ║"
@@ -158,6 +159,29 @@ echo ""
run_rule "R09" "Tests" "scripts/gates/gate-test.sh" "true" "true"
# R10: E2E (opt-in only)
((TOTAL++))
echo "── R10 E2E System Tests ─────────────────────────"
if [[ "$ENABLE_E2E" != "1" ]]; then
echo " SKIP set ENABLE_E2E=1 to run system E2E"
((SKIPPED++))
write_rule_json "R10" "E2E System Tests" "SKIP" 0 0 "disabled by ENABLE_E2E"
else
if [[ "$SKIP_HEAVY_GATES" == "true" ]]; then
echo " SKIP heavy gate skipped in batch mode"
((SKIPPED++))
write_rule_json "R10" "E2E System Tests" "SKIP" 0 0 "skipped by SKIP_HEAVY_GATES=true"
elif bash scripts/gates/gate-e2e.sh >/dev/null 2>&1; then
echo " PASS scripts/gates/gate-e2e.sh"
((PASSED++))
else
echo " FAIL scripts/gates/gate-e2e.sh"
((FAILED++))
FAILED_NAMES+=("R10")
fi
fi
echo ""
if [[ -x "scripts/reports/collect-artifacts.sh" ]]; then
bash scripts/reports/collect-artifacts.sh "$PHASE" >/dev/null 2>&1 || true
fi
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_DIR="${TMP_DIR:-/tmp/ljwx-gate-results}"
mkdir -p "$TMP_DIR"
OUT_JSON="$TMP_DIR/R10.json"
if ! command -v jq >/dev/null 2>&1; then
echo "jq is required" >&2
exit 1
fi
run_k6() {
if command -v k6 >/dev/null 2>&1; then
k6 "$@"
return
fi
if command -v docker >/dev/null 2>&1; then
docker run --rm -i \
-v "$PROJECT_ROOT:/work" \
-w /work \
grafana/k6:0.49.0 "$@"
return
fi
echo "k6 is not available and docker fallback is unavailable" >&2
return 127
}
collect_failed_checks() {
local summary_file="$1"
jq -r '
def walk_groups($g):
([($g.checks // [])[] | select((.fails // 0) > 0) | .name]
+ [($g.groups // [])[] | walk_groups(.)] | add);
(if .root_group then walk_groups(.root_group) else [] end)[]?
' "$summary_file" 2>/dev/null || true
}
build_violations_from_checks() {
local summary_file="$1"
local script_name="$2"
local lines
lines="$(collect_failed_checks "$summary_file")"
if [[ -z "$lines" ]]; then
echo '[]'
return
fi
while IFS= read -r name; do
[[ -n "$name" ]] || continue
local severity="WARNING"
local rule="e2e_check_failed"
if [[ "$name" == *"tenantB_"* || "$name" == *"cross_tenant"* || "$name" == *"404"* ]]; then
severity="CRITICAL"
rule="tenant_isolation_cross_tenant_must_404"
fi
jq -nc \
--arg file "$script_name" \
--arg rule "$rule" \
--arg severity "$severity" \
--arg message "failed check: $name" \
--arg fix "Ensure cross-tenant access on selectById/updateById/deleteById returns 404 and tenant interceptor applies uniformly." \
'{file:$file,rule:$rule,severity:$severity,message:$message,fix:$fix}'
done <<<"$lines" | jq -s '.'
}
build_violations_from_log() {
local log_file="$1"
local script_name="$2"
local rows
rows="$(grep -E 'expected=404 actual=|tenant isolation violation|ERRO\[|panic|Exception' "$log_file" | head -n 20 || true)"
if [[ -z "$rows" ]]; then
echo '[]'
return
fi
while IFS= read -r row; do
[[ -n "$row" ]] || continue
local severity="WARNING"
local rule="e2e_runtime_error"
if [[ "$row" == *"expected=404 actual="* || "$row" == *"tenant isolation violation"* ]]; then
severity="CRITICAL"
rule="tenant_isolation_cross_tenant_must_404"
fi
jq -nc \
--arg file "$script_name" \
--arg rule "$rule" \
--arg severity "$severity" \
--arg message "$row" \
--arg fix "Validate tenant interceptor coverage for read/write-by-id APIs and map cross-tenant hits to HTTP 404." \
'{file:$file,rule:$rule,severity:$severity,message:$message,fix:$fix}'
done <<<"$rows" | jq -s '.'
}
run_case() {
local id="$1"
local script="$2"
local summary_file raw_file log_file
summary_file="$(mktemp)"
raw_file="$(mktemp)"
log_file="$(mktemp)"
local rc=0
set +e
run_k6 run \
--summary-export "$summary_file" \
--out "json=$raw_file" \
"$script" >"$log_file" 2>&1
rc=$?
set -e
local checks_total checks_passed checks_failed p95
checks_passed="$(jq -r '.metrics.checks.values.passes // 0' "$summary_file" 2>/dev/null || echo 0)"
checks_failed="$(jq -r '.metrics.checks.values.fails // 0' "$summary_file" 2>/dev/null || echo 0)"
checks_total=$((checks_passed + checks_failed))
p95="$(jq -r '.metrics.http_req_duration.values["p(95)"] // 0' "$summary_file" 2>/dev/null || echo 0)"
local v_checks v_logs
v_checks="$(build_violations_from_checks "$summary_file" "$script")"
v_logs="$(build_violations_from_log "$log_file" "$script")"
jq -nc \
--arg id "$id" \
--arg script "$script" \
--argjson rc "$rc" \
--argjson checks "$checks_total" \
--argjson passed "$checks_passed" \
--argjson failed "$checks_failed" \
--argjson p95 "$p95" \
--argjson v_checks "$v_checks" \
--argjson v_logs "$v_logs" \
'{id:$id,script:$script,rc:$rc,checks:$checks,passed:$passed,failed:$failed,p95:$p95,violations:($v_checks + $v_logs)}'
}
cd "$PROJECT_ROOT"
case1="$(run_case e2e_01 tests/e2e/e2e-01-auth-rbac.js)"
case2="$(run_case e2e_02 tests/e2e/e2e-02-tenant-isolation.js)"
aggregate="$(jq -nc --argjson a "$case1" --argjson b "$case2" '
[$a,$b] as $all
| {
checks: ($all | map(.checks) | add),
passed: ($all | map(.passed) | add),
failed: ($all | map(.failed) | add),
p95: (($all | map(.p95) | add) / (if ($all|length)==0 then 1 else ($all|length) end)),
rc_fail: ($all | map(select(.rc != 0)) | length),
violations: ($all | map(.violations) | add)
}
')"
checks="$(jq -r '.checks' <<<"$aggregate")"
passed="$(jq -r '.passed' <<<"$aggregate")"
failed="$(jq -r '.failed' <<<"$aggregate")"
p95="$(jq -r '.p95' <<<"$aggregate")"
rc_fail="$(jq -r '.rc_fail' <<<"$aggregate")"
violations="$(jq '.violations' <<<"$aggregate")"
status="PASS"
if [[ "$rc_fail" -gt 0 || "$failed" -gt 0 ]]; then
status="FAIL"
fi
critical="$(jq '[.[] | select((.severity // "") == "CRITICAL")] | length' <<<"$violations")"
warnings="$(jq '[.[] | select((.severity // "") != "CRITICAL")] | length' <<<"$violations")"
message="R10 e2e checks=${checks} passed=${passed} failed=${failed} p95_ms=$(printf '%.2f' "$p95")"
jq -n \
--arg id "R10" \
--arg name "E2E System Tests" \
--arg status "$status" \
--arg message "$message" \
--argjson critical "$critical" \
--argjson warnings "$warnings" \
--argjson checks "$checks" \
--argjson passed "$passed" \
--argjson failed "$failed" \
--argjson p95_ms "$p95" \
--argjson violations "$violations" \
'{id:$id,name:$name,status:$status,critical:$critical,warnings:$warnings,message:$message,checks:$checks,passed:$passed,failed:$failed,p95_ms:$p95_ms,violations:$violations}' > "$OUT_JSON"
echo "$OUT_JSON"
if [[ "$status" == "FAIL" ]]; then
exit 1
fi
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
PROJECT_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
TMP_DIR="${TMP_DIR:-/tmp/ljwx-gate-results}"
mkdir -p "$TMP_DIR"
OUT_JSON="$TMP_DIR/R11.json"
if ! command -v jq >/dev/null 2>&1; then
echo "jq is required" >&2
exit 1
fi
run_k6() {
if command -v k6 >/dev/null 2>&1; then
k6 "$@"
return
fi
if command -v docker >/dev/null 2>&1; then
docker run --rm -i -v "$PROJECT_ROOT:/work" -w /work grafana/k6:0.49.0 "$@"
return
fi
echo "k6 is not available and docker fallback is unavailable" >&2
return 127
}
cd "$PROJECT_ROOT"
summary_file="$(mktemp)"
raw_file="$(mktemp)"
log_file="$(mktemp)"
set +e
run_k6 run \
--summary-export "$summary_file" \
--out "json=$raw_file" \
tests/perf/baseline.js >"$log_file" 2>&1
rc=$?
set -e
checks_passed="$(jq -r '.metrics.checks.values.passes // 0' "$summary_file" 2>/dev/null || echo 0)"
checks_failed="$(jq -r '.metrics.checks.values.fails // 0' "$summary_file" 2>/dev/null || echo 0)"
checks_total=$((checks_passed + checks_failed))
p95="$(jq -r '.metrics.http_req_duration.values["p(95)"] // 0' "$summary_file" 2>/dev/null || echo 0)"
avg="$(jq -r '.metrics.http_req_duration.values.avg // 0' "$summary_file" 2>/dev/null || echo 0)"
status="PASS"
violations='[]'
critical=0
warnings=0
if [[ "$rc" -ne 0 ]]; then
status="FAIL"
critical=1
violations="$(jq -nc --arg file "tests/perf/baseline.js" --arg rule "perf_baseline_execution" --arg severity "CRITICAL" --arg message "k6 perf run failed rc=$rc" --arg fix "Verify test env readiness and endpoint availability before running R11 baseline." '[{file:$file,rule:$rule,severity:$severity,message:$message,fix:$fix}]')"
fi
message="R11 perf baseline (observation) checks=${checks_total} passed=${checks_passed} failed=${checks_failed} p95_ms=$(printf '%.2f' "$p95") avg_ms=$(printf '%.2f' "$avg")"
jq -n \
--arg id "R11" \
--arg name "Performance Baseline" \
--arg status "$status" \
--arg message "$message" \
--argjson critical "$critical" \
--argjson warnings "$warnings" \
--argjson checks "$checks_total" \
--argjson passed "$checks_passed" \
--argjson failed "$checks_failed" \
--argjson p95_ms "$p95" \
--argjson avg_ms "$avg" \
--argjson violations "$violations" \
'{id:$id,name:$name,status:$status,critical:$critical,warnings:$warnings,message:$message,checks:$checks,passed:$passed,failed:$failed,p95_ms:$p95_ms,avg_ms:$avg_ms,violations:$violations}' > "$OUT_JSON"
echo "$OUT_JSON"
if [[ "$status" == "FAIL" ]]; then
exit 1
fi
@@ -6,6 +6,7 @@ COMPOSE_FILE="${ROOT_DIR}/docker-compose.test.yml"
BASE_URL="${BASE_URL:-http://localhost:8080}"
HEALTH_PATH="${HEALTH_PATH:-/actuator/health}"
WAIT_SECONDS="${WAIT_SECONDS:-180}"
START_APP_CONTAINER="${START_APP_CONTAINER:-true}"
usage() {
cat <<USAGE
@@ -13,10 +14,16 @@ Usage:
bash scripts/setup-test-env.sh <up|down|clean|status>
Commands:
up Start test services and wait until app health endpoint is ready
up Start test services (mysql/redis/app by default) and wait health when app is started
down Stop test services
clean Stop services and remove volumes
status Show service status
Env:
START_APP_CONTAINER=true|false Start app service from compose (default true)
BASE_URL=http://localhost:8080 Health check base URL
HEALTH_PATH=/actuator/health Health endpoint path
WAIT_SECONDS=180 Health wait timeout
USAGE
}
@@ -65,9 +72,14 @@ main() {
case "${action}" in
up)
echo "[setup-test-env] starting services from ${COMPOSE_FILE}"
${compose} -f "${COMPOSE_FILE}" up -d
wait_http_ready "${BASE_URL}${HEALTH_PATH}" "${WAIT_SECONDS}"
echo "[setup-test-env] BASE_URL=${BASE_URL}"
if [[ "$START_APP_CONTAINER" == "true" ]]; then
${compose} -f "${COMPOSE_FILE}" up -d mysql redis app
wait_http_ready "${BASE_URL}${HEALTH_PATH}" "${WAIT_SECONDS}"
echo "[setup-test-env] BASE_URL=${BASE_URL}"
else
${compose} -f "${COMPOSE_FILE}" up -d mysql redis
echo "[setup-test-env] started mysql/redis only (START_APP_CONTAINER=false)"
fi
;;
down)
${compose} -f "${COMPOSE_FILE}" down
@@ -17,17 +17,29 @@ Examples:
USAGE
}
run_k6() {
if command -v k6 >/dev/null 2>&1; then
k6 "$@"
return
fi
if command -v docker >/dev/null 2>&1; then
docker run --rm -i \
-v "${ROOT_DIR}:/work" \
-w /work \
grafana/k6:0.49.0 "$@"
return
fi
echo "[k6-run] neither k6 binary nor docker fallback is available"
exit 1
}
if [[ $# -lt 1 ]]; then
usage
exit 1
fi
if ! command -v k6 >/dev/null 2>&1; then
echo "[k6-run] k6 command not found in PATH"
echo "Install: https://grafana.com/docs/k6/latest/set-up/install-k6/"
exit 1
fi
NAME="$1"
shift || true
@@ -57,7 +69,7 @@ echo "[k6-run] script=${SCRIPT}"
echo "[k6-run] summary=${SUMMARY_FILE}"
echo "[k6-run] raw=${RAW_FILE}"
k6 run \
run_k6 run \
--summary-export "${SUMMARY_FILE}" \
--out "json=${RAW_FILE}" \
"${SCRIPT}" "$@"