Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add EoL and new release reminder #399

Merged
merged 3 commits into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 236 additions & 0 deletions .github/scripts/check-eol-newrelease.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,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);
}
};
31 changes: 31 additions & 0 deletions .github/workflows/check-eol-newrelease.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: "Check EoL & New Releases"

on:
schedule:
# Every day at 22:00 UTC -> 07:00 JST
- cron: '0 22 * * *'
workflow_dispatch:

jobs:
check-go:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4

- name: Run EoL & NewRelease check
uses: actions/github-script@v7
with:
script: |
const checkEolAndNewReleases = require('.github/scripts/check-eol-newrelease.cjs');
await checkEolAndNewReleases({ github, context, core }, {
languageName: 'Ruby',
eolJsonUrl: 'https://endoflife.date/api/ruby.json',
eolViewUrl: 'https://endoflife.date/ruby',
eolLookbackDays: 100,
newReleaseThresholdDays: 100,
ltsOnly: false,
retryCount: 3,
retryIntervalSec: 30
});
github-token: ${{ secrets.GITHUB_TOKEN }}