mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🎨 ci: Gate Frontend Jest on Codegraph Selection (Stage 1.5) (#15145)
* 🎨 ci: Gate Frontend Jest on Codegraph Selection (Stage 1.5)
* ci: a malformed FILES decision runs FULL, never skips (Codex)
* ci: dev-push runs never cancel each other (Codex P2)
* ci: workflow-file push baseline, cancellable gated jobs, pull-requests read (Codex r4)
* ci: selected paths must live under their workspace, else FULL (Codex r5)
* ci: drop stale selected paths, run FULL when none exist (Codex r6)
This commit is contained in:
parent
d85bea41cc
commit
345c7aea7d
2 changed files with 251 additions and 22 deletions
99
.github/workflows/backend-review.yml
vendored
99
.github/workflows/backend-review.yml
vendored
|
|
@ -27,9 +27,15 @@ on:
|
|||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# PR pushes supersede each other (per-PR canceling group). Push events get a PER-COMMIT group:
|
||||
# dev-push runs are the post-merge safety net and the full-run baseline, and with a shared
|
||||
# canceling group closely spaced merges cancel each other's runs — observed live on 2026-08-23,
|
||||
# when three consecutive dev merges cancelled the runs that would have caught #15142's red
|
||||
# (Codex P2 on #15145).
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
|
|
@ -198,10 +204,24 @@ jobs:
|
|||
mode=$(echo "$RESP" | jq -r --arg w "$ws" '.selected[$w].mode')
|
||||
files=""
|
||||
if [ "$mode" = "FILES" ]; then
|
||||
files=$(echo "$RESP" | jq -r --arg w "$ws" --arg p "$ws/" --arg ig "$ignore" '.selected[$w].files // [] | map(select((test(" ") | not) and (($ig == "") or (test($ig) | not)))) | map(ltrimstr($p)) | join(" ")')
|
||||
spaced=$(echo "$RESP" | jq -r --arg w "$ws" '[.selected[$w].files // [] | .[] | select(test(" "))] | length')
|
||||
if [ "$spaced" != "0" ]; then mode="FULL"; files=""; fi
|
||||
if [ "$mode" = "FILES" ] && [ -z "$files" ]; then mode="NONE"; fi
|
||||
# Only an explicit NONE may skip. FILES with a missing/empty list is malformed and
|
||||
# runs FULL (Codex P1 on #15145). A NON-empty list that the ignore regex filters to
|
||||
# nothing is different and legitimately NONE: those files are exactly what this
|
||||
# workspace's own jest run excludes, so full CI would not run them either.
|
||||
raw_n=$(echo "$RESP" | jq -r --arg w "$ws" '.selected[$w].files // [] | length')
|
||||
# Every selected path must live under the workspace: a wrong-prefixed path would
|
||||
# survive ltrimstr, match nothing in the workspace cwd, and --passWithNoTests would
|
||||
# turn "ran nothing" into green — a silent fail-closed (Codex P1 on #15145).
|
||||
misplaced=$(echo "$RESP" | jq -r --arg w "$ws" --arg p "$ws/" '[.selected[$w].files // [] | .[] | select(startswith($p) | not)] | length')
|
||||
if [ "$raw_n" = "0" ] || [ "$misplaced" != "0" ]; then
|
||||
mode="FULL"
|
||||
note "| $ws | malformed FILES decision ($raw_n files, $misplaced outside $ws/); running FULL |"
|
||||
else
|
||||
files=$(echo "$RESP" | jq -r --arg w "$ws" --arg p "$ws/" --arg ig "$ignore" '.selected[$w].files // [] | map(select((test(" ") | not) and (($ig == "") or (test($ig) | not)))) | map(ltrimstr($p)) | join(" ")')
|
||||
spaced=$(echo "$RESP" | jq -r --arg w "$ws" '[.selected[$w].files // [] | .[] | select(test(" "))] | length')
|
||||
if [ "$spaced" != "0" ]; then mode="FULL"; files=""; fi
|
||||
if [ "$mode" = "FILES" ] && [ -z "$files" ]; then mode="NONE"; fi
|
||||
fi
|
||||
fi
|
||||
if [ "$mode" = "NONE" ]; then
|
||||
echo "${key}_run=false" >> "$GITHUB_OUTPUT"
|
||||
|
|
@ -325,7 +345,7 @@ jobs:
|
|||
test-api:
|
||||
name: 'Tests: api (shard ${{ matrix.shard }}/3)'
|
||||
needs: [build, codegraph_select]
|
||||
if: always() && needs.build.result == 'success' && needs.codegraph_select.outputs.api_run != 'false'
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' && needs.codegraph_select.outputs.api_run != 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
|
|
@ -409,9 +429,24 @@ jobs:
|
|||
SELECTED: ${{ needs.codegraph_select.outputs.api_files }}
|
||||
run: |
|
||||
cd api
|
||||
# A selected path can be stale in exactly two ways at this checkout (Codex P2, #15145 r6):
|
||||
# deleted on the branch — dropped, which matches full CI (the file runs nowhere) — or
|
||||
# renamed, where the NEW path is a changed test file and is selected independently. If
|
||||
# NOTHING selected exists, the selection is stale wholesale and the suite runs FULL;
|
||||
# --passWithNoTests must never turn "ran nothing" into green.
|
||||
if [ -n "$SELECTED" ]; then
|
||||
echo "codegraph: $(echo $SELECTED | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/3 --passWithNoTests --runTestsByPath $SELECTED
|
||||
KEEP=""
|
||||
for f in $SELECTED; do
|
||||
if [ -f "$f" ]; then KEEP="$KEEP $f"; else echo "dropping selected path absent at HEAD (deleted or renamed): $f"; fi
|
||||
done
|
||||
KEEP="${KEEP# }"
|
||||
if [ -z "$KEEP" ]; then
|
||||
echo "no selected test file exists at HEAD (stale selection); running FULL"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/3
|
||||
else
|
||||
echo "codegraph: $(echo $KEEP | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/3 --passWithNoTests --runTestsByPath $KEEP
|
||||
fi
|
||||
else
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/3
|
||||
fi
|
||||
|
|
@ -419,7 +454,7 @@ jobs:
|
|||
test-data-provider:
|
||||
name: 'Tests: data-provider'
|
||||
needs: [build, codegraph_select]
|
||||
if: always() && needs.build.result == 'success' && needs.codegraph_select.outputs.dataprovider_run != 'false'
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' && needs.codegraph_select.outputs.dataprovider_run != 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
|
|
@ -471,8 +506,18 @@ jobs:
|
|||
run: |
|
||||
cd packages/data-provider
|
||||
if [ -n "$SELECTED" ]; then
|
||||
echo "codegraph: $(echo $SELECTED | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --passWithNoTests --runTestsByPath $SELECTED
|
||||
KEEP=""
|
||||
for f in $SELECTED; do
|
||||
if [ -f "$f" ]; then KEEP="$KEEP $f"; else echo "dropping selected path absent at HEAD (deleted or renamed): $f"; fi
|
||||
done
|
||||
KEEP="${KEEP# }"
|
||||
if [ -z "$KEEP" ]; then
|
||||
echo "no selected test file exists at HEAD (stale selection); running FULL"
|
||||
npm run test:ci
|
||||
else
|
||||
echo "codegraph: $(echo $KEEP | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --passWithNoTests --runTestsByPath $KEEP
|
||||
fi
|
||||
else
|
||||
npm run test:ci
|
||||
fi
|
||||
|
|
@ -480,7 +525,7 @@ jobs:
|
|||
test-data-schemas:
|
||||
name: 'Tests: data-schemas'
|
||||
needs: [build, codegraph_select]
|
||||
if: always() && needs.build.result == 'success' && needs.codegraph_select.outputs.dataschemas_run != 'false'
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' && needs.codegraph_select.outputs.dataschemas_run != 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
|
|
@ -538,8 +583,18 @@ jobs:
|
|||
run: |
|
||||
cd packages/data-schemas
|
||||
if [ -n "$SELECTED" ]; then
|
||||
echo "codegraph: $(echo $SELECTED | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --passWithNoTests --runTestsByPath $SELECTED
|
||||
KEEP=""
|
||||
for f in $SELECTED; do
|
||||
if [ -f "$f" ]; then KEEP="$KEEP $f"; else echo "dropping selected path absent at HEAD (deleted or renamed): $f"; fi
|
||||
done
|
||||
KEEP="${KEEP# }"
|
||||
if [ -z "$KEEP" ]; then
|
||||
echo "no selected test file exists at HEAD (stale selection); running FULL"
|
||||
npm run test:ci
|
||||
else
|
||||
echo "codegraph: $(echo $KEEP | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --passWithNoTests --runTestsByPath $KEEP
|
||||
fi
|
||||
else
|
||||
npm run test:ci
|
||||
fi
|
||||
|
|
@ -547,7 +602,7 @@ jobs:
|
|||
test-packages-api:
|
||||
name: 'Tests: @librechat/api (shard ${{ matrix.shard }}/4)'
|
||||
needs: [build, codegraph_select]
|
||||
if: always() && needs.build.result == 'success' && needs.codegraph_select.outputs.pkgapi_run != 'false'
|
||||
if: ${{ !cancelled() && needs.build.result == 'success' && needs.codegraph_select.outputs.pkgapi_run != 'false' }}
|
||||
runs-on: ubuntu-latest
|
||||
# Suite typically completes in ~5 min on a warm runner, but tail-latency
|
||||
# cancellations have started showing up: tests are actively passing right
|
||||
|
|
@ -619,8 +674,18 @@ jobs:
|
|||
run: |
|
||||
cd packages/api
|
||||
if [ -n "$SELECTED" ]; then
|
||||
echo "codegraph: $(echo $SELECTED | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/4 --passWithNoTests --runTestsByPath $SELECTED
|
||||
KEEP=""
|
||||
for f in $SELECTED; do
|
||||
if [ -f "$f" ]; then KEEP="$KEEP $f"; else echo "dropping selected path absent at HEAD (deleted or renamed): $f"; fi
|
||||
done
|
||||
KEEP="${KEEP# }"
|
||||
if [ -z "$KEEP" ]; then
|
||||
echo "no selected test file exists at HEAD (stale selection); running FULL"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/4
|
||||
else
|
||||
echo "codegraph: $(echo $KEEP | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/4 --passWithNoTests --runTestsByPath $KEEP
|
||||
fi
|
||||
else
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/4
|
||||
fi
|
||||
|
|
|
|||
174
.github/workflows/frontend-review.yml
vendored
174
.github/workflows/frontend-review.yml
vendored
|
|
@ -10,18 +10,135 @@ on:
|
|||
- 'package-lock.json'
|
||||
- '.github/workflows/frontend-review.yml'
|
||||
- '!**.md'
|
||||
# Post-merge safety net and full-run baseline for gated selection (same rationale as
|
||||
# backend-review.yml stage 1): every dev merge touching frontend paths runs the full suite.
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- 'client/**'
|
||||
- 'packages/client/**'
|
||||
- 'packages/data-provider/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- '.github/workflows/frontend-review.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
# PR pushes supersede each other (per-PR canceling group). Push events get a PER-COMMIT group:
|
||||
# dev-push runs are the post-merge safety net and the full-run baseline, and with a shared
|
||||
# canceling group closely spaced merges cancel each other's runs — observed live on 2026-08-23,
|
||||
# when three consecutive dev merges cancelled the runs that would have caught #15142's red
|
||||
# (Codex P2 on #15145).
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=${{ secrets.NODE_MAX_OLD_SPACE_SIZE || 6144 }}'
|
||||
|
||||
jobs:
|
||||
# Stage 1.5 of codegraph gating (stage 1 = backend jest, #15132; stage 2 = matrix lanes,
|
||||
# #15136). Same mechanism, same record: across 604 finalized shadow receipts the frontend
|
||||
# selection has zero structural misses (its one raw MISSED was a chronic flake), over 500
|
||||
# narrowed decisions. Selected (safe mode) on synchronize only; full on PR open/reopen, on
|
||||
# every dev push, and on any doubt. A workspace skips ONLY on an explicit NONE. Kill switch:
|
||||
# repo variable CODEGRAPH_GATING=off. Neither frontend workspace defines
|
||||
# testPathIgnorePatterns, so --runTestsByPath needs no exclude mirroring here.
|
||||
codegraph_select:
|
||||
name: Codegraph select
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.action == 'synchronize' &&
|
||||
vars.CODEGRAPH_GATING != 'off'
|
||||
outputs:
|
||||
decided: ${{ steps.sel.outputs.decided }}
|
||||
client_run: ${{ steps.sel.outputs.client_run }}
|
||||
client_files: ${{ steps.sel.outputs.client_files }}
|
||||
clientpkg_run: ${{ steps.sel.outputs.clientpkg_run }}
|
||||
clientpkg_files: ${{ steps.sel.outputs.clientpkg_files }}
|
||||
steps:
|
||||
- name: Select tests, fail open on any doubt
|
||||
id: sel
|
||||
env:
|
||||
URL: ${{ secrets.CODEGRAPH_URL }}
|
||||
TOKEN: ${{ secrets.CODEGRAPH_TOKEN }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
PR: ${{ github.event.pull_request.number }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
CHANGED: ${{ github.event.pull_request.changed_files }}
|
||||
run: |
|
||||
set +e
|
||||
note() { echo "$1" >> "$GITHUB_STEP_SUMMARY"; }
|
||||
note "### Codegraph select — GATING (frontend jest)"
|
||||
if [ -z "$URL" ] || [ -z "$TOKEN" ]; then note "_no codegraph config; running FULL_"; exit 0; fi
|
||||
if ! gh api "repos/$REPO/pulls/$PR/files" --paginate \
|
||||
--jq '.[] | {path: .filename, status, patch}' > files.ndjson; then
|
||||
note "_could not fetch changed files; running FULL_"; exit 0
|
||||
fi
|
||||
jq -s . files.ndjson > files.json
|
||||
N=$(jq 'length' files.json)
|
||||
if [ "$N" -eq 0 ] || { [ -n "$CHANGED" ] && [ "$N" -ne "$CHANGED" ]; }; then
|
||||
note "_changed-file list incomplete ($N of ${CHANGED:-?}); running FULL_"; exit 0
|
||||
fi
|
||||
jq -c --arg b "$BASE_SHA" --arg h "$HEAD_SHA" \
|
||||
'{files: ., mode: "safe", lockBaseSha: $b, lockHeadSha: $h}' files.json > body.json
|
||||
RESP=$(curl -sS --fail-with-body -m 45 -H "Authorization: Bearer $TOKEN" \
|
||||
-H 'content-type: application/json' --data-binary @body.json "$URL/v1/select"); RC=$?
|
||||
if [ "$RC" -ne 0 ] || [ -z "$RESP" ] || ! echo "$RESP" | jq -e '.selected.client.mode' >/dev/null 2>&1; then
|
||||
note "_codegraph unavailable (curl exit $RC: ${RESP:0:120}); running FULL_"
|
||||
exit 0
|
||||
fi
|
||||
emit() {
|
||||
key="$1"; ws="$2"
|
||||
mode=$(echo "$RESP" | jq -r --arg w "$ws" '.selected[$w].mode')
|
||||
files=""
|
||||
if [ "$mode" = "FILES" ]; then
|
||||
# Only an explicit NONE may skip. FILES with a missing/empty list is a malformed
|
||||
# decision (service/schema skew) and must run FULL (Codex P1 on #15145).
|
||||
raw_n=$(echo "$RESP" | jq -r --arg w "$ws" '.selected[$w].files // [] | length')
|
||||
# Every selected path must live under the workspace: a wrong-prefixed path would
|
||||
# survive ltrimstr, match nothing in the workspace cwd, and --passWithNoTests would
|
||||
# turn "ran nothing" into green — a silent fail-closed (Codex P1 on #15145).
|
||||
misplaced=$(echo "$RESP" | jq -r --arg w "$ws" --arg p "$ws/" '[.selected[$w].files // [] | .[] | select(startswith($p) | not)] | length')
|
||||
if [ "$raw_n" = "0" ] || [ "$misplaced" != "0" ]; then
|
||||
mode="FULL"
|
||||
note "| $ws | malformed FILES decision ($raw_n files, $misplaced outside $ws/); running FULL |"
|
||||
else
|
||||
files=$(echo "$RESP" | jq -r --arg w "$ws" --arg p "$ws/" \
|
||||
'.selected[$w].files // [] | map(select(test(" ") | not)) | map(ltrimstr($p)) | join(" ")')
|
||||
spaced=$(echo "$RESP" | jq -r --arg w "$ws" '[.selected[$w].files // [] | .[] | select(test(" "))] | length')
|
||||
if [ "$spaced" != "0" ]; then mode="FULL"; files=""; fi
|
||||
fi
|
||||
fi
|
||||
if [ "$mode" = "NONE" ]; then
|
||||
echo "${key}_run=false" >> "$GITHUB_OUTPUT"
|
||||
note "| $ws | skip (no reachable tests) |"
|
||||
elif [ "$mode" = "FILES" ]; then
|
||||
n=$(echo "$files" | wc -w | tr -d ' ')
|
||||
echo "${key}_run=true" >> "$GITHUB_OUTPUT"
|
||||
echo "${key}_files=$files" >> "$GITHUB_OUTPUT"
|
||||
note "| $ws | $n selected files |"
|
||||
else
|
||||
echo "${key}_run=true" >> "$GITHUB_OUTPUT"
|
||||
note "| $ws | FULL |"
|
||||
fi
|
||||
}
|
||||
note "| workspace | decision |"
|
||||
note "|---|---|"
|
||||
emit client client
|
||||
emit clientpkg packages/client
|
||||
echo "decided=true" >> "$GITHUB_OUTPUT"
|
||||
note ""
|
||||
note "kill switch: repo variable \`CODEGRAPH_GATING=off\`; full runs remain on PR open and on every dev push"
|
||||
exit 0
|
||||
|
||||
build:
|
||||
name: Build packages
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -131,7 +248,10 @@ jobs:
|
|||
|
||||
test-packages-client:
|
||||
name: 'Tests: @librechat/client'
|
||||
needs: build
|
||||
needs: [build, codegraph_select]
|
||||
if: >-
|
||||
!cancelled() && needs.build.result == 'success' &&
|
||||
needs.codegraph_select.outputs.clientpkg_run != 'false'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
|
|
@ -164,12 +284,38 @@ jobs:
|
|||
path: packages/data-provider/dist
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:ci
|
||||
env:
|
||||
SELECTED: ${{ needs.codegraph_select.outputs.clientpkg_files }}
|
||||
run: |
|
||||
# A selected path can be stale in exactly two ways at this checkout (Codex P2, #15145 r6):
|
||||
# deleted on the branch — dropped, which matches full CI (the file runs nowhere) — or
|
||||
# renamed, where the NEW path is a changed test file and is selected independently. If
|
||||
# NOTHING selected exists, the selection is stale wholesale and the suite runs FULL;
|
||||
# --passWithNoTests must never turn "ran nothing" into green.
|
||||
if [ -n "$SELECTED" ]; then
|
||||
KEEP=""
|
||||
for f in $SELECTED; do
|
||||
if [ -f "$f" ]; then KEEP="$KEEP $f"; else echo "dropping selected path absent at HEAD (deleted or renamed): $f"; fi
|
||||
done
|
||||
KEEP="${KEEP# }"
|
||||
if [ -z "$KEEP" ]; then
|
||||
echo "no selected test file exists at HEAD (stale selection); running FULL"
|
||||
npm run test:ci
|
||||
else
|
||||
echo "codegraph: $(echo $KEEP | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --passWithNoTests --runTestsByPath $KEEP
|
||||
fi
|
||||
else
|
||||
npm run test:ci
|
||||
fi
|
||||
working-directory: packages/client
|
||||
|
||||
test-ubuntu:
|
||||
name: 'Tests: Ubuntu (shard ${{ matrix.shard }}/2)'
|
||||
needs: build
|
||||
needs: [build, codegraph_select]
|
||||
if: >-
|
||||
!cancelled() && needs.build.result == 'success' &&
|
||||
needs.codegraph_select.outputs.client_run != 'false'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
strategy:
|
||||
|
|
@ -212,7 +358,25 @@ jobs:
|
|||
path: packages/client/dist
|
||||
|
||||
- name: Run unit tests (shard ${{ matrix.shard }}/2)
|
||||
run: npm run test:ci -- --shard=${{ matrix.shard }}/2
|
||||
env:
|
||||
SELECTED: ${{ needs.codegraph_select.outputs.client_files }}
|
||||
run: |
|
||||
if [ -n "$SELECTED" ]; then
|
||||
KEEP=""
|
||||
for f in $SELECTED; do
|
||||
if [ -f "$f" ]; then KEEP="$KEEP $f"; else echo "dropping selected path absent at HEAD (deleted or renamed): $f"; fi
|
||||
done
|
||||
KEEP="${KEEP# }"
|
||||
if [ -z "$KEEP" ]; then
|
||||
echo "no selected test file exists at HEAD (stale selection); running FULL"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/2
|
||||
else
|
||||
echo "codegraph: $(echo $KEEP | wc -w) selected test files (safe mode)"
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/2 --passWithNoTests --runTestsByPath $KEEP
|
||||
fi
|
||||
else
|
||||
npm run test:ci -- --shard=${{ matrix.shard }}/2
|
||||
fi
|
||||
working-directory: client
|
||||
|
||||
build-verify:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue