Skip to content

Commit 180da6c

Browse files
authored
Refine Opera release updater + update releases (#29414)
* fix(update-browser-releases): update Opera blog post title pattern * feat(update-browser-releases): find all releases until current * Update Opera releases * refator(update-browser-releases): extract feed item generator * chore: run `lint:fix` once
1 parent 5b834ce commit 180da6c

File tree

4 files changed

+166
-92
lines changed

4 files changed

+166
-92
lines changed

api/Element.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8022,10 +8022,7 @@
80228022
],
80238023
"firefox_android": "mirror",
80248024
"oculus": "mirror",
8025-
"opera": {
8026-
"version_added": "64",
8027-
"notes": "Before version 126, `pointerrawupdate` events were exposed to non-secure contexts."
8028-
},
8025+
"opera": "mirror",
80298026
"opera_android": "mirror",
80308027
"safari": {
80318028
"version_added": false

browsers/opera.json

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -921,19 +921,61 @@
921921
"123": {
922922
"release_date": "2025-10-28",
923923
"release_notes": "https://blogs.opera.com/desktop/2025/10/opera-123/",
924-
"status": "current",
924+
"status": "retired",
925925
"engine": "Blink",
926926
"engine_version": "139"
927927
},
928928
"124": {
929-
"status": "beta",
929+
"release_date": "2025-11-13",
930+
"release_notes": "https://blogs.opera.com/desktop/2025/11/opera-124-stable/",
931+
"status": "retired",
930932
"engine": "Blink",
931933
"engine_version": "140"
932934
},
933935
"125": {
934-
"status": "nightly",
936+
"release_date": "2025-12-04",
937+
"release_notes": "https://blogs.opera.com/desktop/2025/12/opera-125-stable/",
938+
"status": "retired",
935939
"engine": "Blink",
936940
"engine_version": "141"
941+
},
942+
"126": {
943+
"release_date": "2026-01-08",
944+
"release_notes": "https://blogs.opera.com/desktop/2026/01/opera-126-stable/",
945+
"status": "retired",
946+
"engine": "Blink",
947+
"engine_version": "142"
948+
},
949+
"127": {
950+
"release_date": "2026-02-02",
951+
"release_notes": "https://blogs.opera.com/desktop/2026/02/opera-127-stable/",
952+
"status": "retired",
953+
"engine": "Blink",
954+
"engine_version": "143"
955+
},
956+
"128": {
957+
"release_date": "2026-02-26",
958+
"release_notes": "https://blogs.opera.com/desktop/2026/02/opera-128-stable/",
959+
"status": "retired",
960+
"engine": "Blink",
961+
"engine_version": "144"
962+
},
963+
"129": {
964+
"release_date": "2026-03-18",
965+
"release_notes": "https://blogs.opera.com/desktop/2026/03/opera-129-stable/",
966+
"status": "current",
967+
"engine": "Blink",
968+
"engine_version": "145"
969+
},
970+
"130": {
971+
"status": "beta",
972+
"engine": "Blink",
973+
"engine_version": "146"
974+
},
975+
"131": {
976+
"status": "nightly",
977+
"engine": "Blink",
978+
"engine_version": "147"
937979
}
938980
}
939981
}

scripts/update-browser-releases/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ const options = {
190190
bcdBrowserName: 'opera',
191191
skippedReleases: [],
192192
releaseFeedURL: 'https://blogs.opera.com/desktop/category/stable-2/feed/',
193-
titleVersionPattern: /^Opera (\d+)$/,
193+
maxFeedPages: 50,
194+
titleVersionPattern: /^Opera (\d+)(?: Stable)?$/,
194195
descriptionEngineVersionPattern: /Chromium(?:\s[^.\d]+)?\s(\d+)(?=[.])/,
195196
},
196197
opera_android: {

scripts/update-browser-releases/opera.js

Lines changed: 118 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,34 @@ import {
2525
} from './utils.js';
2626

2727
/**
28-
* Extracts the latest release from the items.
29-
* @param {RSSItem[]} items the RSS items.
28+
* Yields RSS items across pages until there are no more items or the page limit is reached.
29+
* @param {string} baseURL the base URL of the RSS feed.
30+
* @param {number} maxPages the maximum number of pages to fetch.
31+
* @yields {RSSItem} the RSS items.
32+
*/
33+
async function* feedItems(baseURL, maxPages = 1) {
34+
for (let page = 1; page <= maxPages; page++) {
35+
const url = page === 1 ? baseURL : `${baseURL}?paged=${page}`;
36+
const items = await getRSSItems(url);
37+
if (!items.length) {
38+
break;
39+
}
40+
yield* items;
41+
}
42+
}
43+
44+
/**
45+
* Builds a Release object from an RSS item.
46+
* @param {RSSItem} item the RSS item.
3047
* @param {RegExp} titleVersionPattern the pattern to match the title and extract the version.
3148
* @param {RegExp} descriptionEngineVersionPattern the pattern to match the description and extract the engine version.
32-
* @returns {Promise<Release | null>} the latest release, if found, otherwise null.
49+
* @returns {Promise<Release>} the release.
3350
*/
34-
const findRelease = async (
35-
items,
51+
const buildRelease = async (
52+
item,
3653
titleVersionPattern,
3754
descriptionEngineVersionPattern,
3855
) => {
39-
const item = items.find(
40-
(item) => titleVersionPattern.test(item.title) /* &&
41-
descriptionEngineVersionPattern.test(item.description)*/,
42-
);
43-
44-
if (!item) {
45-
return null;
46-
}
47-
4856
const version = /** @type {RegExpMatchArray} */ (
4957
item.title.match(titleVersionPattern)
5058
)[1];
@@ -109,99 +117,125 @@ export const updateOperaReleases = async (options) => {
109117

110118
let result = '';
111119

112-
const items = await getRSSItems(options.releaseFeedURL);
113-
114-
const release = await findRelease(
115-
items.filter(
116-
(item) =>
117-
options.releaseFilterCreator?.includes(item['dc:creator']) ?? true,
118-
),
119-
options.titleVersionPattern,
120-
options.descriptionEngineVersionPattern,
121-
);
122-
123-
if (!release) {
124-
return gfmNoteblock(
125-
'NOTE',
126-
`**${options.browserName}**: No release announcement found among ${items.length} items in [this RSS feed](<${options.releaseFeedURL}>).`,
127-
);
128-
}
129-
130120
const file = await fs.readFile(`${options.bcdFile}`, 'utf-8');
131121
const data = JSON.parse(file.toString());
132122

133-
const current = structuredClone(
134-
data.browsers[browser].releases[release.version],
135-
);
136-
137-
if (!release.engineVersion) {
138-
const currentEngineVersion = current.engine_version;
139-
if (!currentEngineVersion) {
140-
return gfmNoteblock(
141-
'CAUTION',
142-
`**${options.browserName}**: No engine version found in [this blog post](<${release.releaseNote}>).`,
143-
);
123+
// Find the version currently tracked as "current" to use as stopping condition.
124+
const [currentBCDVersion] =
125+
Object.entries(data.browsers[browser].releases).find(
126+
([, r]) => r.status === 'current',
127+
) ?? [];
128+
129+
const newItems = /** @type {RSSItem[]} */ ([]);
130+
131+
for await (const item of feedItems(
132+
options.releaseFeedURL,
133+
options.maxFeedPages ?? 1,
134+
)) {
135+
if (
136+
options.releaseFilterCreator &&
137+
!options.releaseFilterCreator.includes(item['dc:creator'])
138+
) {
139+
continue;
144140
}
145-
146-
result += gfmNoteblock(
147-
'WARNING',
148-
`**${options.browserName}**: No engine version found in [this blog post](<${release.releaseNote}>). Using (previous engine version + 1) instead.`,
149-
);
150-
release.engineVersion = currentEngineVersion;
141+
if (!options.titleVersionPattern.test(item.title)) {
142+
continue;
143+
}
144+
const version = /** @type {RegExpMatchArray} */ (
145+
item.title.match(options.titleVersionPattern)
146+
)[1];
147+
if (version === currentBCDVersion) {
148+
break;
149+
}
150+
newItems.push(item);
151151
}
152152

153-
if (isDesktop && !current) {
153+
if (!newItems.length) {
154154
return gfmNoteblock(
155-
'WARNING',
156-
`Latest stable **${options.browserName}** release **${release.version}** not yet tracked.`,
155+
'NOTE',
156+
`**${options.browserName}**: No new release announcements found in [this RSS feed](<${options.releaseFeedURL}>).`,
157157
);
158158
}
159159

160-
result += createOrUpdateBrowserEntry(
161-
data,
162-
browser,
163-
release.version,
164-
release.channel,
165-
release.engine,
166-
release.engineVersion,
167-
release.date,
168-
release.releaseNote,
169-
);
160+
// Process releases from oldest to newest.
161+
for (const item of newItems.reverse()) {
162+
const release = await buildRelease(
163+
item,
164+
options.titleVersionPattern,
165+
options.descriptionEngineVersionPattern,
166+
);
170167

171-
// Set previous release to "retired".
172-
const previousVersion = String(Number(release.version) - 1);
173-
result += updateBrowserEntry(
174-
data,
175-
browser,
176-
previousVersion,
177-
undefined,
178-
'retired',
179-
undefined,
180-
undefined,
181-
);
168+
if (!release.engineVersion) {
169+
const existingEngineVersion =
170+
data.browsers[browser].releases[release.version]?.engine_version;
171+
if (!existingEngineVersion) {
172+
result += gfmNoteblock(
173+
'CAUTION',
174+
`**${options.browserName}**: No engine version found in [this blog post](<${release.releaseNote}>).`,
175+
);
176+
continue;
177+
}
178+
result += gfmNoteblock(
179+
'WARNING',
180+
`**${options.browserName}**: No engine version found in [this blog post](<${release.releaseNote}>). Using existing engine version instead.`,
181+
);
182+
release.engineVersion = existingEngineVersion;
183+
}
182184

183-
if (isDesktop) {
184-
// 1. Set next release to "beta".
185185
result += createOrUpdateBrowserEntry(
186186
data,
187187
browser,
188-
String(Number(release.version) + 1),
189-
'beta',
188+
release.version,
189+
release.channel,
190190
release.engine,
191-
String(Number(release.engineVersion) + 1),
191+
release.engineVersion,
192+
release.date,
193+
release.releaseNote,
192194
);
193195

194-
// 2. Add another release as "nightly".
195-
result += createOrUpdateBrowserEntry(
196+
// Set previous release to "retired".
197+
result += updateBrowserEntry(
196198
data,
197199
browser,
198-
String(Number(release.version) + 2),
199-
'nightly',
200-
release.engine,
201-
String(Number(release.engineVersion) + 2),
200+
String(Number(release.version) - 1),
201+
undefined,
202+
'retired',
203+
undefined,
204+
undefined,
202205
);
203206
}
204207

208+
if (isDesktop) {
209+
// Determine the latest processed release (last item after oldest-to-newest reversal).
210+
const latestVersion = /** @type {RegExpMatchArray} */ (
211+
newItems[newItems.length - 1].title.match(options.titleVersionPattern)
212+
)[1];
213+
const latestEngineVersion =
214+
data.browsers[browser].releases[latestVersion]?.engine_version;
215+
216+
if (latestEngineVersion) {
217+
// 1. Set next release to "beta".
218+
result += createOrUpdateBrowserEntry(
219+
data,
220+
browser,
221+
String(Number(latestVersion) + 1),
222+
'beta',
223+
'Blink',
224+
String(Number(latestEngineVersion) + 1),
225+
);
226+
227+
// 2. Add another release as "nightly".
228+
result += createOrUpdateBrowserEntry(
229+
data,
230+
browser,
231+
String(Number(latestVersion) + 2),
232+
'nightly',
233+
'Blink',
234+
String(Number(latestEngineVersion) + 2),
235+
);
236+
}
237+
}
238+
205239
await fs.writeFile(`./${options.bcdFile}`, stringify(data) + '\n');
206240

207241
// Returns the log

0 commit comments

Comments
 (0)