@gfxlabs/third-eye-cli 3.24.2 → 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +109 -31
- package/dist/bin.mjs.map +1 -1
- package/package.json +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -184,7 +184,8 @@ z$1.object({
|
|
|
184
184
|
storyName: z$1.string().optional(),
|
|
185
185
|
storyId: z$1.string().optional(),
|
|
186
186
|
storyArgs: z$1.record(z$1.string(), z$1.unknown()).optional(),
|
|
187
|
-
tags: z$1.array(z$1.string()).optional()
|
|
187
|
+
tags: z$1.array(z$1.string()).optional(),
|
|
188
|
+
designUrl: z$1.string().optional()
|
|
188
189
|
});
|
|
189
190
|
//#endregion
|
|
190
191
|
//#region src/config.ts
|
|
@@ -1035,7 +1036,8 @@ const generateStorybookShotItems = (baseUrl, stories, mask, modeBreakpoints, bro
|
|
|
1035
1036
|
componentPath: story.kind,
|
|
1036
1037
|
storyName: story.story,
|
|
1037
1038
|
storyId: story.id,
|
|
1038
|
-
storyArgs: story.parameters?.thirdeye?.args
|
|
1039
|
+
storyArgs: story.parameters?.thirdeye?.args,
|
|
1040
|
+
designUrl: (story.parameters?.design)?.url
|
|
1039
1041
|
};
|
|
1040
1042
|
const storyLevelBreakpoints = story.parameters?.thirdeye?.breakpoints ?? [];
|
|
1041
1043
|
const breakpoints = selectBreakpoints(config.breakpoints, modeBreakpoints, storyLevelBreakpoints);
|
|
@@ -1640,6 +1642,12 @@ const createShots = async (turboSnapFilter) => {
|
|
|
1640
1642
|
//#region src/git.ts
|
|
1641
1643
|
const INITIAL_BATCH_SIZE = 20;
|
|
1642
1644
|
/**
|
|
1645
|
+
* Check if a commit exists in the local git index.
|
|
1646
|
+
*/
|
|
1647
|
+
const commitExists = (commit) => {
|
|
1648
|
+
return execGit(`git cat-file -t ${commit}`) === "commit";
|
|
1649
|
+
};
|
|
1650
|
+
/**
|
|
1643
1651
|
* Execute a git command and return the trimmed output.
|
|
1644
1652
|
*/
|
|
1645
1653
|
const execGit = (command) => {
|
|
@@ -1652,34 +1660,28 @@ const execGit = (command) => {
|
|
|
1652
1660
|
return "";
|
|
1653
1661
|
}
|
|
1654
1662
|
};
|
|
1655
|
-
|
|
1656
|
-
* Find the "covering" set of ancestor commits that have builds on the server.
|
|
1657
|
-
*
|
|
1658
|
-
* This mirrors Chromatic's approach:
|
|
1659
|
-
* 1. Walk git history with `git rev-list`
|
|
1660
|
-
* 2. Ask the server which commits have builds
|
|
1661
|
-
* 3. Use `--not` to exclude ancestors of commits with builds
|
|
1662
|
-
* 4. Repeat with exponentially larger batches until no more uncovered commits
|
|
1663
|
-
*
|
|
1664
|
-
* The result is the minimal set of ancestor commits with builds such that
|
|
1665
|
-
* every ancestor of HEAD either has a build or is an ancestor of a commit
|
|
1666
|
-
* with a build.
|
|
1667
|
-
*/
|
|
1668
|
-
const getParentCommits = async (hasBuildsWithCommits) => {
|
|
1663
|
+
const getParentCommits = async (hasBuildsWithCommits, options) => {
|
|
1669
1664
|
if (!execGit("git rev-parse HEAD")) {
|
|
1670
1665
|
log.process("info", "general", "Not a git repository, skipping ancestor detection");
|
|
1671
|
-
return
|
|
1666
|
+
return {
|
|
1667
|
+
parentCommits: [],
|
|
1668
|
+
visitedCommitsWithoutBuilds: []
|
|
1669
|
+
};
|
|
1672
1670
|
}
|
|
1673
1671
|
if (!execGit("git --no-pager log -n 1 --skip=1 --format=\"%H\"")) {
|
|
1674
1672
|
log.process("info", "general", "Initial commit, no ancestors");
|
|
1675
|
-
return
|
|
1673
|
+
return {
|
|
1674
|
+
parentCommits: [],
|
|
1675
|
+
visitedCommitsWithoutBuilds: []
|
|
1676
|
+
};
|
|
1676
1677
|
}
|
|
1677
|
-
|
|
1678
|
+
const sinceArg = options?.firstCommittedAtSeconds ? `--since ${options.firstCommittedAtSeconds}` : "";
|
|
1679
|
+
let commitsWithBuilds = [...options?.initialCommitsWithBuilds ?? []];
|
|
1678
1680
|
let commitsWithoutBuilds = [];
|
|
1679
1681
|
let limit = INITIAL_BATCH_SIZE;
|
|
1680
1682
|
for (;;) {
|
|
1681
1683
|
const notArgs = commitsWithBuilds.map((c) => c.trim()).join(" ");
|
|
1682
|
-
const output = execGit(`git rev-list HEAD -n ${limit + commitsWithoutBuilds.length}${notArgs ? ` --not ${notArgs}` : ""}`);
|
|
1684
|
+
const output = execGit(`git rev-list HEAD ${sinceArg} -n ${limit + commitsWithoutBuilds.length}${notArgs ? ` --not ${notArgs}` : ""}`);
|
|
1683
1685
|
const candidates = (output ? output.split("\n").filter(Boolean) : []).filter((c) => !commitsWithBuilds.includes(c)).filter((c) => !commitsWithoutBuilds.includes(c)).slice(0, limit);
|
|
1684
1686
|
if (candidates.length === 0) break;
|
|
1685
1687
|
log.process("info", "general", `🔍 Checking ${candidates.length} commits for builds (batch size ${limit})`);
|
|
@@ -1695,7 +1697,10 @@ const getParentCommits = async (hasBuildsWithCommits) => {
|
|
|
1695
1697
|
}
|
|
1696
1698
|
if (commitsWithBuilds.length === 0) {
|
|
1697
1699
|
log.process("info", "general", "No ancestor builds found — this may be the first build");
|
|
1698
|
-
return
|
|
1700
|
+
return {
|
|
1701
|
+
parentCommits: [],
|
|
1702
|
+
visitedCommitsWithoutBuilds: commitsWithoutBuilds
|
|
1703
|
+
};
|
|
1699
1704
|
}
|
|
1700
1705
|
if (commitsWithBuilds.length > 1) {
|
|
1701
1706
|
const parentArgs = commitsWithBuilds.map((c) => `"${c}^@"`).join(" ");
|
|
@@ -1704,7 +1709,31 @@ const getParentCommits = async (hasBuildsWithCommits) => {
|
|
|
1704
1709
|
if (maxCommits.length > 0) commitsWithBuilds = maxCommits;
|
|
1705
1710
|
}
|
|
1706
1711
|
log.process("info", "general", `📌 Found ${commitsWithBuilds.length} ancestor build(s): ${commitsWithBuilds.map((c) => c.slice(0, 7)).join(", ")}`);
|
|
1707
|
-
return
|
|
1712
|
+
return {
|
|
1713
|
+
parentCommits: commitsWithBuilds,
|
|
1714
|
+
visitedCommitsWithoutBuilds: commitsWithoutBuilds
|
|
1715
|
+
};
|
|
1716
|
+
};
|
|
1717
|
+
/**
|
|
1718
|
+
* Compute the merge base commit between the current HEAD and a base branch,
|
|
1719
|
+
* plus a list of ancestor commits on the base branch from that point backwards.
|
|
1720
|
+
* This gives the server enough git history to walk back and find a build
|
|
1721
|
+
* even if the exact merge base commit doesn't have one.
|
|
1722
|
+
*/
|
|
1723
|
+
const getMergeBaseInfo = (baseBranch) => {
|
|
1724
|
+
const mergeBaseCommit = execGit(`git merge-base HEAD ${execGit(`git rev-parse origin/${baseBranch}`) ? `origin/${baseBranch}` : baseBranch}`);
|
|
1725
|
+
if (!mergeBaseCommit) {
|
|
1726
|
+
log.process("info", "general", `Could not compute merge base with ${baseBranch}`);
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
log.process("info", "general", `📍 Merge base with ${baseBranch}: ${mergeBaseCommit.slice(0, 7)}`);
|
|
1730
|
+
const ancestorsOutput = execGit(`git rev-list ${mergeBaseCommit} -n 50`);
|
|
1731
|
+
const mergeBaseAncestors = ancestorsOutput ? ancestorsOutput.split("\n").filter(Boolean) : [];
|
|
1732
|
+
log.process("info", "general", `📍 Got ${mergeBaseAncestors.length} merge base ancestors for build lookup`);
|
|
1733
|
+
return {
|
|
1734
|
+
mergeBaseCommit,
|
|
1735
|
+
mergeBaseAncestors
|
|
1736
|
+
};
|
|
1708
1737
|
};
|
|
1709
1738
|
//#endregion
|
|
1710
1739
|
//#region ../shared/dist/client.js
|
|
@@ -1775,7 +1804,7 @@ const getApiToken = async (config) => {
|
|
|
1775
1804
|
process.exit(1);
|
|
1776
1805
|
}
|
|
1777
1806
|
};
|
|
1778
|
-
const sendInitToAPI = async (config, apiToken, parentCommits) => {
|
|
1807
|
+
const sendInitToAPI = async (config, apiToken, parentCommits, mergeBaseInfo) => {
|
|
1779
1808
|
const client = createClient(config.thirdEyePlatform, void 0, apiToken);
|
|
1780
1809
|
return withRetry("init", () => client.orgs.projects.builds.init({
|
|
1781
1810
|
orgId: config.thirdEyeOrgId,
|
|
@@ -1785,7 +1814,9 @@ const sendInitToAPI = async (config, apiToken, parentCommits) => {
|
|
|
1785
1814
|
buildNumber: config.ciBuildNumber,
|
|
1786
1815
|
baseBranch: config.baseBranch || void 0,
|
|
1787
1816
|
prNumber: config.prNumber,
|
|
1788
|
-
parentCommits
|
|
1817
|
+
parentCommits,
|
|
1818
|
+
mergeBaseCommit: mergeBaseInfo?.mergeBaseCommit,
|
|
1819
|
+
mergeBaseAncestors: mergeBaseInfo?.mergeBaseAncestors
|
|
1789
1820
|
}));
|
|
1790
1821
|
};
|
|
1791
1822
|
const sendHasBuildsWithCommitsToAPI = async (config, apiToken, commits) => {
|
|
@@ -1796,6 +1827,22 @@ const sendHasBuildsWithCommitsToAPI = async (config, apiToken, commits) => {
|
|
|
1796
1827
|
commits
|
|
1797
1828
|
}))).commits;
|
|
1798
1829
|
};
|
|
1830
|
+
const getGitInfoFromAPI = async (config, apiToken, branch) => {
|
|
1831
|
+
const client = createClient(config.thirdEyePlatform, void 0, apiToken);
|
|
1832
|
+
return withRetry("getGitInfo", () => client.orgs.projects.builds.getGitInfo({
|
|
1833
|
+
orgId: config.thirdEyeOrgId,
|
|
1834
|
+
projectId: config.thirdEyeProjectId,
|
|
1835
|
+
branch
|
|
1836
|
+
}));
|
|
1837
|
+
};
|
|
1838
|
+
const sendFindSquashMergeParentsToAPI = async (config, apiToken, commits) => {
|
|
1839
|
+
const client = createClient(config.thirdEyePlatform, void 0, apiToken);
|
|
1840
|
+
return (await withRetry("findSquashMergeParents", () => client.orgs.projects.builds.findSquashMergeParents({
|
|
1841
|
+
orgId: config.thirdEyeOrgId,
|
|
1842
|
+
projectId: config.thirdEyeProjectId,
|
|
1843
|
+
commits
|
|
1844
|
+
}))).parents;
|
|
1845
|
+
};
|
|
1799
1846
|
const sendFinalizeToAPI = async (config, apiToken) => {
|
|
1800
1847
|
const client = createClient(config.thirdEyePlatform, void 0, apiToken);
|
|
1801
1848
|
return withRetry("finalize", () => client.orgs.projects.builds.finalize({
|
|
@@ -1843,7 +1890,7 @@ const uploadShot = async ({ config, apiToken, uploadToken, name, file, shotMode,
|
|
|
1843
1890
|
metadata
|
|
1844
1891
|
}), logFn);
|
|
1845
1892
|
};
|
|
1846
|
-
const processShots = async (config, apiToken, uploadToken, shotsConfig, cacheKey, turboSnap) => {
|
|
1893
|
+
const processShots = async (config, apiToken, uploadToken, shotsConfig, cacheKey, turboSnap, turboSnapAllStoryNames) => {
|
|
1847
1894
|
const client = createTypedClient({
|
|
1848
1895
|
url: config.thirdEyePlatform,
|
|
1849
1896
|
apiToken,
|
|
@@ -1859,7 +1906,8 @@ const processShots = async (config, apiToken, uploadToken, shotsConfig, cacheKey
|
|
|
1859
1906
|
},
|
|
1860
1907
|
log: logMemory,
|
|
1861
1908
|
cacheKey,
|
|
1862
|
-
turboSnap
|
|
1909
|
+
turboSnap,
|
|
1910
|
+
turboSnapAllStoryNames
|
|
1863
1911
|
});
|
|
1864
1912
|
log.process("info", "api", "Successfully sent to API [processShots]");
|
|
1865
1913
|
return result;
|
|
@@ -1917,7 +1965,8 @@ const uploadRequiredShots = async ({ config, apiToken, uploadToken, requiredFile
|
|
|
1917
1965
|
...shotItem.viewport ? { viewport: shotItem.viewport } : {},
|
|
1918
1966
|
...shotItem.breakpoint !== void 0 ? { breakpoint: shotItem.breakpoint } : {},
|
|
1919
1967
|
...domHtml ? { dom_html: domHtml } : {},
|
|
1920
|
-
...dependencyMap && shotItem.importPath && dependencyMap.has(shotItem.importPath) ? { dependencies: dependencyMap.get(shotItem.importPath) } : {}
|
|
1968
|
+
...dependencyMap && shotItem.importPath && dependencyMap.has(shotItem.importPath) ? { dependencies: dependencyMap.get(shotItem.importPath) } : {},
|
|
1969
|
+
...shotItem.designUrl ? { designUrl: shotItem.designUrl } : {}
|
|
1921
1970
|
},
|
|
1922
1971
|
logger
|
|
1923
1972
|
});
|
|
@@ -2194,14 +2243,41 @@ const platformRunner = async (config, apiToken) => {
|
|
|
2194
2243
|
`commitRefName = ${config.commitRefName}`,
|
|
2195
2244
|
`commitHash = ${config.commitHash}`
|
|
2196
2245
|
].join("\n - "));
|
|
2246
|
+
log.process("info", "general", "🔍 Querying server for git info...");
|
|
2247
|
+
const gitInfo = await getGitInfoFromAPI(config, apiToken, config.commitRefName);
|
|
2248
|
+
const initialCommitsWithBuilds = [];
|
|
2249
|
+
if (gitInfo.lastBuildOnBranch) {
|
|
2250
|
+
const lastCommit = gitInfo.lastBuildOnBranch.commit;
|
|
2251
|
+
if (commitExists(lastCommit)) {
|
|
2252
|
+
log.process("info", "general", `📌 Seeding with last build on branch: ${lastCommit.slice(0, 7)}`);
|
|
2253
|
+
initialCommitsWithBuilds.push(lastCommit);
|
|
2254
|
+
} else log.process("info", "general", `📌 Last build on branch ${lastCommit.slice(0, 7)} not in local git (rebase?), skipping`);
|
|
2255
|
+
}
|
|
2197
2256
|
log.process("info", "general", "🔍 Resolving ancestor builds from git history...");
|
|
2198
|
-
|
|
2257
|
+
const { parentCommits, visitedCommitsWithoutBuilds } = await getParentCommits((commits) => sendHasBuildsWithCommitsToAPI(config, apiToken, commits), {
|
|
2258
|
+
firstCommittedAtSeconds: gitInfo.firstBuildCreatedAt ?? void 0,
|
|
2259
|
+
initialCommitsWithBuilds: initialCommitsWithBuilds.length > 0 ? initialCommitsWithBuilds : void 0
|
|
2260
|
+
});
|
|
2261
|
+
if (visitedCommitsWithoutBuilds.length > 0) {
|
|
2262
|
+
log.process("info", "general", `🔀 Checking ${Math.min(visitedCommitsWithoutBuilds.length, 100)} commits for squash merges...`);
|
|
2263
|
+
try {
|
|
2264
|
+
const squashResult = await sendFindSquashMergeParentsToAPI(config, apiToken, visitedCommitsWithoutBuilds.slice(0, 100));
|
|
2265
|
+
for (const { buildCommit } of squashResult) if (commitExists(buildCommit) && !parentCommits.includes(buildCommit)) {
|
|
2266
|
+
log.process("info", "general", `🔀 Found squash merge parent: ${buildCommit.slice(0, 7)}`);
|
|
2267
|
+
parentCommits.push(buildCommit);
|
|
2268
|
+
}
|
|
2269
|
+
} catch (err) {
|
|
2270
|
+
log.process("info", "general", `Squash merge detection failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
await sendInitToAPI(config, apiToken, parentCommits, config.baseBranch ? getMergeBaseInfo(config.baseBranch) : null);
|
|
2199
2274
|
if (!await checkForCachedBuild(config, apiToken)) {
|
|
2200
2275
|
log.process("info", "general", "📂 Creating shot folders");
|
|
2201
2276
|
const createShotsStart = process.hrtime();
|
|
2202
2277
|
createShotsFolders();
|
|
2203
2278
|
let turboSnapFilter;
|
|
2204
2279
|
let turboSnapDependencyMap;
|
|
2280
|
+
let turboSnapAllStoryNames;
|
|
2205
2281
|
if (config.turboSnap && config.baseBranch) {
|
|
2206
2282
|
log.process("info", "general", `⚡ TurboSnap enabled, checking changed files against ${config.baseBranch}`);
|
|
2207
2283
|
const changedFiles = getChangedFiles(config.baseBranch);
|
|
@@ -2213,10 +2289,12 @@ const platformRunner = async (config, apiToken) => {
|
|
|
2213
2289
|
if (existsSync(indexJsonPath)) {
|
|
2214
2290
|
const indexJson = JSON.parse(fs.readFileSync(indexJsonPath, "utf-8"));
|
|
2215
2291
|
const entries = indexJson.entries ?? indexJson.stories ?? {};
|
|
2216
|
-
const
|
|
2292
|
+
const stories = Object.values(entries).filter((e) => e.type !== "docs").map((e) => ({
|
|
2217
2293
|
shotName: generateShotName(e.title, e.name),
|
|
2218
2294
|
importPath: e.importPath
|
|
2219
|
-
}))
|
|
2295
|
+
}));
|
|
2296
|
+
const turboResult = getAffectedStoriesLocal(stories, changedFiles, process.cwd());
|
|
2297
|
+
turboSnapAllStoryNames = stories.map((s) => `storybook/${s.shotName}`);
|
|
2220
2298
|
log.process("info", "general", `⚡ TurboSnap: ${turboResult.affected.length} affected, ${turboResult.skipped.length} skipped out of ${turboResult.total} stories`);
|
|
2221
2299
|
turboSnapFilter = new Set(turboResult.affected);
|
|
2222
2300
|
turboSnapDependencyMap = turboResult.dependencyMap;
|
|
@@ -2275,7 +2353,7 @@ const platformRunner = async (config, apiToken) => {
|
|
|
2275
2353
|
await processShots(config, apiToken, uploadToken, shotItems.map((shotItem) => ({
|
|
2276
2354
|
name: `${shotItem.shotMode}/${shotItem.shotName}`,
|
|
2277
2355
|
threshold: shotItem.threshold
|
|
2278
|
-
})), process.env.THIRD_EYE_CACHE_KEY, !!turboSnapFilter);
|
|
2356
|
+
})), process.env.THIRD_EYE_CACHE_KEY, !!turboSnapFilter, turboSnapAllStoryNames);
|
|
2279
2357
|
if (config.storybookStaticDir && existsSync(config.storybookStaticDir)) {
|
|
2280
2358
|
log.process("info", "general", "Uploading Storybook archive...");
|
|
2281
2359
|
const archive = execSync(`tar -czf - -C ${JSON.stringify(config.storybookStaticDir)} .`, { maxBuffer: 50 * 1024 * 1024 }).toString("base64");
|