forked from tinymce/tinymce
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJenkinsfile
More file actions
345 lines (308 loc) · 12.7 KB
/
Copy pathJenkinsfile
File metadata and controls
345 lines (308 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
#!groovy
@Library('waluigi@release/7') _
import groovy.transform.Field
@Field String ciAccountId = "103651136441"
@Field String ciRegistry = "${ciAccountId}.dkr.ecr.us-east-2.amazonaws.com"
standardProperties()
def runBedrockTest(String name, String command, Boolean runAll, int retry = 0, int timeout = 0) {
def bedrockCmd = command + (runAll ? " --ignore-lerna-changed=true" : "")
echo "Running Bedrock cmd: ${bedrockCmd}"
def testStatus = sh(script: bedrockCmd, returnStatus: true)
junit allowEmptyResults: true, testResults: "scratch/TEST-${name}.xml"
if (testStatus == 4) {
unstable("Tests failed for ${name}")
} else if (testStatus != 0) {
if (retry > 0) {
echo "Running retry [${retry}] after [${timeout}]"
sleep(timeout)
runBedrockTest(name, command, runAll, retry - 1, timeout)
} else {
error("Unexpected error in ${name} ")
}
}
}
def runHeadlessTests(Boolean runAll) {
def bedrockCmd = "bun grunt headless-auto --useSelenium=true"
runBedrockTest('headless', bedrockCmd, runAll)
}
def runSeleniumTests(String name, String browser, String bucket, String buckets, Boolean runAll, int retry = 0, int timeout = 0) {
def bedrockCommand =
"bun browser-test" +
" --chunk=2000" +
" --bedrock-browser=" + browser +
" --bucket=" + bucket +
" --buckets=" + buckets +
" --name=" + name +
" --useSelenium=true"
runBedrockTest(name, bedrockCommand, runAll, retry, timeout)
}
def runRemoteTests(String name, String browser, String provider, String platform, String version, String bucket, String buckets, Boolean runAll, int retry = 0, int timeout = 0) {
def awsOpts = " --sishDomain=sish.osu.tiny.work"
def platformName = platform != null ? " --platformName='${platform}'" : ""
def browserVersion = version != null ? " --browserVersion=${version}" : ""
def bedrockCommand =
"bun browser-test" +
" --bundler=rspack" +
" --skipTypecheck" +
" --chunk=2000" +
" --bedrock-browser=" + browser +
" --remote=" + provider +
" --bucket=" + bucket +
" --buckets=" + buckets +
" --name=" + name +
"${provider == 'aws' ? awsOpts : ''}" +
"${platformName}" +
"${browserVersion}"
runBedrockTest(name, bedrockCommand, runAll, retry, timeout)
}
def runBrowserTests(String name, String browser, String platform, String bucket, String buckets, Boolean runAll) {
def bedrockCommand =
"bun grunt browser-auto" +
" --chunk=2000" +
" --bedrock-os=" + platform +
" --bedrock-browser=" + browser +
" --bucket=" + bucket +
" --buckets=" + buckets;
// Clean scratch dir for existing nodes
dir('scratch') {
if (isUnix()) {
sh "rm -f *.xml"
} else {
bat "del *.xml"
}
}
runBedrockTest(name, bedrockCommand, runAll)
}
def gitMerge(String primaryBranch) {
if (env.BRANCH_NAME != primaryBranch) {
echo "Merging ${primaryBranch} into this branch to run tests"
exec("git merge --no-commit --no-ff origin/${primaryBranch}")
}
}
def cleanBuildName(String name) {
def parts = name.split('/')
return parts[parts.size() - 1]
}
def props
def cacheName = "cache_${BUILD_TAG}"
def testPrefix = "tinymce_${cleanBuildName(env.BRANCH_NAME)}-build${env.BUILD_NUMBER}"
def nodeImg = devPods.getContainerDefaultArgs([
name: 'node',
image: "${ciRegistry}/build-containers/node-lts:lts",
alwaysPullImage: true,
runAsGroup: '1000',
runAsUser: '1000'
]) + [
// we can probably make this leaner if we condition primary vs branch
// primary needs more io/mem/cpu to handle extra concurrent tests
resourceRequestCpu: '8',
resourceLimitCpu: '8',
resourceRequestMemory: '10Gi',
resourceLimitMemory: '10Gi'
] + devPods.highStorage()
def seleniumImg = [
name: "selenium",
image: tinyAws.getPullThroughCacheImage("selenium/standalone-chrome", '127.0'),
livenessProbe: [
execArgs: "curl --fail --silent --output /dev/null http://localhost:4444/wd/hub/status",
initialDelaySeconds: 30,
periodSeconds: 5,
timeoutSeconds: 15,
failureThreshold: 6
]
] + [
// lower resources than this and selenium has trouble staying up
// selenium can go down to 500m/500Mi but crashes if things in the pod start leaking
resourceRequestCpu: '600m',
resourceLimitCpu: '600m',
resourceRequestMemory: '600Mi',
resourceLimitMemory: '600Mi'
] + devPods.lowStorage()
def playwrightImg = [
name: 'playwright',
// Use the mirrored image when possible
// if there is no mirror only use mcr for testing and request a mirror
image: 'mcr.microsoft.com/playwright:v1.58.2-noble',
// image: '103651136441.dkr.ecr.us-east-2.amazonaws.com/mirror/playwright:v1.58.2-noble',
command: 'sleep',
args: 'infinity',
alwaysPullImage: true
] + [
resourceRequestCpu: '1',
resourceLimitCpu: '2',
resourceRequestMemory: '2Gi',
resourceLimitMemory: '2Gi'
] + devPods.lowStorage()
timestamps { notifyStatusChange(
cleanupStep: { devPods.cleanUpPod(build: cacheName) },
branches: ['main', 'release/7', 'release/8'],
channel: '#tinymce-build-status',
name: 'TinyMCE',
mention: true
) {
// 90 min absolute ceiling for the whole pipeline; ensures notifyStatusChange
// handles alerting even if a pod crash or unknown hang bypasses stage-level timeouts.
// Set above the sum of inner stage timeouts (deps 5 + build 10 + tests 40 = 55 min + overhead)
timeout(time: 90, unit: 'MINUTES') {
// Retry on K8s pod scheduling failures; count: 2 = 1 original + 1 retry.
// On retry, Deps + Build re-run (~4 min overhead) before tests resume.
// handleNonKubernetes: true ensures the condition degrades gracefully on non-K8s agents.
retry(conditions: [agent(), kubernetesAgent(handleNonKubernetes: true)], count: 2) {
devPods.custom(
containers: [
nodeImg,
seleniumImg,
playwrightImg
],
checkoutStep: {
tinyGit.addGitHubToKnownHosts()
checkout localBranch(scm, [ lfs() ])
}
) {
container("node") {
// Setup git information
tinyGit.addAuthorConfig()
tinyGit.addGitHubToKnownHosts()
props = readProperties(file: 'build.properties')
String primaryBranch = props.primaryBranch
assert primaryBranch != null && primaryBranch != ""
stage('Deps') {
// 5 min: bun install should never take this long; fail fast if it hangs
// actual avg ~1 min, max observed ~2 min
timeout(time: 5, unit: 'MINUTES') {
// cancel build if primary branch doesn't merge cleanly
gitMerge(primaryBranch)
bunInstall()
}
}
stage('Build') {
// 10 min: full build + type-check; actual avg ~2 min, max observed ~7 min
timeout(time: 10, unit: 'MINUTES') {
// verify no errors in changelog merge
exec("bun changie-merge")
withEnv(["NODE_OPTIONS=--max-old-space-size=1936"]) {
// type check and build TinyMCE
exec("bun ci-all-seq")
// validate documentation generator
exec("bun tinymce-grunt shell:moxiedoc")
}
}
}
}
def winChrome = [ browser: 'chrome', provider: 'aws', os: 'windows', buckets: 1 ]
def winFirefox = [ browser: 'firefox', provider: 'lambdatest', os: 'windows', buckets: 1 ]
def winEdge = [ browser: 'edge', provider: 'lambdatest', os: 'windows', buckets: 1 ]
def macChrome = [ browser: 'chrome', provider: 'lambdatest', os: 'macOS Sequoia', buckets: 1 ]
def macFirefox = [ browser: 'firefox', provider: 'lambdatest', os: 'macOS Sequoia', buckets: 1 ]
def macSafari = [ browser: 'safari', provider: 'lambdatest', os: 'macOS Sequoia', buckets: 1 ]
def branchBuildPlatforms = [
winChrome,
winFirefox,
macSafari,
]
def primaryBuildPlatforms = branchBuildPlatforms + [
winEdge,
macChrome,
macFirefox
];
def buildingPrimary = env.BRANCH_NAME == props.primaryBranch
def platforms = buildingPrimary ? primaryBuildPlatforms : branchBuildPlatforms
def processes = [:]
def runAllTests = buildingPrimary
def stagger = 0
for (int i = 0; i < platforms.size(); i++) {
def platform = platforms.get(i)
def buckets = platform.buckets ?: 1
for (int bucket = 1; bucket <= buckets; bucket ++) {
def suffix = buckets == 1 ? "" : "-" + bucket + "-" + buckets
def os = String.valueOf(platform.os).startsWith('mac') ? 'Mac' : 'Win'
def s_bucket = "${bucket}"
def s_buckets = "${buckets}"
def name = "${os}-${platform.browser}${platform.version ?: ''}-${platform.provider}${suffix}"
def delaySeconds = stagger * 10
stagger++
processes[name] = {
stage(name) {
container('node') {
// 35 min activity-based: resets on any log output; catches a single branch stalling
// silently while other branches are still active (absolute outer Tests timeout won't
// detect this). Must be set below the outer Tests timeout (40 min) to be reachable.
timeout(time: 35, unit: 'MINUTES', activity: true) {
sleep( time: delaySeconds, unit: 'SECONDS')
grunt('list-changed-browser')
bedrockRemoteTools.withRemoteCreds(platform.provider) {
runRemoteTests(name, platform.browser, platform.provider, platform.os, platform.version, s_bucket, s_buckets, runAllTests)
}
}
}
}
}
}
}
processes['headless'] = {
stage('headless') {
container('node') {
// 5 min: headless Selenium tests average ~1 min, max observed ~1m10s
timeout(time: 5, unit: 'MINUTES') {
grunt('list-changed-headless')
runHeadlessTests(runAllTests)
}
}
}
}
processes['playwright'] = {
stage('playwright') {
container('playwright') {
// 20 min: playwright unit + visual tests average ~8 min, max observed ~10m37s
timeout(time: 20, unit: 'MINUTES') {
// bun install (hoisted linker) produces an npm-compatible node_modules,
// so we can run the scripts with the playwright image's existing npm
// instead of installing bun in the container.
sh 'npm run --prefix modules/oxide-components --silent test-ci'
junit allowEmptyResults: true, testResults: 'modules/oxide-components/scratch/test-results.xml'
def visualTestStatus
// Limit the number of workers allowed to avoid hanging IO
withEnv(["PW_WORKERS=1"]) {
visualTestStatus = sh(script: 'npm run --prefix modules/oxide-components --silent test-visual-ci', returnStatus: true)
}
if (visualTestStatus == 4) {
unstable("Visual tests failed")
} else if (visualTestStatus != 0) {
error("Unexpected error running visual tests")
}
junit allowEmptyResults: true, testResults: 'modules/oxide-components/scratch/test-results-visual.xml'
exec('find modules/oxide-components -name "*.png" -type f || echo "No PNG files found"')
archiveArtifacts artifacts: 'modules/oxide-components/.vitest-attachments/**/*.png', allowEmptyArchive: true, fingerprint: true
}
}
}
}
stage('Tests') {
// 40 min: ceiling across all parallel branches; branches run concurrently so this only needs
// to cover the slowest single branch. Max observed ~28 min (Mac-firefox/Win-edge); ~1.4x buffer.
timeout(time: 40, unit: 'MINUTES') {
// TODO: consider wrapping in try/finally to publish scratch/TEST-*.xml as a safety net
// for results written before a timeout fires. Held back due to potential double-reporting
// since runBedrockTest already publishes junit per test.
container('node') {
bedrockRemoteTools.tinyWorkSishTunnel()
}
parallel processes
}
}
container('node') {
stage('Deploy Storybook') {
if (env.BRANCH_NAME == props.primaryBranch) {
echo "Deploying Storybook"
tinyGit.withGitHubSSHCredentials {
exec('bun --silent --cwd modules/oxide-components deploy-storybook')
}
} else {
echo "Skipping Storybook deployment as the pipeline is not running on the primary branch"
}
}
} // close container
} // close pod
} // close retry
} // close outer timeout
}} // close #notification and #timestamp