@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.
@@ -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 in job pages getting */
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
- job.message = 'Getting the deployment logs URL...'
223
+
224
+ // Wait for the GitLab job to complete
225
+ job.message = 'Starting build...'
121
226
  job.logs[0].push(job.message)
122
- const gitlabJobLogsUrl = await this.getGitlabJobLogsUrl(session, websiteId, job, { startJob, jobSuccess, jobError }, adminUrl, successTag)
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
- throw new Error(errorMessage)
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
- // waiting for the job corresponding to the current tag
231
- async getGitlabJobLogsUrl(session: GitlabSession, websiteId: WebsiteId, job: PublicationJobData, { startJob, jobSuccess, jobError }: JobManager, projectUrl: string, tag): Promise<string | null> {
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
- do {
234
- const jobs = await this.callApi({
235
- session,
236
- path: `api/v4/projects/${websiteId}/jobs`,
237
- method: 'GET'
238
- })
239
- // Find job matching our tag (may not exist yet on first publish)
240
- const matchingJob = jobs.find(j => j.ref === tag)
241
- if (matchingJob) {
242
- return `${projectUrl}/-/jobs/${matchingJob.id}`
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
- // No matching job yet - wait and retry
245
- await setTimeout(waitTimeOut)
246
- } while ((Date.now() - t0) < this.options.timeOut)
247
-
248
- // failed in timelaps allowed (avoiding infinite loop)
249
- jobError(job.jobId, 'Failed to get job id')
250
- job.message = 'Unable to get job id'
251
- job.logs[0].push(job.message)
252
- return `${projectUrl}/-/jobs/`
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 = `Creating new tag ${newTag}...`
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 create new tag: ${error.message}`)
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
  }