From 55926c5fb3f14204a68bbd37e24de7e0b4086990 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Vladovi=C4=87?= Date: Wed, 20 May 2020 17:49:46 +0200 Subject: [PATCH] badge: add winget support (#392) * badge: add winget support * refactor: introduce github rest/graphql client lib --- api/github.ts | 34 +--------- api/winget.ts | 150 +++++++++++++++++++++++++++++++++++++++++++++ libs/badge-list.ts | 1 + libs/github.ts | 33 ++++++++++ 4 files changed, 186 insertions(+), 32 deletions(-) create mode 100644 api/winget.ts create mode 100644 libs/github.ts diff --git a/api/github.ts b/api/github.ts index ded5162..e391c32 100644 --- a/api/github.ts +++ b/api/github.ts @@ -2,8 +2,9 @@ import cheerio from 'cheerio' import distanceToNow from 'date-fns/formatDistanceToNow' import got from '../libs/got' +import { restGithub, queryGithub } from '../libs/github' +import { createBadgenHandler, PathArgs } from '../libs/create-badgen-handler' import { version, millify, coverageColor } from '../libs/utils' -import { createBadgenHandler, BadgenError, PathArgs } from '../libs/create-badgen-handler' export default createBadgenHandler({ title: 'GitHub', @@ -68,37 +69,6 @@ export default createBadgenHandler({ } }) -const pickGithubToken = () => { - const { GH_TOKENS } = process.env - if (!GH_TOKENS) { - throw new BadgenError({ - status: 'token required' - }) - } - - const tokens = GH_TOKENS.split(',') - return tokens[Math.floor(Math.random() * tokens.length)] -} - -// request github api v3 (rest) -const restGithub = (path, preview = 'hellcat') => got.get(`https://api.github.com/${path}`, { - headers: { - Authorization: `token ${pickGithubToken()}`, - Accept: `application/vnd.github.${preview}-preview+json` - } -}).json() - -// request github api v4 (graphql) -const queryGithub = query => { - return got.post('https://api.github.com/graphql', { - json: { query }, - headers: { - Authorization: `token ${pickGithubToken()}`, - Accept: 'application/vnd.github.hawkgirl-preview+json' - } - }).json() -} - // https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref const statesColor = { pending: 'orange', diff --git a/api/winget.ts b/api/winget.ts new file mode 100644 index 0000000..9295335 --- /dev/null +++ b/api/winget.ts @@ -0,0 +1,150 @@ +import got from '../libs/got' +import { restGithub } from '../libs/github' +import { parseDocument } from 'yaml' +import { basename, extname } from 'path' +import { version, versionColor } from '../libs/utils' +import { createBadgenHandler, PathArgs } from '../libs/create-badgen-handler' + +const last = (arr: T[]): T => arr[arr.length - 1] + +interface Part { + number: number + other: string +} + +class Version { + _source: string + _parts: Part[] + + constructor (input: string) { + this._source = input + + const parts = input.split('.').map(segment => { + return new Version.Part(segment) + }) + + while (parts.length) { + const part = last(parts) + if (part.number || part.other) break + parts.pop() + } + + this._parts = parts + } + + get parts() { + return this._parts + } + + toString() { + return this._source + } + + static comparator(versionA: Version, versionB: Version): number { + let i = 0 + while (i < versionA.parts.length) { + if (i >= versionB.parts.length) break + + const partA = versionA.parts[i] + const partB = versionB.parts[i] + const result = Version.Part.comparator(partA, partB) + if (result) return result + + i += 1 + } + + if (versionA.parts.length < versionB.parts.length) return -1 + if (versionA.parts.length > versionB.parts.length) return 1 + return 0 + } + + private static Part = class implements Part { + _source: string + _number: number + _other: string + + constructor(input: string) { + this._source = input + + const [num, rest] = input.split(/([^\d]+)/) + this._number = parseInt(num, 10) || 0 + this._other = rest + } + + get number() { + return this._number + } + + get other() { + return this._other + } + + toString() { + return this._source + } + + static comparator(partA: Part, partB: Part): number { + if (partA.number < partB.number) return -1 + if (partA.number > partB.number) return 1 + if (partA.other < partB.other) return -1 + if (partA.other > partB.other) return 1 + return 0 + } + } +} + +export default createBadgenHandler({ + title: 'winget', + examples: { + '/winget/v/GitHub.cli': 'version', + '/winget/v/Balena.Etcher': 'version', + '/winget/license/Arduino.Arduino': 'license' + }, + handlers: { + '/winget/:topic/:appId': handler + } +}) + +async function handler ({ topic, appId }: PathArgs) { + switch (topic) { + case 'v': { + const versions = await fetchVersions(appId) + const ver = last(versions).toString() + + return { + subject: 'winget', + status: version(ver), + color: versionColor(ver) + } + } + case 'license': { + const yaml = await fetchManifest(appId) + const manifest = parseDocument(yaml) + const license = manifest.get('License') + + return { + subject: 'license', + status: license || 'unknown', + color: 'blue' + } + } + } +} + +async function fetchManifest(appId: string) { + const versions = await fetchVersions(appId) + const version = last(versions) + const path = [...appId.split('.'), `${version}.yaml`].join('/') + return got(`https://github.com/microsoft/winget-pkgs/raw/master/manifests/${path}`).text() +} + +async function fetchVersions(appId: string): Promise { + const path = appId.replace(/\./, '/') + const files = await restGithub(`repos/microsoft/winget-pkgs/contents/manifests/${path}`) + const versions = files.map(file => { + const name = basename(file.name, extname(file.name)) + return new Version(name) + }) + versions.sort(Version.comparator) + return versions +} diff --git a/libs/badge-list.ts b/libs/badge-list.ts index 89701f9..b370f05 100644 --- a/libs/badge-list.ts +++ b/libs/badge-list.ts @@ -29,6 +29,7 @@ export const liveBadgeList = [ 'haxelib', 'opam', 'scoop', + 'winget', 'f-droid', 'pub', // CI diff --git a/libs/github.ts b/libs/github.ts new file mode 100644 index 0000000..b34ed7a --- /dev/null +++ b/libs/github.ts @@ -0,0 +1,33 @@ +import got from './got' +import { BadgenError } from './create-badgen-handler' + +const rand = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)] + +// request github api v3 (rest) +export function restGithub(path: string, preview = 'hellcat') { + const headers = { + authorization: `token ${pickGithubToken()}`, + accept: `application/vnd.github.${preview}-preview+json` + } + const prefixUrl = 'https://api.github.com/' + return got.get(path, { prefixUrl, headers }).json() +} + +// request github api v4 (graphql) +export function queryGithub(query) { + const headers = { + authorization: `token ${pickGithubToken()}`, + accept: 'application/vnd.github.hawkgirl-preview+json' + } + const json = { query } + return got.post('https://api.github.com/graphql', { json, headers }).json() +} + +function pickGithubToken() { + const { GH_TOKENS } = process.env + if (!GH_TOKENS) { + throw new BadgenError({ status: 'token required' }) + } + const tokens = GH_TOKENS.split(',').map(segment => segment.trim()) + return rand(tokens) +}