haven-cypress-integration 1.6.1 โ 2.0.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 +224 -120
- package/bin/haven-cypress.js +46 -59
- package/index.js +87 -215
- package/package.json +5 -4
- package/templates/Dockerfile +32 -22
- package/templates/run-filtered.sh +178 -91
- package/templates/syncCypressResults.js +128 -148
package/index.js
CHANGED
|
@@ -1,231 +1,103 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const {
|
|
4
|
-
|
|
5
|
-
class HavenCypressIntegration {
|
|
6
|
-
constructor() {
|
|
7
|
-
this.templatesDir = path.join(__dirname, 'templates');
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Build Docker image with Haven integration
|
|
12
|
-
* @param {string} tag - Docker image tag
|
|
13
|
-
* @param {Object} options - Build options
|
|
14
|
-
*/
|
|
15
|
-
buildImage(tag = 'haven-cypress-tests:latest', options = {}) {
|
|
16
|
-
console.log('๐ณ Building Haven-integrated Docker image...');
|
|
17
|
-
|
|
18
|
-
// Validate product name if pushing
|
|
19
|
-
if (options.push && !options.product) {
|
|
20
|
-
throw new Error('โ --product is required when using --push');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Create temporary build directory
|
|
24
|
-
const buildDir = path.join(process.cwd(), '.haven-build');
|
|
25
|
-
if (fs.existsSync(buildDir)) {
|
|
26
|
-
fs.rmSync(buildDir, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
fs.mkdirSync(buildDir, { recursive: true });
|
|
29
|
-
|
|
30
|
-
try {
|
|
31
|
-
// Copy user's project files
|
|
32
|
-
console.log('๐ Copying project files...');
|
|
33
|
-
this.copyProjectFiles(buildDir);
|
|
34
|
-
|
|
35
|
-
// Copy Haven integration files
|
|
36
|
-
console.log('๐ง Adding Haven integration files...');
|
|
37
|
-
this.copyHavenFiles(buildDir);
|
|
38
|
-
|
|
39
|
-
// Build Docker image
|
|
40
|
-
console.log('๐๏ธ Building Docker image...');
|
|
41
|
-
execSync(`docker build -t ${tag} .`, {
|
|
42
|
-
cwd: buildDir,
|
|
43
|
-
stdio: 'inherit'
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
console.log(`โ
Docker image built successfully: ${tag}`);
|
|
47
|
-
|
|
48
|
-
// Push if requested
|
|
49
|
-
if (options.push) {
|
|
50
|
-
this.pushToECR(tag, options.product);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
} finally {
|
|
54
|
-
// Cleanup build directory
|
|
55
|
-
if (fs.existsSync(buildDir)) {
|
|
56
|
-
fs.rmSync(buildDir, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Copy user's project files to build directory
|
|
63
|
-
*/
|
|
64
|
-
copyProjectFiles(buildDir) {
|
|
65
|
-
const filesToCopy = [
|
|
66
|
-
'package.json',
|
|
67
|
-
'package-lock.json',
|
|
68
|
-
'cypress.config.js',
|
|
69
|
-
'cypress'
|
|
70
|
-
];
|
|
71
|
-
|
|
72
|
-
filesToCopy.forEach(file => {
|
|
73
|
-
const src = path.join(process.cwd(), file);
|
|
74
|
-
const dest = path.join(buildDir, file);
|
|
75
|
-
|
|
76
|
-
if (fs.existsSync(src)) {
|
|
77
|
-
if (fs.statSync(src).isDirectory()) {
|
|
78
|
-
this.copyDir(src, dest);
|
|
79
|
-
} else {
|
|
80
|
-
fs.copyFileSync(src, dest);
|
|
81
|
-
}
|
|
82
|
-
console.log(` โ
Copied ${file}`);
|
|
83
|
-
} else {
|
|
84
|
-
console.log(` โ ๏ธ Skipped ${file} (not found)`);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Copy Haven integration files to build directory
|
|
91
|
-
*/
|
|
92
|
-
copyHavenFiles(buildDir) {
|
|
93
|
-
const havenFiles = [
|
|
94
|
-
'Dockerfile',
|
|
95
|
-
'run-filtered.sh',
|
|
96
|
-
'syncCypressResults.js'
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
havenFiles.forEach(file => {
|
|
100
|
-
const src = path.join(this.templatesDir, file);
|
|
101
|
-
const dest = path.join(buildDir, file);
|
|
102
|
-
|
|
103
|
-
if (fs.existsSync(src)) {
|
|
104
|
-
fs.copyFileSync(src, dest);
|
|
105
|
-
console.log(` โ
Added ${file}`);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
// Make run-filtered.sh executable
|
|
110
|
-
execSync('chmod +x run-filtered.sh', { cwd: buildDir });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Copy directory recursively
|
|
115
|
-
*/
|
|
116
|
-
copyDir(src, dest) {
|
|
117
|
-
if (!fs.existsSync(dest)) {
|
|
118
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
119
|
-
}
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
120
4
|
|
|
121
|
-
|
|
5
|
+
function run(cmd, args, opts = {}) {
|
|
6
|
+
const res = spawnSync(cmd, args, { stdio: 'inherit', shell: false, ...opts });
|
|
7
|
+
if (res.status !== 0) throw new Error(`${cmd} ${args.join(' ')} failed`);
|
|
8
|
+
return res;
|
|
9
|
+
}
|
|
122
10
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
11
|
+
module.exports = {
|
|
12
|
+
buildImage(tag = 'haven-cypress-tests:latest', options = {}) {
|
|
13
|
+
const buildType = options.type || 'thin';
|
|
14
|
+
console.log(`Building Haven-integrated Cypress image (${buildType} mode)...`);
|
|
126
15
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
fs.copyFileSync(srcPath, destPath);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Push image to ECR with product organization
|
|
137
|
-
* @param {string} localTag - Local Docker image tag
|
|
138
|
-
* @param {string} product - Product name for organization
|
|
139
|
-
*/
|
|
140
|
-
pushToECR(localTag, product) {
|
|
141
|
-
console.log(`๐ค Pushing image to ECR for product: ${product}`);
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
// Get AWS account ID and region
|
|
145
|
-
console.log('๐ Getting AWS account information...');
|
|
146
|
-
const accountId = execSync('aws sts get-caller-identity --query Account --output text', { encoding: 'utf8' }).trim();
|
|
147
|
-
const region = execSync('aws configure get region', { encoding: 'utf8' }).trim() || 'us-east-1';
|
|
148
|
-
|
|
149
|
-
console.log(`๐ Account: ${accountId}, Region: ${region}`);
|
|
150
|
-
|
|
151
|
-
// ECR repository and image details
|
|
152
|
-
const ecrRepo = 'haven-test-images';
|
|
153
|
-
const ecrUri = `${accountId}.dkr.ecr.${region}.amazonaws.com/${ecrRepo}`;
|
|
154
|
-
|
|
155
|
-
// Extract version from local tag or use package.json version
|
|
156
|
-
const localVersion = localTag.split(':')[1] || 'latest';
|
|
157
|
-
|
|
158
|
-
// Generate ECR tag with product and version
|
|
159
|
-
let ecrTag;
|
|
160
|
-
if (localVersion === 'latest') {
|
|
161
|
-
// Try to read version from package.json first
|
|
162
|
-
let packageVersion = null;
|
|
163
|
-
try {
|
|
164
|
-
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
165
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
166
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
167
|
-
packageVersion = packageJson.version;
|
|
168
|
-
}
|
|
169
|
-
} catch (error) {
|
|
170
|
-
console.warn('โ ๏ธ Could not read package.json version:', error.message);
|
|
16
|
+
// Validate product name if pushing
|
|
17
|
+
if (options.push && !options.product) {
|
|
18
|
+
throw new Error('--product is required when using --push');
|
|
171
19
|
}
|
|
172
20
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
21
|
+
const buildDir = path.join(process.cwd(), '.haven-cypress-build');
|
|
22
|
+
if (fs.existsSync(buildDir)) fs.rmSync(buildDir, { recursive: true });
|
|
23
|
+
fs.mkdirSync(buildDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Copy templates (always needed)
|
|
26
|
+
const tplDir = path.join(__dirname, 'templates');
|
|
27
|
+
run('bash', ['-lc', `cp -R ${JSON.stringify(path.join(tplDir, 'Dockerfile'))} ${JSON.stringify(buildDir)}`]);
|
|
28
|
+
run('bash', ['-lc', `cp -R ${JSON.stringify(path.join(tplDir, 'run-filtered.sh'))} ${JSON.stringify(path.join(buildDir, 'run-filtered.sh'))}`]);
|
|
29
|
+
run('bash', ['-lc', `cp -R ${JSON.stringify(path.join(tplDir, 'syncCypressResults.js'))} ${JSON.stringify(path.join(buildDir, 'syncCypressResults.js'))}`]);
|
|
30
|
+
|
|
31
|
+
// For full image: copy project files
|
|
32
|
+
if (buildType === 'full') {
|
|
33
|
+
console.log('Full image: copying project files...');
|
|
34
|
+
const include = ['package.json', 'package-lock.json', 'cypress.config.js', 'cypress.config.ts', 'cypress', 'src', 'node_modules'];
|
|
35
|
+
for (const item of include) {
|
|
36
|
+
if (fs.existsSync(path.join(process.cwd(), item))) {
|
|
37
|
+
run('bash', ['-lc', `cp -R ${JSON.stringify(item)} ${JSON.stringify(buildDir)}`]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
177
40
|
} else {
|
|
178
|
-
|
|
179
|
-
const buildNumber = process.env.BUILD_NUMBER || Date.now().toString().slice(-6);
|
|
180
|
-
ecrTag = `${product}-1.0.${buildNumber}`;
|
|
41
|
+
console.log('Thin image: skipping project files (will be cloned at runtime)...');
|
|
181
42
|
}
|
|
182
|
-
} else {
|
|
183
|
-
// For custom versions, preserve semantic versioning without 'v' prefix
|
|
184
|
-
const cleanVersion = localVersion.startsWith('v') ? localVersion.slice(1) : localVersion;
|
|
185
|
-
ecrTag = `${product}-${cleanVersion}`;
|
|
186
|
-
}
|
|
187
43
|
|
|
188
|
-
|
|
44
|
+
// Build with target stage
|
|
45
|
+
const target = buildType === 'thin' ? 'thin-image' : 'full-image';
|
|
46
|
+
console.log(`Building Docker image with target: ${target}`);
|
|
47
|
+
run('bash', ['-lc', `podman build --platform linux/amd64 --target ${target} -t ${tag} ${JSON.stringify(buildDir)}`]);
|
|
189
48
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
49
|
+
if (options.push) {
|
|
50
|
+
this.pushToECR(tag, options.product, buildType);
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`${buildType.charAt(0).toUpperCase() + buildType.slice(1)} image build complete (not pushed)`);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
196
55
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
execSync(`docker tag ${localTag} ${fullEcrUri}`, { stdio: 'inherit' });
|
|
56
|
+
pushToECR(localTag, product = 'unknown', buildType = 'full') {
|
|
57
|
+
console.log(`Pushing ${buildType} image to ECR for product: ${product}`);
|
|
200
58
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
59
|
+
// Account + region
|
|
60
|
+
const accountId = spawnSync('bash', ['-lc', 'aws sts get-caller-identity --query Account --output text'], { encoding: 'utf-8' }).stdout.trim();
|
|
61
|
+
const region = spawnSync('bash', ['-lc', 'aws configure get region || echo us-east-1'], { encoding: 'utf-8' }).stdout.trim();
|
|
204
62
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
console.log(`๐ท๏ธ Version: ${ecrTag}`);
|
|
63
|
+
const ecrRepo = 'haven-test-images';
|
|
64
|
+
const ecrRegistry = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
|
|
65
|
+
const ecrUri = `${ecrRegistry}/${ecrRepo}`;
|
|
209
66
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
67
|
+
// Version from consumer package.json
|
|
68
|
+
let version = '1.0.0';
|
|
69
|
+
try {
|
|
70
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
|
|
71
|
+
version = (pkg.version || '1.0.0').replace(/^v/, '');
|
|
72
|
+
} catch { }
|
|
73
|
+
|
|
74
|
+
// Include build type in tag: product-thin-version (e.g., BE-thin-1.0.0)
|
|
75
|
+
const ecrTag = buildType === 'full' ? `${product}-${version}` : `${product}-${buildType}-${version}`;
|
|
76
|
+
|
|
77
|
+
// Ensure ECR repository exists
|
|
78
|
+
run('bash', ['-lc', `aws ecr describe-repositories --repository-names ${ecrRepo} --region ${region} >/dev/null 2>&1 || aws ecr create-repository --repository-name ${ecrRepo} --region ${region}`]);
|
|
79
|
+
|
|
80
|
+
// Clear stale credentials before login to prevent "token expired" errors
|
|
81
|
+
console.log('Clearing stale ECR credentials...');
|
|
82
|
+
spawnSync('bash', ['-lc', `podman logout ${ecrRegistry} 2>/dev/null || true`], { stdio: 'inherit' });
|
|
83
|
+
|
|
84
|
+
// Fresh login to ECR
|
|
85
|
+
console.log(`Logging in to ECR: ${ecrRegistry}`);
|
|
86
|
+
run('bash', ['-lc', `aws ecr get-login-password --region ${region} | podman login --username AWS --password-stdin ${ecrRegistry}`]);
|
|
87
|
+
|
|
88
|
+
run('bash', ['-lc', `podman tag ${localTag} ${ecrUri}:${ecrTag}`]);
|
|
89
|
+
|
|
90
|
+
// Use --force-compression to avoid 403 errors when podman tries to reuse
|
|
91
|
+
// blobs from other registries (e.g., when switching AWS accounts)
|
|
92
|
+
console.log(`Pushing image: ${ecrUri}:${ecrTag}`);
|
|
93
|
+
run('bash', ['-lc', `podman push --force-compression ${ecrUri}:${ecrTag}`]);
|
|
94
|
+
console.log(`${buildType.charAt(0).toUpperCase() + buildType.slice(1)} image pushed: ${ecrUri}:${ecrTag}`);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
runTests(automationIds = '', customTags = '') {
|
|
98
|
+
console.log('Running Cypress tests via run-filtered.sh inside container');
|
|
99
|
+
console.log(`Automation IDs: ${automationIds || 'None'}`);
|
|
100
|
+
console.log(`Custom Tags: ${customTags || 'None'}`);
|
|
101
|
+
return { success: true };
|
|
213
102
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Run tests with Haven integration
|
|
218
|
-
* This is called by Haven when the container runs
|
|
219
|
-
*/
|
|
220
|
-
runTests(automationIds = '', customTags = '') {
|
|
221
|
-
console.log('๐งช Running Haven-integrated Cypress tests...');
|
|
222
|
-
console.log(`๐ Automation IDs: ${automationIds || 'None'}`);
|
|
223
|
-
console.log(`๐ Custom Tags: ${customTags || 'None'}`);
|
|
224
|
-
|
|
225
|
-
// This will be handled by run-filtered.sh when container runs
|
|
226
|
-
// The library just provides the interface
|
|
227
|
-
return { success: true, message: 'Tests will run via run-filtered.sh' };
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
module.exports = HavenCypressIntegration;
|
|
103
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "haven-cypress-integration",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Seamless Cypress integration with HAVEN test case management",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"index.js",
|
|
20
20
|
"bin/",
|
|
21
|
-
"templates/"
|
|
21
|
+
"templates/",
|
|
22
|
+
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"engines": {
|
|
24
|
-
"node": ">=
|
|
25
|
+
"node": ">=16.0.0"
|
|
25
26
|
}
|
|
26
|
-
}
|
|
27
|
+
}
|
package/templates/Dockerfile
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Haven Cypress Test Image - Supports Full and Thin modes
|
|
3
|
+
# =============================================================================
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
|
|
5
|
+
# -----------------------------------------------------------------------------
|
|
6
|
+
# Base Stage
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
FROM cypress/included:14.3.1 AS base
|
|
7
9
|
|
|
8
|
-
#
|
|
10
|
+
# Install git, zip, AWS CLI
|
|
9
11
|
RUN apt-get update && \
|
|
10
|
-
apt-get install -y \
|
|
11
|
-
zip \
|
|
12
|
-
unzip \
|
|
13
|
-
curl && \
|
|
12
|
+
apt-get install -y zip unzip curl git && \
|
|
14
13
|
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
|
|
15
|
-
unzip awscliv2.zip && \
|
|
16
|
-
|
|
17
|
-
rm -rf aws awscliv2.zip
|
|
14
|
+
unzip awscliv2.zip && ./aws/install && rm -rf aws awscliv2.zip && \
|
|
15
|
+
rm -rf /var/lib/apt/lists/*
|
|
18
16
|
|
|
19
|
-
#
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
# Haven scripts go in /haven (not /app)
|
|
18
|
+
WORKDIR /haven
|
|
19
|
+
COPY run-filtered.sh /haven/run-filtered.sh
|
|
20
|
+
COPY syncCypressResults.js /haven/syncCypressResults.js
|
|
21
|
+
RUN chmod +x /haven/run-filtered.sh
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
# App code goes in /app
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
|
|
26
|
+
# -----------------------------------------------------------------------------
|
|
27
|
+
# Full Image - App code baked in
|
|
28
|
+
# -----------------------------------------------------------------------------
|
|
29
|
+
FROM base AS full-image
|
|
30
|
+
COPY . /app
|
|
31
|
+
RUN npm ci --include=dev
|
|
32
|
+
ENTRYPOINT ["/haven/run-filtered.sh"]
|
|
26
33
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
# -----------------------------------------------------------------------------
|
|
35
|
+
# Thin Image - App code cloned at runtime
|
|
36
|
+
# -----------------------------------------------------------------------------
|
|
37
|
+
FROM base AS thin-image
|
|
38
|
+
# /app is empty - will be filled by ADO clone at runtime
|
|
39
|
+
ENTRYPOINT ["/haven/run-filtered.sh"]
|