@vizzly-testing/cli 0.4.0 → 0.6.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 +35 -5
- package/dist/cli.js +17 -1
- package/dist/commands/run.js +62 -21
- package/dist/commands/tdd.js +35 -35
- package/dist/commands/upload.js +4 -2
- package/dist/server/handlers/tdd-handler.js +82 -8
- package/dist/services/html-report-generator.js +377 -0
- package/dist/services/report-generator/report.css +355 -0
- package/dist/services/report-generator/viewer.js +100 -0
- package/dist/services/server-manager.js +3 -2
- package/dist/services/tdd-service.js +375 -66
- package/dist/services/test-runner.js +64 -35
- package/dist/services/uploader.js +5 -3
- package/dist/types/commands/tdd.d.ts +3 -2
- package/dist/types/server/handlers/tdd-handler.d.ts +18 -1
- package/dist/types/services/html-report-generator.d.ts +52 -0
- package/dist/types/services/report-generator/viewer.d.ts +0 -0
- package/dist/types/services/server-manager.d.ts +19 -1
- package/dist/types/services/tdd-service.d.ts +24 -3
- package/dist/types/services/uploader.d.ts +2 -1
- package/dist/types/utils/ci-env.d.ts +55 -0
- package/dist/types/utils/config-loader.d.ts +3 -0
- package/dist/types/utils/git.d.ts +12 -0
- package/dist/types/utils/security.d.ts +29 -0
- package/dist/utils/ci-env.js +293 -0
- package/dist/utils/config-loader.js +7 -0
- package/dist/utils/git.js +38 -0
- package/dist/utils/security.js +154 -0
- package/docs/api-reference.md +15 -0
- package/docs/tdd-mode.md +58 -12
- package/docs/test-integration.md +6 -0
- package/package.json +6 -6
|
@@ -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
|
+
}
|
|
@@ -25,6 +25,10 @@ const DEFAULT_CONFIG = {
|
|
|
25
25
|
// Comparison Configuration
|
|
26
26
|
comparison: {
|
|
27
27
|
threshold: 0.1
|
|
28
|
+
},
|
|
29
|
+
// TDD Configuration
|
|
30
|
+
tdd: {
|
|
31
|
+
openReport: false // Whether to auto-open HTML report in browser
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
34
|
export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
@@ -42,6 +46,9 @@ export async function loadConfig(configPath = null, cliOverrides = {}) {
|
|
|
42
46
|
},
|
|
43
47
|
comparison: {
|
|
44
48
|
...DEFAULT_CONFIG.comparison
|
|
49
|
+
},
|
|
50
|
+
tdd: {
|
|
51
|
+
...DEFAULT_CONFIG.tdd
|
|
45
52
|
}
|
|
46
53
|
};
|
|
47
54
|
|
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
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security utilities for path sanitization and validation
|
|
3
|
+
* Protects against path traversal attacks and ensures safe file operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resolve, normalize, isAbsolute, join } from 'path';
|
|
7
|
+
import { createServiceLogger } from './logger-factory.js';
|
|
8
|
+
const logger = createServiceLogger('SECURITY');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sanitizes a screenshot name to prevent path traversal and ensure safe file naming
|
|
12
|
+
* @param {string} name - Original screenshot name
|
|
13
|
+
* @param {number} maxLength - Maximum allowed length (default: 255)
|
|
14
|
+
* @returns {string} Sanitized screenshot name
|
|
15
|
+
*/
|
|
16
|
+
export function sanitizeScreenshotName(name, maxLength = 255) {
|
|
17
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
18
|
+
throw new Error('Screenshot name must be a non-empty string');
|
|
19
|
+
}
|
|
20
|
+
if (name.length > maxLength) {
|
|
21
|
+
throw new Error(`Screenshot name exceeds maximum length of ${maxLength} characters`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Block directory traversal patterns
|
|
25
|
+
if (name.includes('..') || name.includes('/') || name.includes('\\')) {
|
|
26
|
+
throw new Error('Screenshot name contains invalid path characters');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Block absolute paths
|
|
30
|
+
if (isAbsolute(name)) {
|
|
31
|
+
throw new Error('Screenshot name cannot be an absolute path');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Allow only safe characters: alphanumeric, hyphens, underscores, and dots
|
|
35
|
+
// Replace other characters with underscores
|
|
36
|
+
let sanitized = name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
37
|
+
|
|
38
|
+
// Prevent names that start with dots (hidden files)
|
|
39
|
+
if (sanitized.startsWith('.')) {
|
|
40
|
+
sanitized = 'file_' + sanitized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Ensure we have a valid filename
|
|
44
|
+
if (sanitized.length === 0 || sanitized === '.' || sanitized === '..') {
|
|
45
|
+
sanitized = 'unnamed_screenshot';
|
|
46
|
+
}
|
|
47
|
+
return sanitized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Validates that a path stays within the allowed working directory bounds
|
|
52
|
+
* @param {string} targetPath - Path to validate
|
|
53
|
+
* @param {string} workingDir - Working directory that serves as the security boundary
|
|
54
|
+
* @returns {string} Resolved and normalized path if valid
|
|
55
|
+
* @throws {Error} If path is invalid or outside bounds
|
|
56
|
+
*/
|
|
57
|
+
export function validatePathSecurity(targetPath, workingDir) {
|
|
58
|
+
if (typeof targetPath !== 'string' || targetPath.length === 0) {
|
|
59
|
+
throw new Error('Path must be a non-empty string');
|
|
60
|
+
}
|
|
61
|
+
if (typeof workingDir !== 'string' || workingDir.length === 0) {
|
|
62
|
+
throw new Error('Working directory must be a non-empty string');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Normalize and resolve both paths
|
|
66
|
+
let resolvedWorkingDir = resolve(normalize(workingDir));
|
|
67
|
+
let resolvedTargetPath = resolve(normalize(targetPath));
|
|
68
|
+
|
|
69
|
+
// Ensure the target path starts with the working directory
|
|
70
|
+
if (!resolvedTargetPath.startsWith(resolvedWorkingDir)) {
|
|
71
|
+
logger.warn(`Path traversal attempt blocked: ${targetPath} (resolved: ${resolvedTargetPath}) is outside working directory: ${resolvedWorkingDir}`);
|
|
72
|
+
throw new Error('Path is outside the allowed working directory');
|
|
73
|
+
}
|
|
74
|
+
return resolvedTargetPath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Safely constructs a path within the working directory
|
|
79
|
+
* @param {string} workingDir - Base working directory
|
|
80
|
+
* @param {...string} pathSegments - Path segments to join
|
|
81
|
+
* @returns {string} Safely constructed path
|
|
82
|
+
* @throws {Error} If resulting path would be outside working directory
|
|
83
|
+
*/
|
|
84
|
+
export function safePath(workingDir, ...pathSegments) {
|
|
85
|
+
if (pathSegments.length === 0) {
|
|
86
|
+
return validatePathSecurity(workingDir, workingDir);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Sanitize each path segment
|
|
90
|
+
let sanitizedSegments = pathSegments.map(segment => {
|
|
91
|
+
if (typeof segment !== 'string') {
|
|
92
|
+
throw new Error('Path segment must be a string');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Block directory traversal in segments
|
|
96
|
+
if (segment.includes('..')) {
|
|
97
|
+
throw new Error('Path segment contains directory traversal sequence');
|
|
98
|
+
}
|
|
99
|
+
return segment;
|
|
100
|
+
});
|
|
101
|
+
let targetPath = join(workingDir, ...sanitizedSegments);
|
|
102
|
+
return validatePathSecurity(targetPath, workingDir);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validates screenshot properties object for safe values
|
|
107
|
+
* @param {Object} properties - Properties to validate
|
|
108
|
+
* @returns {Object} Validated properties object
|
|
109
|
+
*/
|
|
110
|
+
export function validateScreenshotProperties(properties = {}) {
|
|
111
|
+
if (properties === null || typeof properties !== 'object') {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
let validated = {};
|
|
115
|
+
|
|
116
|
+
// Validate common properties with safe constraints
|
|
117
|
+
if (properties.browser && typeof properties.browser === 'string') {
|
|
118
|
+
try {
|
|
119
|
+
validated.browser = sanitizeScreenshotName(properties.browser, 50);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// Skip invalid browser names, don't include them
|
|
122
|
+
logger.warn(`Invalid browser name '${properties.browser}': ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (properties.viewport && typeof properties.viewport === 'object') {
|
|
126
|
+
let viewport = {};
|
|
127
|
+
if (typeof properties.viewport.width === 'number' && properties.viewport.width > 0 && properties.viewport.width <= 10000) {
|
|
128
|
+
viewport.width = Math.floor(properties.viewport.width);
|
|
129
|
+
}
|
|
130
|
+
if (typeof properties.viewport.height === 'number' && properties.viewport.height > 0 && properties.viewport.height <= 10000) {
|
|
131
|
+
viewport.height = Math.floor(properties.viewport.height);
|
|
132
|
+
}
|
|
133
|
+
if (Object.keys(viewport).length > 0) {
|
|
134
|
+
validated.viewport = viewport;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Allow other safe string properties but sanitize them
|
|
139
|
+
for (let [key, value] of Object.entries(properties)) {
|
|
140
|
+
if (key === 'browser' || key === 'viewport') continue; // Already handled
|
|
141
|
+
|
|
142
|
+
if (typeof key === 'string' && key.length <= 50 && /^[a-zA-Z0-9_-]+$/.test(key)) {
|
|
143
|
+
if (typeof value === 'string' && value.length <= 200) {
|
|
144
|
+
// Store sanitized version of string values
|
|
145
|
+
validated[key] = value.replace(/[<>&"']/g, ''); // Basic HTML entity prevention
|
|
146
|
+
} else if (typeof value === 'number' && !isNaN(value) && isFinite(value)) {
|
|
147
|
+
validated[key] = value;
|
|
148
|
+
} else if (typeof value === 'boolean') {
|
|
149
|
+
validated[key] = value;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return validated;
|
|
154
|
+
}
|
package/docs/api-reference.md
CHANGED
|
@@ -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
|