Merge 1f49ae14d7
into 18c36b18d2
This commit is contained in:
commit
4d8909b09f
5 changed files with 209 additions and 11 deletions
|
@ -1,6 +1,7 @@
|
|||
export {};
|
||||
|
||||
import { getServersList } from './servers';
|
||||
import { getLatestRelease } from './releases';
|
||||
|
||||
// Main entry point.
|
||||
addEventListener('fetch', (event) => {
|
||||
|
@ -15,6 +16,8 @@ export async function handleRequest(request: Request) {
|
|||
case '/resources': // Public for resources.
|
||||
case '/servers':
|
||||
return getServersList(request);
|
||||
case '/releases':
|
||||
return getLatestRelease(request);
|
||||
}
|
||||
return new Response('', { status: 404 });
|
||||
}
|
||||
|
|
95
src/releases.ts
Normal file
95
src/releases.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { parseAppVersion, parseApkName } from './versions';
|
||||
|
||||
const GITHUB_RELEASES_URL: string = 'https://api.github.com/repos/organicmaps/organicmaps/releases';
|
||||
// https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api?apiVersion=2022-11-28#authenticating-with-a-personal-access-token
|
||||
const GITHUB_BEARER_TOKEN: string =
|
||||
'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';
|
||||
|
||||
interface AppReleaseMetadata {
|
||||
published_at: Date;
|
||||
code: number;
|
||||
flavor?: string;
|
||||
type?: string;
|
||||
apk: {
|
||||
url: string;
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
// TODO: figure out how to define map properly.
|
||||
news: {
|
||||
'en-US': string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GitHubReleaseAssetMetadata {
|
||||
browser_download_url: string;
|
||||
name: string;
|
||||
size: number;
|
||||
content_type: string;
|
||||
state: string;
|
||||
}
|
||||
|
||||
interface GitHubReleaseMetadata {
|
||||
published_at: Date;
|
||||
draft: boolean;
|
||||
prerelease: boolean;
|
||||
body: string;
|
||||
assets: [GitHubReleaseAssetMetadata];
|
||||
}
|
||||
|
||||
export async function getLatestRelease(request: Request) {
|
||||
const appVersion = parseAppVersion(request.headers.get('x-om-appversion'));
|
||||
if (!appVersion) return new Response('Unknown app version', { status: 400 });
|
||||
|
||||
// The release version doesn't have `-release` suffix, thus type should be `undefined`.
|
||||
if (appVersion.flavor != 'web' || appVersion.type !== undefined)
|
||||
return new Response('Unknown app version', { status: 400 });
|
||||
|
||||
const response = await fetch(GITHUB_RELEASES_URL, {
|
||||
cf: {
|
||||
// Always cache this fetch (including 404 responses) regardless of content type
|
||||
// for a max of 30 minutes before revalidating the resource
|
||||
cacheTtl: 30 * 60,
|
||||
cacheEverything: true,
|
||||
},
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
'User-Agent': 'curl/8.4.0', // GitHub returns 403 without this.
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
Authorization: `Bearer ${GITHUB_BEARER_TOKEN}`,
|
||||
},
|
||||
});
|
||||
if (response.status != 200)
|
||||
return new Response(`Bad response status ${response.status} ${response.statusText} ${response.body} from GitHub`, {
|
||||
status: 500,
|
||||
});
|
||||
|
||||
const releases = (await response.json()) as [GitHubReleaseMetadata];
|
||||
const release = releases.find((release) => release.draft == false && release.prerelease == false);
|
||||
if (release == undefined) return new Response('No published release in GitHub response', { status: 500 });
|
||||
|
||||
const apk = release.assets.find(
|
||||
(asset) => asset.content_type == 'application/vnd.android.package-archive' && asset.name.endsWith('.apk'),
|
||||
);
|
||||
if (!apk) throw new Error('The latest release does not have APK asset');
|
||||
const apkVersion = parseApkName(apk.name);
|
||||
if (!apkVersion) throw new Error(`Failed to parse APK name: ${apk}`);
|
||||
if (apkVersion.flavor != 'web' || apkVersion.type != 'release') throw new Error(`Unsupported APK name: ${apk}`);
|
||||
|
||||
const result: AppReleaseMetadata = {
|
||||
published_at: release.published_at,
|
||||
code: apkVersion.code,
|
||||
news: {
|
||||
'en-US': release.body,
|
||||
},
|
||||
apk: {
|
||||
name: apk.name,
|
||||
size: apk.size,
|
||||
url: apk.browser_download_url,
|
||||
},
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
|
@ -13,17 +13,40 @@ export function parseDataVersion(strDataVersion: string | null): number | null {
|
|||
return dataVersion;
|
||||
}
|
||||
|
||||
interface AppVersion {
|
||||
code: number;
|
||||
build?: number;
|
||||
flavor?: string;
|
||||
type?: string; // 'debug' | 'beta'
|
||||
}
|
||||
|
||||
const APK_NAME_RE = /^OrganicMaps-(?<code>2\d{7})-(?<flavor>[A-Za-z3264]+)-(?<type>beta|debug|release)\.apk$/;
|
||||
|
||||
export function parseApkName(apkName: string): AppVersion | null {
|
||||
const m = apkName.match(APK_NAME_RE);
|
||||
if (m === null || !m.groups) return null;
|
||||
const code = parseInt(m.groups.code);
|
||||
if (Number.isNaN(code) || code < 20000000 || code > 30000000) return null;
|
||||
const flavor = m.groups.flavor;
|
||||
const type = m.groups.type;
|
||||
const apkVersion: AppVersion = {
|
||||
code: code,
|
||||
flavor: flavor,
|
||||
type: type,
|
||||
};
|
||||
return apkVersion;
|
||||
}
|
||||
|
||||
// 2022.11.20 for iOS versions released before November 21 (without donate menu)
|
||||
// 2022.11.24-4-ios for newer iOS versions (with donate menu)
|
||||
// 2022.12.24-10-Google for Android
|
||||
// 2022.12.24-10-Google-beta for Android
|
||||
// 2022.12.24-3-3f4ca43-Linux or 2022.12.24-3-3f4ca43-dirty-Linux for Linux
|
||||
// 2022.12.24-3-3f4ca43-Darwin for Mac
|
||||
const VERSION_RE =
|
||||
/(?<year>\d{4})\.(?<month>\d{1,2})\.(?<day>\d{1,2})(?:$|-(?<build>[0-9]+)(?:-[0-9a-f]+)?(?:-dirty)?-(?<flavor>[A-Za-z3264]+))/;
|
||||
/(?<year>\d{4})\.(?<month>\d{1,2})\.(?<day>\d{1,2})(?:$|-(?<build>[0-9]+)(?:-[0-9a-f]+)?(?:-dirty)?-(?<flavor>[A-Za-z3264]+))(?:-(?<type>beta|debug))?/;
|
||||
// Returns code like 221224 for both platforms, build and flavor for Android and newer iOS versions.
|
||||
export function parseAppVersion(
|
||||
versionName: string | null,
|
||||
): { code: number; build?: number; flavor?: string | undefined } | null {
|
||||
export function parseAppVersion(versionName: string | null): AppVersion | null {
|
||||
if (!versionName) {
|
||||
return null;
|
||||
}
|
||||
|
@ -51,14 +74,18 @@ export function parseAppVersion(
|
|||
return { code: code };
|
||||
}
|
||||
|
||||
const buildNumber = parseInt(m.groups.build);
|
||||
const build = Number.isNaN(buildNumber) ? 0 : buildNumber;
|
||||
// 'ios' for iOS devices.
|
||||
const flavor = (m.groups.flavor !== undefined && m.groups.flavor.toLowerCase()) || undefined;
|
||||
|
||||
return {
|
||||
const appVersion: AppVersion = {
|
||||
code: code,
|
||||
flavor: flavor,
|
||||
build: build,
|
||||
};
|
||||
|
||||
const buildNumber = parseInt(m.groups.build);
|
||||
if (!Number.isNaN(buildNumber)) appVersion.build = buildNumber;
|
||||
|
||||
if (m.groups.type !== undefined) appVersion.type = m.groups.type;
|
||||
|
||||
return appVersion;
|
||||
}
|
||||
|
|
57
test/releases.test.ts
Normal file
57
test/releases.test.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { describe, expect, test } from '@jest/globals';
|
||||
import { getLatestRelease } from '../src/releases';
|
||||
|
||||
describe('Get app release version for flavor', () => {
|
||||
const flavors = ['2022.08.23-1-web'];
|
||||
for (let flavor of flavors) {
|
||||
test(flavor, async () => {
|
||||
let req = new Request('http://127.0.0.1:8787/releases', {
|
||||
headers: {
|
||||
'X-OM-AppVersion': flavor.toLowerCase(),
|
||||
},
|
||||
});
|
||||
const response = await getLatestRelease(req);
|
||||
// TODO: How to print response.text in case of error?
|
||||
expect(response.status).toBe(200);
|
||||
const result = JSON.parse(await response.text());
|
||||
expect(Number.parseInt(result.code)).toBeGreaterThanOrEqual(23040200);
|
||||
expect(result.apk).toBeDefined();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Unsupported flavors for app update checks', () => {
|
||||
const unsupported = [
|
||||
'garbage',
|
||||
'',
|
||||
'20220823',
|
||||
'2022.08',
|
||||
'2022.08.23', // Older iOS clients
|
||||
'2022.08.23-1-Google-beta',
|
||||
'2022.08.23-5-Google-debug',
|
||||
'2022.08.23-1-fdroid-beta',
|
||||
'2022.08.23-1-fdroid-debug',
|
||||
'2022.08.23-1-web-beta',
|
||||
'2022.08.23-1-web-debug',
|
||||
'2022.08.23-1-Huawei-beta',
|
||||
'2022.08.23-1-Huawei-debug',
|
||||
// Mac OS version is not published yet anywhere.
|
||||
'2023.04.28-9-592bca9a-dirty-Darwin',
|
||||
'2023.04.28-9-592bca9a-Darwin',
|
||||
];
|
||||
for (let flavor of unsupported) {
|
||||
test(flavor, async () => {
|
||||
let req = new Request('http://127.0.0.1:8787/releases', {
|
||||
headers: {
|
||||
'X-OM-AppVersion': flavor.toLowerCase(),
|
||||
},
|
||||
});
|
||||
try {
|
||||
const response = await getLatestRelease(req);
|
||||
expect(response.status).toBeGreaterThanOrEqual(400);
|
||||
} catch (err) {
|
||||
expect(err).toContain('Unsupported app version');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from '@jest/globals';
|
||||
import { parseDataVersion, parseAppVersion } from '../src/versions';
|
||||
import { parseDataVersion, parseAppVersion, parseApkName } from '../src/versions';
|
||||
|
||||
describe('parseDataVersion', () => {
|
||||
const tests: { [key: string]: number | null } = {
|
||||
|
@ -18,6 +18,22 @@ describe('parseDataVersion', () => {
|
|||
test('', () => expect(parseDataVersion(null)).toEqual(null));
|
||||
});
|
||||
|
||||
describe('parseApkName', () => {
|
||||
const tests: { [key: string]: object | null } = {
|
||||
'OrganicMaps-24020611-web-release.apk': { code: 24020611, flavor: 'web', type: 'release' },
|
||||
'OrganicMaps-24020611-web-release': null,
|
||||
'OrganicMaps-24020611-web-.apk': null,
|
||||
'OrganicMaps-24020611- -.apk': null,
|
||||
'OrganicMaps-2402061-web-release.apk': null,
|
||||
garbage: null,
|
||||
'': null,
|
||||
null: null,
|
||||
};
|
||||
for (const input in tests) {
|
||||
test(input, () => expect(parseApkName(input)).toEqual(tests[input]));
|
||||
}
|
||||
});
|
||||
|
||||
describe('parseAppVersion', () => {
|
||||
const tests: { [key: string]: object | null } = {
|
||||
// Older iOS releases without donate menu
|
||||
|
@ -27,8 +43,8 @@ describe('parseAppVersion', () => {
|
|||
// There were no such versions in production.
|
||||
'2022.08.01-1': null,
|
||||
'2022.08.01-1-Google': { code: 220801, build: 1, flavor: 'google' },
|
||||
// -debug is ignored
|
||||
'2022.08.01-1-Google-debug': { code: 220801, build: 1, flavor: 'google' },
|
||||
'2022.08.01-1-Google-debug': { code: 220801, build: 1, flavor: 'google', type: 'debug' },
|
||||
'2022.08.01-1-Google-beta': { code: 220801, build: 1, flavor: 'google', type: 'beta' },
|
||||
// TODO: Fix regexp. Not it should not happen in production.
|
||||
//'2022.08.01-1-fd-debug': { code: 220801, build: 1, flavor: 'fd' },
|
||||
'2022.1.1-0': null,
|
||||
|
|
Loading…
Add table
Reference in a new issue