Skip to content

feat(ci): run liquid tests in parallel, grouped per firm and template type#27

Merged
Benjvandam merged 8 commits into
mainfrom
run_tests_in_parallel
Jul 3, 2026
Merged

feat(ci): run liquid tests in parallel, grouped per firm and template type#27
Benjvandam merged 8 commits into
mainfrom
run_tests_in_parallel

Conversation

@Benjvandam

@Benjvandam Benjvandam commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What this does

Two things:

  1. Runs the liquid tests in parallel instead of one template at a time.
  2. Also tests account templates, not just reconciliations.

The silverfin-cli can already run several templates in a single run-test --status call (it runs them at the same time via Promise.all). The workflow wasn't using that - it called the CLI once per template in a bash loop. Now we group the changed templates and run each group in one call.

Account templates (new)

Before this PR only reconciliation_texts were tested. The CLI can also test account_templates (--account-template), and a repo like be_market has 20 of them that were never run in CI.

This was confirmed on the current @main workflow: see silverfin/be_market#2950, where a PR that changes only an account template skips the test job entirely - the template isn't even detected (the old filter only matches reconciliation_texts|shared_parts, and the old test step only ran reconciliation_texts).

This PR tests them the same way as reconciliations. The CLI takes one firm and one template type per call, so templates are grouped by <firm>|<type> and each group is one call. Account template folder names contain spaces and characters like & ( ) ', so the whole path is made space-safe: the changed-files list is passed around as JSON, config reads are quoted, and the output is matched by exact template name instead of a regex.

Bugs we found and fixed along the way

  • Shared-parts-only PRs crashed the test step. The old workflow resolved a firm for every changed template, but shared parts have .id as a plain string (not a firm -> id map), so jq errored and aborted the step. We now decide the template type first and skip anything without liquid tests.
  • Names with &, ( or ) silently skipped all tests. tj-actions/changed-files defaults (escape_json, safe_output) backslash-escape the JSON values, which makes them invalid JSON - jq failed, the template list came out empty, and the job skipped while staying green. Fixed with escape_json: false + safe_output: false (safe: the list is parsed with jq, never eval'd by the shell), and the filter now fails loudly if the JSON can't be parsed, so this class of bug can't silently skip tests again.
  • Names with non-ASCII characters were silently dropped. A folder like ...creditnota's (curly apostrophe) was emitted octal-escaped and never matched the changed-files glob - so the template was never tested. Fixed with the changed-files action's quotepath: false input (its own default overrides git's global core.quotepath, so the input is the knob that matters).
  • Templates with an empty test YAML were reported as FAILED. The CLI returns FAILED when the test file has no content, but a template without tests shouldn't fail CI. The workflow now skips those before queuing ("no liquid tests defined").

Details

  • Firm is resolved with the existing rules (test_firm_id -> SF_TEST_FIRM_ID -> first key of .id).
  • Each group runs in batches capped at MAX_PARALLEL_TESTS (default 10), batches run one after another, so no more than that many tests run at once (the CLI has no rate limiting).
  • run-test --status exits 0 even when a test fails, so we read the <name>: PASSED/FAILED lines from stdout and fail the job if any failed. In CI consola prefixes those lines with [log] and the spinner interleaves frames, so the output is normalised before parsing (trailing whitespace is tolerated too).
  • Shared parts are excluded in the detection step, so a shared-parts-only PR skips the test job entirely.
  • Each parallel batch is folded into a ::group:: log block, and the job ends with a flat summary: every template with its status and firm, plus a failed/passed count.

Dropped: per-template re-read of CONFIG_JSON (intentional)

The old loop re-wrote $HOME/.silverfin/config.json from the secret before every template, "in case the tokens were updated by a concurrent workflow". That was a no-op: secrets are resolved when the job starts, so it kept writing the same value. It also wouldn't help mid-batch now that templates run in parallel. The config is loaded once at job start; keeping tokens fresh is handled separately (scheduled refresh).

How to test (for reviewers)

The reusable workflow can't run on its own - a template repo has to call it. To test this branch end to end:

  1. In a template repo (e.g. be_market), point the ref at this branch in .github/workflows/run_tests.yml:
    uses: silverfin/bso_github_actions/.github/workflows/run_tests.yml@run_tests_in_parallel
  2. On a throwaway branch, add a no-op change to a few templates so they get picked up - e.g. a {% comment %} test {% endcomment %} line in their main.liquid. Touch both reconciliations and account templates to cover both types.
  3. Add the no-test-required label so the unrelated check_tests check stays green.
  4. Open a PR and look at the Run liquid tests run. Each batch shows as a collapsible group (firm 542 · reconciliationText · 3 in parallel) and the job ends with a per-template summary.

Before / after (live runs on be_market)

Before - current @main: silverfin/be_market#2950 - changes only an account template. The test-templates job is skipped; account templates are not detected or tested today.

After - this branch: silverfin/be_market#2949 - tests both types at once, grouped per firm, including the hard names (Leningen & Leasings (42), the non-ASCII creditnota's folder) and an empty-YAML template that is now skipped instead of failing.

Follow-ups (not in this PR)

  • Cross-firm parallelism with a GitHub matrix, for PRs that touch many firms.
  • silverfin-cli hardening (separate repo):
    • make the spinner a no-op when not on a TTY, so this workflow can drop its output scrubbing;
    • emit a distinct SKIPPED/NO_TESTS status for templates with an empty test YAML instead of FAILED;
    • non-zero exit code on FAILED, a concurrency cap on Promise.all, and isolating a single hard error instead of process.exit(1) killing the whole batch.

Replace the per-template sequential test loop with a two-phase approach:
group changed reconciliations by their resolved firm, then run each firm's
handles through a single `run-test --status` call that executes them
concurrently (CLI Promise.all). Batches are capped at MAX_PARALLEL_TESTS
(default 5) and run sequentially so the total number of in-flight tests
against the live platform stays bounded, since the CLI has no client-side
rate limiting.

Also gate the reconciliation_texts check before firm resolution so
shared-part changes (whose ".id" is a plain string) are skipped cleanly
instead of aborting the step on a jq error, and drop the per-iteration
CONFIG_JSON re-write (a no-op; token refresh is handled elsewhere).
In CI, consola prefixes every line with a '[log] ' tag and the test
spinner interleaves ANSI/braille frames onto the same line, so the
per-handle status is not at the start of a line. Normalise carriage
returns and ANSI escapes and match '<handle>: PASSED/FAILED' on a left
word boundary instead of anchoring to '^', which previously caused every
handle to be reported as 'status could not be determined'.
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The workflow now detects changed template directories as JSON, batches template test runs by firm and template type, caps concurrent identifiers per invocation, and parses batched silverfin-cli run-test --status output into per-template results.

Changes

Firm-grouped batch test execution

Layer / File(s) Summary
Template change detection
.github/workflows/run_tests.yml
Changed-file detection now validates JSON output, extracts unique reconciliation_texts and account_templates directories, and emits changed_templates as a JSON array.
Batching and firm resolution
.github/workflows/run_tests.yml
Adds MAX_PARALLEL_TESTS, iterates CHANGED_TEMPLATES as JSON, derives template type and identifier, validates liquid test YAML content, and resolves FIRM_ID from test_firm_id with SF_TEST_FIRM_ID fallback handling.
Batched test execution and status parsing
.github/workflows/run_tests.yml
Template identifiers are grouped by firm and type, deduplicated, chunked into bounded batches, run through silverfin-cli run-test --status, and mapped back from normalized [log] output to PASS/FAIL summaries.

Estimated code review effort: 3 (Moderate) | ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has a strong summary but is missing the template's issue link, type-of-change section, and checklist items. Add the missing sections from the template: Fixes # link, type of change checkboxes, and the README/version/documentation checklist.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main CI change: running liquid tests in parallel grouped by firm and template type.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch run_tests_in_parallel

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
.github/workflows/run_tests.yml (1)

209-218: 💤 Low value

Handle names with regex metacharacters may cause incorrect status matching.

The HANDLE variable is interpolated directly into the grep regex pattern without escaping. If a handle contains regex metacharacters (e.g., ., +, *), the pattern could match unintended strings or fail to match correctly.

For typical alphanumeric handles this is unlikely to be an issue, but for robustness you could escape the handle:

♻️ Optional: escape handle for regex
               for HANDLE in "${BATCH[@]}"; do
+                ESCAPED_HANDLE=$(printf '%s' "${HANDLE}" | sed 's/[.[\*^$()+?{|\\]/\\&/g')
-                if printf '%s\n' "${CLEAN_OUTPUT}" | grep -qE "(^|[^[:alnum:]_])${HANDLE}: PASSED"; then
+                if printf '%s\n' "${CLEAN_OUTPUT}" | grep -qE "(^|[^[:alnum:]_])${ESCAPED_HANDLE}: PASSED"; then
                   echo "${HANDLE}: passed"
-                elif printf '%s\n' "${CLEAN_OUTPUT}" | grep -qE "(^|[^[:alnum:]_])${HANDLE}: FAILED"; then
+                elif printf '%s\n' "${CLEAN_OUTPUT}" | grep -qE "(^|[^[:alnum:]_])${ESCAPED_HANDLE}: FAILED"; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/run_tests.yml around lines 209 - 218, The HANDLE variable
is being used directly in the grep regex patterns without escaping special regex
metacharacters, which could cause incorrect pattern matching if the handle
contains characters like dot, plus, asterisk, or other regex special characters.
Before using HANDLE in the grep patterns on the lines that check for PASSED and
FAILED status, escape the variable by replacing regex metacharacters with their
escaped equivalents. You can do this by creating an escaped version of HANDLE
using sed or a similar method to replace characters like dot, asterisk, plus,
question mark, brackets, pipes, and parentheses with their backslash-escaped
versions, then use this escaped variable in the grep regex patterns instead of
the raw HANDLE variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/run_tests.yml:
- Line 107: The unquoted `${{ env.CHANGED_TEMPLATES }}` variable expansion in
the for loop is vulnerable to command injection if directory names contain shell
metacharacters. Quote the variable expansion by wrapping it as `"${{
env.CHANGED_TEMPLATES }}"` in the for loop statement to prevent unintended shell
interpretation, or alternatively refactor to use a safer iteration method such
as passing the templates as a JSON array and parsing them with jq to avoid
direct bash expansion of potentially malicious directory names.

---

Nitpick comments:
In @.github/workflows/run_tests.yml:
- Around line 209-218: The HANDLE variable is being used directly in the grep
regex patterns without escaping special regex metacharacters, which could cause
incorrect pattern matching if the handle contains characters like dot, plus,
asterisk, or other regex special characters. Before using HANDLE in the grep
patterns on the lines that check for PASSED and FAILED status, escape the
variable by replacing regex metacharacters with their escaped equivalents. You
can do this by creating an escaped version of HANDLE using sed or a similar
method to replace characters like dot, asterisk, plus, question mark, brackets,
pipes, and parentheses with their backslash-escaped versions, then use this
escaped variable in the grep regex patterns instead of the raw HANDLE variable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e87b6978-d9be-4112-9bf8-bae3ac6a0967

📥 Commits

Reviewing files that changed from the base of the PR and between d31f61b and ddf487e.

📒 Files selected for processing (1)
  • .github/workflows/run_tests.yml

Comment thread .github/workflows/run_tests.yml Outdated
The CLI runs liquid tests for both reconciliation_texts (--handle) and
account_templates (--account-template). Group changed templates by
'<firm>|<type>' so each bucket maps to one run-test call (the CLI takes a
single firm and a single type per call) and pick the flag per bucket.

Account template folder names contain spaces and characters like & ( ) ',
so make the whole path space-safe: emit the changed-files list as JSON,
hand it between steps/jobs as a JSON array, quote the config reads, and
parse the run-test output by exact template name instead of a regex.
Shared parts (and anything without liquid tests) are still skipped.
With json: true the changed-files action defaults to escape_json: true,
which double-escapes the quotes ([\"a\"] instead of ["a"]). jq then
fails to parse it, no templates are detected, and the test job is skipped.
escape_json: false emits plain JSON that jq can read.
@Benjvandam Benjvandam changed the title feat(ci): run liquid tests in parallel grouped per firm feat(ci): run liquid tests in parallel, grouped per firm and template type Jun 17, 2026
@Benjvandam Benjvandam self-assigned this Jun 17, 2026
@Benjvandam Benjvandam added the enhancement New feature or request label Jun 17, 2026
@michieldegezelle

Copy link
Copy Markdown
Collaborator

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/run_tests.yml:
- Line 231: The grep pattern on this line uses a strict `$` anchor immediately
after `(PASSED|FAILED)` which fails to match lines with trailing whitespace or
invisible characters. Modify the pattern to allow optional trailing whitespace
by inserting `[[:space:]]*` between the `(PASSED|FAILED)` group and the `$`
anchor. This will match test result lines regardless of whether they have
trailing spaces or other whitespace characters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37dcc4f2-491e-4166-9276-09aa873dc355

📥 Commits

Reviewing files that changed from the base of the PR and between d31f61b and cbdd219.

📒 Files selected for processing (1)
  • .github/workflows/run_tests.yml

Comment thread .github/workflows/run_tests.yml Outdated
Comment thread .github/workflows/run_tests.yml
Comment thread .github/workflows/run_tests.yml Outdated

@michieldegezelle michieldegezelle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Minor / question — the per-template re-fetch of CONFIG_JSON from secrets was dropped; was that intentional?

The old test loop re-ran echo '${{ secrets.CONFIG_JSON }}' > $HOME/.silverfin/config.json before each reconciliation, with the comment "fetch the newest version of the tokens from the secrets, in case they were updated by the initiation of a concurrent workflow." This PR removes that and relies on the single load in the Load Silverfin config file from secrets step at job start.

Benign in the common case (the CLI refreshes the access token locally during the run). The only scenario it changes: if a concurrent workflow rotates the OAuth refresh token mid-run, this job can no longer pick up the new value and its remaining batches would fail to authenticate. Given the move to batched parallel runs the old per-template re-read wouldn't have helped mid-batch anyway, so this is likely fine — just flagging the dropped safeguard for a conscious sign-off, since it isn't mentioned in the PR description.

Comment thread .github/workflows/run_tests.yml Outdated
Comment thread .github/workflows/run_tests.yml
Comment thread .github/workflows/run_tests.yml Outdated

@michieldegezelle michieldegezelle left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential improvements — silverfin-cli follow-ups (separate repo, not this PR)

Two CLI changes would let this workflow drop its output-scrubbing and stop mis-reporting:

  1. Spinner in non-interactive CI. lib/cli/spinner.js spin() starts unconditionally; in CI it animates via readline.clearLine/cursor escapes on a 120 ms interval — the interleaved noise this workflow strips with tr '\r' '\n' + an ANSI sed. One-line guard makes it a no-op off-TTY:
    spin(text = "…") { if (this.running || !process.stdout.isTTY) return; /* … */ }
  2. Distinct status for test-less templates. runTestsStatusOnly returns FAILED when buildTestParams bails on an empty YAML (there are no tests stored in the YAML file). Emitting SKIPPED/NO_TESTS instead would let every consumer treat "no tests yet" as not-a-failure (see the inline empty-YAML note).

…s noise

Four fixes from PR review:

- safe_output: false on changed-files. The default backslash-escapes shell
  metacharacters (& ( ) ...) inside the JSON values, which is invalid JSON,
  so jq silently emptied the template list and every test was skipped while
  CI stayed green. Safe to disable: the list is passed via env and parsed
  with jq, never eval'd by the shell.
- Fail loudly when the changed-files output is not valid JSON, so a parse
  problem can never silently skip all tests again.
- Disable git core.quotepath before changed-files, so paths with non-ASCII
  characters (e.g. a curly apostrophe in an account template name) are not
  octal-escaped and silently dropped by the glob filter.
- Skip templates whose test YAML is missing or has no real content instead
  of letting the CLI report them as FAILED - a template without tests
  should not fail CI.
- Drop shared_parts from the detection pattern: it has no liquid tests, so
  a shared-parts-only PR now skips the test job entirely instead of
  spinning it up just to skip every template.
…whitespace

Logging improvements from PR review:

- Queue phase prints one line per template ('+ <name> -> firm <id>') and
  keeps only the firm-fallback warnings; the 'Checking ...' and happy-path
  'Using ... firm ID' lines are dropped.
- Each parallel batch is wrapped in a ::group:: log group, so the raw CLI
  output (spinner frames included) folds away unless opened.
- A flat results summary at the end lists every template with its status
  and firm, plus a failed/passed count.
- The status parser now tolerates trailing whitespace on the CLI's
  'PASSED/FAILED' lines (grep anchor relaxed and the parsed status
  trimmed), so an output formatting quirk can only cause a false failure,
  never a wrong verdict.
The previous 'git config --global core.quotepath false' step was ignored:
tj-actions/changed-files sets quotepath from its own input (default true)
and overrides the global config, so a folder with a non-ASCII character
(e.g. a curly apostrophe) was still octal-escaped and dropped. Confirmed
live: the path appeared 0 times in all_changed_files. Set quotepath: false
as an action input instead.
@Benjvandam

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @michieldegezelle — all points addressed. Summary:

Finding Fix Commit
safe_output silently skips PR when a path has & ( ) safe_output: false + fail-loud JSON guard a586877
Empty test YAML reported as FAILED pre-flight skip ("no liquid tests defined") a586877
Non-ASCII folder name dropped quotepath: false action input (global git config is ignored by the action) 48d1231
shared_parts extracted only to be skipped dropped from the detection pattern a586877
Batch logging / summary ::group:: per batch + flat results summary ac9eee5
CodeRabbit: trailing whitespace relaxed grep anchor and trimmed status ac9eee5

Dropped CONFIG_JSON re-read: intentional — it was a no-op (secrets fixed at job start) and wouldn't help mid-batch anyway. Written up in the PR description so it's a conscious sign-off.

silverfin-cli follow-ups (separate repo) captured in the description: spinner no-op off-TTY, and a distinct SKIPPED/NO_TESTS status for empty-YAML templates.

Re-validated end to end on be_market run 28644775530: 15 changed templates across 4 firms — the & ( ) name, the non-ASCII creditnota’s folder, and an empty-YAML template all handled correctly. 0 failed, 14 passed (1 skipped, no tests defined).

@Benjvandam Benjvandam merged commit 4764ae6 into main Jul 3, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants