@ckeditor/ckeditor5-dev-ci 54.3.3 → 54.3.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 +5 -0
- package/bin/circle-workflow-notifier.js +25 -15
- package/lib/utils/get-other-workflow-jobs.js +163 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
124
|
-
|
|
130
|
+
if ( !CIRCLE_WORKFLOW_ID ) {
|
|
131
|
+
throw new Error( 'Missing environment variable: CIRCLE_WORKFLOW_ID' );
|
|
132
|
+
}
|
|
125
133
|
|
|
126
|
-
|
|
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
|
+
}
|