badge: add winget support (#392)

* badge: add winget support

* refactor: introduce github rest/graphql client lib
pull/397/head
Dario Vladović 2020-05-20 17:49:46 +02:00 zatwierdzone przez GitHub
rodzic d155bd63a6
commit 55926c5fb3
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
4 zmienionych plików z 186 dodań i 32 usunięć

Wyświetl plik

@ -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<any>()
// 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<any>()
}
// https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
const statesColor = {
pending: 'orange',

150
api/winget.ts 100644
Wyświetl plik

@ -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 = <T>(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<v|license>/: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<Version[]> {
const path = appId.replace(/\./, '/')
const files = await restGithub<any[]>(`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
}

Wyświetl plik

@ -29,6 +29,7 @@ export const liveBadgeList = [
'haxelib',
'opam',
'scoop',
'winget',
'f-droid',
'pub',
// CI

33
libs/github.ts 100644
Wyświetl plik

@ -0,0 +1,33 @@
import got from './got'
import { BadgenError } from './create-badgen-handler'
const rand = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]
// request github api v3 (rest)
export function restGithub<T = any>(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<T>()
}
// request github api v4 (graphql)
export function queryGithub<T = any>(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<T>()
}
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)
}