Skip to content

Commit b22d7a8

Browse files
committed
Merge branch 'master' into users/habara-keigo/fix/freeze-package-versions
2 parents 754bead + bd85b97 commit b22d7a8

17 files changed

+450
-25
lines changed
Lines changed: 236 additions & 0 deletions
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+
};

.github/workflows/auto-testing.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ jobs:
1818
- '3.10'
1919
- '3.11'
2020
- '3.12'
21+
- '3.13'
2122
steps:
2223
- uses: actions/checkout@v4
2324
with:
@@ -50,6 +51,7 @@ jobs:
5051
- '3.10'
5152
- '3.11'
5253
- '3.12'
54+
- '3.13'
5355
steps:
5456
- uses: actions/checkout@v4
5557
with:
Lines changed: 31 additions & 0 deletions
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-eol-newrelease:
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: 'Python',
23+
eolJsonUrl: 'https://endoflife.date/api/python.json',
24+
eolViewUrl: 'https://endoflife.date/python',
25+
eolLookbackDays: 100,
26+
newReleaseThresholdDays: 100,
27+
ltsOnly: false,
28+
retryCount: 3,
29+
retryIntervalSec: 30
30+
});
31+
github-token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/publish-to-pypi.yml

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ on:
1313
required: true
1414

1515
jobs:
16-
deploy:
17-
16+
release-build:
1817
runs-on: ubuntu-latest
19-
18+
permissions:
19+
issues: write
2020
steps:
2121
- uses: actions/checkout@v4
2222
with:
@@ -40,13 +40,15 @@ jobs:
4040
VERSION=${VERSION#v}
4141
echo "VERSION=$VERSION" >> $GITHUB_ENV
4242
python tools/update_version.py $VERSION
43-
- name: Build and publish
44-
env:
45-
TWINE_USERNAME: ${{ secrets.PYPI_API_USER }}
46-
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
43+
- name: Build
4744
run: |
4845
python setup.py sdist bdist_wheel
49-
twine upload dist/*
46+
47+
- name: upload artifact
48+
uses: actions/upload-artifact@v4
49+
with:
50+
name: release-dists
51+
path: dist/
5052

5153
- name: Create GitHub Issue on Failure
5254
if: failure()
@@ -66,3 +68,41 @@ jobs:
6668
body: issueBody,
6769
assignees
6870
});
71+
72+
pypi-publish:
73+
runs-on: ubuntu-latest
74+
needs:
75+
- release-build
76+
environment:
77+
name: release
78+
permissions:
79+
id-token: write
80+
issues: write
81+
82+
steps:
83+
- name: Retrieve release distributions
84+
uses: actions/download-artifact@v4
85+
with:
86+
name: release-dists
87+
path: dist/
88+
89+
- name: Publish release distributions to PyPI
90+
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4
91+
92+
- name: Create GitHub Issue on Failure
93+
if: failure()
94+
uses: actions/github-script@v7
95+
with:
96+
script: |
97+
const { owner, repo } = context.repo;
98+
const issueTitle = `Release job for failed`;
99+
const issueBody = `The release job failed. Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.`;
100+
const assignees = [context.actor];
101+
102+
await github.rest.issues.create({
103+
owner,
104+
repo,
105+
title: issueTitle,
106+
body: issueBody,
107+
assignees
108+
});

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ Test by using tox. We test against the following versions.
412412
- 3.10
413413
- 3.11
414414
- 3.12
415+
- 3.13
415416

416417
To run all tests and to run ``flake8`` against all versions, use:
417418

generator/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@
120120
<dependency>
121121
<groupId>ch.qos.logback</groupId>
122122
<artifactId>logback-classic</artifactId>
123-
<version>1.5.16</version>
123+
<version>1.5.18</version>
124124
</dependency>
125125
</dependencies>
126126
<properties>
127127
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
128-
<openapi-generator-version>7.11.0</openapi-generator-version>
128+
<openapi-generator-version>7.12.0</openapi-generator-version>
129129
<maven-plugin-version>1.0.0</maven-plugin-version>
130130
<junit-version>4.13.2</junit-version>
131131
</properties>

0 commit comments

Comments
 (0)