Skip to content

Commit be93f79

Browse files
Ryan DombrowskiRyan Dombrowski
authored andcommitted
feat(Phase 14F): Multi-Source CI (Matrix) + Deterministic Source Discovery
Implements multi-source reconciliation for CI with GitHub Actions matrix strategy: - Source Discovery: - Manifest file support (reconcile.sources.json) - Glob pattern matching with ignore patterns - Deterministic lexicographic sorting - Path normalization and deduplication - Chunking: - Balanced distribution for matrix jobs - Configurable chunk size with min/max constraints - Matrix indices output for GitHub Actions - Aggregation: - FAIL > WARN > PASS precedence - Summary counts and exit codes - Git SHA traceability - CLI (figma:sources): - Discovery with --glob, --manifest, --source flags - Chunking with --chunk-size, --chunk-index - Matrix output with --matrix-indices - Multiple output formats (json, list, count) - Matrix Workflow: - .github/workflows/figma-reconcile-ci-matrix.yml - 3 jobs: discover, reconcile (matrix), aggregate - Artifact upload per chunk - Tests: 52 new tests (all passing) - README: Updated feature table and documentation
1 parent a6b4c80 commit be93f79

12 files changed

Lines changed: 2514 additions & 93 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
# Phase 14F: Matrix Reconciliation CI Workflow
2+
#
3+
# WHY: Runs figma:reconcile across multiple source files in parallel using
4+
# GitHub Actions matrix strategy, then aggregates results into a single verdict.
5+
#
6+
# TRIGGERS:
7+
# - Pull requests touching React components
8+
# - Manual workflow_dispatch with optional source filter
9+
#
10+
# STRATEGY:
11+
# 1. Discovery job: Find all source files, chunk into batches
12+
# 2. Matrix job: Run reconcile for each chunk in parallel
13+
# 3. Aggregate job: Combine verdicts, produce overall PASS/WARN/FAIL
14+
15+
name: Figma Reconcile (Matrix)
16+
17+
on:
18+
pull_request:
19+
paths:
20+
- 'demo-app/**/*.tsx'
21+
- 'packages/**/*.tsx'
22+
workflow_dispatch:
23+
inputs:
24+
source-glob:
25+
description: 'Glob pattern for source files'
26+
required: false
27+
default: '**/*.tsx'
28+
chunk-size:
29+
description: 'Number of sources per matrix job'
30+
required: false
31+
default: '10'
32+
33+
concurrency:
34+
group: figma-reconcile-matrix-${{ github.ref }}
35+
cancel-in-progress: true
36+
37+
jobs:
38+
# =========================================================================
39+
# DISCOVERY JOB
40+
# =========================================================================
41+
discover:
42+
name: Discover Sources
43+
runs-on: ubuntu-latest
44+
outputs:
45+
sources: ${{ steps.discover.outputs.sources }}
46+
chunk-indices: ${{ steps.discover.outputs.chunk-indices }}
47+
total-chunks: ${{ steps.discover.outputs.total-chunks }}
48+
total-sources: ${{ steps.discover.outputs.total-sources }}
49+
steps:
50+
- name: Checkout
51+
uses: actions/checkout@v4
52+
53+
- name: Setup pnpm
54+
uses: pnpm/action-setup@v4
55+
with:
56+
version: 9
57+
58+
- name: Setup Node.js
59+
uses: actions/setup-node@v4
60+
with:
61+
node-version: '20'
62+
cache: 'pnpm'
63+
64+
- name: Install dependencies
65+
run: pnpm install --frozen-lockfile
66+
67+
- name: Discover sources
68+
id: discover
69+
run: |
70+
cd packages/watcher
71+
72+
# Get chunk size from inputs or default
73+
CHUNK_SIZE="${{ github.event.inputs.chunk-size || '10' }}"
74+
GLOB="${{ github.event.inputs.source-glob || '**/*.tsx' }}"
75+
76+
# Run discovery
77+
RESULT=$(pnpm figma:sources --glob "$GLOB" --repo-root "${{ github.workspace }}" 2>&1 || true)
78+
79+
# Parse results
80+
SOURCES=$(echo "$RESULT" | jq -c '.sources // []')
81+
COUNT=$(echo "$RESULT" | jq -r '.count // 0')
82+
83+
# Get chunk indices
84+
INDICES=$(pnpm figma:sources --glob "$GLOB" --repo-root "${{ github.workspace }}" --matrix-indices --chunk-size "$CHUNK_SIZE" 2>&1 || echo "[]")
85+
TOTAL_CHUNKS=$(echo "$INDICES" | jq 'length')
86+
87+
# Output
88+
echo "sources=$SOURCES" >> $GITHUB_OUTPUT
89+
echo "chunk-indices=$INDICES" >> $GITHUB_OUTPUT
90+
echo "total-chunks=$TOTAL_CHUNKS" >> $GITHUB_OUTPUT
91+
echo "total-sources=$COUNT" >> $GITHUB_OUTPUT
92+
93+
echo "::notice::Discovered $COUNT sources across $TOTAL_CHUNKS chunks"
94+
95+
# =========================================================================
96+
# MATRIX RECONCILE JOB
97+
# =========================================================================
98+
reconcile:
99+
name: Reconcile Chunk ${{ matrix.chunk-index }}
100+
runs-on: ubuntu-latest
101+
needs: discover
102+
if: needs.discover.outputs.total-chunks != '0'
103+
strategy:
104+
fail-fast: false
105+
matrix:
106+
chunk-index: ${{ fromJson(needs.discover.outputs.chunk-indices) }}
107+
outputs:
108+
verdict: ${{ steps.reconcile.outputs.verdict }}
109+
steps:
110+
- name: Checkout
111+
uses: actions/checkout@v4
112+
113+
- name: Setup pnpm
114+
uses: pnpm/action-setup@v4
115+
with:
116+
version: 9
117+
118+
- name: Setup Node.js
119+
uses: actions/setup-node@v4
120+
with:
121+
node-version: '20'
122+
cache: 'pnpm'
123+
124+
- name: Install dependencies
125+
run: pnpm install --frozen-lockfile
126+
127+
- name: Get chunk sources
128+
id: chunk
129+
run: |
130+
cd packages/watcher
131+
132+
CHUNK_SIZE="${{ github.event.inputs.chunk-size || '10' }}"
133+
GLOB="${{ github.event.inputs.source-glob || '**/*.tsx' }}"
134+
CHUNK_INDEX="${{ matrix.chunk-index }}"
135+
136+
# Get sources for this chunk
137+
SOURCES=$(pnpm figma:sources --glob "$GLOB" --repo-root "${{ github.workspace }}" --chunk-size "$CHUNK_SIZE" --chunk-index "$CHUNK_INDEX" 2>&1 || echo "[]")
138+
139+
echo "sources=$SOURCES" >> $GITHUB_OUTPUT
140+
echo "::notice::Chunk $CHUNK_INDEX sources: $SOURCES"
141+
142+
- name: Run reconciliation
143+
id: reconcile
144+
run: |
145+
cd packages/watcher
146+
147+
SOURCES='${{ steps.chunk.outputs.sources }}'
148+
CHUNK_INDEX="${{ matrix.chunk-index }}"
149+
VERDICT="PASS"
150+
ARTIFACT_DIR="artifacts/chunk-$CHUNK_INDEX"
151+
152+
mkdir -p "$ARTIFACT_DIR"
153+
154+
# Process each source in this chunk
155+
for SOURCE in $(echo "$SOURCES" | jq -r '.[]'); do
156+
echo "::group::Reconciling $SOURCE"
157+
158+
# Run reconcile for this source
159+
RESULT=$(pnpm figma:reconcile --source "$SOURCE" --profile ci --no-fail 2>&1 || true)
160+
SOURCE_VERDICT=$(echo "$RESULT" | grep -o 'verdict:[A-Z]*' | cut -d: -f2 || echo "PASS")
161+
162+
# Update overall verdict (FAIL > WARN > PASS)
163+
if [ "$SOURCE_VERDICT" = "FAIL" ]; then
164+
VERDICT="FAIL"
165+
elif [ "$SOURCE_VERDICT" = "WARN" ] && [ "$VERDICT" != "FAIL" ]; then
166+
VERDICT="WARN"
167+
fi
168+
169+
# Save result to artifact
170+
echo "$RESULT" > "$ARTIFACT_DIR/$(echo $SOURCE | tr '/' '_').txt"
171+
172+
echo "::endgroup::"
173+
done
174+
175+
echo "verdict=$VERDICT" >> $GITHUB_OUTPUT
176+
echo "::notice::Chunk $CHUNK_INDEX verdict: $VERDICT"
177+
178+
- name: Upload chunk artifacts
179+
uses: actions/upload-artifact@v4
180+
with:
181+
name: reconcile-chunk-${{ matrix.chunk-index }}
182+
path: packages/watcher/artifacts/chunk-${{ matrix.chunk-index }}/
183+
retention-days: 7
184+
if-no-files-found: ignore
185+
186+
# =========================================================================
187+
# AGGREGATION JOB
188+
# =========================================================================
189+
aggregate:
190+
name: Aggregate Results
191+
runs-on: ubuntu-latest
192+
needs: [discover, reconcile]
193+
if: always() && needs.discover.result == 'success'
194+
steps:
195+
- name: Download all artifacts
196+
uses: actions/download-artifact@v4
197+
with:
198+
path: artifacts
199+
pattern: reconcile-chunk-*
200+
merge-multiple: false
201+
202+
- name: Aggregate verdicts
203+
id: aggregate
204+
run: |
205+
TOTAL_SOURCES="${{ needs.discover.outputs.total-sources }}"
206+
207+
# Collect all verdicts from matrix outputs
208+
# Since we can't easily get matrix outputs, we'll scan artifact files
209+
210+
PASS_COUNT=0
211+
WARN_COUNT=0
212+
FAIL_COUNT=0
213+
OVERALL="PASS"
214+
215+
# Scan artifact directories for verdicts
216+
if [ -d "artifacts" ]; then
217+
for CHUNK_DIR in artifacts/reconcile-chunk-*; do
218+
if [ -d "$CHUNK_DIR" ]; then
219+
for FILE in "$CHUNK_DIR"/*.txt; do
220+
if [ -f "$FILE" ]; then
221+
if grep -q "verdict:FAIL" "$FILE" 2>/dev/null; then
222+
FAIL_COUNT=$((FAIL_COUNT + 1))
223+
OVERALL="FAIL"
224+
elif grep -q "verdict:WARN" "$FILE" 2>/dev/null; then
225+
WARN_COUNT=$((WARN_COUNT + 1))
226+
if [ "$OVERALL" != "FAIL" ]; then
227+
OVERALL="WARN"
228+
fi
229+
else
230+
PASS_COUNT=$((PASS_COUNT + 1))
231+
fi
232+
fi
233+
done
234+
fi
235+
done
236+
fi
237+
238+
echo "## Reconciliation Summary" >> $GITHUB_STEP_SUMMARY
239+
echo "" >> $GITHUB_STEP_SUMMARY
240+
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
241+
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
242+
echo "| Overall Verdict | **$OVERALL** |" >> $GITHUB_STEP_SUMMARY
243+
echo "| Total Sources | $TOTAL_SOURCES |" >> $GITHUB_STEP_SUMMARY
244+
echo "| PASS | $PASS_COUNT |" >> $GITHUB_STEP_SUMMARY
245+
echo "| WARN | $WARN_COUNT |" >> $GITHUB_STEP_SUMMARY
246+
echo "| FAIL | $FAIL_COUNT |" >> $GITHUB_STEP_SUMMARY
247+
echo "" >> $GITHUB_STEP_SUMMARY
248+
echo "Git SHA: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
249+
250+
echo "overall=$OVERALL" >> $GITHUB_OUTPUT
251+
echo "::notice::Overall verdict: $OVERALL (PASS: $PASS_COUNT, WARN: $WARN_COUNT, FAIL: $FAIL_COUNT)"
252+
253+
# Fail if FAIL verdict
254+
if [ "$OVERALL" = "FAIL" ]; then
255+
echo "::error::Reconciliation failed for one or more sources"
256+
exit 1
257+
fi
258+
259+
- name: Upload aggregated summary
260+
uses: actions/upload-artifact@v4
261+
with:
262+
name: reconcile-summary
263+
path: ${{ github.step_summary }}
264+
retention-days: 30
265+
266+
# =========================================================================
267+
# SKIP JOB (when no sources found)
268+
# =========================================================================
269+
skip:
270+
name: Skip (No Sources)
271+
runs-on: ubuntu-latest
272+
needs: discover
273+
if: needs.discover.outputs.total-chunks == '0'
274+
steps:
275+
- name: No sources to reconcile
276+
run: |
277+
echo "::notice::No sources found matching the specified patterns. Skipping reconciliation."
278+
echo "## Reconciliation Skipped" >> $GITHUB_STEP_SUMMARY
279+
echo "" >> $GITHUB_STEP_SUMMARY
280+
echo "No sources found matching the specified patterns." >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)