@vizzly-testing/cli 0.4.0 → 0.5.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/README.md CHANGED
@@ -277,6 +277,10 @@ For CI/CD pipelines, use the `--wait` flag to wait for visual comparison results
277
277
  run: npx vizzly run "npm test" --wait
278
278
  env:
279
279
  VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
280
+ # Optional: Provide correct git information from GitHub context
281
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
282
+ VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
283
+ VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
280
284
  ```
281
285
 
282
286
  ### GitLab CI
@@ -319,10 +323,30 @@ Check if Vizzly is enabled in the current environment.
319
323
 
320
324
  ## Environment Variables
321
325
 
326
+ ### Core Configuration
322
327
  - `VIZZLY_TOKEN`: API authentication token. Example: `export VIZZLY_TOKEN=your-token`.
323
328
  - `VIZZLY_API_URL`: Override API base URL. Default: `https://vizzly.dev`.
324
329
  - `VIZZLY_LOG_LEVEL`: Logger level. One of `debug`, `info`, `warn`, `error`. Example: `export VIZZLY_LOG_LEVEL=debug`.
325
330
 
331
+ ### Git Information Override
332
+ For enhanced CI/CD integration, you can override git detection with these environment variables:
333
+
334
+ - `VIZZLY_COMMIT_SHA`: Override detected commit SHA. Useful in CI environments.
335
+ - `VIZZLY_COMMIT_MESSAGE`: Override detected commit message. Useful in CI environments.
336
+ - `VIZZLY_BRANCH`: Override detected branch name. Useful in CI environments.
337
+ - `VIZZLY_PR_NUMBER`: Override detected pull request number. Useful for PR-specific builds.
338
+
339
+ **Example for GitHub Actions:**
340
+ ```yaml
341
+ env:
342
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
343
+ VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
344
+ VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
345
+ VIZZLY_PR_NUMBER: ${{ github.event.pull_request.number }}
346
+ ```
347
+
348
+ These variables take highest priority over both CLI arguments and automatic git detection.
349
+
326
350
  ## Contributing
327
351
 
328
352
  We welcome contributions! Whether you're fixing bugs, adding features, or improving documentation, your help makes Vizzly better for everyone.
package/dist/cli.js CHANGED
@@ -38,10 +38,26 @@ program.command('tdd').description('Run tests in TDD mode with local visual comp
38
38
  validationErrors.forEach(error => console.error(` - ${error}`));
39
39
  process.exit(1);
40
40
  }
41
- const result = await tddCommand(command, options, globalOptions);
41
+ const {
42
+ result,
43
+ cleanup
44
+ } = await tddCommand(command, options, globalOptions);
45
+
46
+ // Set up cleanup on process signals
47
+ const handleCleanup = async () => {
48
+ await cleanup();
49
+ };
50
+ process.once('SIGINT', () => {
51
+ handleCleanup().then(() => process.exit(1));
52
+ });
53
+ process.once('SIGTERM', () => {
54
+ handleCleanup().then(() => process.exit(1));
55
+ });
42
56
  if (result && !result.success && result.exitCode > 0) {
57
+ await cleanup();
43
58
  process.exit(result.exitCode);
44
59
  }
60
+ await cleanup();
45
61
  });
46
62
  program.command('run').description('Run tests with Vizzly integration').argument('<command>', 'Test command to run').option('--port <port>', 'Port for screenshot server', '47392').option('-b, --build-name <name>', 'Custom build name').option('--branch <branch>', 'Git branch override').option('--commit <sha>', 'Git commit SHA').option('--message <msg>', 'Commit message').option('--environment <env>', 'Environment name', 'test').option('--token <token>', 'API token override').option('--wait', 'Wait for build completion').option('--timeout <ms>', 'Server timeout in milliseconds', '30000').option('--allow-no-token', 'Allow running without API token').option('--upload-all', 'Upload all screenshots without SHA deduplication').action(async (command, options) => {
47
63
  const globalOptions = program.opts();
@@ -1,7 +1,7 @@
1
1
  import { loadConfig } from '../utils/config-loader.js';
2
2
  import { ConsoleUI } from '../utils/console-ui.js';
3
3
  import { createServiceContainer } from '../container/index.js';
4
- import { detectBranch, detectCommit, getCommitMessage, generateBuildNameWithGit } from '../utils/git.js';
4
+ import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
5
5
 
6
6
  /**
7
7
  * Run command implementation
@@ -55,8 +55,9 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
55
55
  // Collect git metadata and build info
56
56
  const branch = await detectBranch(options.branch);
57
57
  const commit = await detectCommit(options.commit);
58
- const message = options.message || (await getCommitMessage());
58
+ const message = options.message || (await detectCommitMessage());
59
59
  const buildName = await generateBuildNameWithGit(options.buildName);
60
+ const pullRequestNumber = detectPullRequestNumber();
60
61
  if (globalOptions.verbose) {
61
62
  ui.info('Configuration loaded', {
62
63
  testCommand,
@@ -146,7 +147,8 @@ export async function runCommand(testCommand, options = {}, globalOptions = {})
146
147
  eager: config.eager || false,
147
148
  allowNoToken: config.allowNoToken || false,
148
149
  wait: config.wait || options.wait || false,
149
- uploadAll: options.uploadAll || false
150
+ uploadAll: options.uploadAll || false,
151
+ pullRequestNumber
150
152
  };
151
153
 
152
154
  // Start test run
@@ -8,6 +8,7 @@ import { detectBranch, detectCommit } from '../utils/git.js';
8
8
  * @param {string} testCommand - Test command to execute
9
9
  * @param {Object} options - Command options
10
10
  * @param {Object} globalOptions - Global CLI options
11
+ * @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
11
12
  */
12
13
  export async function tddCommand(testCommand, options = {}, globalOptions = {}) {
13
14
  // Create UI handler
@@ -17,20 +18,17 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
17
18
  color: !globalOptions.noColor
18
19
  });
19
20
  let testRunner = null;
21
+ let isCleanedUp = false;
20
22
 
21
- // Ensure cleanup on exit - store listeners for proper cleanup
23
+ // Create cleanup function that can be called by the caller
22
24
  const cleanup = async () => {
25
+ if (isCleanedUp) return;
26
+ isCleanedUp = true;
23
27
  ui.cleanup();
24
- // The test runner's finally block will handle server cleanup
25
- // We just need to ensure UI cleanup happens
26
- };
27
- const sigintHandler = async () => {
28
- await cleanup();
29
- process.exit(1);
28
+ if (testRunner?.cancel) {
29
+ await testRunner.cancel();
30
+ }
30
31
  };
31
- const exitHandler = () => ui.cleanup();
32
- process.on('SIGINT', sigintHandler);
33
- process.on('exit', exitHandler);
34
32
  try {
35
33
  // Load configuration with CLI overrides
36
34
  const allOptions = {
@@ -166,22 +164,31 @@ export async function tddCommand(testCommand, options = {}, globalOptions = {})
166
164
  }
167
165
  ui.success('TDD test run completed');
168
166
 
169
- // Exit with appropriate code based on comparison results
170
- if (result.failed || result.comparisons && result.comparisons.some(c => c.status === 'failed')) {
167
+ // Determine success based on comparison results
168
+ const hasFailures = result.failed || result.comparisons && result.comparisons.some(c => c.status === 'failed');
169
+ if (hasFailures) {
171
170
  ui.error('Visual differences detected in TDD mode', {}, 0);
172
- // Return error status without calling process.exit in tests
173
- return {
174
- success: false,
175
- exitCode: 1
176
- };
177
171
  }
178
- ui.cleanup();
172
+
173
+ // Return result and cleanup function
174
+ return {
175
+ result: {
176
+ success: !hasFailures,
177
+ exitCode: hasFailures ? 1 : 0,
178
+ ...result
179
+ },
180
+ cleanup
181
+ };
179
182
  } catch (error) {
180
183
  ui.error('TDD test run failed', error);
181
- } finally {
182
- // Remove event listeners to prevent memory leaks
183
- process.removeListener('SIGINT', sigintHandler);
184
- process.removeListener('exit', exitHandler);
184
+ return {
185
+ result: {
186
+ success: false,
187
+ exitCode: 1,
188
+ error: error.message
189
+ },
190
+ cleanup
191
+ };
185
192
  }
186
193
  }
187
194
 
@@ -1,7 +1,7 @@
1
1
  import { loadConfig } from '../utils/config-loader.js';
2
2
  import { ConsoleUI } from '../utils/console-ui.js';
3
3
  import { createServiceContainer } from '../container/index.js';
4
- import { detectBranch, detectCommit, getCommitMessage, generateBuildNameWithGit } from '../utils/git.js';
4
+ import { detectBranch, detectCommit, detectCommitMessage, detectPullRequestNumber, generateBuildNameWithGit } from '../utils/git.js';
5
5
  import { ApiService } from '../services/api-service.js';
6
6
 
7
7
  /**
@@ -71,8 +71,9 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
71
71
  // Collect git metadata if not provided
72
72
  const branch = await detectBranch(options.branch);
73
73
  const commit = await detectCommit(options.commit);
74
- const message = options.message || (await getCommitMessage());
74
+ const message = options.message || (await detectCommitMessage());
75
75
  const buildName = await generateBuildNameWithGit(options.buildName);
76
+ const pullRequestNumber = detectPullRequestNumber();
76
77
  ui.info(`Uploading screenshots from: ${screenshotsPath}`);
77
78
  if (globalOptions.verbose) {
78
79
  ui.info('Configuration loaded', {
@@ -99,6 +100,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
99
100
  threshold: config.comparison.threshold,
100
101
  uploadAll: options.uploadAll || false,
101
102
  metadata: options.metadata ? JSON.parse(options.metadata) : {},
103
+ pullRequestNumber,
102
104
  onProgress: progressData => {
103
105
  const {
104
106
  message: progressMessage,
@@ -114,13 +114,12 @@ export class TestRunner extends BaseService {
114
114
  const apiService = await this.createApiService();
115
115
  if (apiService) {
116
116
  const buildResult = await apiService.createBuild({
117
- build: {
118
- name: options.buildName || `Test Run ${new Date().toISOString()}`,
119
- branch: options.branch || 'main',
120
- environment: options.environment || 'test',
121
- commit_sha: options.commit,
122
- commit_message: options.message
123
- }
117
+ name: options.buildName || `Test Run ${new Date().toISOString()}`,
118
+ branch: options.branch || 'main',
119
+ environment: options.environment || 'test',
120
+ commit_sha: options.commit,
121
+ commit_message: options.message,
122
+ github_pull_request_number: options.pullRequestNumber
124
123
  });
125
124
  this.logger.debug(`Build created with ID: ${buildResult.id}`);
126
125
 
@@ -154,10 +153,13 @@ export class TestRunner extends BaseService {
154
153
  }
155
154
  try {
156
155
  if (isTddMode) {
157
- // TDD mode: use server handler to finalize
156
+ // TDD mode: use server handler to finalize (local-only)
158
157
  if (this.serverManager.server?.finishBuild) {
159
158
  await this.serverManager.server.finishBuild(buildId);
160
159
  this.logger.debug(`TDD build ${buildId} finalized with success: ${success}`);
160
+ } else {
161
+ // In TDD mode without a server, just log that finalization is skipped
162
+ this.logger.debug(`TDD build ${buildId} finalization skipped (local-only mode)`);
161
163
  }
162
164
  } else {
163
165
  // API mode: use API service to update build status
@@ -50,6 +50,7 @@ export function createUploader({
50
50
  message,
51
51
  environment = 'production',
52
52
  threshold,
53
+ pullRequestNumber,
53
54
  onProgress = () => {}
54
55
  }) {
55
56
  try {
@@ -96,10 +97,11 @@ export function createUploader({
96
97
  const buildInfo = {
97
98
  name: buildName || `Upload ${new Date().toISOString()}`,
98
99
  branch: branch || (await getDefaultBranch()) || 'main',
99
- commitSha: commit,
100
- commitMessage: message,
100
+ commit_sha: commit,
101
+ commit_message: message,
101
102
  environment,
102
- threshold
103
+ threshold,
104
+ github_pull_request_number: pullRequestNumber
103
105
  };
104
106
  const build = await api.createBuild(buildInfo);
105
107
  const buildId = build.id;
@@ -3,10 +3,11 @@
3
3
  * @param {string} testCommand - Test command to execute
4
4
  * @param {Object} options - Command options
5
5
  * @param {Object} globalOptions - Global CLI options
6
+ * @returns {Promise<{result: Object, cleanup: Function}>} Result and cleanup function
6
7
  */
7
8
  export function tddCommand(testCommand: string, options?: any, globalOptions?: any): Promise<{
8
- success: boolean;
9
- exitCode: number;
9
+ result: any;
10
+ cleanup: Function;
10
11
  }>;
11
12
  /**
12
13
  * Validate TDD options
@@ -8,7 +8,7 @@ export function createUploader({ apiKey, apiUrl, userAgent, command, upload: upl
8
8
  command: any;
9
9
  upload?: {};
10
10
  }, options?: {}): {
11
- upload: ({ screenshotsDir, buildName, branch, commit, message, environment, threshold, onProgress, }: {
11
+ upload: ({ screenshotsDir, buildName, branch, commit, message, environment, threshold, pullRequestNumber, onProgress, }: {
12
12
  screenshotsDir: any;
13
13
  buildName: any;
14
14
  branch: any;
@@ -16,6 +16,7 @@ export function createUploader({ apiKey, apiUrl, userAgent, command, upload: upl
16
16
  message: any;
17
17
  environment?: string;
18
18
  threshold: any;
19
+ pullRequestNumber: any;
19
20
  onProgress?: () => void;
20
21
  }) => Promise<{
21
22
  success: boolean;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CI Environment Detection
3
+ *
4
+ * Generic functions to extract git and PR information from any CI provider
5
+ */
6
+ /**
7
+ * Get the branch name from CI environment variables
8
+ * @returns {string|null} Branch name or null if not available
9
+ */
10
+ export function getBranch(): string | null;
11
+ /**
12
+ * Get the commit SHA from CI environment variables
13
+ * @returns {string|null} Commit SHA or null if not available
14
+ */
15
+ export function getCommit(): string | null;
16
+ /**
17
+ * Get the commit message from CI environment variables
18
+ * @returns {string|null} Commit message or null if not available
19
+ */
20
+ export function getCommitMessage(): string | null;
21
+ /**
22
+ * Get the pull request number from CI environment variables
23
+ * @returns {number|null} PR number or null if not available/not a PR
24
+ */
25
+ export function getPullRequestNumber(): number | null;
26
+ /**
27
+ * Get the PR head SHA from CI environment variables
28
+ * @returns {string|null} PR head SHA or null if not available
29
+ */
30
+ export function getPullRequestHeadSha(): string | null;
31
+ /**
32
+ * Get the PR base SHA from CI environment variables
33
+ * @returns {string|null} PR base SHA or null if not available
34
+ */
35
+ export function getPullRequestBaseSha(): string | null;
36
+ /**
37
+ * Get the PR head ref (branch) from CI environment variables
38
+ * @returns {string|null} PR head ref or null if not available
39
+ */
40
+ export function getPullRequestHeadRef(): string | null;
41
+ /**
42
+ * Get the PR base ref (target branch) from CI environment variables
43
+ * @returns {string|null} PR base ref or null if not available
44
+ */
45
+ export function getPullRequestBaseRef(): string | null;
46
+ /**
47
+ * Check if we're currently in a pull request context
48
+ * @returns {boolean} True if in a PR context
49
+ */
50
+ export function isPullRequest(): boolean;
51
+ /**
52
+ * Get the CI provider name
53
+ * @returns {string} CI provider name or 'unknown'
54
+ */
55
+ export function getCIProvider(): string;
@@ -9,6 +9,13 @@ export function generateBuildName(): string;
9
9
  * @returns {Promise<string|null>} Commit message or null if not available
10
10
  */
11
11
  export function getCommitMessage(cwd?: string): Promise<string | null>;
12
+ /**
13
+ * Detect commit message with override and environment variable support
14
+ * @param {string} override - Commit message override from CLI
15
+ * @param {string} cwd - Working directory
16
+ * @returns {Promise<string|null>} Commit message or null if not available
17
+ */
18
+ export function detectCommitMessage(override?: string, cwd?: string): Promise<string | null>;
12
19
  /**
13
20
  * Check if the working directory is a git repository
14
21
  * @param {string} cwd - Working directory
@@ -42,3 +49,8 @@ export function detectCommit(override?: string, cwd?: string): Promise<string |
42
49
  * @returns {Promise<string>}
43
50
  */
44
51
  export function generateBuildNameWithGit(override?: string, cwd?: string): Promise<string>;
52
+ /**
53
+ * Detect pull request number from CI environment
54
+ * @returns {number|null} Pull request number or null if not in PR context
55
+ */
56
+ export function detectPullRequestNumber(): number | null;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * CI Environment Detection
3
+ *
4
+ * Generic functions to extract git and PR information from any CI provider
5
+ */
6
+
7
+ /**
8
+ * Get the branch name from CI environment variables
9
+ * @returns {string|null} Branch name or null if not available
10
+ */
11
+ export function getBranch() {
12
+ return process.env.VIZZLY_BRANCH ||
13
+ // Vizzly override
14
+ process.env.GITHUB_HEAD_REF ||
15
+ // GitHub Actions (for PRs)
16
+ process.env.GITHUB_REF_NAME ||
17
+ // GitHub Actions (for pushes)
18
+ process.env.CI_COMMIT_REF_NAME ||
19
+ // GitLab CI
20
+ process.env.CIRCLE_BRANCH ||
21
+ // CircleCI
22
+ process.env.TRAVIS_BRANCH ||
23
+ // Travis CI
24
+ process.env.BUILDKITE_BRANCH ||
25
+ // Buildkite
26
+ process.env.DRONE_BRANCH ||
27
+ // Drone CI
28
+ process.env.BRANCH_NAME ||
29
+ // Jenkins
30
+ process.env.GIT_BRANCH ||
31
+ // Jenkins (alternative)
32
+ process.env.BITBUCKET_BRANCH ||
33
+ // Bitbucket Pipelines
34
+ process.env.WERCKER_GIT_BRANCH ||
35
+ // Wercker
36
+ process.env.APPVEYOR_REPO_BRANCH ||
37
+ // AppVeyor
38
+ process.env.BUILD_SOURCEBRANCH?.replace(/^refs\/heads\//, '') ||
39
+ // Azure DevOps
40
+ process.env.CODEBUILD_WEBHOOK_HEAD_REF?.replace(/^refs\/heads\//, '') ||
41
+ // AWS CodeBuild
42
+ process.env.SEMAPHORE_GIT_BRANCH ||
43
+ // Semaphore
44
+ null;
45
+ }
46
+
47
+ /**
48
+ * Get the commit SHA from CI environment variables
49
+ * @returns {string|null} Commit SHA or null if not available
50
+ */
51
+ export function getCommit() {
52
+ return process.env.VIZZLY_COMMIT_SHA ||
53
+ // Vizzly override
54
+ process.env.GITHUB_SHA ||
55
+ // GitHub Actions
56
+ process.env.CI_COMMIT_SHA ||
57
+ // GitLab CI
58
+ process.env.CIRCLE_SHA1 ||
59
+ // CircleCI
60
+ process.env.TRAVIS_COMMIT ||
61
+ // Travis CI
62
+ process.env.BUILDKITE_COMMIT ||
63
+ // Buildkite
64
+ process.env.DRONE_COMMIT_SHA ||
65
+ // Drone CI
66
+ process.env.GIT_COMMIT ||
67
+ // Jenkins
68
+ process.env.BITBUCKET_COMMIT ||
69
+ // Bitbucket Pipelines
70
+ process.env.WERCKER_GIT_COMMIT ||
71
+ // Wercker
72
+ process.env.APPVEYOR_REPO_COMMIT ||
73
+ // AppVeyor
74
+ process.env.BUILD_SOURCEVERSION ||
75
+ // Azure DevOps
76
+ process.env.CODEBUILD_RESOLVED_SOURCE_VERSION ||
77
+ // AWS CodeBuild
78
+ process.env.SEMAPHORE_GIT_SHA ||
79
+ // Semaphore
80
+ process.env.HEROKU_TEST_RUN_COMMIT_VERSION ||
81
+ // Heroku CI
82
+ process.env.COMMIT_SHA ||
83
+ // Generic
84
+ process.env.HEAD_COMMIT ||
85
+ // Alternative generic
86
+ process.env.SHA ||
87
+ // Another generic option
88
+ null;
89
+ }
90
+
91
+ /**
92
+ * Get the commit message from CI environment variables
93
+ * @returns {string|null} Commit message or null if not available
94
+ */
95
+ export function getCommitMessage() {
96
+ return process.env.VIZZLY_COMMIT_MESSAGE ||
97
+ // Vizzly override
98
+ process.env.CI_COMMIT_MESSAGE ||
99
+ // GitLab CI
100
+ process.env.TRAVIS_COMMIT_MESSAGE ||
101
+ // Travis CI
102
+ process.env.BUILDKITE_MESSAGE ||
103
+ // Buildkite
104
+ process.env.DRONE_COMMIT_MESSAGE ||
105
+ // Drone CI
106
+ process.env.APPVEYOR_REPO_COMMIT_MESSAGE ||
107
+ // AppVeyor
108
+ process.env.COMMIT_MESSAGE ||
109
+ // Generic
110
+ null;
111
+ }
112
+
113
+ /**
114
+ * Get the pull request number from CI environment variables
115
+ * @returns {number|null} PR number or null if not available/not a PR
116
+ */
117
+ export function getPullRequestNumber() {
118
+ // Check VIZZLY override first
119
+ if (process.env.VIZZLY_PR_NUMBER) {
120
+ return parseInt(process.env.VIZZLY_PR_NUMBER, 10);
121
+ }
122
+
123
+ // GitHub Actions - extract from GITHUB_REF
124
+ if (process.env.GITHUB_ACTIONS && process.env.GITHUB_EVENT_NAME === 'pull_request') {
125
+ const prMatch = process.env.GITHUB_REF?.match(/refs\/pull\/(\d+)\/merge/);
126
+ if (prMatch?.[1]) return parseInt(prMatch[1], 10);
127
+ }
128
+
129
+ // GitLab CI
130
+ if (process.env.CI_MERGE_REQUEST_ID) {
131
+ return parseInt(process.env.CI_MERGE_REQUEST_ID, 10);
132
+ }
133
+
134
+ // CircleCI - extract from PR URL
135
+ if (process.env.CIRCLE_PULL_REQUEST) {
136
+ const prMatch = process.env.CIRCLE_PULL_REQUEST.match(/\/pull\/(\d+)$/);
137
+ if (prMatch?.[1]) return parseInt(prMatch[1], 10);
138
+ }
139
+
140
+ // Travis CI
141
+ if (process.env.TRAVIS_PULL_REQUEST && process.env.TRAVIS_PULL_REQUEST !== 'false') {
142
+ return parseInt(process.env.TRAVIS_PULL_REQUEST, 10);
143
+ }
144
+
145
+ // Buildkite
146
+ if (process.env.BUILDKITE_PULL_REQUEST && process.env.BUILDKITE_PULL_REQUEST !== 'false') {
147
+ return parseInt(process.env.BUILDKITE_PULL_REQUEST, 10);
148
+ }
149
+
150
+ // Drone CI
151
+ if (process.env.DRONE_PULL_REQUEST) {
152
+ return parseInt(process.env.DRONE_PULL_REQUEST, 10);
153
+ }
154
+
155
+ // Jenkins (GitHub Pull Request Builder plugin)
156
+ if (process.env.ghprbPullId) {
157
+ return parseInt(process.env.ghprbPullId, 10);
158
+ }
159
+
160
+ // Azure DevOps
161
+ if (process.env.SYSTEM_PULLREQUEST_PULLREQUESTID) {
162
+ return parseInt(process.env.SYSTEM_PULLREQUEST_PULLREQUESTID, 10);
163
+ }
164
+
165
+ // AppVeyor
166
+ if (process.env.APPVEYOR_PULL_REQUEST_NUMBER) {
167
+ return parseInt(process.env.APPVEYOR_PULL_REQUEST_NUMBER, 10);
168
+ }
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Get the PR head SHA from CI environment variables
174
+ * @returns {string|null} PR head SHA or null if not available
175
+ */
176
+ export function getPullRequestHeadSha() {
177
+ return process.env.VIZZLY_PR_HEAD_SHA ||
178
+ // Vizzly override
179
+ process.env.GITHUB_SHA ||
180
+ // GitHub Actions
181
+ process.env.CI_COMMIT_SHA ||
182
+ // GitLab CI
183
+ process.env.CIRCLE_SHA1 ||
184
+ // CircleCI
185
+ process.env.TRAVIS_COMMIT ||
186
+ // Travis CI
187
+ process.env.BUILDKITE_COMMIT ||
188
+ // Buildkite
189
+ process.env.DRONE_COMMIT_SHA ||
190
+ // Drone CI
191
+ process.env.ghprbActualCommit ||
192
+ // Jenkins
193
+ process.env.GIT_COMMIT ||
194
+ // Jenkins fallback
195
+ process.env.BUILD_SOURCEVERSION ||
196
+ // Azure DevOps
197
+ process.env.APPVEYOR_REPO_COMMIT ||
198
+ // AppVeyor
199
+ null;
200
+ }
201
+
202
+ /**
203
+ * Get the PR base SHA from CI environment variables
204
+ * @returns {string|null} PR base SHA or null if not available
205
+ */
206
+ export function getPullRequestBaseSha() {
207
+ return process.env.VIZZLY_PR_BASE_SHA ||
208
+ // Vizzly override
209
+ process.env.CI_MERGE_REQUEST_TARGET_BRANCH_SHA ||
210
+ // GitLab CI
211
+ null // Most CIs don't provide this
212
+ ;
213
+ }
214
+
215
+ /**
216
+ * Get the PR head ref (branch) from CI environment variables
217
+ * @returns {string|null} PR head ref or null if not available
218
+ */
219
+ export function getPullRequestHeadRef() {
220
+ return process.env.VIZZLY_PR_HEAD_REF ||
221
+ // Vizzly override
222
+ process.env.GITHUB_HEAD_REF ||
223
+ // GitHub Actions
224
+ process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME ||
225
+ // GitLab CI
226
+ process.env.TRAVIS_PULL_REQUEST_BRANCH ||
227
+ // Travis CI
228
+ process.env.DRONE_SOURCE_BRANCH ||
229
+ // Drone CI
230
+ process.env.ghprbSourceBranch ||
231
+ // Jenkins
232
+ process.env.SYSTEM_PULLREQUEST_SOURCEBRANCH?.replace(/^refs\/heads\//, '') ||
233
+ // Azure DevOps
234
+ process.env.APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH ||
235
+ // AppVeyor
236
+ null;
237
+ }
238
+
239
+ /**
240
+ * Get the PR base ref (target branch) from CI environment variables
241
+ * @returns {string|null} PR base ref or null if not available
242
+ */
243
+ export function getPullRequestBaseRef() {
244
+ return process.env.VIZZLY_PR_BASE_REF ||
245
+ // Vizzly override
246
+ process.env.GITHUB_BASE_REF ||
247
+ // GitHub Actions
248
+ process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME ||
249
+ // GitLab CI
250
+ process.env.TRAVIS_BRANCH ||
251
+ // Travis CI (target branch)
252
+ process.env.BUILDKITE_PULL_REQUEST_BASE_BRANCH ||
253
+ // Buildkite
254
+ process.env.DRONE_TARGET_BRANCH ||
255
+ // Drone CI
256
+ process.env.ghprbTargetBranch ||
257
+ // Jenkins
258
+ process.env.SYSTEM_PULLREQUEST_TARGETBRANCH?.replace(/^refs\/heads\//, '') ||
259
+ // Azure DevOps
260
+ process.env.APPVEYOR_REPO_BRANCH ||
261
+ // AppVeyor (target branch)
262
+ null;
263
+ }
264
+
265
+ /**
266
+ * Check if we're currently in a pull request context
267
+ * @returns {boolean} True if in a PR context
268
+ */
269
+ export function isPullRequest() {
270
+ return getPullRequestNumber() !== null;
271
+ }
272
+
273
+ /**
274
+ * Get the CI provider name
275
+ * @returns {string} CI provider name or 'unknown'
276
+ */
277
+ export function getCIProvider() {
278
+ if (process.env.GITHUB_ACTIONS) return 'github-actions';
279
+ if (process.env.GITLAB_CI) return 'gitlab-ci';
280
+ if (process.env.CIRCLECI) return 'circleci';
281
+ if (process.env.TRAVIS) return 'travis-ci';
282
+ if (process.env.BUILDKITE) return 'buildkite';
283
+ if (process.env.DRONE) return 'drone-ci';
284
+ if (process.env.JENKINS_URL) return 'jenkins';
285
+ if (process.env.AZURE_HTTP_USER_AGENT || process.env.TF_BUILD) return 'azure-devops';
286
+ if (process.env.CODEBUILD_BUILD_ID) return 'aws-codebuild';
287
+ if (process.env.APPVEYOR) return 'appveyor';
288
+ if (process.env.SEMAPHORE) return 'semaphore';
289
+ if (process.env.WERCKER) return 'wercker';
290
+ if (process.env.BITBUCKET_BUILD_NUMBER) return 'bitbucket-pipelines';
291
+ if (process.env.HEROKU_TEST_RUN_ID) return 'heroku-ci';
292
+ return 'unknown';
293
+ }
package/dist/utils/git.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { exec } from 'child_process';
2
2
  import { promisify } from 'util';
3
+ import { getBranch as getCIBranch, getCommit as getCICommit, getCommitMessage as getCICommitMessage, getPullRequestNumber } from './ci-env.js';
3
4
  const execAsync = promisify(exec);
4
5
  export async function getCommonAncestor(commit1, commit2, cwd = process.cwd()) {
5
6
  try {
@@ -144,6 +145,23 @@ export async function getCommitMessage(cwd = process.cwd()) {
144
145
  }
145
146
  }
146
147
 
148
+ /**
149
+ * Detect commit message with override and environment variable support
150
+ * @param {string} override - Commit message override from CLI
151
+ * @param {string} cwd - Working directory
152
+ * @returns {Promise<string|null>} Commit message or null if not available
153
+ */
154
+ export async function detectCommitMessage(override = null, cwd = process.cwd()) {
155
+ if (override) return override;
156
+
157
+ // Try CI environment variables first
158
+ const ciCommitMessage = getCICommitMessage();
159
+ if (ciCommitMessage) return ciCommitMessage;
160
+
161
+ // Fallback to regular git log
162
+ return await getCommitMessage(cwd);
163
+ }
164
+
147
165
  /**
148
166
  * Check if the working directory is a git repository
149
167
  * @param {string} cwd - Working directory
@@ -193,6 +211,12 @@ export async function getGitStatus(cwd = process.cwd()) {
193
211
  */
194
212
  export async function detectBranch(override = null, cwd = process.cwd()) {
195
213
  if (override) return override;
214
+
215
+ // Try CI environment variables first
216
+ const ciBranch = getCIBranch();
217
+ if (ciBranch) return ciBranch;
218
+
219
+ // Fallback to git command when no CI environment variables
196
220
  const currentBranch = await getCurrentBranch(cwd);
197
221
  return currentBranch || 'unknown';
198
222
  }
@@ -205,6 +229,12 @@ export async function detectBranch(override = null, cwd = process.cwd()) {
205
229
  */
206
230
  export async function detectCommit(override = null, cwd = process.cwd()) {
207
231
  if (override) return override;
232
+
233
+ // Try CI environment variables first
234
+ const ciCommit = getCICommit();
235
+ if (ciCommit) return ciCommit;
236
+
237
+ // Fallback to git command when no CI environment variables
208
238
  return await getCurrentCommitSha(cwd);
209
239
  }
210
240
 
@@ -223,4 +253,12 @@ export async function generateBuildNameWithGit(override = null, cwd = process.cw
223
253
  return `${branch}-${shortCommit}`;
224
254
  }
225
255
  return generateBuildName();
256
+ }
257
+
258
+ /**
259
+ * Detect pull request number from CI environment
260
+ * @returns {number|null} Pull request number or null if not in PR context
261
+ */
262
+ export function detectPullRequestNumber() {
263
+ return getPullRequestNumber();
226
264
  }
@@ -481,14 +481,29 @@ Configuration loaded via cosmiconfig in this order:
481
481
 
482
482
  ### Environment Variables
483
483
 
484
+ **Core Configuration:**
484
485
  - `VIZZLY_TOKEN` - API authentication token
485
486
  - `VIZZLY_API_URL` - API base URL override
486
487
  - `VIZZLY_LOG_LEVEL` - Logger level (`debug`, `info`, `warn`, `error`)
488
+
489
+ **Git Information Override (CI/CD Enhancement):**
490
+ - `VIZZLY_COMMIT_SHA` - Override detected commit SHA
491
+ - `VIZZLY_COMMIT_MESSAGE` - Override detected commit message
492
+ - `VIZZLY_BRANCH` - Override detected branch name
493
+ - `VIZZLY_PR_NUMBER` - Override detected pull request number
494
+
495
+ **Runtime (Set by CLI):**
487
496
  - `VIZZLY_SERVER_URL` - Screenshot server URL (set by CLI)
488
497
  - `VIZZLY_ENABLED` - Enable/disable client (set by CLI)
489
498
  - `VIZZLY_BUILD_ID` - Current build ID (set by CLI)
490
499
  - `VIZZLY_TDD_MODE` - TDD mode active (set by CLI)
491
500
 
501
+ **Priority Order for Git Information:**
502
+ 1. CLI arguments (`--commit`, `--branch`, `--message`)
503
+ 2. `VIZZLY_*` environment variables
504
+ 3. CI-specific environment variables (e.g., `GITHUB_SHA`, `CI_COMMIT_SHA`)
505
+ 4. Git command detection
506
+
492
507
  ## Error Handling
493
508
 
494
509
  ### Client Errors
@@ -275,8 +275,14 @@ jobs:
275
275
  - run: npx vizzly run "npm test" --wait
276
276
  env:
277
277
  VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
278
+ # Optional: Enhanced git information from GitHub context
279
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
280
+ VIZZLY_COMMIT_SHA: ${{ github.event.head_commit.id }}
281
+ VIZZLY_BRANCH: ${{ github.head_ref || github.ref_name }}
278
282
  ```
279
283
 
284
+ **Enhanced Git Information:** The `VIZZLY_*` environment variables ensure accurate git metadata is captured in your builds, avoiding issues with merge commits that can occur in CI environments.
285
+
280
286
  ### GitLab CI
281
287
 
282
288
  ```yaml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Visual review platform for UI developers and designers",
5
5
  "keywords": [
6
6
  "visual-testing",
@@ -73,11 +73,11 @@
73
73
  "registry": "https://registry.npmjs.org/"
74
74
  },
75
75
  "dependencies": {
76
- "commander": "^11.1.0",
76
+ "commander": "^14.0.0",
77
77
  "cosmiconfig": "^9.0.0",
78
78
  "dotenv": "^17.2.1",
79
79
  "form-data": "^4.0.0",
80
- "glob": "^10.3.10",
80
+ "glob": "^11.0.3",
81
81
  "odiff-bin": "^3.2.1"
82
82
  },
83
83
  "devDependencies": {
@@ -93,8 +93,7 @@
93
93
  "eslint-config-prettier": "^10.1.8",
94
94
  "eslint-plugin-prettier": "^5.5.3",
95
95
  "prettier": "^3.6.2",
96
- "puppeteer": "^24.16.1",
97
- "rimraf": "^5.0.5",
96
+ "rimraf": "^6.0.1",
98
97
  "typescript": "^5.0.4",
99
98
  "vite": "^7.1.2",
100
99
  "vitest": "^3.2.4"