From 3b51c877b88aa3e8788ae39df0353ac328542c8b Mon Sep 17 00:00:00 2001 From: Danilo Leone Date: Wed, 20 May 2026 21:58:10 -0300 Subject: [PATCH 1/2] ci(event-names): add cross-repo EVENT_NAMES sync gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds scripts/check-event-names-sync.sh — pure Bash, no Ruby/Node runtime, that extracts the canonical event-names list from evo-ai-crm-community/lib/events/evo_flow_event_names.rb and the TS mirror in evo-flow/src/modules/events/event-names.enum.ts, sorts and diffs them. Both sides use scoped extraction (awk anchored to the %w[ ] block on Ruby and EVENT_NAMES = [ ] on TS) so a stray quoted string in a comment cannot leak in. EXPECTED_COUNT=16 catches the case where both sides grow to the same wrong size in lockstep — bump it in the same PR that adds entries. --- .github/workflows/ci.yml | 28 +++++++++++ scripts/check-event-names-sync.sh | 84 +++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100755 scripts/check-event-names-sync.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99d269ae..f03f6d16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,3 +56,31 @@ jobs: with: dockerfile: evo-ai-core-service-community/Dockerfile failure-threshold: error + + event-names-sync: + name: EvoFlow event-names sync + runs-on: ubuntu-latest + # No `paths:` filter at the workflow level (the workflow guards other jobs + # too), so this job short-circuits via a path check when nothing relevant + # changed. Cost is one ~5s job otherwise. + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Detect relevant changes + id: changed + run: | + base="${{ github.event.pull_request.base.sha }}" + head="${{ github.event.pull_request.head.sha }}" + if git diff --name-only "$base...$head" | grep -E '^(evo-ai-crm-community/lib/events/|evo-flow/src/modules/events/|scripts/check-event-names-sync\.sh|\.github/workflows/ci\.yml)' >/dev/null; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + echo "No event-names-related files changed — skipping sync check." + fi + + - name: Check event_names sync + if: steps.changed.outputs.run == 'true' + run: bash scripts/check-event-names-sync.sh diff --git a/scripts/check-event-names-sync.sh b/scripts/check-event-names-sync.sh new file mode 100755 index 00000000..38387a8f --- /dev/null +++ b/scripts/check-event-names-sync.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Verifies that the CRM canonical event-names list and the evo-flow TS mirror +# stay in lockstep. Pure Bash + grep — no Ruby/Node/jq runtime required. +# +# Ruby source: evo-ai-crm-community/lib/events/evo_flow_event_names.rb +# (EvoFlow::EVENT_NAMES, %w[...] block) +# TS source: evo-flow/src/modules/events/event-names.enum.ts +# (export const EVENT_NAMES = [ 'foo', 'bar', ... ] as const;) +# +# Exit 0 on match. Exit 1 on any of: +# - the two lists disagree (DIVERGENT) +# - the lists agree but the combined size != EXPECTED_COUNT (DIVERGENT count) +# Exit 2 on missing source files. +# +# When the canonical list legitimately grows, bump EXPECTED_COUNT here in the +# same PR that adds the entries — this is the gate that catches a 17-entry +# Ruby list + 17-entry TS list that nobody told the integration story about. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +EXPECTED_COUNT=16 + +RUBY_FILE="$REPO_ROOT/evo-ai-crm-community/lib/events/evo_flow_event_names.rb" +TS_FILE="$REPO_ROOT/evo-flow/src/modules/events/event-names.enum.ts" + +for f in "$RUBY_FILE" "$TS_FILE"; do + if [[ ! -f "$f" ]]; then + echo "event_names_sync: ERROR — file not found: $f" >&2 + exit 2 + fi +done + +# Ruby side: scope to the %w[ ... ] block, then tokenise on whitespace. +ruby_list=$( + awk ' + /%w\[/ { capture = 1; sub(/.*%w\[/, "") } + capture { + line = $0 + if (sub(/\].*/, "", line)) { print line; capture = 0; exit } + print line + } + ' "$RUBY_FILE" | tr -s '[:space:]' '\n' | sed '/^$/d' | sort -u +) + +# TS side: scope to the EVENT_NAMES = [ ... ] block, THEN extract single-quoted +# literals. Anchoring to the block prevents a stray single-quoted string in a +# comment, JSDoc, or future export from leaking into the canonical set. +ts_list=$( + awk ' + /EVENT_NAMES[[:space:]]*=[[:space:]]*\[/ { capture = 1 } + capture { + line = $0 + if (match(line, /\]/)) { print substr(line, 1, RSTART - 1); capture = 0; exit } + print line + } + ' "$TS_FILE" | grep -oE "'[a-z][a-z._]*'" | tr -d "'" | sort -u +) + +ruby_tmp=$(mktemp) +ts_tmp=$(mktemp) +diff_tmp=$(mktemp) +trap 'rm -f "$ruby_tmp" "$ts_tmp" "$diff_tmp"' EXIT +printf '%s\n' "$ruby_list" > "$ruby_tmp" +printf '%s\n' "$ts_list" > "$ts_tmp" + +if ! diff -u "$ruby_tmp" "$ts_tmp" > "$diff_tmp"; then + echo "event_names_sync: DIVERGENT — see diff below" + echo " (--- Ruby: $RUBY_FILE)" + echo " (+++ TS: $TS_FILE)" + cat "$diff_tmp" + exit 1 +fi + +count=$(wc -l < "$ruby_tmp" | tr -d '[:space:]') +if [[ "$count" != "$EXPECTED_COUNT" ]]; then + echo "event_names_sync: DIVERGENT — lists match each other but count is $count (expected $EXPECTED_COUNT)." + echo "If this growth is intentional, bump EXPECTED_COUNT in $SCRIPT_DIR/check-event-names-sync.sh in the same PR." + exit 1 +fi + +echo "event_names_sync: OK ($EXPECTED_COUNT entries)" +exit 0 From e6dff23ec682b04cea33ac2c9dc31022e5815ca6 Mon Sep 17 00:00:00 2001 From: Danilo Leone Date: Thu, 21 May 2026 09:29:07 -0300 Subject: [PATCH 2/2] ci: always run EvoFlow event-names sync check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous "Detect relevant changes" gate filtered the diff by path prefix (`^evo-ai-crm-community/lib/events/`, `^evo-flow/src/modules/events/`), which is the exact pattern `git diff --name-only` does NOT emit for the workflow that matters most here: a submodule-pointer bump. Bumps appear as a bare directory line (`evo-ai-crm-community`, `evo-flow`), so the gate evaluated to `run=false` and the sync check was silently skipped precisely when one side moved to a new EVENT_NAMES list and the other did not — defeating AC6 of the story. Drop the gate entirely. The script is pure bash and finishes in ~5s, so the cost of running unconditionally is negligible compared to the cost of chasing regex coverage for every diff shape git can emit. --- .github/workflows/ci.yml | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f03f6d16..dce64c89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,27 +60,20 @@ jobs: event-names-sync: name: EvoFlow event-names sync runs-on: ubuntu-latest - # No `paths:` filter at the workflow level (the workflow guards other jobs - # too), so this job short-circuits via a path check when nothing relevant - # changed. Cost is one ~5s job otherwise. + # Always-on (~5s of pure bash). The previous "Detect relevant changes" + # gate filtered by file paths, which was silently skipped on the workflow + # that needs the gate the most: submodule-pointer bumps. `git diff + # --name-only` for a bump emits the submodule directory as a bare line + # (e.g. `evo-ai-crm-community`), which the `^evo-ai-crm-community/lib/…` + # regex did not match — so the check was bypassed precisely when one + # submodule moved to a new event_names list and the other did not. + # Cheaper to run unconditionally than to chase regex coverage for every + # diff shape git can emit. steps: - uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - - name: Detect relevant changes - id: changed - run: | - base="${{ github.event.pull_request.base.sha }}" - head="${{ github.event.pull_request.head.sha }}" - if git diff --name-only "$base...$head" | grep -E '^(evo-ai-crm-community/lib/events/|evo-flow/src/modules/events/|scripts/check-event-names-sync\.sh|\.github/workflows/ci\.yml)' >/dev/null; then - echo "run=true" >> "$GITHUB_OUTPUT" - else - echo "run=false" >> "$GITHUB_OUTPUT" - echo "No event-names-related files changed — skipping sync check." - fi - - name: Check event_names sync - if: steps.changed.outputs.run == 'true' run: bash scripts/check-event-names-sync.sh