Skip to content

Commit

Permalink
Add Opera release update script
Browse files Browse the repository at this point in the history
  • Loading branch information
caugner committed Jan 31, 2025
1 parent 70ecabb commit 4da2a0d
Show file tree
Hide file tree
Showing 5 changed files with 327 additions and 0 deletions.
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"typescript": "~5.7.2",
"web-features": "^2.15.0",
"web-specs": "^3.0.0",
"xml2js": "^0.6.2",
"yargs": "~17.7.0"
},
"scripts": {
Expand Down
36 changes: 36 additions & 0 deletions scripts/update-browser-releases/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { updateChromiumReleases } from './chrome.js';
import { updateEdgeReleases } from './edge.js';
import { updateFirefoxReleases } from './firefox.js';
import { updateSafariReleases } from './safari.js';
import { updateOperaReleases } from './opera.js';

const argv = yargs(process.argv.slice(2))
.usage('Usage: npm run update-browser-releases -- (flags)')
Expand All @@ -29,6 +30,11 @@ const argv = yargs(process.argv.slice(2))
type: 'boolean',
group: 'Engine selection:',
})
.option('opera', {
describe: 'Update Opera',
type: 'boolean',
group: 'Engine selection:',
})
.option('safari', {
describe: 'Update Apple Safari',
type: 'boolean',
Expand Down Expand Up @@ -65,13 +71,15 @@ const updateAllBrowsers =
argv['webview'] ||
argv['firefox'] ||
argv['edge'] ||
argv['opera'] ||
argv['safari']
);
const updateChrome = argv['chrome'] || updateAllBrowsers;
const updateWebview = argv['webview'] || updateAllBrowsers;
const updateFirefox = argv['firefox'] || updateAllBrowsers;
const updateEdge = argv['edge'] || updateAllBrowsers;
const updateSafari = argv['safari'] || updateAllBrowsers;
const updateOpera = argv['opera'] || updateAllBrowsers;
const updateAllDevices =
argv['alldevices'] || !(argv['mobile'] || argv['desktop']);
const updateMobile = argv['mobile'] || updateAllDevices;
Expand Down Expand Up @@ -190,6 +198,24 @@ const options = {
'https://developer.apple.com/tutorials/data/documentation/safari-release-notes.json',
releaseNoteURLBase: 'https://developer.apple.com',
},
opera: {
browserName: 'Opera for Desktop',
bcdFile: './browsers/opera.json',
bcdBrowserName: 'opera',
skippedReleases: [],
releaseFeedURL: 'https://blogs.opera.com/desktop/category/stable-2/feed/',
titleVersionPattern: /^Opera (\d+)$/,
descriptionEngineVersionPattern: /Chromium (\d+)/,
},
opera_android: {
browserName: 'Opera for Android',
bcdFile: './browsers/opera_android.json',
bcdBrowserName: 'opera_android',
skippedReleases: [],
releaseFeedURL: 'https://forums.opera.com/category/20.rss',
titleVersionPattern: /^Opera for Android (\d+)$/,
descriptionEngineVersionPattern: /Chromium (\d+)/,
},
};

let result = '';
Expand Down Expand Up @@ -224,6 +250,16 @@ if (updateFirefox && updateMobile) {
result += (result && add ? '\n' : '') + add;
}

if (updateOpera && updateDesktop) {
const add = await updateOperaReleases(options.opera);
result += (result && add ? '\n' : '') + add;
}

if (updateOpera && updateMobile) {
const add = await updateOperaReleases(options.opera_android);
result += (result && add ? '\n' : '') + add;
}

if (updateSafari && updateDesktop) {
const add = await updateSafariReleases(options.safari_desktop);
result += (result && add ? '\n' : '') + add;
Expand Down
213 changes: 213 additions & 0 deletions scripts/update-browser-releases/opera.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import fs from 'node:fs/promises';

import xml2js from 'xml2js';

import stringify from '../lib/stringify-and-order-properties.js';

import { createOrUpdateBrowserEntry, updateBrowserEntry } from './utils';

const USER_AGENT =
'MDN-Browser-Release-Update-Bot/1.0 (+https://developer.mozilla.org/)';

interface RSSItem {
title: string;
pubDate: string;
description: string;
link: string;
}

interface Release {
version: string;
date: string;
releaseNote: string;
channel: 'current';
engine: 'Blink';
engineVersion: string;
}

/**
* Fetches an RSS feed, using a typical RSS user agent.
* @param url The URL of the RSS feed.
* @returns Promise
*/
const fetchRSS = async (url: string) => {
const response = await fetch(url, {
headers: {
'User-Agent': USER_AGENT,
},
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return await response.text();
};

/**
* Parses an RSS feed into a JSON object.
* @param rssText The content of the RSS feed.
* @returns the RSS items.
*/
const parseRSS = async (rssText: string): Promise<RSSItem[]> => {
const parser = new xml2js.Parser({ explicitArray: false });

const result = await parser.parseStringPromise(rssText);

return result.rss.channel.item;
};

/**
* Fetches and parses an RSS feed.
* @param url The URL of the RSS feed.
* @returns the RSS items.
*/
const getRSSItems = async (url): Promise<RSSItem[]> => {
const rssText = await fetchRSS(url);
const items = await parseRSS(rssText);

return Array.isArray(items) ? items : [items];
};

/**
* Extracts the latest release from the items.
* @param items
* @param versionPattern
* @param engineVersionPattern
* @returns the latest release, if found, otherwise null.
*/
const findRelease = (
items: RSSItem[],
versionPattern: RegExp,
engineVersionPattern: RegExp,
): Release | null => {
const item = items.find(
(item) =>
versionPattern.test(item.title) &&
engineVersionPattern.test(item.description),
);

if (!item) {
return null;
}

const version = (item.title.match(versionPattern) as RegExpMatchArray)[1];
const date = new Date(item.pubDate).toISOString().split('T')[0];
const releaseNote = item.link;
const engineVersion = (
item.description.match(engineVersionPattern) as RegExpMatchArray
)[1];

return {
version,
date,
releaseNote,
channel: 'current',
engine: 'Blink',
engineVersion,
};
};

/**
* Converts a message into a GFM Markdown warning.
* @param message the message of the warning.
* @returns the message as a GFM warning.
*/
const warn = (message: string) =>
`> [!WARN]\n${message
.split('\n')
.map((line) => `> ${line}`)
.join('\n')}`;

/**
* Updates the JSON files listing the Opera browser releases.
* @param options The list of options for this type of Safari.
* @returns The log of what has been generated (empty if nothing)
*/
export const updateOperaReleases = async (options) => {
const browser = options.bcdBrowserName;

const isDesktop = browser === 'opera';

let result = '';

const items = await getRSSItems(options.releaseFeedURL);
const release = findRelease(
items,
options.titleVersionPattern,
options.descriptionEngineVersionPattern,
);

if (!release) {
return warn(
`**${options.browserName}**: No stable release found among ${items.length} items in [this RSS feed](<${options.releaseFeedURL}>)!`,
);
}

const file = await fs.readFile(`${options.bcdFile}`, 'utf-8');
const data = JSON.parse(file.toString());

const current = structuredClone(
data.browsers[browser].releases[release.version],
);

if (isDesktop && !current) {
return warn(
`Latest stable **${options.browserName}** release **${release.version}** not yet tracked.`,
);
}

result += createOrUpdateBrowserEntry(
data,
browser,
release.version,
release.channel,
release.engine,
release.engineVersion,
release.date,
release.releaseNote,
);

// Set previous release to "retired".
const previousVersion = String(Number(release.version) - 1);
result += updateBrowserEntry(
data,
browser,
previousVersion,
undefined,
'retired',
undefined,
undefined,
);

if (isDesktop) {
// 1. Set next release to "beta".
result += createOrUpdateBrowserEntry(
data,
browser,
String(Number(release.version) + 1),
'beta',
release.engine,
String(Number(release.engineVersion) + 1),
);

// 2. Add another release as "nightly".
result += createOrUpdateBrowserEntry(
data,
browser,
String(Number(release.version) + 2),
'nightly',
release.engine,
String(Number(release.engineVersion) + 2),
);
}

await fs.writeFile(`./${options.bcdFile}`, stringify(data) + '\n');

// Returns the log
if (result) {
result = `### Updates for ${options.browserName}${result}`;
}

return result;
};
Loading

0 comments on commit 4da2a0d

Please sign in to comment.