Use a docker image to support puppeteer (#7)

This commit is contained in:
Simon Lecoq
2020-10-26 13:22:51 +01:00
committed by GitHub
parent 198fd7db67
commit 8baedc87a7
11 changed files with 94595 additions and 47 deletions

1
.dockerignore Normal file
View File

@@ -0,0 +1 @@
node_modules

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# Based on https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker
# Base image
FROM node:15-slim
# Install latest chrome dev package and fonts to support major charsets
RUN apt-get update \
&& apt-get install -y wget gnupg ca-certificates libgconf-2-4 \
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Uncomment to skip the chromium download when installing puppeteer
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV PUPPETEER_BROWSER_PATH "google-chrome-stable"
# Copy action
COPY action/dist/index.js /index.js
RUN chmod +x /index.js
# Execute action
ENTRYPOINT node /index.js

View File

@@ -6,8 +6,8 @@ branding:
icon: user-check icon: user-check
color: gray-dark color: gray-dark
runs: runs:
using: node12 using: docker
main: action/dist/index.js image: Dockerfile
# Inputs # Inputs
inputs: inputs:
@@ -125,7 +125,8 @@ inputs:
default: no default: no
# Music provider # Music provider
# This is required when you enable the music plugin # This is required in "recent" mode
# This is optional in "playlist" mode, in this case it will be deduced from "plugin_music_playlist" url
# Supported values are : # Supported values are :
# - "apple" for Apple Music # - "apple" for Apple Music
# - "spotify" for Spotify # - "spotify" for Spotify
@@ -147,11 +148,11 @@ inputs:
# - "recent" : display recently played tracks # - "recent" : display recently played tracks
plugin_music_mode: plugin_music_mode:
description: Use "recent" to display recently played music and "playlist" to display tracks randomly from a given playlist (*required if music plugin is enabled) description: Use "recent" to display recently played music and "playlist" to display tracks randomly from a given playlist (*required if music plugin is enabled)
default: "" default: "recent"
# Music playlist # Music playlist
# Required when using "plugin_music_mode" as "playlist"
# The embed playlist url (the one used for music player iframe) # The embed playlist url (the one used for music player iframe)
# Will default mode to "playlist" when set
plugin_music_playlist: plugin_music_playlist:
description: Embed playlist url description: Embed playlist url
default: "" default: ""

94473
action/dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -35,7 +35,11 @@
"base.repositories":"Repositories metrics", "base.repositories":"Repositories metrics",
"base.metadata":"Metadata", "base.metadata":"Metadata",
}, },
options:{}, options:{
"habits.from":100,
"music.playlist":"",
"music.mode":"playlist",
},
}, },
templates:{ templates:{
list:templates, list:templates,
@@ -66,6 +70,10 @@
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]]) .flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value) .filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
.map(([key, value]) => `${key}=${+value}`) .map(([key, value]) => `${key}=${+value}`)
//Plugins options
const options = Object.entries(this.plugins.options)
.filter(([key, value]) => `${value}`.length)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
//Template //Template
const template = (this.templates.selected !== templates[0]) ? [`template=${this.templates.selected}`] : [] const template = (this.templates.selected !== templates[0]) ? [`template=${this.templates.selected}`] : []
//Generated url //Generated url

View File

@@ -21,7 +21,7 @@
<section class="steps"> <section class="steps">
<div class="step"> <div class="step">
<h2>1. Enter your GitHub username</h2> <h2>1. Enter your GitHub username</h2>
<input type="text" v-model="user" maxlength="39" placeholder="GitHub username" :disabled="generated.pending"> <input type="text" name="user" v-model="user" maxlength="39" placeholder="GitHub username" :disabled="generated.pending">
</div> </div>
<div class="step"> <div class="step">
<h2>2. Select a template</h2> <h2>2. Select a template</h2>
@@ -48,18 +48,20 @@
{{ plugins.descriptions[plugin] || plugin }} {{ plugins.descriptions[plugin] || plugin }}
</label> </label>
</div> </div>
<template v-if="(plugins.enabled.habits)||(plugins.enabled.music)">
<h3>2.3 Configure additional plugins</h3>
<div class="options">
<label v-if="(plugins.enabled.music)&&(plugins.options['music.mode'] === 'playlist')">
Playlist embed link
<input type="text" v-model="plugins.options['music.playlist']" placeholder="https://embed.music.apple.com/en/playlist/">
</label>
<label v-if="plugins.enabled.habits">
Number of events for habits
<input type="number" v-model="plugins.options['habits.from']" min="1" max="100">
</label>
</div>
</template>
</template> </template>
<div class="palette">
Generated metrics use transparency and colors which can be read on both light and dark modes, so everyone can see your stats whatever their preferred color scheme !
<div class="palettes">
<label>
<input type="radio" v-model="palette" value="light"> ☀️ Light mode
</label>
<label>
<input type="radio" v-model="palette" value="dark"> 🌙 Night mode
</label>
</div>
</div>
</div> </div>
<div class="step"> <div class="step">
<h2>3. Generate your metrics</h2> <h2>3. Generate your metrics</h2>
@@ -78,6 +80,17 @@
<template v-if="user"> <template v-if="user">
<button @click="generate" :disabled="generated.pending">{{ generated.pending ? "Working on it :)" : "Generate your metrics !" }}</button> <button @click="generate" :disabled="generated.pending">{{ generated.pending ? "Working on it :)" : "Generate your metrics !" }}</button>
</template> </template>
<div class="palette">
Generated metrics use transparency and colors which can be read on both light and dark modes, so everyone can see your stats whatever their preferred color scheme !
<div class="palettes">
<label>
<input type="radio" v-model="palette" value="light"> ☀️ Light mode
</label>
<label>
<input type="radio" v-model="palette" value="dark"> 🌙 Night mode
</label>
</div>
</div>
</div> </div>
<div class="step"> <div class="step">
<h2>4. Embed these metrics on your GitHub profile</h2> <h2>4. Embed these metrics on your GitHub profile</h2>

View File

@@ -56,9 +56,11 @@
input:focus { input:focus {
outline: none; outline: none;
} }
input[name=user] {
font-size: 1.1rem;
}
input[type=text], select, button { input[type=text], select, button {
min-width: 50%; min-width: 50%;
font-size: 1.1rem;
} }
option { option {
text-align: center; text-align: center;
@@ -133,6 +135,10 @@
.plugins label, .palettes label { .plugins label, .palettes label {
margin: 0 1rem; margin: 0 1rem;
} }
.options {
display: flex;
flex-direction: column;
}
/* Code snippets */ /* Code snippets */
.code { .code {
display: flex; display: flex;

View File

@@ -1,5 +1,5 @@
//Setup //Setup
export default function ({login, imports, rest, computed, pending, q}, {enabled = false, from:_from = 100} = {}) { export default function ({login, imports, rest, computed, pending, q}, {enabled = false, from:defaults = 100} = {}) {
//Check if plugin is enabled and requirements are met //Check if plugin is enabled and requirements are met
if (!enabled) if (!enabled)
return computed.plugins.habits = null return computed.plugins.habits = null
@@ -8,9 +8,11 @@
console.debug(`metrics/compute/${login}/plugins > habits`) console.debug(`metrics/compute/${login}/plugins > habits`)
//Parameters override //Parameters override
let {"habits.from":from = defaults.from ?? 100} = q
//Events //Events
const from = Math.max(1, Math.min(100, "habits.from" in q ? Number(q["habits.from"])||0 : _from)) from = Math.max(1, Math.min(100, Number(from)))
console.debug(`metrics/compute/${login}/plugins > habits > events = ${from}`) //Debug
console.debug(`metrics/compute/${login}/plugins > habits > ${JSON.stringify({from})}`)
//Plugin execution //Plugin execution
pending.push(new Promise(async solve => { pending.push(new Promise(async solve => {
@@ -27,7 +29,7 @@
//Compute commit hours //Compute commit hours
const hours = commits.map(({created_at}) => (new Date(created_at)).getHours()) const hours = commits.map(({created_at}) => (new Date(created_at)).getHours())
for (const hour of hours) for (const hour of hours)
habits.commits.hours[hour] = (habits.commits.hours[hour] || 0) + 1 habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
//Compute hour with most commits //Compute hour with most commits
habits.commits.hour = hours.length ? Object.entries(habits.commits.hours).sort(([an, a], [bn, b]) => b - a).map(([hour, occurence]) => hour)[0] : NaN habits.commits.hour = hours.length ? Object.entries(habits.commits.hours).sort(([an, a], [bn, b]) => b - a).map(([hour, occurence]) => hour)[0] : NaN
} }
@@ -42,7 +44,7 @@
edited edited
.filter(({status}) => status === "fulfilled") .filter(({status}) => status === "fulfilled")
.map(({value}) => value) .map(({value}) => value)
.flatMap(files => files.flatMap(file => (file.patch||"").match(/(?<=^[+])((?:\t)|(?: )) /gm)||[])) .flatMap(files => files.flatMap(file => (file.patch ?? "").match(/(?<=^[+])((?:\t)|(?: )) /gm) ?? []))
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++) .forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
//Compute indent style //Compute indent style
habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : "" habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : ""

View File

@@ -1,7 +1,13 @@
//Supported providers //Supported providers
const providers = { const providers = {
apple:"Apple Music", apple:{
spotify:"Spotify", name:"Apple Music",
embed:/^https:..embed.music.apple.com.\w+.playlist/,
},
spotify:{
name:"Spotify",
embed:/^https:..open.spotify.com.embed.playlist/,
},
} }
//Supported modes //Supported modes
@@ -18,32 +24,39 @@
if (!q.music) if (!q.music)
return computed.plugins.music = null return computed.plugins.music = null
console.debug(`metrics/compute/${login}/plugins > music`) console.debug(`metrics/compute/${login}/plugins > music`)
const raw = {
get provider() { return providers[provider]?.name ?? "" },
get mode() { return modes[mode] ?? "Unconfigured music plugin"},
}
//Parameters override and checks //Parameters override and checks
let {"music.provider":provider = null, "music.mode":mode = null, "music.playlist":playlist = null, "music.limit":limit = 4} = q
//Auto-guess parameters
if ((playlist)&&(mode === null))
mode = "playlist"
if ((playlist)&&(provider === null))
for (const [name, {embed}] of Object.entries(providers))
if (embed.test(playlist))
provider = name
if (!mode)
mode = "recent"
//Provider //Provider
const provider = q["music.provider"]||""
if (!(provider in providers)) if (!(provider in providers))
return computed.plugins.music = {error:provider ? `Unsupported provider "${provider}"` : `Missing provider`, mode:"Unconfigured music plugin"} return computed.plugins.music = {...raw, error:provider ? `Unsupported provider "${provider}"` : `Missing provider`}
console.debug(`metrics/compute/${login}/plugins > music > provider "${provider}"`)
//Mode //Mode
const mode = q["music.mode"]||""
if (!(mode in modes)) if (!(mode in modes))
return computed.plugins.music = {error:mode ? `Unsupported mode "${mode}"` : `Missing mode`, provider:providers[provider], mode:"Unconfigured music plugin"} return computed.plugins.music = {...raw, error:`Unsupported mode "${mode}"`}
console.debug(`metrics/compute/${login}/plugins > music > mode "${mode}"`)
//Playlist mode //Playlist mode
const playlist = q["music.playlist"]||""
if (mode === "playlist") { if (mode === "playlist") {
if (!playlist) if (!playlist)
return computed.plugins.music = {error:`Missing playlist url`, provider:providers[provider], mode:modes[mode]} return computed.plugins.music = {...raw, error:`Missing playlist url`}
if ((provider === "spotify")&&(!/^https:..open.spotify.com.embed.playlist/.test(playlist))) if (!providers[provider].embed.test(playlist))
return computed.plugins.music = {error:`Unsupported playlist url format`, provider:providers[provider], mode:modes[mode]} return computed.plugins.music = {...raw, error:`Unsupported playlist url format`}
if ((provider === "apple")&&(!/^https:..embed.music.apple.com.\w+.playlist/.test(playlist)))
return computed.plugins.music = {error:`Unsupported playlist url format`, provider:providers[provider], mode:modes[mode]}
console.debug(`metrics/compute/${login}/plugins > music > playlist = ${playlist}`)
} }
//Limit //Limit
const limit = Math.max(1, Math.min(100, "music.limit" in q ? Number(q["music.limit"])||0 : 4)) limit = Math.max(1, Math.min(100, Number(limit)))
console.debug(`metrics/compute/${login}/plugins > music > limit = ${limit}`) //Debug
console.debug(`metrics/compute/${login}/plugins > habits > ${JSON.stringify({provider, mode, playlist, limit})}`)
//Plugin execution //Plugin execution
pending.push(new Promise(async solve => { pending.push(new Promise(async solve => {
@@ -57,7 +70,8 @@
case "playlist":{ case "playlist":{
//Start puppeteer and navigate to playlist //Start puppeteer and navigate to playlist
console.debug(`metrics/compute/${login}/plugins > music > starting browser`) console.debug(`metrics/compute/${login}/plugins > music > starting browser`)
const browser = await imports.puppeteer.launch() const browser = await imports.puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]})
console.debug(`metrics/compute/${login}/plugins > music > loaded ${await browser.version()}`)
const page = await browser.newPage() const page = await browser.newPage()
console.debug(`metrics/compute/${login}/plugins > music > loading page`) console.debug(`metrics/compute/${login}/plugins > music > loading page`)
await page.goto(playlist) await page.goto(playlist)
@@ -174,7 +188,7 @@
} }
//Save results //Save results
console.debug(`metrics/compute/${login}/plugins > music > success`) console.debug(`metrics/compute/${login}/plugins > music > success`)
computed.plugins.music = {provider:providers[provider], mode:modes[mode], tracks} computed.plugins.music = {...raw, tracks}
solve() solve()
return return
} }
@@ -184,12 +198,12 @@
catch (error) { catch (error) {
//Plugin error //Plugin error
if (error.status) { if (error.status) {
computed.plugins.music = {provider:providers[provider], mode:modes[mode], error:error.status} computed.plugins.music = {...raw, error:error.status}
console.debug(`metrics/compute/${login}/plugins > music > error > ${error.status}`) console.debug(`metrics/compute/${login}/plugins > music > error > ${error.status}`)
return solve() return solve()
} }
//Generic error //Generic error
computed.plugins.music = {provider:providers[provider], mode:modes[mode], error:`An error occured`} computed.plugins.music = {...raw, error:`An error occured`}
console.debug(`metrics/compute/${login}/plugins > music > error`) console.debug(`metrics/compute/${login}/plugins > music > error`)
console.debug(error) console.debug(error)
solve() solve()

View File

@@ -9,6 +9,7 @@
+ (!!computed.plugins.habits)*68 + (!!computed.plugins.habits)*68
+ (!!computed.plugins.languages)*96 + (!!computed.plugins.languages)*96
+ (!!computed.plugins.music)*64 + (computed.plugins.music ? computed.plugins.music.tracks ? 14+Math.max(0, computed.plugins.music.tracks.length-1)*36 : 0 : 0) + (!!computed.plugins.music)*64 + (computed.plugins.music ? computed.plugins.music.tracks ? 14+Math.max(0, computed.plugins.music.tracks.length-1)*36 : 0 : 0)
+ Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+((!!computed.plugins.habits))+(!!computed.plugins.pagespeed)+(!!computed.plugins.languages)+(!!computed.plugins.music))-1))*4
%>"> %>">
<defs><style><%= fonts %></style></defs> <defs><style><%= fonts %></style></defs>

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -39,7 +39,12 @@
{traffic:1}, {traffic:1},
{selfskip:1}, {selfskip:1},
{pagespeed:1}, {pagespeed:1},
{followup:1, languages:1, habits:1, "habits.events":1, lines:1, traffic:1, selfskip:1, pagespeed:1}, {music:1},
{music:1, "music.mode":"recent"},
{music:1, "music.mode":"recent", "music.provider":"apple"},
{music:1, "music.mode":"recent", "music.provider":"spotify"},
{music:1, "music.mode":"playlist"},
{followup:1, languages:1, habits:1, "habits.events":1, lines:1, traffic:1, selfskip:1, pagespeed:1, music:1},
]) { ]) {
await test.metrics({graphql, rest, q:{template, repositories:1, ...q}}) await test.metrics({graphql, rest, q:{template, repositories:1, ...q}})
} }
@@ -58,6 +63,7 @@
selfskip:{enabled:true}, selfskip:{enabled:true},
languages:{enabled:true}, languages:{enabled:true},
followup:{enabled:true}, followup:{enabled:true},
musit:{enabled:true},
} }
//Compute render //Compute render