@ckeditor/ckeditor5-dev-ci 54.7.2 → 54.7.3

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
@@ -6,7 +6,7 @@ CKEditor 5 CI utilities
6
6
 
7
7
  Utils for [CKEditor 5](https://ckeditor.com) CI builds.
8
8
 
9
- Contains tools for sending Slack notifications by Circle CI.
9
+ Contains tools for sending Slack notifications from Circle CI and GitHub Actions workflows.
10
10
 
11
11
  ## Available scripts
12
12
 
@@ -111,6 +111,31 @@ These commands accept a mix of environment variables and command line arguments.
111
111
  - `--trigger-commit-hash` — Commit SHA to construct the commit URL. Useful when a pipeline was triggered via a different repository.
112
112
  - `--hide-author` — `"true"`/`"false"` to hide the author in Slack.
113
113
 
114
+ - ⚙️ **`ckeditor5-dev-ci-notify-github-actions-status`**
115
+
116
+ Sends a Slack notification summarizing the current GitHub Actions workflow run.
117
+ Designed to be called from a step (or dedicated job) gated by `if: failure()`; the script does not check the workflow status before sending the notification.
118
+ Fetches the commit author and workflow run start time via the GitHub API (works with private repositories).
119
+
120
+ **Environment variables:**
121
+ - `CKE5_GITHUB_TOKEN` — GitHub token with the `repo` scope, used to fetch commit author and workflow run metadata.
122
+ - `CKE5_SLACK_WEBHOOK_URL` — Incoming Webhook URL for the Slack channel receiving notifications.
123
+
124
+ **GitHub-provided variables:**
125
+ - `GITHUB_REPOSITORY` &mdash; `<owner>/<repo>` of the current workflow.
126
+ - `GITHUB_REF_NAME` &mdash; Branch or tag name that triggered the workflow.
127
+ - `GITHUB_SHA` &mdash; Commit SHA of the current run.
128
+ - `GITHUB_RUN_ID` &mdash; ID of the current workflow run.
129
+ - `GITHUB_RUN_ATTEMPT` &mdash; *(Optional)* Run attempt number; included in the Slack message when greater than `1`.
130
+ - `GITHUB_WORKFLOW` &mdash; *(Optional)* Display name of the current workflow.
131
+ - `GITHUB_SERVER_URL` &mdash; *(Optional)* Server URL; defaults to `https://github.com`.
132
+ - `GITHUB_API_URL` &mdash; *(Optional)* API base URL; defaults to `https://api.github.com`.
133
+
134
+ **Parameters:**
135
+ - `--trigger-repository-slug` &mdash; `<org>/<repo>` to construct the commit URL when provided with `--trigger-commit-hash`. Useful when a workflow was triggered via a different repository.
136
+ - `--trigger-commit-hash` &mdash; Commit SHA to construct the commit URL. Useful when a workflow was triggered via a different repository.
137
+ - `--hide-author` &mdash; `"true"`/`"false"` to hide the author in Slack.
138
+
114
139
  - ⚙️ **`ckeditor5-dev-ci-trigger-circle-build`**
115
140
 
116
141
  Triggers a **new CircleCI pipeline** for a specified repository.
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
5
+ * For licensing, see LICENSE.md.
6
+ */
7
+
8
+ import { parseArgs } from 'node:util';
9
+ import slackNotify from 'slack-notify';
10
+ import formatMessage from '../lib/format-message.js';
11
+
12
+ // This script assumes that it is being executed in a GitHub Actions runner.
13
+ // The step using it should be conditional on the workflow having failed
14
+ // (for example via `if: failure()` or a dedicated job depending on the failed one),
15
+ // since the script itself does not check whether the workflow failed before sending
16
+ // the notification.
17
+ //
18
+ // Described environment variables starting with "CKE5" must be added by the integrator.
19
+
20
+ const {
21
+ /**
22
+ * Required. Token to a GitHub account with the `repo` scope. It is required for obtaining
23
+ * the author of the commit that triggered the failed workflow run. The repository can be
24
+ * private, so the public, unauthenticated API cannot be used.
25
+ */
26
+ CKE5_GITHUB_TOKEN,
27
+
28
+ /**
29
+ * Required. Webhook URL of the Slack channel where the notification should be sent.
30
+ */
31
+ CKE5_SLACK_WEBHOOK_URL,
32
+
33
+ // Variables that are available by default in the GitHub Actions environment.
34
+ // See: https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables.
35
+ GITHUB_REPOSITORY,
36
+ GITHUB_REF_NAME,
37
+ GITHUB_SHA,
38
+ GITHUB_RUN_ID,
39
+ GITHUB_RUN_ATTEMPT,
40
+ GITHUB_WORKFLOW,
41
+ GITHUB_SERVER_URL,
42
+ GITHUB_API_URL
43
+ } = process.env;
44
+
45
+ const { values: cliArguments } = parseArgs( {
46
+ options: {
47
+ /**
48
+ * Optional. If both are defined, the script will use the URL as the commit URL.
49
+ * Otherwise, the URL will be constructed using the current repository data.
50
+ */
51
+ 'trigger-repository-slug': {
52
+ type: 'string',
53
+ default: process.env.CKE5_TRIGGER_REPOSITORY_SLUG
54
+ },
55
+ 'trigger-commit-hash': {
56
+ type: 'string',
57
+ default: process.env.CKE5_TRIGGER_COMMIT_HASH
58
+ },
59
+
60
+ /**
61
+ * Optional. If set to "true" or "1", commit author will be hidden.
62
+ * See: https://github.com/ckeditor/ckeditor5/issues/9252.
63
+ */
64
+ 'hide-author': {
65
+ type: 'string',
66
+ default: process.env.CKE5_SLACK_NOTIFY_HIDE_AUTHOR
67
+ }
68
+ }
69
+ } );
70
+
71
+ notifyGitHubActionsStatus().catch( err => {
72
+ console.error( err );
73
+ process.exit( 1 );
74
+ } );
75
+
76
+ async function notifyGitHubActionsStatus() {
77
+ assertRequiredEnvironmentVariables();
78
+
79
+ const serverUrl = ( GITHUB_SERVER_URL || 'https://github.com' ).replace( /\/$/, '' );
80
+ const apiUrl = ( GITHUB_API_URL || 'https://api.github.com' ).replace( /\/$/, '' );
81
+ const [ repositoryOwner, repositoryName ] = GITHUB_REPOSITORY.split( '/' );
82
+ const runAttempt = GITHUB_RUN_ATTEMPT ? Number( GITHUB_RUN_ATTEMPT ) : 1;
83
+
84
+ const runData = await getWorkflowRunData( { apiUrl, repositoryOwner, repositoryName } );
85
+ const buildUrl = [ serverUrl, GITHUB_REPOSITORY, 'actions', 'runs', GITHUB_RUN_ID, 'attempts', runAttempt ].join( '/' );
86
+ const startedAtIso = runData.run_started_at || runData.created_at;
87
+
88
+ const message = await formatMessage( {
89
+ slackMessageUsername: 'GitHub Actions',
90
+ iconUrl: 'https://avatars.githubusercontent.com/in/15368?s=80&v=4',
91
+ repositoryOwner,
92
+ repositoryName,
93
+ branch: GITHUB_REF_NAME,
94
+ buildTitle: GITHUB_WORKFLOW || 'Workflow run',
95
+ buildUrl,
96
+ buildId: `#${ GITHUB_RUN_ID }${ runAttempt > 1 ? ` (attempt ${ runAttempt })` : '' }`,
97
+ githubToken: CKE5_GITHUB_TOKEN,
98
+ triggeringCommitUrl: getTriggeringCommitUrl( serverUrl ),
99
+ apiUrl,
100
+ startTime: startedAtIso ? Math.ceil( new Date( startedAtIso ).getTime() / 1000 ) : null,
101
+ endTime: Math.ceil( Date.now() / 1000 ),
102
+ shouldHideAuthor: isTrueLike( cliArguments[ 'hide-author' ] )
103
+ } );
104
+
105
+ return slackNotify( CKE5_SLACK_WEBHOOK_URL )
106
+ .send( message )
107
+ .catch( err => console.log( 'API error occurred:', err ) );
108
+ }
109
+
110
+ function assertRequiredEnvironmentVariables() {
111
+ const required = {
112
+ CKE5_GITHUB_TOKEN,
113
+ CKE5_SLACK_WEBHOOK_URL,
114
+ GITHUB_REPOSITORY,
115
+ GITHUB_REF_NAME,
116
+ GITHUB_SHA,
117
+ GITHUB_RUN_ID
118
+ };
119
+
120
+ for ( const [ name, value ] of Object.entries( required ) ) {
121
+ if ( !value ) {
122
+ throw new Error( `Missing environment variable: ${ name }` );
123
+ }
124
+ }
125
+ }
126
+
127
+ async function getWorkflowRunData( { apiUrl, repositoryOwner, repositoryName } ) {
128
+ const fetchUrl = [ apiUrl, 'repos', repositoryOwner, repositoryName, 'actions', 'runs', GITHUB_RUN_ID ].join( '/' );
129
+ const fetchOptions = {
130
+ method: 'GET',
131
+ headers: {
132
+ 'Accept': 'application/vnd.github+json',
133
+ 'Authorization': `Bearer ${ CKE5_GITHUB_TOKEN }`,
134
+ 'X-GitHub-Api-Version': '2022-11-28'
135
+ }
136
+ };
137
+
138
+ const response = await fetch( fetchUrl, fetchOptions );
139
+
140
+ if ( !response.ok ) {
141
+ throw new Error( `Failed to fetch workflow run ${ GITHUB_RUN_ID }: HTTP ${ response.status }.` );
142
+ }
143
+
144
+ return response.json();
145
+ }
146
+
147
+ function getTriggeringCommitUrl( serverUrl ) {
148
+ const cliRepoSlug = cliArguments[ 'trigger-repository-slug' ];
149
+ const cliCommitHash = cliArguments[ 'trigger-commit-hash' ];
150
+
151
+ const repoSlug = cliRepoSlug && cliCommitHash ? cliRepoSlug.trim() : GITHUB_REPOSITORY;
152
+ const hash = cliRepoSlug && cliCommitHash ? cliCommitHash.trim() : GITHUB_SHA;
153
+
154
+ return [ serverUrl, repoSlug, 'commit', hash ].join( '/' );
155
+ }
156
+
157
+ function isTrueLike( value ) {
158
+ return value === true || value === 1 || value === '1' || value === 'true';
159
+ }
@@ -5,8 +5,6 @@
5
5
 
6
6
  import { bots, members } from './data/index.js';
7
7
 
8
- const REPOSITORY_REGEXP = /github\.com\/([^/]+)\/([^/]+)/;
9
-
10
8
  /**
11
9
  * @param {object} options
12
10
  * @param {string} options.slackMessageUsername
@@ -19,13 +17,15 @@ const REPOSITORY_REGEXP = /github\.com\/([^/]+)\/([^/]+)/;
19
17
  * @param {string} options.buildId
20
18
  * @param {string} options.githubToken
21
19
  * @param {string} options.triggeringCommitUrl
20
+ * @param {string} [options.apiUrl]
22
21
  * @param {number} options.startTime
23
22
  * @param {number} options.endTime
24
23
  * @param {boolean} options.shouldHideAuthor
25
24
  */
26
25
  export default async function formatMessage( options ) {
27
- const commitDetails = await getCommitDetails( options.triggeringCommitUrl, options.githubToken );
28
- const repoUrl = `https://github.com/${ options.repositoryOwner }/${ options.repositoryName }`;
26
+ const commitDetails = await getCommitDetails( options.triggeringCommitUrl, options.githubToken, options.apiUrl );
27
+ const serverUrl = new URL( options.triggeringCommitUrl ).origin;
28
+ const repoUrl = `${ serverUrl }/${ options.repositoryOwner }/${ options.repositoryName }`;
29
29
 
30
30
  return {
31
31
  username: options.slackMessageUsername,
@@ -162,14 +162,16 @@ function getFormattedMessage( commitMessage, triggeringCommitUrl ) {
162
162
  return '_Unavailable._';
163
163
  }
164
164
 
165
- const [ , repoOwner, repoName ] = triggeringCommitUrl.match( REPOSITORY_REGEXP );
165
+ const url = new URL( triggeringCommitUrl );
166
+ const serverUrl = url.origin;
167
+ const [ , repoOwner, repoName ] = url.pathname.split( '/' );
166
168
 
167
169
  return commitMessage
168
170
  .replace( / #(\d+)/g, ( _, issueId ) => {
169
- return ` <https://github.com/${ repoOwner }/${ repoName }/issues/${ issueId }|#${ issueId }>`;
171
+ return ` <${ serverUrl }/${ repoOwner }/${ repoName }/issues/${ issueId }|#${ issueId }>`;
170
172
  } )
171
173
  .replace( /([\w-]+\/[\w-]+)#(\d+)/g, ( _, repoSlug, issueId ) => {
172
- return `<https://github.com/${ repoSlug }/issues/${ issueId }|${ repoSlug }#${ issueId }>`;
174
+ return `<${ serverUrl }/${ repoSlug }/issues/${ issueId }|${ repoSlug }#${ issueId }>`;
173
175
  } );
174
176
  }
175
177
 
@@ -178,10 +180,11 @@ function getFormattedMessage( commitMessage, triggeringCommitUrl ) {
178
180
  *
179
181
  * @param {string} triggeringCommitUrl The URL to the commit on GitHub.
180
182
  * @param {string} githubToken Github token used for authorization a request,
183
+ * @param {string} [apiUrl] Optional base URL of the GitHub API.
181
184
  * @returns {Promise.<object>}
182
185
  */
183
- function getCommitDetails( triggeringCommitUrl, githubToken ) {
184
- const apiGithubUrlCommit = getGithubApiUrl( triggeringCommitUrl );
186
+ function getCommitDetails( triggeringCommitUrl, githubToken, apiUrl ) {
187
+ const apiGithubUrlCommit = getGithubApiUrl( triggeringCommitUrl, apiUrl );
185
188
  const options = {
186
189
  method: 'GET',
187
190
  credentials: 'include',
@@ -204,8 +207,15 @@ function getCommitDetails( triggeringCommitUrl, githubToken ) {
204
207
  * Returns a URL to GitHub API which returns details of the commit that caused the CI to fail its job.
205
208
  *
206
209
  * @param {string} triggeringCommitUrl The URL to the commit on GitHub.
210
+ * @param {string} [apiUrl] Optional base URL of the GitHub API.
207
211
  * @returns {string}
208
212
  */
209
- function getGithubApiUrl( triggeringCommitUrl ) {
213
+ function getGithubApiUrl( triggeringCommitUrl, apiUrl ) {
214
+ if ( apiUrl ) {
215
+ const [ , owner, repo, , sha ] = new URL( triggeringCommitUrl ).pathname.split( '/' );
216
+
217
+ return `${ apiUrl.replace( /\/$/, '' ) }/repos/${ owner }/${ repo }/commits/${ sha }`;
218
+ }
219
+
210
220
  return triggeringCommitUrl.replace( 'github.com/', 'api.github.com/repos/' ).replace( '/commit/', '/commits/' );
211
221
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-dev-ci",
3
- "version": "54.7.2",
3
+ "version": "54.7.3",
4
4
  "description": "Utils used on various Continuous Integration services.",
5
5
  "keywords": [],
6
6
  "author": "CKSource (http://cksource.com/)",
@@ -29,6 +29,7 @@
29
29
  "ckeditor5-dev-ci-is-job-triggered-by-member": "bin/is-job-triggered-by-member.js",
30
30
  "ckeditor5-dev-ci-is-workflow-restarted": "bin/is-workflow-restarted.js",
31
31
  "ckeditor5-dev-ci-notify-circle-status": "bin/notify-circle-status.js",
32
+ "ckeditor5-dev-ci-notify-github-actions-status": "bin/notify-github-actions-status.js",
32
33
  "ckeditor5-dev-ci-trigger-circle-build": "bin/trigger-circle-build.js",
33
34
  "ckeditor5-dev-ci-trigger-snyk-scan": "bin/trigger-snyk-scan.js"
34
35
  },