name: k6 nightly load test # v1.0.9 W4 Day 20 — runs the mixed-scenarios k6 script against the # staging environment every night at 02:30 UTC. The acceptance gate # is "pass green 3 nuits consécutives" before flipping a release — # the artifact uploaded by this workflow carries the JSON summary # the operator inspects. # # Scope deliberately narrow : runs ONLY on staging, NEVER on prod. # A separate manually-triggered workflow (workflow_dispatch) covers # pre-launch capacity drills with a longer ramp. on: # GATED — k6 hammer is too heavy for the single self-hosted runner. # Re-enable the cron once a dedicated load-test runner exists. # schedule: # - cron: "30 2 * * *" workflow_dispatch: inputs: duration: description: "Duration per scenario (e.g. 5m, 15m, 1h)" required: false default: "5m" type: string base_url: description: "Override staging URL" required: false default: "" type: string env: GIT_SSL_NO_VERIFY: "true" # Defaults — override via workflow_dispatch input or repo vars. DEFAULT_BASE_URL: "https://staging.veza.fr" jobs: loadtest: name: k6 mixed scenarios (1650 VU steady) runs-on: [self-hosted, incus] timeout-minutes: 30 steps: - name: Checkout uses: actions/checkout@v4 - name: Install k6 run: | set -euo pipefail sudo gpg -k sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg \ --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" \ | sudo tee /etc/apt/sources.list.d/k6.list sudo apt-get update sudo apt-get install -y k6 k6 version - name: Resolve test inputs id: inputs run: | set -euo pipefail BASE_URL="${{ github.event.inputs.base_url }}" if [ -z "$BASE_URL" ]; then BASE_URL="${{ vars.STAGING_BASE_URL || env.DEFAULT_BASE_URL }}" fi DURATION="${{ github.event.inputs.duration }}" if [ -z "$DURATION" ]; then DURATION="5m" fi echo "base_url=$BASE_URL" >> "$GITHUB_OUTPUT" echo "duration=$DURATION" >> "$GITHUB_OUTPUT" - name: Pre-flight — staging is reachable run: | set -euo pipefail url="${{ steps.inputs.outputs.base_url }}/api/v1/health" echo "::notice::Pre-flight GET $url" status=$(curl -k -sS --max-time 10 -o /dev/null -w "%{http_code}" "$url" || echo "000") if [ "$status" != "200" ]; then echo "::error::Staging /health returned $status — aborting load test." exit 1 fi - name: Run k6 mixed scenarios id: run env: BASE_URL: ${{ steps.inputs.outputs.base_url }} DURATION: ${{ steps.inputs.outputs.duration }} USER_TOKEN: ${{ secrets.STAGING_LOADTEST_TOKEN }} STREAM_TRACK_ID: ${{ vars.STAGING_LOADTEST_TRACK_ID || '00000000-0000-0000-0000-000000000001' }} run: | set -euo pipefail if [ -z "$USER_TOKEN" ]; then echo "::warning::STAGING_LOADTEST_TOKEN secret is empty — auth-required scenarios will record 401s as errors." fi k6 run --quiet \ --summary-export=k6-summary.json \ scripts/loadtest/k6_mixed_scenarios.js - name: Upload k6 summary artifact if: always() uses: actions/upload-artifact@v4 with: name: k6-summary-${{ github.run_number }} path: | k6-summary.json scripts/loadtest/k6_mixed_scenarios.js retention-days: 30 - name: Annotate thresholds in summary if: always() run: | set -euo pipefail if [ ! -f k6-summary.json ]; then echo "::warning::No summary artifact — k6 likely failed before write." exit 0 fi echo "## k6 load test summary" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" jq -r ' (.metrics.http_reqs.values.count // 0) as $reqs | (.metrics.http_req_failed.values.rate // 0) as $err | (.metrics.http_req_duration.values["p(95)"] // 0) as $p95 | (.metrics.http_req_duration.values["p(99)"] // 0) as $p99 | "- requests: \($reqs)\n- failed rate: \($err * 100 | round)/100 %\n- p95: \($p95 | round) ms\n- p99: \($p99 | round) ms" ' k6-summary.json >> "$GITHUB_STEP_SUMMARY"