Skip to content

Commit 2bf8c37

Browse files
authored
✨ Add git detection API to plugin services (#194)
## Summary - Adds `services.git.detect()` to the plugin API for correct git info detection - Replaces fragile dynamic imports in storybook/static-site plugins with clean API - Handles CI environments correctly (GitHub Actions PR merge commits, GitLab, etc.) - Graceful fallback for older CLI versions, with peer dep warning ## Problem The storybook and static-site plugins were using a fragile dynamic import to access the CLI's git utilities: ```javascript gitUtils = await import('@vizzly-testing/cli/dist/utils/git.js').catch(() => null); ``` This was breaking in some environments, causing incorrect git info (wrong branch, wrong commit SHA) in PR builds. ## Solution Add a proper plugin API for git detection: ```javascript // Plugin can now do: let gitInfo = await services.git.detect({ buildPrefix: 'Storybook' }); // Returns: { branch, commit, message, prNumber, buildName } ``` The CLI handles all the complexity of CI environment detection (GitHub Actions uses `GITHUB_HEAD_REF` for PRs, reads event payload for correct commit SHA, etc.). ## Compatibility | Plugin | CLI | Result | |--------|-----|--------| | Old | Old | ✅ Works | | Old | New | ✅ Works (new API is additive) | | New | New | ✅ Uses proper git detection | | New | Old | ✅ Falls back to env vars (with peer dep warning) | ## Test plan - [x] CLI build passes - [x] CLI tests pass - [x] Storybook plugin builds and tests pass - [x] Static-site plugin builds and tests pass - [x] Verified `services.git.detect()` returns correct values locally - [x] Verified CI env var simulation returns correct branch/PR number
1 parent f940ebf commit 2bf8c37

8 files changed

Lines changed: 291 additions & 75 deletions

File tree

clients/static-site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"registry": "https://registry.npmjs.org/"
6363
},
6464
"peerDependencies": {
65-
"@vizzly-testing/cli": ">=0.9.0"
65+
"@vizzly-testing/cli": ">=0.24.0"
6666
},
6767
"dependencies": {
6868
"cosmiconfig": "^9.0.0",

clients/static-site/src/index.js

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -166,43 +166,33 @@ export async function run(buildPath, options = {}, context = {}) {
166166
}
167167
});
168168

169-
// Detect git info - use dynamic import to access internal utils
170-
let gitUtils;
171-
try {
172-
// Try to import from the installed CLI package
173-
let cliPath = await import.meta.resolve?.('@vizzly-testing/cli');
174-
if (cliPath) {
175-
gitUtils = await import(
176-
'@vizzly-testing/cli/dist/utils/git.js'
177-
).catch(() => null);
178-
}
179-
} catch {
180-
// Fallback: try relative path if in monorepo
181-
try {
182-
gitUtils = await import('../../../src/utils/git.js').catch(
183-
() => null
184-
);
185-
} catch {
186-
gitUtils = null;
187-
}
169+
// Detect git info using CLI's plugin API (preferred) or fallback to env vars
170+
let branch, commit, message, buildName, pullRequestNumber;
171+
172+
if (services.git?.detect) {
173+
// Use CLI's git detection (correct handling of CI environments)
174+
let gitInfo = await services.git.detect({
175+
buildPrefix: 'Static Site',
176+
});
177+
branch = gitInfo.branch;
178+
commit = gitInfo.commit;
179+
message = gitInfo.message;
180+
buildName = gitInfo.buildName;
181+
pullRequestNumber = gitInfo.prNumber;
182+
} else {
183+
// Fallback for older CLI versions - use environment variables
184+
logger.warn(
185+
'⚠️ Upgrade to @vizzly-testing/cli@>=0.25.0 for improved git detection'
186+
);
187+
branch = process.env.VIZZLY_BRANCH || 'main';
188+
commit = process.env.VIZZLY_COMMIT_SHA || undefined;
189+
message = process.env.VIZZLY_COMMIT_MESSAGE || undefined;
190+
buildName = `Static Site ${new Date().toISOString()}`;
191+
pullRequestNumber = process.env.VIZZLY_PR_NUMBER
192+
? parseInt(process.env.VIZZLY_PR_NUMBER, 10)
193+
: undefined;
188194
}
189195

190-
let branch = gitUtils
191-
? await gitUtils.detectBranch()
192-
: process.env.VIZZLY_BRANCH || 'main';
193-
let commit = gitUtils
194-
? await gitUtils.detectCommit()
195-
: process.env.VIZZLY_COMMIT_SHA || undefined;
196-
let message = gitUtils
197-
? await gitUtils.detectCommitMessage()
198-
: process.env.VIZZLY_COMMIT_MESSAGE || undefined;
199-
let buildName = gitUtils
200-
? await gitUtils.generateBuildNameWithGit('Static Site')
201-
: `Static Site ${new Date().toISOString()}`;
202-
let pullRequestNumber = gitUtils
203-
? gitUtils.detectPullRequestNumber()
204-
: process.env.VIZZLY_PR_NUMBER || undefined;
205-
206196
// Build options for API
207197
let runOptions = {
208198
port: vizzlyConfig?.server?.port || 47392,

clients/storybook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
"registry": "https://registry.npmjs.org/"
5959
},
6060
"peerDependencies": {
61-
"@vizzly-testing/cli": ">=0.9.0"
61+
"@vizzly-testing/cli": ">=0.24.0"
6262
},
6363
"dependencies": {
6464
"playwright-core": "^1.50.0",

clients/storybook/src/index.js

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -112,43 +112,31 @@ export async function run(storybookPath, options = {}, context = {}) {
112112
}
113113
});
114114

115-
// Detect git info - use dynamic import to access internal utils
116-
let gitUtils;
117-
try {
118-
// Try to import from the installed CLI package
119-
let cliPath = await import.meta.resolve?.('@vizzly-testing/cli');
120-
if (cliPath) {
121-
gitUtils = await import(
122-
'@vizzly-testing/cli/dist/utils/git.js'
123-
).catch(() => null);
124-
}
125-
} catch {
126-
// Fallback: try relative path if in monorepo
127-
try {
128-
gitUtils = await import('../../../src/utils/git.js').catch(
129-
() => null
130-
);
131-
} catch {
132-
gitUtils = null;
133-
}
115+
// Detect git info using CLI's plugin API (preferred) or fallback to env vars
116+
let branch, commit, message, buildName, pullRequestNumber;
117+
118+
if (services.git?.detect) {
119+
// Use CLI's git detection (correct handling of CI environments)
120+
let gitInfo = await services.git.detect({ buildPrefix: 'Storybook' });
121+
branch = gitInfo.branch;
122+
commit = gitInfo.commit;
123+
message = gitInfo.message;
124+
buildName = gitInfo.buildName;
125+
pullRequestNumber = gitInfo.prNumber;
126+
} else {
127+
// Fallback for older CLI versions - use environment variables
128+
logger.warn(
129+
'⚠️ Upgrade to @vizzly-testing/cli@>=0.25.0 for improved git detection'
130+
);
131+
branch = process.env.VIZZLY_BRANCH || 'main';
132+
commit = process.env.VIZZLY_COMMIT_SHA || undefined;
133+
message = process.env.VIZZLY_COMMIT_MESSAGE || undefined;
134+
buildName = `Storybook ${new Date().toISOString()}`;
135+
pullRequestNumber = process.env.VIZZLY_PR_NUMBER
136+
? parseInt(process.env.VIZZLY_PR_NUMBER, 10)
137+
: undefined;
134138
}
135139

136-
let branch = gitUtils
137-
? await gitUtils.detectBranch()
138-
: process.env.VIZZLY_BRANCH || 'main';
139-
let commit = gitUtils
140-
? await gitUtils.detectCommit()
141-
: process.env.VIZZLY_COMMIT_SHA || undefined;
142-
let message = gitUtils
143-
? await gitUtils.detectCommitMessage()
144-
: process.env.VIZZLY_COMMIT_MESSAGE || undefined;
145-
let buildName = gitUtils
146-
? await gitUtils.generateBuildNameWithGit('Storybook')
147-
: `Storybook ${new Date().toISOString()}`;
148-
let pullRequestNumber = gitUtils
149-
? gitUtils.detectPullRequestNumber()
150-
: process.env.VIZZLY_PR_NUMBER || undefined;
151-
152140
// Build options for API
153141
let runOptions = {
154142
port: vizzlyConfig?.server?.port || 47392,

examples/custom-plugin/README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,28 @@ The `register` function receives:
7171
1. **`program`** - Commander.js program instance for adding commands
7272
2. **`context`** - Object containing:
7373
- `config` - Merged Vizzly configuration
74-
- `logger` - Component logger for consistent output
75-
- `services` - Service container with API client, uploader, etc.
74+
- `output` - Output utilities for consistent CLI output
75+
- `services` - Service container (see below)
76+
77+
### Services API
78+
79+
The `services` object provides stable APIs for plugins:
80+
81+
```javascript
82+
let { git, testRunner, serverManager } = services;
83+
84+
// Git detection (v0.25.0+) - handles CI environments correctly
85+
let gitInfo = await git.detect({ buildPrefix: 'MyPlugin' });
86+
// Returns: { branch, commit, message, prNumber, buildName }
87+
88+
// Build lifecycle
89+
let buildId = await testRunner.createBuild(options);
90+
await testRunner.finalizeBuild(buildId, wait, success, executionTime);
91+
92+
// Server control
93+
await serverManager.start(buildId, tddMode, setBaseline);
94+
await serverManager.stop();
95+
```
7696

7797
## Creating Your Own Plugin
7898

examples/custom-plugin/plugin.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,18 @@ export default {
5353
output.info('Checking Vizzly services...');
5454

5555
// Access services from the stable API
56-
let { testRunner, serverManager } = services;
56+
let { git, testRunner, serverManager } = services;
57+
58+
// Verify git detection is available (v0.25.0+)
59+
if (git?.detect) {
60+
output.success('git.detect is available');
61+
let gitInfo = await git.detect({ buildPrefix: 'Example' });
62+
output.info(` Branch: ${gitInfo.branch}`);
63+
output.info(` Commit: ${gitInfo.commit?.slice(0, 7) || 'unknown'}`);
64+
output.info(` PR: ${gitInfo.prNumber || 'none'}`);
65+
} else {
66+
output.warn('git.detect not available (requires CLI v0.25.0+)');
67+
}
5768

5869
// Verify testRunner is available
5970
if (typeof testRunner.createBuild === 'function') {

src/plugin-api.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,19 @@
99
* exposed to plugins to prevent coupling to implementation details.
1010
*/
1111

12+
import {
13+
detectBranch,
14+
detectCommit,
15+
detectCommitMessage,
16+
detectPullRequestNumber,
17+
generateBuildNameWithGit,
18+
} from './utils/git.js';
19+
1220
/**
1321
* Creates a stable plugin services object from the internal services
1422
*
1523
* Only exposes:
24+
* - git: Git information detection (branch, commit, PR number, etc.)
1625
* - testRunner: Build lifecycle management (createBuild, finalizeBuild, events)
1726
* - serverManager: Screenshot server control (start, stop)
1827
*
@@ -23,6 +32,35 @@ export function createPluginServices(services) {
2332
let { testRunner, serverManager } = services;
2433

2534
return Object.freeze({
35+
// Git detection utilities - provides correct git info from CI environments
36+
git: Object.freeze({
37+
/**
38+
* Detect git information for build creation
39+
* Handles CI environment variables correctly (GitHub Actions, GitLab, etc.)
40+
*
41+
* @param {Object} [options] - Detection options
42+
* @param {string} [options.buildPrefix] - Prefix for generated build name
43+
* @returns {Promise<Object>} Git info: { branch, commit, message, prNumber, buildName }
44+
*/
45+
async detect(options = {}) {
46+
let [branch, commit, message] = await Promise.all([
47+
detectBranch(),
48+
detectCommit(),
49+
detectCommitMessage(),
50+
]);
51+
let prNumber = detectPullRequestNumber();
52+
let buildName = await generateBuildNameWithGit(options.buildPrefix);
53+
54+
return {
55+
branch,
56+
commit,
57+
message,
58+
prNumber,
59+
buildName,
60+
};
61+
},
62+
}),
63+
2664
testRunner: Object.freeze({
2765
// EventEmitter methods for build lifecycle events
2866
once: testRunner.once.bind(testRunner),

0 commit comments

Comments
 (0)