Skip to content

Commit 9fb92bc

Browse files
authored
Add EoL and new release reminder (#666)
This change utilizes https://endoflife.date to create reminders. The line-bot-sdk repositories use this site as a reference when changing library version support. After an EOL or a new release is created, if an issue with the same name has not yet been issued, an issue as a reminder will be created automatically. The script (`.github/scripts/check-eol-newrelease.cjs`) itself is the same across all repositories. The configuration passed from the GitHub workflow to the script only needs to account for differences specific to each repository. Only Node.js and Java consider LTS versions, while all other versions are monitored. It has already been tested in my repository. original: line/line-bot-sdk-nodejs#1189
1 parent 5a8a5b3 commit 9fb92bc

File tree

2 files changed

+267
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)