@ckeditor/ckeditor5-dev-ci 55.0.0-alpha.3 → 55.0.0-alpha.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/README.md CHANGED
@@ -55,6 +55,11 @@ These commands accept a mix of environment variables and command line arguments.
55
55
  - `--task` — Command to execute at the end; default: `pnpm ckeditor5-dev-ci-notify-circle-status`.
56
56
  - `--ignore` — Job name to ignore when waiting (repeatable; can be passed multiple times).
57
57
 
58
+ **Behavior:**
59
+ - Retries transient CircleCI API errors up to **5 attempts** with a **10s delay** between attempts.
60
+ - Fails immediately for non-retryable API errors (for example: invalid token or wrong workflow ID).
61
+ - If transient errors persist, it exits with an explicit message asking to verify workflow results manually.
62
+
58
63
  - ⚙️ **`ckeditor5-dev-ci-is-job-triggered-by-member`**
59
64
 
60
65
  Verifies that a **CircleCI approval job** was approved by a user who belongs to a specified GitHub team.
@@ -9,6 +9,7 @@ import { parseArgs } from 'node:util';
9
9
  import { execSync } from 'node:child_process';
10
10
  import processJobStatuses from '../lib/process-job-statuses.js';
11
11
  import isWorkflowFinished from '../lib/utils/is-workflow-finished.js';
12
+ import getOtherWorkflowJobs from '../lib/utils/get-other-workflow-jobs.js';
12
13
 
13
14
  // This script allows the creation of a new job within a workflow that will be executed
14
15
  // in the end, when all other jobs will be finished or errored.
@@ -81,6 +82,9 @@ const { values: { task, ignore } } = parseArgs( {
81
82
  }
82
83
  } );
83
84
 
85
+ const CIRCLE_API_MAX_ATTEMPTS = 5;
86
+ const CIRCLE_API_RETRY_DELAY_MS = 10 * 1000;
87
+
84
88
  waitForOtherJobsAndSendNotification()
85
89
  .catch( err => {
86
90
  console.error( err );
@@ -89,7 +93,17 @@ waitForOtherJobsAndSendNotification()
89
93
  } );
90
94
 
91
95
  async function waitForOtherJobsAndSendNotification() {
92
- const jobs = processJobStatuses( await getOtherJobsData() )
96
+ assertRequiredEnvironmentVariables();
97
+
98
+ const allJobs = await getOtherWorkflowJobs( {
99
+ circleToken: CKE5_CIRCLE_TOKEN,
100
+ workflowId: CIRCLE_WORKFLOW_ID,
101
+ currentJobName: CIRCLE_JOB,
102
+ maxAttempts: CIRCLE_API_MAX_ATTEMPTS,
103
+ retryDelayMs: CIRCLE_API_RETRY_DELAY_MS
104
+ } );
105
+
106
+ const jobs = processJobStatuses( allJobs )
93
107
  .filter( job => !ignore.includes( job.name ) );
94
108
 
95
109
  if ( !isWorkflowFinished( jobs ) ) {
@@ -108,20 +122,16 @@ async function waitForOtherJobsAndSendNotification() {
108
122
  console.log( 'All jobs were successful.' );
109
123
  }
110
124
 
111
- /**
112
- * Fetches and returns data of all jobs except the one where this script runs.
113
- */
114
- async function getOtherJobsData() {
115
- const url = `https://circleci.com/api/v2/workflow/${ CIRCLE_WORKFLOW_ID }/job`;
116
- const options = {
117
- method: 'GET',
118
- headers: {
119
- 'Circle-Token': CKE5_CIRCLE_TOKEN
120
- }
121
- };
125
+ function assertRequiredEnvironmentVariables() {
126
+ if ( !CKE5_CIRCLE_TOKEN ) {
127
+ throw new Error( 'Missing environment variable: CKE5_CIRCLE_TOKEN' );
128
+ }
122
129
 
123
- const response = await fetch( url, options );
124
- const data = await response.json();
130
+ if ( !CIRCLE_WORKFLOW_ID ) {
131
+ throw new Error( 'Missing environment variable: CIRCLE_WORKFLOW_ID' );
132
+ }
125
133
 
126
- return data.items.filter( job => job.name !== CIRCLE_JOB );
134
+ if ( !CIRCLE_JOB ) {
135
+ throw new Error( 'Missing environment variable: CIRCLE_JOB' );
136
+ }
127
137
  }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+
6
+ const RETRYABLE_STATUSES = new Set( [ 429, 500, 502, 503, 504 ] );
7
+
8
+ /**
9
+ * Fetches and returns all jobs from a workflow except the currently running job.
10
+ *
11
+ * Retries transient API errors up to `maxAttempts` times.
12
+ *
13
+ * @param {object} options
14
+ * @param {string} options.circleToken
15
+ * @param {string} options.workflowId
16
+ * @param {string} options.currentJobName
17
+ * @param {number} [options.maxAttempts=5]
18
+ * @param {number} [options.retryDelayMs=10000]
19
+ * @returns {Promise<Array.<object>>}
20
+ */
21
+ export default async function getOtherWorkflowJobs( options ) {
22
+ const {
23
+ circleToken,
24
+ workflowId,
25
+ currentJobName,
26
+ maxAttempts = 5,
27
+ retryDelayMs = 10 * 1000
28
+ } = options;
29
+
30
+ if ( !Number.isInteger( maxAttempts ) || maxAttempts < 1 ) {
31
+ throw new Error( `Invalid option: "maxAttempts" must be a positive integer (received: ${ maxAttempts }).` );
32
+ }
33
+
34
+ if ( retryDelayMs < 0 ) {
35
+ throw new Error( `Invalid option: "retryDelayMs" must be greater than or equal to 0 (received: ${ retryDelayMs }).` );
36
+ }
37
+
38
+ const requestUrl = `https://circleci.com/api/v2/workflow/${ workflowId }/job`;
39
+ const requestOptions = {
40
+ method: 'GET',
41
+ headers: {
42
+ 'Circle-Token': circleToken
43
+ }
44
+ };
45
+
46
+ for ( let attempt = 1; attempt <= maxAttempts; attempt++ ) {
47
+ try {
48
+ const response = await fetch( requestUrl, requestOptions )
49
+ .catch( error => {
50
+ throw createTransientError( `CircleCI API request failed due to a network error: ${ error.message }`, error );
51
+ } );
52
+
53
+ if ( !response.ok ) {
54
+ const responseData = await parseResponseDataSafely( response );
55
+ const details = getResponseMessage( responseData );
56
+ const statusMessage = details ? `${ response.status }: ${ details }` : String( response.status );
57
+
58
+ if ( RETRYABLE_STATUSES.has( response.status ) ) {
59
+ throw createTransientError( `CircleCI API request failed with a retryable status (${ statusMessage }).` );
60
+ }
61
+
62
+ throw new Error(
63
+ `CircleCI API request failed with a non-retryable status (${ statusMessage }). ` +
64
+ 'Verify CircleCI token and workflow configuration.'
65
+ );
66
+ }
67
+
68
+ const responseData = await parseResponseData( response );
69
+
70
+ if ( !responseData || !Array.isArray( responseData.items ) ) {
71
+ throw createTransientError( 'CircleCI API response does not contain the "items" array.' );
72
+ }
73
+
74
+ return responseData.items.filter( job => job.name !== currentJobName );
75
+ } catch ( error ) {
76
+ if ( !isTransientError( error ) ) {
77
+ throw error;
78
+ }
79
+
80
+ if ( attempt === maxAttempts ) {
81
+ throw new Error(
82
+ `CircleCI API seems unstable. Failed to fetch workflow jobs after ${ maxAttempts } attempts. ` +
83
+ `Last error: ${ error.message } Please verify workflow results manually.`
84
+ );
85
+ }
86
+
87
+ console.warn(
88
+ `CircleCI API request failed (attempt ${ attempt }/${ maxAttempts }): ${ error.message } ` +
89
+ `Retrying in ${ retryDelayMs }ms...`
90
+ );
91
+
92
+ await wait( retryDelayMs );
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param {Response} response
99
+ * @returns {Promise<object>}
100
+ */
101
+ async function parseResponseData( response ) {
102
+ try {
103
+ return await response.json();
104
+ } catch ( error ) {
105
+ throw createTransientError( 'CircleCI API returned an invalid JSON response.', error );
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @param {Response} response
111
+ * @returns {Promise<object|null>}
112
+ */
113
+ async function parseResponseDataSafely( response ) {
114
+ try {
115
+ return await response.json();
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * @param {object} responseData
123
+ * @returns {string|null}
124
+ */
125
+ function getResponseMessage( responseData ) {
126
+ if ( responseData && typeof responseData.message === 'string' ) {
127
+ return responseData.message;
128
+ }
129
+
130
+ if ( responseData && typeof responseData.error === 'string' ) {
131
+ return responseData.error;
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * @param {number} delay
139
+ * @returns {Promise<void>}
140
+ */
141
+ function wait( delay ) {
142
+ return new Promise( resolve => setTimeout( resolve, delay ) );
143
+ }
144
+
145
+ /**
146
+ * @param {string} message
147
+ * @param {Error} [cause]
148
+ * @returns {Error & {code: 'CIRCLE_API_TRANSIENT'}}
149
+ */
150
+ function createTransientError( message, cause ) {
151
+ const error = new Error( message, { cause } );
152
+ error.code = 'CIRCLE_API_TRANSIENT';
153
+
154
+ return error;
155
+ }
156
+
157
+ /**
158
+ * @param {unknown} error
159
+ * @returns {error is Error & {code: 'CIRCLE_API_TRANSIENT'}}
160
+ */
161
+ function isTransientError( error ) {
162
+ return error instanceof Error && error.code === 'CIRCLE_API_TRANSIENT';
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-dev-ci",
3
- "version": "55.0.0-alpha.3",
3
+ "version": "55.0.0-alpha.4",
4
4
  "description": "Utils used on various Continuous Integration services.",
5
5
  "keywords": [],
6
6
  "author": "CKSource (http://cksource.com/)",