@silexlabs/silex 3.6.3 → 3.6.4
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/dist/client/index.html +2 -2
- package/dist/client/js/main.js +4 -4
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.d.ts +8 -1
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js +276 -46
- package/dist/plugins/server/plugins/server/GitlabHostingConnector.js.map +1 -1
- package/package.json +1 -1
- package/src/ts/client/grapesjs/PublicationUi.ts +2 -2
- package/src/ts/client/grapesjs/storage.ts +26 -16
- package/src/ts/plugins/server/GitlabHostingConnector.ts +305 -49
|
@@ -30,9 +30,112 @@ import { JobManager } from '../../server/jobs'
|
|
|
30
30
|
import { ServerConfig } from '../../server/config'
|
|
31
31
|
import { setTimeout } from 'timers/promises'
|
|
32
32
|
|
|
33
|
-
const waitTimeOut = 5000 /* for wait loop
|
|
33
|
+
const waitTimeOut = 5000 /* for wait loop waiting for job to appear */
|
|
34
|
+
const jobPollInterval = 10000 /* poll job status every 10 seconds */
|
|
35
|
+
const jobCompletionTimeOut = 45 * 60 * 1000 /* 45 minutes max for job completion */
|
|
34
36
|
const SILEX_OVERWRITE_NOTICE = '# silexOverwrite: true'
|
|
35
37
|
|
|
38
|
+
// GitLab job statuses that indicate the job is still running
|
|
39
|
+
const GITLAB_RUNNING_STATUSES = ['created', 'waiting_for_resource', 'preparing', 'pending', 'running']
|
|
40
|
+
// GitLab job statuses that indicate success
|
|
41
|
+
const GITLAB_SUCCESS_STATUSES = ['success']
|
|
42
|
+
// GitLab job statuses that indicate failure
|
|
43
|
+
const GITLAB_FAILED_STATUSES = ['failed', 'canceled', 'skipped']
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Filter GitLab trace to remove technical noise
|
|
47
|
+
* Returns an array of meaningful log lines
|
|
48
|
+
*/
|
|
49
|
+
function filterTraceLines(trace: string): string[] {
|
|
50
|
+
return trace.split('\n')
|
|
51
|
+
.map(l => l.trim())
|
|
52
|
+
.filter(l => l.length > 0)
|
|
53
|
+
// Remove GitLab internal markers
|
|
54
|
+
.filter(l => !l.match(/^section_/))
|
|
55
|
+
// eslint-disable-next-line no-control-regex
|
|
56
|
+
.filter(l => !l.match(/^\x1b/)) // Remove ANSI escape sequences at start
|
|
57
|
+
// Remove timestamps at the beginning of lines
|
|
58
|
+
.map(l => l.replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*\d*O?\s*/, ''))
|
|
59
|
+
// Remove Docker/Git setup noise
|
|
60
|
+
.filter(l => !l.includes('Using Docker executor'))
|
|
61
|
+
.filter(l => !l.includes('Pulling docker image'))
|
|
62
|
+
.filter(l => !l.includes('Fetching changes'))
|
|
63
|
+
.filter(l => !l.includes('Checking out'))
|
|
64
|
+
.filter(l => !l.includes('Skipping Git submodules'))
|
|
65
|
+
.filter(l => !l.includes('Getting source from Git'))
|
|
66
|
+
.filter(l => !l.includes('Executing "step_script"'))
|
|
67
|
+
.filter(l => !l.includes('Restoring cache'))
|
|
68
|
+
.filter(l => !l.includes('Saving cache'))
|
|
69
|
+
.filter(l => !l.match(/^\s*\$\s/)) // Remove shell command echoes
|
|
70
|
+
.filter(l => !l.match(/^Running with gitlab-runner/))
|
|
71
|
+
.filter(l => !l.match(/^Preparing the .* executor/))
|
|
72
|
+
.filter(l => !l.match(/^Using .* as base image/))
|
|
73
|
+
// Keep non-empty lines after filtering
|
|
74
|
+
.filter(l => l.trim().length > 0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parse GitLab/11ty job logs to extract meaningful status
|
|
79
|
+
* Returns a short user-friendly status message
|
|
80
|
+
*/
|
|
81
|
+
function parseJobTrace(trace: string): string {
|
|
82
|
+
const lines = filterTraceLines(trace)
|
|
83
|
+
|
|
84
|
+
// Look for 11ty build progress
|
|
85
|
+
if (trace.includes('[11ty]')) {
|
|
86
|
+
// Look for 11ty completion first
|
|
87
|
+
if (trace.includes('[11ty] Wrote') || trace.includes('[11ty] Copied')) {
|
|
88
|
+
const wroteMatch = trace.match(/\[11ty\] Wrote (\d+)/i)
|
|
89
|
+
const copiedMatch = trace.match(/\[11ty\] Copied (\d+)/i)
|
|
90
|
+
const parts = []
|
|
91
|
+
if (wroteMatch) parts.push(`${wroteMatch[1]} pages generated`)
|
|
92
|
+
if (copiedMatch) parts.push(`${copiedMatch[1]} files copied`)
|
|
93
|
+
if (parts.length) return parts.join(', ')
|
|
94
|
+
}
|
|
95
|
+
// Look for 11ty processing
|
|
96
|
+
const eleventyMatch = trace.match(/\[11ty\].*?(\d+)\s+files?/i)
|
|
97
|
+
if (eleventyMatch) {
|
|
98
|
+
return `Generating ${eleventyMatch[1]} pages...`
|
|
99
|
+
}
|
|
100
|
+
return 'Building pages...'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Look for npm/npx installing
|
|
104
|
+
if (trace.includes('npm') && trace.includes('install')) {
|
|
105
|
+
return 'Installing dependencies...'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Look for npx running eleventy
|
|
109
|
+
if (trace.includes('npx') && trace.includes('eleventy')) {
|
|
110
|
+
return 'Running page generator...'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Look for artifact upload
|
|
114
|
+
if (trace.includes('Uploading artifacts')) {
|
|
115
|
+
return 'Preparing files for deployment...'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Look for pages deployment
|
|
119
|
+
if (trace.includes('CI_PAGES_URL') || trace.includes('will be deployed')) {
|
|
120
|
+
return 'Finalizing deployment...'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Look for job completion
|
|
124
|
+
if (trace.includes('Job succeeded')) {
|
|
125
|
+
return 'Build complete'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Look for errors
|
|
129
|
+
const errorMatch = trace.match(/error[:\s]+(.{0,100})/i)
|
|
130
|
+
if (errorMatch) {
|
|
131
|
+
const errorMsg = errorMatch[1].trim().substring(0, 60)
|
|
132
|
+
return `Error: ${errorMsg}${errorMsg.length >= 60 ? '...' : ''}`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Return empty string to avoid showing technical noise
|
|
136
|
+
return ''
|
|
137
|
+
}
|
|
138
|
+
|
|
36
139
|
export default class GitlabHostingConnector extends GitlabConnector implements HostingConnector {
|
|
37
140
|
|
|
38
141
|
displayName = 'Gitlab hosting'
|
|
@@ -117,32 +220,17 @@ export default class GitlabHostingConnector extends GitlabConnector implements H
|
|
|
117
220
|
job.logs[0].push(job.message)
|
|
118
221
|
const pageUrl = await this.getPageUrl(session, websiteId, adminUrl)
|
|
119
222
|
job.logs[0].push(`Page URL: ${pageUrl}`)
|
|
120
|
-
|
|
223
|
+
|
|
224
|
+
// Wait for the GitLab job to complete
|
|
225
|
+
job.message = 'Starting build...'
|
|
121
226
|
job.logs[0].push(job.message)
|
|
122
|
-
const
|
|
123
|
-
// Because of the GitLab policy, this can be null (and we suggest the user to verify their account)
|
|
124
|
-
if (!gitlabJobLogsUrl) {
|
|
125
|
-
let errorMessage = 'Could not retrieve the deployment logs URL.'
|
|
126
|
-
|
|
127
|
-
if (this.isUsingOfficialInstance()) {
|
|
128
|
-
const verifyURL = 'https://gitlab.com/-/identity_verification'
|
|
129
|
-
errorMessage +=
|
|
130
|
-
`<div class="notice">
|
|
131
|
-
If your GitLab account is recent, you may need to verify it <a href="${verifyURL}" target="_blank">here</a>
|
|
132
|
-
in order to be able to use pipelines (this is GitLab's policy, not Silex's).
|
|
133
|
-
</div>`
|
|
134
|
-
}
|
|
227
|
+
const result = await this.waitForGitlabJobCompletion(session, websiteId, job, adminUrl, successTag, gitlabUrl, pageUrl)
|
|
135
228
|
|
|
136
|
-
|
|
229
|
+
if (result.success) {
|
|
230
|
+
jobSuccess(job.jobId, result.message)
|
|
231
|
+
} else {
|
|
232
|
+
jobError(job.jobId, result.message)
|
|
137
233
|
}
|
|
138
|
-
job.logs[0].push(`Deployment logs URL: ${gitlabJobLogsUrl}`)
|
|
139
|
-
const message = `
|
|
140
|
-
<p><a href="${gitlabUrl}" target="_blank">Your website will soon be live here</a>.</p>
|
|
141
|
-
<p>Changes may take a few minutes to appear, <a href="${gitlabJobLogsUrl}" target="_blank">follow deployment here</a>.</p>
|
|
142
|
-
<p>Manage <a href="${pageUrl}" target="_blank">GitLab Pages settings</a>.</p>
|
|
143
|
-
`
|
|
144
|
-
job.logs[0].push(message)
|
|
145
|
-
jobSuccess(job.jobId, message)
|
|
146
234
|
} catch (error) {
|
|
147
235
|
console.error('Error during getting the website URLs:', error.message)
|
|
148
236
|
jobError(job.jobId, `Failed to get the website URLs: ${error.message}`)
|
|
@@ -227,37 +315,205 @@ export default class GitlabHostingConnector extends GitlabConnector implements H
|
|
|
227
315
|
return `${projectUrl}/pages`
|
|
228
316
|
}
|
|
229
317
|
|
|
230
|
-
|
|
231
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Wait for the GitLab CI/CD job to complete
|
|
320
|
+
* Polls the job status and updates the Silex job with progress
|
|
321
|
+
*/
|
|
322
|
+
async waitForGitlabJobCompletion(
|
|
323
|
+
session: GitlabSession,
|
|
324
|
+
websiteId: WebsiteId,
|
|
325
|
+
job: PublicationJobData,
|
|
326
|
+
projectUrl: string,
|
|
327
|
+
tag: string,
|
|
328
|
+
gitlabUrl: string,
|
|
329
|
+
pageUrl: string
|
|
330
|
+
): Promise<{ success: boolean; message: string }> {
|
|
232
331
|
const t0 = Date.now()
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
332
|
+
let gitlabJobId: number | null = null
|
|
333
|
+
let gitlabJobLogsUrl: string | null = null
|
|
334
|
+
|
|
335
|
+
// First, wait for the job to appear
|
|
336
|
+
while (!gitlabJobId && (Date.now() - t0) < this.options.timeOut) {
|
|
337
|
+
try {
|
|
338
|
+
const jobs = await this.callApi({
|
|
339
|
+
session,
|
|
340
|
+
path: `api/v4/projects/${websiteId}/jobs`,
|
|
341
|
+
method: 'GET'
|
|
342
|
+
})
|
|
343
|
+
const matchingJob = jobs.find(j => j.ref === tag)
|
|
344
|
+
if (matchingJob) {
|
|
345
|
+
gitlabJobId = matchingJob.id
|
|
346
|
+
gitlabJobLogsUrl = `${projectUrl}/-/jobs/${matchingJob.id}`
|
|
347
|
+
job.logs[0].push('Build started')
|
|
348
|
+
job.message = `
|
|
349
|
+
<p><strong>Building...</strong></p>
|
|
350
|
+
<p><a href="${gitlabJobLogsUrl}" target="_blank">View build logs</a></p>
|
|
351
|
+
`
|
|
352
|
+
}
|
|
353
|
+
} catch (e) {
|
|
354
|
+
console.error('Error fetching GitLab jobs:', e.message)
|
|
355
|
+
}
|
|
356
|
+
if (!gitlabJobId) {
|
|
357
|
+
await setTimeout(waitTimeOut)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// If job never appeared, handle the error
|
|
362
|
+
if (!gitlabJobId) {
|
|
363
|
+
let errorMessage = 'Could not start the build. The server may be busy or unavailable.'
|
|
364
|
+
|
|
365
|
+
if (this.isUsingOfficialInstance()) {
|
|
366
|
+
const verifyURL = 'https://gitlab.com/-/identity_verification'
|
|
367
|
+
errorMessage +=
|
|
368
|
+
`<div class="notice">
|
|
369
|
+
If your GitLab account is recent, you may need to <a href="${verifyURL}" target="_blank">verify it here</a>
|
|
370
|
+
before you can publish websites.
|
|
371
|
+
</div>`
|
|
243
372
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
373
|
+
|
|
374
|
+
return { success: false, message: errorMessage }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Now poll the job status until it completes
|
|
378
|
+
let lastTraceLength = 0
|
|
379
|
+
while ((Date.now() - t0) < jobCompletionTimeOut) {
|
|
380
|
+
try {
|
|
381
|
+
// Get job status
|
|
382
|
+
const gitlabJob = await this.callApi({
|
|
383
|
+
session,
|
|
384
|
+
path: `api/v4/projects/${websiteId}/jobs/${gitlabJobId}`,
|
|
385
|
+
method: 'GET'
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
const jobStatus = gitlabJob.status
|
|
389
|
+
|
|
390
|
+
// Try to get job trace (logs) for progress info
|
|
391
|
+
let traceInfo = ''
|
|
392
|
+
try {
|
|
393
|
+
const trace = await this.callApi({
|
|
394
|
+
session,
|
|
395
|
+
path: `api/v4/projects/${websiteId}/jobs/${gitlabJobId}/trace`,
|
|
396
|
+
method: 'GET'
|
|
397
|
+
})
|
|
398
|
+
if (typeof trace === 'string' && trace.length > lastTraceLength) {
|
|
399
|
+
// Parse the new portion of the trace
|
|
400
|
+
const newTrace = trace.substring(lastTraceLength)
|
|
401
|
+
traceInfo = parseJobTrace(newTrace)
|
|
402
|
+
lastTraceLength = trace.length
|
|
403
|
+
|
|
404
|
+
// Add filtered log lines to the job logs for the <details> section
|
|
405
|
+
const filteredLines = filterTraceLines(newTrace)
|
|
406
|
+
if (filteredLines.length > 0) {
|
|
407
|
+
// Limit to last 50 lines to avoid overwhelming the logs
|
|
408
|
+
const linesToAdd = filteredLines.slice(-50)
|
|
409
|
+
job.logs[0].push(...linesToAdd)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch (e) {
|
|
413
|
+
// Trace might not be available yet, that's OK
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Map GitLab status to user-friendly status
|
|
417
|
+
const statusMap = {
|
|
418
|
+
'created': 'Starting',
|
|
419
|
+
'waiting_for_resource': 'Waiting for server',
|
|
420
|
+
'preparing': 'Preparing',
|
|
421
|
+
'pending': 'Queued',
|
|
422
|
+
'running': 'Building',
|
|
423
|
+
'success': 'Complete',
|
|
424
|
+
'failed': 'Failed',
|
|
425
|
+
'canceled': 'Canceled',
|
|
426
|
+
'skipped': 'Skipped',
|
|
427
|
+
}
|
|
428
|
+
const statusDisplay = statusMap[jobStatus] || jobStatus
|
|
429
|
+
const progressInfo = traceInfo ? ` - ${traceInfo}` : ''
|
|
430
|
+
job.message = `
|
|
431
|
+
<p><strong>${statusDisplay}</strong>${progressInfo}</p>
|
|
432
|
+
<p><a href="${gitlabJobLogsUrl}" target="_blank">View build logs</a></p>
|
|
433
|
+
`
|
|
434
|
+
|
|
435
|
+
// Check if job completed
|
|
436
|
+
if (GITLAB_SUCCESS_STATUSES.includes(jobStatus)) {
|
|
437
|
+
// Fetch final trace to include in logs
|
|
438
|
+
try {
|
|
439
|
+
const finalTrace = await this.callApi({
|
|
440
|
+
session,
|
|
441
|
+
path: `api/v4/projects/${websiteId}/jobs/${gitlabJobId}/trace`,
|
|
442
|
+
method: 'GET'
|
|
443
|
+
})
|
|
444
|
+
if (typeof finalTrace === 'string') {
|
|
445
|
+
const allFilteredLines = filterTraceLines(finalTrace)
|
|
446
|
+
// Add last 30 lines as summary
|
|
447
|
+
job.logs[0].push('--- Build Summary ---')
|
|
448
|
+
job.logs[0].push(...allFilteredLines.slice(-30))
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {
|
|
451
|
+
// Ignore trace fetch errors
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const message = `
|
|
455
|
+
<p><strong>Your website is now live!</strong></p>
|
|
456
|
+
<p><a href="${gitlabUrl}" target="_blank" class="silex-button silex-button--primary">View your website</a></p>
|
|
457
|
+
`
|
|
458
|
+
job.logs[0].push('Build completed successfully')
|
|
459
|
+
return { success: true, message }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (GITLAB_FAILED_STATUSES.includes(jobStatus)) {
|
|
463
|
+
const failureReason = gitlabJob.failure_reason || 'Build error'
|
|
464
|
+
|
|
465
|
+
// Fetch final trace to include error details in logs
|
|
466
|
+
try {
|
|
467
|
+
const finalTrace = await this.callApi({
|
|
468
|
+
session,
|
|
469
|
+
path: `api/v4/projects/${websiteId}/jobs/${gitlabJobId}/trace`,
|
|
470
|
+
method: 'GET'
|
|
471
|
+
})
|
|
472
|
+
if (typeof finalTrace === 'string') {
|
|
473
|
+
const allFilteredLines = filterTraceLines(finalTrace)
|
|
474
|
+
// Add last 50 lines to help debug the error
|
|
475
|
+
job.logs[0].push('--- Error Details ---')
|
|
476
|
+
job.logs[0].push(...allFilteredLines.slice(-50))
|
|
477
|
+
}
|
|
478
|
+
} catch (e) {
|
|
479
|
+
// Ignore trace fetch errors
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const message = `
|
|
483
|
+
<p><strong>Build failed:</strong> ${failureReason}</p>
|
|
484
|
+
<p><a href="${gitlabJobLogsUrl}" target="_blank">View build logs for details</a></p>
|
|
485
|
+
<p><a href="${pageUrl}" target="_blank">Settings</a></p>
|
|
486
|
+
`
|
|
487
|
+
job.logs[0].push(`Build failed: ${failureReason}`)
|
|
488
|
+
return { success: false, message }
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Job still running, wait before next poll
|
|
492
|
+
await setTimeout(jobPollInterval)
|
|
493
|
+
|
|
494
|
+
} catch (e) {
|
|
495
|
+
console.error('Error polling GitLab job status:', e.message)
|
|
496
|
+
// Don't fail immediately, just wait and retry
|
|
497
|
+
await setTimeout(jobPollInterval)
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Timeout reached
|
|
502
|
+
const message = `
|
|
503
|
+
<p><strong>Build is taking longer than expected</strong></p>
|
|
504
|
+
<p>Your website may still be published successfully.</p>
|
|
505
|
+
<p><a href="${gitlabJobLogsUrl}" target="_blank">Check build status</a> | <a href="${gitlabUrl}" target="_blank">Check your website</a></p>
|
|
506
|
+
`
|
|
507
|
+
job.logs[0].push('Timeout waiting for build completion')
|
|
508
|
+
return { success: false, message }
|
|
253
509
|
}
|
|
254
510
|
|
|
255
511
|
async createTag(session: GitlabSession, websiteId: WebsiteId, job: PublicationJobData, { startJob, jobSuccess, jobError }: JobManager): Promise<string | null> {
|
|
256
512
|
const projectId = websiteId // Assuming websiteId corresponds to GitLab project ID
|
|
257
513
|
const newTag = '_silex_' + Date.now()
|
|
258
|
-
// Create a new tag
|
|
514
|
+
// Create a new tag to trigger the build
|
|
259
515
|
try {
|
|
260
|
-
job.message =
|
|
516
|
+
job.message = 'Triggering build...'
|
|
261
517
|
job.logs[0].push(job.message)
|
|
262
518
|
await this.callApi({
|
|
263
519
|
session,
|
|
@@ -271,10 +527,10 @@ export default class GitlabHostingConnector extends GitlabConnector implements H
|
|
|
271
527
|
})
|
|
272
528
|
} catch (error) {
|
|
273
529
|
console.error('Error during creating new tag:', error.message)
|
|
274
|
-
jobError(job.jobId, `Failed to
|
|
530
|
+
jobError(job.jobId, `Failed to start build: ${error.message}`)
|
|
275
531
|
return null
|
|
276
532
|
}
|
|
277
|
-
|
|
533
|
+
|
|
278
534
|
// Return new tag
|
|
279
535
|
return newTag
|
|
280
536
|
}
|