forked from line/line-bot-sdk-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck-eol-newrelease.cjs
More file actions
236 lines (220 loc) · 9.26 KB
/
check-eol-newrelease.cjs
File metadata and controls
236 lines (220 loc) · 9.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
// @ts-check
/**
* @typedef {object} ReleaseCycle
* @property {string|number} cycle - Release cycle (e.g. "1.20", 8.1, etc.)
* @property {string} [releaseDate] - YYYY-MM-DD string for the first release in this cycle
* @property {string|boolean} [eol] - End of Life date (YYYY-MM-DD) or false if not EoL
* @property {string} [latest] - Latest release in this cycle
* @property {string|null} [link] - Link to changelog or similar, if available
* @property {boolean|string} [lts] - Whether this cycle is non-LTS (false), or LTS starting on a given date
* @property {string|boolean} [support] - Active support date (YYYY-MM-DD) or boolean
* @property {string|boolean} [discontinued] - Discontinued date (YYYY-MM-DD) or boolean
*/
/**
* @typedef {object} EolNewReleaseConfig
* @property {string} languageName
* @property {string} eolJsonUrl
* @property {string} eolViewUrl
* @property {number} eolLookbackDays
* @property {number} newReleaseThresholdDays
* @property {boolean} ltsOnly
* @property {number} retryCount
* @property {number} retryIntervalSec
*/
/**
* This script checks EoL and new releases from endoflife.date JSON.
* It creates Issues for:
* - EoL reached within eolLookbackDays
* - New releases within newReleaseThresholdDays
* If fetching fails after multiple retries, an error Issue is created once per week.
*
* Note this script is used in a GitHub Action workflow, and some line/line-bot-sdk-* repositories.
* If you modify this script, please consider syncing the changes to other repositories.
*
* @param {import('@actions/github-script').AsyncFunctionArguments} actionCtx
* @param {EolNewReleaseConfig} config
*/
module.exports = async function checkEolAndNewReleases(actionCtx, config) {
const { github, context, core } = actionCtx;
const {
languageName,
eolJsonUrl,
eolViewUrl,
eolLookbackDays,
newReleaseThresholdDays,
ltsOnly,
retryCount,
retryIntervalSec,
} = config;
/**
* Returns a simple "year-week" string like "2025-W09".
* This is a rough calculation (not strictly ISO-8601).
* @param {Date} date
* @returns {string}
*/
const getYearWeek = (date) => {
const startOfYear = new Date(date.getFullYear(), 0, 1);
const dayOfYear = Math.floor((date - startOfYear) / 86400000) + 1;
const weekNum = Math.ceil(dayOfYear / 7);
return `${date.getFullYear()}-W${String(weekNum).padStart(2, '0')}`;
};
/**
* Simple dedent function.
* Removes common leading indentation based on the minimum indent across all lines.
* Also trims empty lines at the start/end.
* @param {string} str
* @returns {string}
*/
const dedent = (str) => {
const lines = str.split('\n');
while (lines.length && lines[0].trim() === '') lines.shift();
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
/** @type {number[]} */
const indents = lines
.filter(line => line.trim() !== '')
.map(line => (line.match(/^(\s+)/)?.[1].length) ?? 0);
const minIndent = indents.length > 0 ? Math.min(...indents) : 0;
return lines.map(line => line.slice(minIndent)).join('\n');
};
/**
* Creates an Issue if an Issue with the same title does not exist (state=all).
* @param {string} title
* @param {string} body
* @param {string[]} [labels]
* @returns {Promise<boolean>} true if created, false if an Issue with same title already exists
*/
const createIssueIfNotExists = async (title, body, labels = []) => {
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'all',
per_page: 100,
});
const found = issues.data.find(i => i.title === title);
if (found) {
core.info(`Issue already exists: "${title}"`);
return false;
}
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title,
body,
labels,
});
core.notice(`Created Issue: "${title}"`);
return true;
};
/**
* Fetch with retry, returning an array of ReleaseCycle objects.
* @param {string} url
* @returns {Promise<ReleaseCycle[]>}
*/
const fetchWithRetry = async (url) => {
let lastErr = null;
for (let i = 1; i <= retryCount; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
/** @type {ReleaseCycle[]} */
const jsonData = await response.json();
return jsonData;
} catch (err) {
lastErr = err;
core.warning(`Fetch failed (attempt ${i}/${retryCount}): ${err.message}`);
if (i < retryCount) {
await new Promise(r => setTimeout(r, retryIntervalSec * 1000));
}
}
}
throw new Error(`Failed to fetch after ${retryCount} attempts: ${lastErr?.message}`);
};
/**
* Check EoL for a single release.
* @param {ReleaseCycle} release
* @param {Date} now
* @param {Date} eolLookbackDate
*/
const checkEoL = async (release, now, eolLookbackDate) => {
if (ltsOnly && release.lts === false) {
core.info(`Skipping non-LTS release: ${release.cycle}`);
return;
}
if (typeof release.eol === 'string') {
const eolDate = new Date(release.eol);
if (!isNaN(eolDate.getTime())) {
// Check if it reached EoL within the last eolLookbackDays
if (eolDate <= now && eolDate >= eolLookbackDate) {
if (!release.cycle) return;
const title = `Drop ${languageName} ${release.cycle} support`;
const body = dedent(`
This version(${languageName} ${release.cycle}) has reached End of Life.
Please drop its support as needed.
**EoL date**: ${release.eol}
endoflife.date for ${languageName}: ${eolViewUrl}
`);
await createIssueIfNotExists(title, body, ['keep']);
}
}
}
};
/**
* Check new release for a single release.
* @param {ReleaseCycle} release
* @param {Date} now
* @param {Date} newReleaseSince
*/
const checkNewRelease = async (release, now, newReleaseSince) => {
if (ltsOnly && release.lts === false) {
core.info(`Skipping non-LTS release: ${release.cycle}`);
return;
}
if (typeof release.releaseDate === 'string') {
const rDate = new Date(release.releaseDate);
if (!isNaN(rDate.getTime())) {
// Check if releaseDate is within newReleaseThresholdDays
if (rDate >= newReleaseSince && rDate <= now) {
if (!release.cycle) return;
const ltsTag = ltsOnly ? ' (LTS)' : '';
const title = `Support ${languageName} ${release.cycle}${ltsTag}`;
const body = dedent(`
A new version(${languageName} ${release.cycle}) has been released.
Please start to support it.
**Release date**: ${release.releaseDate}
endoflife.date for ${languageName}: ${eolViewUrl}
`);
await createIssueIfNotExists(title, body, ['keep']);
}
}
}
};
core.info(`Starting EoL & NewRelease check for ${languageName} ...`);
const now = new Date();
const eolLookbackDate = new Date(now);
eolLookbackDate.setDate(eolLookbackDate.getDate() - eolLookbackDays);
const newReleaseSince = new Date(now);
newReleaseSince.setDate(newReleaseSince.getDate() - newReleaseThresholdDays);
try {
const data = await fetchWithRetry(eolJsonUrl);
for (const release of data) {
core.info(`Checking release: ${JSON.stringify(release)}`);
await checkEoL(release, now, eolLookbackDate);
await checkNewRelease(release, now, newReleaseSince);
}
} catch (err) {
core.error(`Error checking EoL/NewReleases for ${languageName}: ${err.message}`);
const yw = getYearWeek(new Date());
const errorTitle = `[CI ERROR] EoL/NewRelease check for ${languageName} in ${yw}`;
const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const body = dedent(`
The automated check for EoL and new releases failed (retried ${retryCount} times).
**Error**: ${err.message}
**Action URL**: [View job log here](${runUrl})
Please investigate (network issues, invalid JSON, etc.) and fix it to monitor EOL site automatically.
`);
await createIssueIfNotExists(errorTitle, body, ['keep']);
core.setFailed(err.message);
}
};