feat(gate): add R10/R11 e2e-perf gates and post-merge/nightly workflows
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+195
@@ -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
|
||||
Executable
+79
@@ -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}" "$@"
|
||||
Reference in New Issue
Block a user