From 9443aa45c407ba66b3255c73d4eab15e0e3b346a Mon Sep 17 00:00:00 2001 From: Jens Holdgaard Pedersen Date: Wed, 10 Jun 2026 21:35:42 +0200 Subject: [PATCH 1/2] ci(capture): failure feature-flag input for OTel-Demo corpus captures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The B1 thesis bench (severity-predicate pushdown, level='ERROR') needs a corpus with a real error band, but the demo's default traffic emits no error logs (verified on corpus/otel-demo-v4: zero literal ERROR texts, zero severities in 17..=20, so the merged b1/real-corpus arm skips it). The demo ships flagd failure flags (adFailure, paymentFailure, cartFailure, ...) that make services emit genuine errors under load — the demo-native error source. New workflow_dispatch input `failure_flags` (default '' = no behavior change): space/comma-separated flagd flag names. When set, a step patches demo/src/flagd/demo.flagd.json before bring-up, flipping each named flag's defaultVariant to its failing variant ("on", or "100%" for the fractional flags). Flag names are validated against ^[A-Za-z0-9_-]+$ and against the actual config (unknown flag fails with the available list). The enabled flags are recorded in the diversity manifest and the release notes, so a corpus's error provenance is auditable. Co-Authored-By: Claude Fable 5 --- .../workflows/capture-otel-demo-corpus.yml | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/.github/workflows/capture-otel-demo-corpus.yml b/.github/workflows/capture-otel-demo-corpus.yml index f7462da92..7224ad8dc 100644 --- a/.github/workflows/capture-otel-demo-corpus.yml +++ b/.github/workflows/capture-otel-demo-corpus.yml @@ -45,6 +45,10 @@ on: description: Concurrent load-generator users (HTTP, browser traffic off — drives corpus volume) default: '40' required: false + failure_flags: + description: Space/comma-separated flagd failure flags to force on (e.g. adFailure paymentFailure) — empty leaves the demo's defaults + default: '' + required: false release_tag: description: If set, publish the corpus as assets on a GitHub release at this tag (e.g. corpus/otel-demo-v1) default: '' @@ -113,6 +117,48 @@ jobs: echo '--- effective extras ---' cat demo/src/otel-collector/otelcol-config-extras.yml + - name: Enable failure feature flags + if: inputs.failure_flags != '' + env: + # Via `env:`, not `${{ … }}` splicing into the script — + # dispatcher-controlled input, same rule as DEMO_REF. + FAILURE_FLAGS: ${{ inputs.failure_flags }} + run: | + set -euxo pipefail + # The demo's default traffic emits no error logs; flagd + # failure flags (adFailure, paymentFailure, …) are the + # demo-native way to make services emit real errors under + # load. flagd bind-mounts demo/src/flagd and reads + # demo.flagd.json from it, so patching the file before + # bring-up is the whole injection — same idea as the + # collector-overlay step above. + cfg=demo/src/flagd/demo.flagd.json + for flag in $(tr ',' ' ' <<< "$FAILURE_FLAGS"); do + if ! [[ "$flag" =~ ^[A-Za-z0-9_-]+$ ]]; then + echo "::error::invalid flag name '$flag' (expected [A-Za-z0-9_-]+)" + exit 1 + fi + if ! jq -e --arg f "$flag" '.flags | has($f)' "$cfg" >/dev/null; then + echo "::error::flagd flag '$flag' not in $cfg at this demo ref; available: $(jq -r '.flags | keys | sort | join(", ")' "$cfg")" + exit 1 + fi + # The failing variant is "on" for most flags; the + # fractional ones (e.g. paymentFailure at 2.2.0) have + # percentage variants instead, where "100%" is the + # always-fail one. + variant="$(jq -r --arg f "$flag" \ + '.flags[$f].variants | if has("on") then "on" elif has("100%") then "100%" else empty end' "$cfg")" + if [ -z "$variant" ]; then + echo "::error::flag '$flag' has no on/100% variant; variants: $(jq -r --arg f "$flag" '.flags[$f].variants | keys | join(", ")' "$cfg")" + exit 1 + fi + jq --arg f "$flag" --arg v "$variant" \ + '.flags[$f].defaultVariant = $v' "$cfg" > "$cfg.tmp" + mv "$cfg.tmp" "$cfg" + done + echo '--- effective defaultVariants ---' + jq '.flags | map_values(.defaultVariant)' "$cfg" + - name: Bring up the demo working-directory: demo run: | @@ -232,6 +278,7 @@ jobs: # template-injection vector (the same rule the clone + # capture steps already follow). DEMO_REF: ${{ inputs.demo_ref }} + FAILURE_FLAGS: ${{ inputs.failure_flags }} run: | set -euxo pipefail cd "$GITHUB_WORKSPACE/captured" @@ -241,6 +288,7 @@ jobs: echo "# OTel Demo corpus capture" echo echo "- demo_ref: ${DEMO_REF}" + echo "- failure_flags: ${FAILURE_FLAGS:-none}" echo "- records (lines): ${lines}" echo "- bytes: ${bytes}" echo @@ -277,13 +325,14 @@ jobs: RELEASE_TAG: ${{ inputs.release_tag }} DEMO_REF: ${{ inputs.demo_ref }} DURATION: ${{ inputs.duration_seconds }} + FAILURE_FLAGS: ${{ inputs.failure_flags }} run: | set -euxo pipefail gzip -kf logs.jsonl # logs.jsonl.gz, keep the original records="$(wc -l < logs.jsonl)" bytes="$(stat -c%s logs.jsonl)" - notes="$(printf 'OTel Demo corpus capture\n\n- demo_ref: %s\n- locust_users: %s\n- duration_seconds: %s\n- source commit: %s\n- records (LogsData batches): %s\n- uncompressed bytes: %s\n' \ - "$DEMO_REF" "$LOCUST_USERS" "$DURATION" "$GITHUB_SHA" "$records" "$bytes")" + notes="$(printf 'OTel Demo corpus capture\n\n- demo_ref: %s\n- locust_users: %s\n- duration_seconds: %s\n- failure_flags: %s\n- source commit: %s\n- records (LogsData batches): %s\n- uncompressed bytes: %s\n' \ + "$DEMO_REF" "$LOCUST_USERS" "$DURATION" "${FAILURE_FLAGS:-none}" "$GITHUB_SHA" "$records" "$bytes")" # Create the release if it's new, else clobber its # assets — re-minting the same tag replaces the corpus # in place rather than erroring. On re-mint the notes From 38e6d967d086b490546c6c915e98e68c3723292e Mon Sep 17 00:00:00 2001 From: Jens Holdgaard Pedersen Date: Wed, 10 Jun 2026 21:47:19 +0200 Subject: [PATCH 2/2] fix(ci): glob-safe flag parse + fail loudly on a whitespace-only failure_flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A whitespace/comma-only input passed the non-empty if: but applied nothing — a silently flagless five-hour capture. Parse via read -ra (no command-substitution globbing) and error when zero flags were applied. Co-Authored-By: Claude Fable 5 --- .github/workflows/capture-otel-demo-corpus.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/capture-otel-demo-corpus.yml b/.github/workflows/capture-otel-demo-corpus.yml index 7224ad8dc..ad256bd31 100644 --- a/.github/workflows/capture-otel-demo-corpus.yml +++ b/.github/workflows/capture-otel-demo-corpus.yml @@ -133,7 +133,15 @@ jobs: # bring-up is the whole injection — same idea as the # collector-overlay step above. cfg=demo/src/flagd/demo.flagd.json - for flag in $(tr ',' ' ' <<< "$FAILURE_FLAGS"); do + # `read -ra` (not command substitution) so glob characters + # can't expand against the workspace before validation; + # commas normalise to spaces first. Count what is applied — + # a whitespace-only input must fail loudly rather than + # silently capturing five hours with no errors enabled. + IFS=' ' read -ra FLAGS <<< "${FAILURE_FLAGS//,/ }" + applied=0 + for flag in "${FLAGS[@]}"; do + [ -n "$flag" ] || continue if ! [[ "$flag" =~ ^[A-Za-z0-9_-]+$ ]]; then echo "::error::invalid flag name '$flag' (expected [A-Za-z0-9_-]+)" exit 1 @@ -155,7 +163,12 @@ jobs: jq --arg f "$flag" --arg v "$variant" \ '.flags[$f].defaultVariant = $v' "$cfg" > "$cfg.tmp" mv "$cfg.tmp" "$cfg" + applied=$((applied + 1)) done + if [ "$applied" -eq 0 ]; then + echo "::error::failure_flags was set but contained no flag names" + exit 1 + fi echo '--- effective defaultVariants ---' jq '.flags | map_values(.defaultVariant)' "$cfg"