Add macOS onboarding Figma sync pipeline#6346
Conversation
Greptile SummaryThis PR adds an end-to-end pipeline that compiles the macOS Swift app in headless mode, renders each onboarding step via
Confidence Score: 4/5Safe to merge after fixing the deduplication key mismatch in the Figma plugin; remaining findings are style-level. One P1 logic bug in code.js means the plugin re-syncs on every poll tick when sourceCommit/generatedAt are absent. The scripts always populate sourceCommit (defaulting to 'local'), so the bug won't trigger on main CI runs, but the plugin code itself has a latent correctness defect worth fixing before shipping to Figma users. All other findings are P2. figma/onboarding-auto-sync/code.js — dedup key mismatch (P1) and sequential asset fetching (P2) Important Files Changed
Sequence DiagramsequenceDiagram
participant GH as GitHub Actions
participant Swift as Swift App (headless)
participant Bundle as Bundle Scripts
participant Branch as figma-onboarding-sync
participant Figma as Figma Plugin
GH->>Swift: swift run --export-onboarding $ASSETS_DIR
loop subprocess per step (18 total)
Swift->>Swift: render OnboardingView(isExportPreview=true, step=N)
Swift-->>GH: stepN.png + stepN.pdf
end
GH->>Bundle: prepare_onboarding_sync_bundle.sh
Bundle->>Bundle: sips dimensions → manifest.json + index.html
GH->>Branch: force-push onboarding/latest/
Note over Branch: served via raw.githubusercontent.com
loop every 15 s
Figma->>Branch: GET manifest.json
alt new sourceCommit
loop sequential fetch (current)
Figma->>Branch: GET asset.png
end
Figma->>Figma: place frames in Onboarding Sync section
end
end
Reviews (1): Last reviewed commit: "Add onboarding sync export pipeline" | Re-trigger Greptile |
| function manifestSignature(manifest) { | ||
| return manifest.sourceCommit || manifest.generatedAt || JSON.stringify(manifest.assets || []); | ||
| } |
There was a problem hiding this comment.
Deduplication key mismatch causes repeated re-sync
manifestSignature() falls back to JSON.stringify(manifest.assets) when both sourceCommit and generatedAt are absent, but applyManifest (line 174) stores "" in config.lastAppliedSourceCommit for the same condition. After the first sync run without those fields, syncOnce compares JSON.stringify(assets) === "", which is always false, so every poll tick re-fetches and re-applies all 18 assets unnecessarily.
Fix by using the same fallback in both places:
| function manifestSignature(manifest) { | |
| return manifest.sourceCommit || manifest.generatedAt || JSON.stringify(manifest.assets || []); | |
| } | |
| function manifestSignature(manifest) { | |
| return manifest.sourceCommit || manifest.generatedAt || JSON.stringify(manifest.assets || []); | |
| } |
And in applyManifest (line 174):
config.lastAppliedSourceCommit = manifest.sourceCommit || manifest.generatedAt || JSON.stringify(manifest.assets || []);| for (const asset of manifest.assets || []) { | ||
| const assetUrl = new URL(asset.png, config.sourceUrl).toString(); | ||
| const bytes = await fetchAssetBytes(assetUrl); | ||
| assets.push({ asset, bytes }); | ||
| } |
There was a problem hiding this comment.
Sequential asset fetching adds unnecessary latency
All 18 onboarding PNGs are fetched one-at-a-time in a for/await loop. On a typical network connection this serialises 18 round-trips before any frame is rendered in Figma. Promise.all would reduce wall-clock time to roughly the slowest single request.
| for (const asset of manifest.assets || []) { | |
| const assetUrl = new URL(asset.png, config.sourceUrl).toString(); | |
| const bytes = await fetchAssetBytes(assetUrl); | |
| assets.push({ asset, bytes }); | |
| } | |
| const assets = await Promise.all( | |
| (manifest.assets || []).map(async (asset) => { | |
| const assetUrl = new URL(asset.png, config.sourceUrl).toString(); | |
| const bytes = await fetchAssetBytes(assetUrl); | |
| return { asset, bytes }; | |
| }) | |
| ); |
| saveConfig(loadConfig()); | ||
| figma.ui.postMessage({ | ||
| type: "start", | ||
| config: loadConfig(), | ||
| }); |
There was a problem hiding this comment.
Double
loadConfig() call at startup
saveConfig(loadConfig()) parses and immediately re-serialises the stored config (a no-op round-trip), then loadConfig() is called again to build the start message — three JSON operations where one suffices.
| saveConfig(loadConfig()); | |
| figma.ui.postMessage({ | |
| type: "start", | |
| config: loadConfig(), | |
| }); | |
| const initialConfig = loadConfig(); | |
| saveConfig(initialConfig); | |
| figma.ui.postMessage({ | |
| type: "start", | |
| config: initialConfig, | |
| }); |
| git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | ||
| git push --force origin "HEAD:${TARGET_BRANCH}" |
There was a problem hiding this comment.
Token embedded in remote URL may leak on push failure
git push failure messages can include the remote URL, which would expose GITHUB_TOKEN in workflow logs. Using a credential helper keeps the token out of any printed output.
| git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" | |
| git push --force origin "HEAD:${TARGET_BRANCH}" | |
| git remote add origin "https://github.com/${GITHUB_REPOSITORY}.git" | |
| git -c credential.helper= \ | |
| -c "url.https://x-access-token:${GITHUB_TOKEN}@github.com.insteadOf=https://github.com" \ | |
| push --force origin "HEAD:${TARGET_BRANCH}" |
Summary
Verification