Mocked tests and remove ncc compilation (#32)

* Mocked tests and remove ncc compilation
* Update workflow.yml
* Update Dockerfile
This commit is contained in:
Simon Lecoq
2020-12-30 00:49:01 +01:00
committed by GitHub
parent 03bede5482
commit b4d0e85148
22 changed files with 6534 additions and 1263 deletions

20
.github/workflows/image.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Publish Docker image
on:
release:
types: [ published ]
jobs:
publish:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Publish image to GitHub Packages
uses: docker/build-push-action@v1
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
registry: docker.pkg.github.com
repository: lowlighter/metrics/metrics
tag_with_ref: true

View File

@@ -8,7 +8,8 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v3 - name: Flag stale issues and pull requests
uses: actions/stale@v3
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: This issue has been open 30 days without activity. It will be closed in 5 days if it remains inactive. stale-issue-message: This issue has been open 30 days without activity. It will be closed in 5 days if it remains inactive.

View File

@@ -1,4 +1,4 @@
name: Build name: Build, tests and analyze
on: on:
push: push:
@@ -10,237 +10,23 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup - name: Build lowlighter/metrics:${{ github.head_ref || github.base_ref }}
uses: actions/setup-node@v1 run: docker build -t lowlighter/metrics:${{ github.head_ref || github.base_ref }} .
with: - name: Run tests
node-version: 15.x run: docker run --workdir=/metrics --entrypoint="" lowlighter/metrics:${{ github.head_ref || github.base_ref }} npm test
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Test
run: npm test
analyze: analyze:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: ["test-master"] needs: [ build ]
steps: steps:
- name: Checkout - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Setup - name: Setup CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@v1
with:
languages: javascript
config-file: ./.github/config/codeql.yml
- name: Analyze
uses: github/codeql-action/analyze@v1
# Tests cases below are auto generated through `npm run build`
# Edit utils/workflow.yml instead if you need to update workflow
test-master:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
template: ["classic","terminal"]
steps:
- name: ${{ matrix.template }} > Base
uses: lowlighter/metrics@master
with: with:
token: ${{ secrets.METRICS_TOKEN }} languages: javascript
dryrun: yes config-file: ./.github/config/codeql.yml
repositories: 0 - name: Analyze code
template: ${{ matrix.template }} uses: github/codeql-action/analyze@v1
base: header, activity, community, repositories, metadata
plugins_errors_fatal: yes
- name: ${{ matrix.template }} > Plugin > PageSpeed
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_pagespeed: yes
plugin_pagespeed_token: ${{ secrets.PAGESPEED_TOKEN }}
plugin_pagespeed_detailed: yes
plugin_pagespeed_screenshot: yes
- name: ${{ matrix.template }} > Plugin > Music (playlist - apple)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_music: yes
plugin_music_playlist: ${{ secrets.MUSIC_PLAYLIST_APPLE }}
- name: ${{ matrix.template }} > Plugin > Music (playlist - spotify)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_music: yes
plugin_music_playlist: ${{ secrets.MUSIC_PLAYLIST_SPOTIFY }}
- name: ${{ matrix.template }} > Plugin > Music (recent - spotify)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_music: yes
plugin_music_provider: spotify
plugin_music_token: ${{ secrets.SPOTIFY_TOKENS }}
- name: ${{ matrix.template }} > Plugin > Posts (dev.to)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_posts: yes
plugin_posts_source: dev.to
- name: ${{ matrix.template }} > Plugin > Isocalendar
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_isocalendar: yes
plugin_isocalendar_duration: full-year
- name: ${{ matrix.template }} > Plugin > Habits
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_habits: yes
plugin_habits_from: 5
plugin_habits_charts: yes
- name: ${{ matrix.template }} > Plugin > Languages
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_languages: yes
- name: ${{ matrix.template }} > Plugin > Follow-up
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_followup: yes
- name: ${{ matrix.template }} > Plugin > Lines and Traffic
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_lines: yes
plugin_traffic: yes
- name: ${{ matrix.template }} > Plugin > Gists
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_gists: yes
- name: ${{ matrix.template }} > Plugin > Topics (starred)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_topics: yes
plugin_topics_mode: starred
plugin_topics_sort: random
- name: ${{ matrix.template }} > Plugin > Topics (mastered)
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_topics: yes
plugin_topics_mode: mastered
plugin_topics_sort: stars
- name: ${{ matrix.template }} > Plugin > Projects
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_projects: yes
plugin_projects_repositories: lowlighter/metrics/projects/1
plugin_projects_limit: 2
- name: ${{ matrix.template }} > Plugin > Tweets
uses: lowlighter/metrics@master
with:
token: ${{ secrets.METRICS_TOKEN }}
dryrun: yes
repositories: 0
template: ${{ matrix.template }}
base: ""
plugins_errors_fatal: yes
plugin_tweets: yes
plugin_tweets_limit: 2
plugin_tweets_token: ${{ secrets.TWITTER_TOKEN }}

View File

@@ -117,7 +117,7 @@ It should be avoided when possible as it increases drastically the size of gener
* To optimize generated SVG * To optimize generated SVG
* [axios/axios](https://github.com/axios/axios) * [axios/axios](https://github.com/axios/axios)
* To make HTTP/S requests * To make HTTP/S requests
* [actions/toolkit](https://github.com/actions/toolkit/tree/master) and [vercel/ncc](https://github.com/vercel/ncc) * [actions/toolkit](https://github.com/actions/toolkit/tree/master)
* To build the GitHub Action * To build the GitHub Action
* [vuejs/vue](https://github.com/vuejs/vue) and [egoist/vue-prism-component](https://github.com/egoist/vue-prism-component) + [PrismJS/prism](https://github.com/PrismJS/prism) * [vuejs/vue](https://github.com/vuejs/vue) and [egoist/vue-prism-component](https://github.com/egoist/vue-prism-component) + [PrismJS/prism](https://github.com/PrismJS/prism)
* To display server application * To display server application
@@ -127,5 +127,5 @@ It should be avoided when possible as it increases drastically the size of gener
* To test and verify SVG validity * To test and verify SVG validity
* [Marak/colors.js](https://github.com/Marak/colors.js) * [Marak/colors.js](https://github.com/Marak/colors.js)
* To print colors in console * To print colors in console
* [babel/minify](https://github.com/babel/minify) * [facebook/jest](https://github.com/facebook/jest) and [nodeca/js-yaml](https://github.com/nodeca/js-yaml)
* To minify code * For unit testing

View File

@@ -1,11 +1,11 @@
# Base image # Base image
FROM node:15-buster-slim FROM node:15-buster-slim
# Copy GitHub action # Copy repository
COPY action/dist/index.js /index.js COPY . /metrics
# Setup # Setup
RUN chmod +x /index.js \ RUN chmod +x /metrics/action/index.mjs \
# Install latest chrome dev package, fonts to support major charsets and skip chromium download on puppeteer install # Install latest chrome dev package, fonts to support major charsets and skip chromium download on puppeteer install
# Based on https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker # Based on https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker
&& apt-get update \ && apt-get update \
@@ -21,9 +21,15 @@ RUN chmod +x /index.js \
&& apt-get update \ && apt-get update \
&& apt-get install -y ruby-full \ && apt-get install -y ruby-full \
&& apt-get install -y git g++ cmake pkg-config libicu-dev zlib1g-dev libcurl4-openssl-dev libssl-dev ruby-dev \ && apt-get install -y git g++ cmake pkg-config libicu-dev zlib1g-dev libcurl4-openssl-dev libssl-dev ruby-dev \
&& gem install github-linguist && gem install github-linguist \
# Install python for node-gyp
&& apt-get update \
&& apt-get install -y python3 \
# Install node modules
&& cd /metrics \
&& npm ci
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV PUPPETEER_BROWSER_PATH "google-chrome-stable" ENV PUPPETEER_BROWSER_PATH "google-chrome-stable"
# Execute GitHub action # Execute GitHub action
ENTRYPOINT node /index.js ENTRYPOINT node /metrics/action/index.mjs

View File

@@ -324,18 +324,18 @@ inputs:
description: Die on plugins errors description: Die on plugins errors
default: no default: no
# Verify SVG after generation
# Test whether SVG can be correctly parsed (used for testing)
verify:
description: Verify SVG after genaration
default: no
# Enable debug mode # Enable debug mode
# Ensure you correctly put all sensitive informations in your repository secrets before ! # Ensure you correctly put all sensitive informations in your repository secrets before !
debug: debug:
description: Enable debug logs description: Enable debug logs
default: no default: no
# Verify SVG after generation
# Test whether SVG can be correctly parsed (used for testing)
verify:
description: Verify SVG after generation
default: no
# Debug flags (used for testing) # Debug flags (used for testing)
debug_flags: debug_flags:
description: Debug flags description: Debug flags
@@ -346,3 +346,9 @@ inputs:
dryrun: dryrun:
description: Enable dry-run description: Enable dry-run
default: no default: no
# Use mocked data
# Bypass external APIs which requires a token and sent mocked data (used for testing)
use_mocked_data:
description: Use mocked data instead of real APIs
default: no

123
action/dist/index.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,12 @@
//Imports //Imports
import * as _setup from "./../src/setup.mjs" import setup from "./../src/setup.mjs"
import * as _metrics from "./../src/metrics.mjs" import metrics from "./../src/metrics.mjs"
import * as _octokit from "@octokit/graphql" import octokit from "@octokit/graphql"
import * as _core from "@actions/core" import core from "@actions/core"
import * as _github from "@actions/github" import github from "@actions/github"
import mocks from "./../src/mocks.mjs"
;((async function () { ;((async function () {
//Hack because ES modules are not correctly transpiled with ncc
const [core, github, octokit, setup, metrics] = [_core, _github, _octokit, _setup, _metrics].map(m => (m && m.default) ? m.default : m)
//Yaml boolean converter //Yaml boolean converter
const bool = (value, defaulted = false) => typeof value === "string" ? /^(?:[Tt]rue|[Oo]n|[Yy]es)$/.test(value) : defaulted const bool = (value, defaulted = false) => typeof value === "string" ? /^(?:[Tt]rue|[Oo]n|[Yy]es)$/.test(value) : defaulted
//Debug message buffer //Debug message buffer
@@ -16,12 +15,11 @@
try { try {
//Initialization //Initialization
console.log(`GitHub metrics`) console.log(`GitHub metrics`)
console.log(`========================================================`) console.log("─".repeat(64))
console.log(`Version | <#version>`)
process.on("unhandledRejection", error => { throw error }) process.on("unhandledRejection", error => { throw error })
//Skip process if needed //Skip process if needed
if ((github.context.eventName === "push")&&(github.context.payload)&&(github.context.payload.head_commit)) { if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) {
if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) { if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
console.log(`Skipped because [Skip GitHub Action] is in commit message`) console.log(`Skipped because [Skip GitHub Action] is in commit message`)
process.exit(0) process.exit(0)
@@ -30,30 +28,47 @@
//Load configuration //Load configuration
const conf = await setup({log:false}) const conf = await setup({log:false})
console.log(`Configuration | loaded`) console.log(`Configuration loaded`)
console.log(`Version │ ${conf.package.version}`)
//Debug mode
const debug = bool(core.getInput("debug"))
if (!debug)
console.debug = message => debugged.push(message)
console.log(`Debug mode │ ${debug}`)
const dflags = (core.getInput("debug_flags") || "").split(" ").filter(flag => flag)
console.log(`Debug flags │ ${dflags.join(" ") || "(none)"}`)
//Load svg template, style, fonts and query //Load svg template, style, fonts and query
const template = core.getInput("template") || "classic" const template = core.getInput("template") || "classic"
console.log(`Template to use | ${template}`) console.log(`Template to use ${template}`)
//Token for data gathering //Token for data gathering
const token = core.getInput("token") || "" const token = core.getInput("token") || ""
console.log(`Github token | ${token ? "provided" : "missing"}`) console.log(`Github token ${/^MOCKED/.test(token) ? "(MOCKED)" : token ? "provided" : "missing"}`)
if (!token) if (!token)
throw new Error("You must provide a valid GitHub token to gather your metrics") throw new Error("You must provide a valid GitHub token to gather your metrics")
const graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}}) const api = {}
console.log(`Github GraphQL API | ok`) api.graphql = octokit.graphql.defaults({headers:{authorization: `token ${token}`}})
const rest = github.getOctokit(token) console.log(`Github GraphQL API │ ok`)
console.log(`Github REST API | ok`) api.rest = github.getOctokit(token)
console.log(`Github REST API │ ok`)
//Apply mocking if needed
if (bool(core.getInput("use_mocked_data"))) {
Object.assign(api, await mocks(api))
console.log(`Mocked Github API │ ok`)
}
//Extract octokits
const {graphql, rest} = api
//SVG output //SVG output
const filename = core.getInput("filename") || "github-metrics.svg" const filename = core.getInput("filename") || "github-metrics.svg"
console.log(`SVG output file | ${filename}`) console.log(`SVG output file ${filename}`)
//SVG optimization //SVG optimization
const optimize = bool(core.getInput("optimize"), true) const optimize = bool(core.getInput("optimize"), true)
conf.optimize = optimize conf.optimize = optimize
console.log(`SVG optimization | ${optimize}`) console.log(`SVG optimization ${optimize}`)
//GitHub user //GitHub user
let authenticated let authenticated
@@ -64,28 +79,20 @@
authenticated = github.context.repo.owner authenticated = github.context.repo.owner
} }
const user = core.getInput("user") || authenticated const user = core.getInput("user") || authenticated
console.log(`GitHub user | ${user}`) console.log(`GitHub user ${user}`)
//Debug mode
const debug = bool(core.getInput("debug"))
if (!debug)
console.debug = message => debugged.push(message)
console.log(`Debug mode | ${debug}`)
const dflags = (core.getInput("debug_flags") || "").split(" ").filter(flag => flag)
console.log(`Debug flags | ${dflags.join(" ") || "(none)"}`)
//Base elements //Base elements
const base = {} const base = {}
let parts = (core.getInput("base") || "").split(",").map(part => part.trim()) let parts = (core.getInput("base") || "").split(",").map(part => part.trim())
for (const part of conf.settings.plugins.base.parts) for (const part of conf.settings.plugins.base.parts)
base[`base.${part}`] = parts.includes(part) base[`base.${part}`] = parts.includes(part)
console.log(`Base parts | ${parts.join(", ") || "(none)"}`) console.log(`Base parts ${parts.join(", ") || "(none)"}`)
//Config //Config
const config = { const config = {
"config.timezone":core.getInput("config_timezone") || "" "config.timezone":core.getInput("config_timezone") || ""
} }
console.log(`Timezone | ${config["config.timezone"] || "(system default)"}`) console.log(`Timezone ${config["config.timezone"] || "(system default)"}`)
//Additional plugins //Additional plugins
const plugins = { const plugins = {
@@ -104,23 +111,23 @@
tweets:{enabled:bool(core.getInput("plugin_tweets"))}, tweets:{enabled:bool(core.getInput("plugin_tweets"))},
} }
let q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true])) let q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true]))
console.log(`Plugins enabled | ${Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key).join(", ")}`) console.log(`Plugins enabled ${Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key).join(", ")}`)
//Additional plugins options //Additional plugins options
//Pagespeed //Pagespeed
if (plugins.pagespeed.enabled) { if (plugins.pagespeed.enabled) {
plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") || "" plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") || ""
q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`)) q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`))
q[`pagespeed.screenshot`] = bool(core.getInput(`plugin_pagespeed_screenshot`)) q[`pagespeed.screenshot`] = bool(core.getInput(`plugin_pagespeed_screenshot`))
console.log(`Pagespeed token | ${plugins.pagespeed.token ? "provided" : "missing"}`) console.log(`Pagespeed token ${/^MOCKED/.test(plugins.pagespeed.token) ? "(MOCKED)" : plugins.pagespeed.token ? "provided" : "missing"}`)
console.log(`Pagespeed detailed | ${q["pagespeed.detailed"]}`) console.log(`Pagespeed detailed ${q["pagespeed.detailed"]}`)
console.log(`Pagespeed screenshot | ${q["pagespeed.screenshot"]}`) console.log(`Pagespeed screenshot ${q["pagespeed.screenshot"]}`)
} }
//Languages //Languages
if (plugins.languages.enabled) { if (plugins.languages.enabled) {
for (const option of ["ignored", "skipped"]) for (const option of ["ignored", "skipped"])
q[`languages.${option}`] = core.getInput(`plugin_languages_${option}`) || null q[`languages.${option}`] = core.getInput(`plugin_languages_${option}`) || null
console.log(`Languages ignored | ${q["languages.ignored"] || "(none)"}`) console.log(`Languages ignored ${q["languages.ignored"] || "(none)"}`)
console.log(`Languages skipped repos | ${q["languages.skipped"] || "(none)"}`) console.log(`Languages skipped repos ${q["languages.skipped"] || "(none)"}`)
} }
//Habits //Habits
if (plugins.habits.enabled) { if (plugins.habits.enabled) {
@@ -128,107 +135,107 @@
q[`habits.${option}`] = core.getInput(`plugin_habits_${option}`) || null q[`habits.${option}`] = core.getInput(`plugin_habits_${option}`) || null
q[`habits.facts`] = bool(core.getInput(`plugin_habits_facts`)) q[`habits.facts`] = bool(core.getInput(`plugin_habits_facts`))
q[`habits.charts`] = bool(core.getInput(`plugin_habits_charts`)) q[`habits.charts`] = bool(core.getInput(`plugin_habits_charts`))
console.log(`Habits facts | ${q["habits.facts"]}`) console.log(`Habits facts ${q["habits.facts"]}`)
console.log(`Habits charts | ${q["habits.charts"]}`) console.log(`Habits charts ${q["habits.charts"]}`)
console.log(`Habits events to use | ${q["habits.from"] || "(default)"}`) console.log(`Habits events to use ${q["habits.from"] || "(default)"}`)
console.log(`Habits days to keep | ${q["habits.days"] || "(default)"}`) console.log(`Habits days to keep ${q["habits.days"] || "(default)"}`)
} }
//Music //Music
if (plugins.music.enabled) { if (plugins.music.enabled) {
plugins.music.token = core.getInput("plugin_music_token") || "" plugins.music.token = core.getInput("plugin_music_token") || ""
for (const option of ["provider", "mode", "playlist", "limit"]) for (const option of ["provider", "mode", "playlist", "limit"])
q[`music.${option}`] = core.getInput(`plugin_music_${option}`) || null q[`music.${option}`] = core.getInput(`plugin_music_${option}`) || null
console.log(`Music provider | ${q["music.provider"] || "(none)"}`) console.log(`Music provider ${q["music.provider"] || "(none)"}`)
console.log(`Music plugin mode | ${q["music.mode"] || "(none)"}`) console.log(`Music plugin mode ${q["music.mode"] || "(none)"}`)
console.log(`Music playlist | ${q["music.playlist"] || "(none)"}`) console.log(`Music playlist ${q["music.playlist"] || "(none)"}`)
console.log(`Music tracks limit | ${q["music.limit"] || "(default)"}`) console.log(`Music tracks limit ${q["music.limit"] || "(default)"}`)
console.log(`Music token | ${plugins.music.token ? "provided" : "missing"}`) console.log(`Music token ${/^MOCKED/.test(plugins.music.token) ? "(MOCKED)" : plugins.music.token ? "provided" : "missing"}`)
} }
//Posts //Posts
if (plugins.posts.enabled) { if (plugins.posts.enabled) {
for (const option of ["source", "limit"]) for (const option of ["source", "limit"])
q[`posts.${option}`] = core.getInput(`plugin_posts_${option}`) || null q[`posts.${option}`] = core.getInput(`plugin_posts_${option}`) || null
console.log(`Posts source | ${q["posts.source"] || "(none)"}`) console.log(`Posts source ${q["posts.source"] || "(none)"}`)
console.log(`Posts limit | ${q["posts.limit"] || "(default)"}`) console.log(`Posts limit ${q["posts.limit"] || "(default)"}`)
} }
//Isocalendar //Isocalendar
if (plugins.isocalendar.enabled) { if (plugins.isocalendar.enabled) {
q["isocalendar.duration"] = core.getInput("plugin_isocalendar_duration") || "half-year" q["isocalendar.duration"] = core.getInput("plugin_isocalendar_duration") || "half-year"
console.log(`Isocalendar duration | ${q["isocalendar.duration"]}`) console.log(`Isocalendar duration ${q["isocalendar.duration"]}`)
} }
//Topics //Topics
if (plugins.topics.enabled) { if (plugins.topics.enabled) {
for (const option of ["mode", "sort", "limit"]) for (const option of ["mode", "sort", "limit"])
q[`topics.${option}`] = core.getInput(`plugin_topics_${option}`) || null q[`topics.${option}`] = core.getInput(`plugin_topics_${option}`) || null
console.log(`Topics mode | ${q["topics.mode"] || "(default)"}`) console.log(`Topics mode ${q["topics.mode"] || "(default)"}`)
console.log(`Topics sort mode | ${q["topics.sort"] || "(default)"}`) console.log(`Topics sort mode ${q["topics.sort"] || "(default)"}`)
console.log(`Topics limit | ${q["topics.limit"] || "(default)"}`) console.log(`Topics limit ${q["topics.limit"] || "(default)"}`)
} }
//Projects //Projects
if (plugins.projects.enabled) { if (plugins.projects.enabled) {
for (const option of ["limit", "repositories"]) for (const option of ["limit", "repositories"])
q[`projects.${option}`] = core.getInput(`plugin_projects_${option}`) || null q[`projects.${option}`] = core.getInput(`plugin_projects_${option}`) || null
console.log(`Projects limit | ${q["projects.limit"] || "(default)"}`) console.log(`Projects limit ${q["projects.limit"] || "(default)"}`)
console.log(`Projects repositories | ${q["projects.repositories"] || "(none)"}`) console.log(`Projects repositories ${q["projects.repositories"] || "(none)"}`)
} }
//Tweets //Tweets
if (plugins.tweets.enabled) { if (plugins.tweets.enabled) {
plugins.tweets.token = core.getInput("plugin_tweets_token") || null plugins.tweets.token = core.getInput("plugin_tweets_token") || null
for (const option of ["limit"]) for (const option of ["limit"])
q[`tweets.${option}`] = core.getInput(`plugin_tweets_${option}`) || null q[`tweets.${option}`] = core.getInput(`plugin_tweets_${option}`) || null
console.log(`Twitter token | ${plugins.tweets.token ? "provided" : "missing"}`) console.log(`Twitter token ${/^MOCKED/.test(plugins.tweets.token) ? "(MOCKED)" : plugins.tweets.token ? "provided" : "missing"}`)
console.log(`Tweets limit | ${q["tweets.limit"] || "(default)"}`) console.log(`Tweets limit ${q["tweets.limit"] || "(default)"}`)
} }
//Repositories to use //Repositories to use
const repositories = Number(core.getInput("repositories")) || 100 const repositories = Number(core.getInput("repositories")) || 100
console.log(`Repositories to use | ${repositories}`) console.log(`Repositories to use ${repositories}`)
//Die on plugins errors //Die on plugins errors
const die = bool(core.getInput("plugins_errors_fatal")) const die = bool(core.getInput("plugins_errors_fatal"))
console.log(`Plugin errors | ${die ? "die" : "warn"}`) console.log(`Plugin errors ${die ? "die" : "warn"}`)
//Build query //Build query
const query = JSON.parse(core.getInput("query") || "{}") const query = JSON.parse(core.getInput("query") || "{}")
console.log(`Query additional params | ${JSON.stringify(query)}`) console.log(`Query additional params ${JSON.stringify(query)}`)
q = {...query, ...q, base:false, ...base, ...config, repositories, template} q = {...query, ...q, base:false, ...base, ...config, repositories, template}
//Render metrics //Render metrics
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die}) const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die})
console.log(`Render | complete`) console.log(`Render complete`)
//Verify svg //Verify svg
const verify = bool(core.getInput("verify")) const verify = bool(core.getInput("verify"))
console.log(`Verify SVG | ${verify}`) console.log(`Verify SVG ${verify}`)
if (verify) { if (verify) {
const [libxmljs] = [await import("libxmljs")].map(m => (m && m.default) ? m.default : m) const [libxmljs] = [await import("libxmljs")].map(m => (m && m.default) ? m.default : m)
const parsed = libxmljs.parseXml(rendered) const parsed = libxmljs.parseXml(rendered)
if (parsed.errors.length) if (parsed.errors.length)
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`) throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
console.log(`SVG valid | yes`) console.log(`SVG valid yes`)
} }
//Commit to repository //Commit to repository
const dryrun = bool(core.getInput("dryrun")) const dryrun = bool(core.getInput("dryrun"))
if (dryrun) if (dryrun)
console.log(`Dry-run | complete`) console.log(`Dry-run complete`)
else { else {
//Repository and branch //Repository and branch
const branch = github.context.ref.replace(/^refs[/]heads[/]/, "") const branch = github.context.ref.replace(/^refs[/]heads[/]/, "")
console.log(`Repository | ${github.context.repo.owner}/${github.context.repo.repo}`) console.log(`Repository ${github.context.repo.owner}/${github.context.repo.repo}`)
console.log(`Branch | ${branch}`) console.log(`Branch ${branch}`)
//Committer token //Committer token
const token = core.getInput("committer_token") || core.getInput("token") || "" const token = core.getInput("committer_token") || core.getInput("token") || ""
console.log(`Committer token | ${token ? "provided" : "missing"}`) console.log(`Committer token ${/^MOCKED/.test(token) ? "(MOCKED)" : token ? "provided" : "missing"}`)
if (!token) if (!token)
throw new Error("You must provide a valid GitHub token to commit your metrics") throw new Error("You must provide a valid GitHub token to commit your metrics")
const rest = github.getOctokit(token) const rest = github.getOctokit(token)
console.log(`Committer REST API | ok`) console.log(`Committer REST API ok`)
try { try {
console.log(`Committer | ${(await rest.users.getAuthenticated()).data.login}`) console.log(`Committer ${(await rest.users.getAuthenticated()).data.login}`)
} }
catch { catch {
console.log(`Committer | (github-actions)`) console.log(`Committer (github-actions)`)
} }
//Retrieve previous render SHA to be able to update file content through API //Retrieve previous render SHA to be able to update file content through API
let sha = null let sha = null
@@ -243,14 +250,14 @@
) )
sha = oid sha = oid
} catch (error) { console.debug(error) } } catch (error) { console.debug(error) }
console.log(`Previous render sha | ${sha ?? "(none)"}`) console.log(`Previous render sha ${sha ?? "(none)"}`)
//Update file content through API //Update file content through API
await rest.repos.createOrUpdateFileContents({ await rest.repos.createOrUpdateFileContents({
...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`, ...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`,
content:Buffer.from(rendered).toString("base64"), content:Buffer.from(rendered).toString("base64"),
...(sha ? {sha} : {}) ...(sha ? {sha} : {})
}) })
console.log(`Commit to repo | ok`) console.log(`Commit to repo ok`)
} }
//Success //Success
@@ -262,7 +269,7 @@
catch (error) { catch (error) {
console.error(error) console.error(error)
if (!bool(core.getInput("debug"))) if (!bool(core.getInput("debug")))
for (const log of ["_".repeat(64), "An error occured, logging debug message :", ...debugged]) for (const log of ["".repeat(64), "An error occured, logging debug message :", ...debugged])
console.log(log) console.log(log)
core.setFailed(error.message) core.setFailed(error.message)
process.exit(1) process.exit(1)

5801
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@
"scripts": { "scripts": {
"start": "node index.mjs", "start": "node index.mjs",
"build": "node utils/build.mjs", "build": "node utils/build.mjs",
"test": "node tests/metrics.mjs", "test": "npx jest",
"upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest colors@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest vue-prism-component@latest @vercel/ncc@latest babel-minify@latest libxmljs@latest" "upgrade": "npm install @actions/core@latest @actions/github@latest @octokit/graphql@latest @octokit/rest@latest axios@latest colors@latest compression@latest ejs@latest express@latest express-rate-limit@latest image-to-base64@latest memory-cache@latest prismjs@latest puppeteer@latest svgo@latest vue@latest vue-prism-component@latest jest@latest js-yaml@latest libxmljs@latest"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -39,8 +39,8 @@
"vue-prism-component": "^1.2.0" "vue-prism-component": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"@vercel/ncc": "^0.26.1", "jest": "^26.6.3",
"babel-minify": "^0.5.1", "js-yaml": "^3.14.1",
"libxmljs": "^0.19.7" "libxmljs": "^0.19.7"
} }
} }

765
src/mocks.mjs Normal file
View File

@@ -0,0 +1,765 @@
//Imports
import axios from "axios"
import urls from "url"
//Mocked state
let mocked = false
//Mocking
export default async function ({graphql, rest}) {
//Check if already mocked
if (mocked)
return {graphql, rest}
mocked = true
console.debug(`metrics/compute/mocks > mocking`)
//GraphQL API mocking
{
console.debug(`metrics/compute/mocks > mocking graphql api`)
const unmocked = graphql
graphql = new Proxy(unmocked, {
apply(target, that, args) {
//Arguments
const [query] = args
//Common query
if (/^query Metrics /.test(query)) {
console.debug(`metrics/compute/mocks > mocking graphql api result > Metrics`)
return ({
user: {
databaseId:22963968,
name:"Simon Lecoq",
login:"lowlighter",
createdAt:"2016-10-20T16:49:29Z",
avatarUrl:"https://avatars0.githubusercontent.com/u/22963968?u=f5097de6f06ed2e31906f784163fc1e9fc84ed57&v=4",
websiteUrl:"https://simon.lecoq.io",
isHireable:false,
twitterUsername:"lecoqsimon",
repositories:{totalCount:Math.floor(Math.random()*100), totalDiskUsage:Math.floor(Math.random()*100000), nodes:[]},
packages:{totalCount:Math.floor(Math.random()*10)},
starredRepositories:{totalCount:Math.floor(Math.random()*1000)},
watching:{totalCount:Math.floor(Math.random()*100)},
sponsorshipsAsSponsor:{totalCount:Math.floor(Math.random()*5)},
sponsorshipsAsMaintainer:{totalCount:Math.floor(Math.random()*5)},
contributionsCollection:{
totalRepositoriesWithContributedCommits:Math.floor(Math.random()*30),
totalCommitContributions:Math.floor(Math.random()*1000),
restrictedContributionsCount:Math.floor(Math.random()*500),
totalIssueContributions:Math.floor(Math.random()*100),
totalPullRequestContributions:Math.floor(Math.random()*100),
totalPullRequestReviewContributions:Math.floor(Math.random()*100)
},
calendar:{
contributionCalendar:{
weeks:[
{
contributionDays:[
{color:"#40c463"},
{color:"#ebedf0"},
{color:"#9be9a8"},
{color:"#ebedf0"},
{color:"#ebedf0"}
]
},
{
contributionDays:[
{color:"#30a14e"},
{color:"#9be9a8"},
{color:"#40c463"},
{color:"#9be9a8"},
{color:"#ebedf0"},
{color:"#ebedf0"},
{color:"#ebedf0"}
]
},
{
contributionDays:[
{color:"#40c463"},
{color:"#216e39"},
{color:"#9be9a8"}
]
}
]
}
},
repositoriesContributedTo:{totalCount:Math.floor(Math.random()*10)},
followers:{totalCount:Math.floor(Math.random()*100)},
following:{totalCount:Math.floor(Math.random()*100)},
issueComments:{totalCount:Math.floor(Math.random()*100)},
organizations:{totalCount:Math.floor(Math.random()*5)}
}
})
}
//Repositories query
if (/^query Repositories /.test(query)) {
console.debug(`metrics/compute/mocks > mocking graphql api result > Repositories`)
return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({
user:{
repositories:{
edges:[],
nodes:[],
}
}
}) : ({
user:{
repositories:{
edges:[
{
cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
],
nodes:[
{
name:"metrics",
watchers:{totalCount:Math.floor(Math.random()*100)},
stargazers:{totalCount:Math.floor(Math.random()*1000)},
languages:{
edges:[
{size:111733, node:{color:"#f1e05a", name:"JavaScript"}
},
{size:14398, node:{color:"#563d7c", name:"CSS"}},
{size:13223, node:{color:"#e34c26", name:"HTML"}},
]
},
issues_open:{totalCount:Math.floor(Math.random()*100)},
issues_closed:{totalCount:Math.floor(Math.random()*100)},
pr_open:{totalCount:Math.floor(Math.random()*100)},
pr_merged:{totalCount:Math.floor(Math.random()*100)},
releases:{totalCount:Math.floor(Math.random()*100)},
forkCount:Math.floor(Math.random()*100),
licenseInfo:{spdxId:"MIT"}
},
]
}
}
})
}
//Single repository query
if (/^query Repository /.test(query)) {
console.debug(`metrics/compute/mocks > mocking graphql api result > Repository`)
return ({
user:{
repository:{
name:"metrics",
createdAt:new Date().toISOString(),
diskUsage:Math.floor(Math.random()*10000),
watchers:{totalCount:Math.floor(Math.random()*100)},
stargazers:{totalCount:Math.floor(Math.random()*1000)},
languages:{
edges:[
{size:111733, node:{color:"#f1e05a", name:"JavaScript"}
},
{size:14398, node:{color:"#563d7c", name:"CSS"}},
{size:13223, node:{color:"#e34c26", name:"HTML"}},
]
},
issues_open:{totalCount:Math.floor(Math.random()*100)},
issues_closed:{totalCount:Math.floor(Math.random()*100)},
pr_open:{totalCount:Math.floor(Math.random()*100)},
pr_merged:{totalCount:Math.floor(Math.random()*100)},
releases:{totalCount:Math.floor(Math.random()*100)},
forkCount:Math.floor(Math.random()*100),
licenseInfo:{spdxId:"MIT"}
},
}
})
}
//Calendar query
if (/^query Calendar /.test(query)) {
console.debug(`metrics/compute/mocks > mocking graphql api result > Calendar`)
//Generate calendar
const date = new Date(query.match(/from: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
const to = new Date(query.match(/to: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
const weeks = []
let contributionDays = []
for (; date <= to; date.setDate(date.getDate()+1)) {
//Create new week on sunday
if (date.getDay() === 0) {
weeks.push({contributionDays})
contributionDays = []
}
//Random contributions
const contributionCount = Math.min(10, Math.max(0, Math.floor(Math.random()*14-4)))
contributionDays.push({
contributionCount,
color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount/10/0.25)],
date:date.toISOString().substring(0, 10)
})
}
return ({
user: {
calendar:{
contributionCalendar:{
weeks
}
}
}
})
}
//Gists query
if (/^query Gists /.test(query)) {
console.debug(`metrics/compute/mocks > mocking graphql api result > Projects`)
return ({
user:{
gists:{
totalCount:1,
nodes:[
{
stargazerCount:Math.floor(Math.random()*10),
isFork:false,
forks:{totalCount:Math.floor(Math.random()*10)},
files:[{name:"example"}],
comments:{totalCount:Math.floor(Math.random()*10)}
}
]
}
}
})
}
//Projects query
if (/^query Projects /.test(query)) {
console.debug(`metrics/compute/mocks > mocking graphql api result > Projects`)
return ({
user:{
projects:{
totalCount:1,
nodes:[
{
name:"User-owned project",
updatedAt:new Date().toISOString(),
progress:{
doneCount:Math.floor(Math.random()*10),
inProgressCount:Math.floor(Math.random()*10),
todoCount:Math.floor(Math.random()*10),
enabled:true
}
}
]
}
}
})
}
//Repository project query
if (/^query RepositoryProject /.test(query)) {
console.debug(`metrics/compute/mocks > mocking graphql api result > RepositoryProject`)
return ({
user:{
repository:{
project:{
name:"Repository project example",
updatedAt:new Date().toISOString(),
progress:{
doneCount:Math.floor(Math.random()*10),
inProgressCount:Math.floor(Math.random()*10),
todoCount:Math.floor(Math.random()*10),
enabled:true
}
}
}
}
})
}
//Unmocked call
return target(...args)
}
})
}
//Rest API mocking
{
console.debug(`metrics/compute/mocks > mocking rest api`)
const unmocked = {
request:rest.request,
rateLimit:rest.rateLimit.get,
listEventsForAuthenticatedUser:rest.activity.listEventsForAuthenticatedUser,
getViews:rest.repos.getViews,
getContributorsStats:rest.repos.getContributorsStats,
listCommits:rest.repos.listCommits,
}
//Raw request
rest.request = new Proxy(unmocked.request, {
apply:function(target, that, args) {
//Arguments
const [url] = args
//Head request
if (/^HEAD .$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request HEAD`)
return ({
status: 200,
url:"https://api.github.com/",
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:undefined
})
}
//Commit content
if (/api.github.com.repos.lowlighter.metrics.commits.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/.test(url)) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request ${url}`)
return ({
status: 200,
url:"https://api.github.com/repos/lowlighter/metrics/commits/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
data:{
sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
commit:{
author:{
name:"lowlighter",
email:"22963968+lowlighter@users.noreply.github.com",
date:new Date().toISOString(),
},
committer:{
name:"lowlighter",
email:"22963968+lowlighter@users.noreply.github.com",
date:new Date().toISOString(),
},
},
author:{
login:"lowlighter",
id:22963968,
},
committer:{
login:"lowlighter",
id:22963968,
},
files: [
{
sha:"5ab8c4fb6a0be4c157419c3b9d7b522dca354b3f",
filename:"index.mjs",
patch:"@@ -0,0 +1,5 @@\n+//Imports\n+ import app from \"./src/app.mjs\"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file"
},
]
}
})
}
return target(...args)
}
})
//Rate limit
rest.rateLimit.get = new Proxy(unmocked.rateLimit, {
apply:function(target, that, args) {
return ({
status: 200,
url:"https://api.github.com/rate_limit",
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:{
resources:{
core:{limit:5000, used:0, remaining:5000, reset:0 },
search:{limit:30, used:0, remaining:30, reset:0 },
graphql:{limit:5000, used:0, remaining:5000, reset:0 },
integration_manifest:{limit:5000, used:0, remaining:5000, reset:0 },
source_import:{limit:100, used:0, remaining:100, reset:0 },
code_scanning_upload:{limit:500, used:0, remaining:500, reset:0 },
},
rate:{limit:5000, used:0, remaining:"MOCKED", reset:0}
}
})
}
})
//Events list
rest.activity.listEventsForAuthenticatedUser = new Proxy(unmocked.listEventsForAuthenticatedUser, {
apply:function(target, that, [{page, per_page}]) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser`)
return ({
status:200,
url:`https://api.github.com/users/lowlighter/events?per_page=${per_page}&page=${page}`,
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:page < 1 ? new Array(10).fill(null).map(() =>
(false ? {
id:"10000000001",
type:"IssueCommentEvent",
} : {
id:"10000000000",
type:"PushEvent",
actor:{
id:22963968,
login:"lowlighter",
},
repo: {
id:293860197,
name:"lowlighter/metrics",
},
payload: {
ref:"refs/heads/master",
commits: [
{
url:"https://api.github.com/repos/lowlighter/metrics/commits/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
]
},
created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString()
})
) : []
})
}
})
//Repository traffic
rest.repos.getViews = new Proxy(unmocked.getViews, {
apply:function(target, that, args) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getViews`)
const count = Math.floor(Math.random()*1000)*2
const uniques = Math.floor(Math.random()*count)*2
return ({
status:200,
url:"https://api.github.com/repos/lowlighter/metrics/traffic/views",
headers:{
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:{
count,
uniques,
views:[
{timestamp:new Date().toISOString(), count:count/2, uniques:uniques/2},
{timestamp:new Date().toISOString(), count:count/2, uniques:uniques/2},
]
}
})
}
})
//Repository contributions
rest.repos.getContributorsStats = new Proxy(unmocked.getContributorsStats, {
apply:function(target, that, args) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats`)
return ({
status:200,
url:"https://api.github.com/repos/lowlighter/metrics/stats/contributors",
headers: {
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:[
{
total:Math.floor(Math.random()*1000),
weeks:[
{w:1, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
{w:2, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
{w:3, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
{w:4, a:Math.floor(Math.random()*10000), d:Math.floor(Math.random()*10000), c:Math.floor(Math.random()*10000)},
],
author: {
login:"lowlighter",
}
}
]
})
}
})
//Repository contributions
rest.repos.listCommits = new Proxy(unmocked.listCommits, {
apply:function(target, that, [{page, per_page}]) {
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.listCommits`)
return ({
status:200,
url:`https://api.github.com/repos/lowlighter/metrics/commits?per_page=${per_page}&page=${page}`,
headers: {
server:"GitHub.com",
status:"200 OK",
"x-oauth-scopes":"repo",
},
data:page < 2 ? new Array(per_page).fill(null).map(() =>
({
sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
commit:{
author:{
name:"lowlighter",
date:new Date(Date.now()-Math.floor(-Math.random()*14)*24*60*60*1000).toISOString()
},
committer:{
name:"lowlighter",
date:new Date(Date.now()-Math.floor(-Math.random()*14)*24*60*60*1000).toISOString()
},
}
})
) : []
})
}
})
}
//Axios mocking
{
console.debug(`metrics/compute/mocks > mocking axios`)
const unmocked = {
get:axios.get,
post:axios.post,
}
//Post requests
axios.post = new Proxy(unmocked.post, {
apply:function(target, that, args) {
//Arguments
const [url, body, options] = args
//Spotify api
if (/accounts.spotify.com.api.token/.test(url)) {
//Access token generator
const params = new urls.URLSearchParams(body)
if ((params.get("grant_type") === "refresh_token")&&(params.get("client_id") === "MOCKED_CLIENT_ID")&&(params.get("client_secret") === "MOCKED_CLIENT_SECRET")&&(params.get("refresh_token") === "MOCKED_REFRESH_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
return ({
status:200,
data:{
access_token:"MOCKED_TOKEN_ACCESS",
token_type:"Bearer",
expires_in:3600,
scope:"user-read-recently-played user-read-private",
}
})
}
}
return target(...args)
}
})
//Get requests
axios.get = new Proxy(unmocked.get, {
apply:function(target, that, args) {
//Arguments
const [url, options] = args
//Pagespeed api
if (/googleapis.com.pagespeedonline.v5/.test(url)) {
//Pagespeed result
if (/v5.runPagespeed.*&key=MOCKED_TOKEN/.test(url)) {
console.debug(`metrics/compute/mocks > mocking pagespeed api result > ${url}`)
return ({
status:200,
data:{
captchaResult:"CAPTCHA_NOT_NEEDED",
id:"https://simon.lecoq.io/",
lighthouseResult:{
requestedUrl:"https://simon.lecoq.io/",
finalUrl:"https://simon.lecoq.io/",
lighthouseVersion:"6.3.0",
audits:{
"final-screenshot":{
id:"final-screenshot",
title:"Final Screenshot",
score: null,
details:{
data:"data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
type:"screenshot",
timestamp:Date.now()
}
},
metrics:{
id:"metrics",
title:"Metrics",
score: null,
details:{
items:[
{
observedFirstContentfulPaint:283,
observedFirstVisualChangeTs:1789259909429,
observedFirstContentfulPaintTs:1789259857628,
firstContentfulPaint:370,
observedDomContentLoaded:251,
observedFirstMeaningfulPaint:642,
maxPotentialFID:203,
observedLoad:330,
firstMeaningfulPaint:370,
observedCumulativeLayoutShift:0.0028944855967078186,
observedSpeedIndex:711,
observedSpeedIndexTs:1789260285891,
observedTimeOriginTs:1789259574429,
observedLargestContentfulPaint:857,
cumulativeLayoutShift:0.0028944855967078186,
observedFirstPaintTs:1789259857628,
observedTraceEndTs:1789261300953,
largestContentfulPaint:1085,
observedTimeOrigin:0,
speedIndex:578,
observedTraceEnd:1727,
observedDomContentLoadedTs:1789259825567,
observedFirstPaint:283,
totalBlockingTime:133,
observedLastVisualChangeTs:1789260426429,
observedFirstVisualChange:335,
observedLargestContentfulPaintTs:1789260431554,
estimatedInputLatency:13,
observedLoadTs:1789259904916,
observedLastVisualChange:852,
firstCPUIdle:773,
interactive:953,
observedNavigationStartTs:1789259574429,
observedNavigationStart:0,
observedFirstMeaningfulPaintTs:1789260216895
},
]
},
},
},
categories:{
"best-practices":{
id:"best-practices",
title:"Best Practices",
score:Math.floor(Math.random()*100)/100,
},
seo:{
id:"seo",
title:"SEO",
score:Math.floor(Math.random()*100)/100,
},
accessibility:{
id:"accessibility",
title:"Accessibility",
score:Math.floor(Math.random()*100)/100,
},
performance: {
id:"performance",
title:"Performance",
score:Math.floor(Math.random()*100)/100,
}
},
},
analysisUTCTimestamp:new Date().toISOString()
}
})
}
}
//Spotify api
if (/api.spotify.com/.test(url)) {
//Get recently played tracks
if (/me.player.recently-played/.test(url)&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
return ({
status:200,
data:{
items:[
{
track:{
album:{
album_type:"single",
artists:[
{
name:"EGOIST",
type:"artist",
}
],
images:[
{
height:640,
url:"https://i.scdn.co/image/ab67616d0000b27366371d0ad05c3f402d9cb2ae",
width:640
},
{
height:300,
url:"https://i.scdn.co/image/ab67616d00001e0266371d0ad05c3f402d9cb2ae",
width:300
},
{
height:64,
url:"https://i.scdn.co/image/ab67616d0000485166371d0ad05c3f402d9cb2ae",
width:64
}
],
name:"Fallen",
release_date:"2014-11-19",
type:"album",
},
artists:[
{
name:"EGOIST",
type:"artist",
}
],
name:"Fallen",
preview_url:"https://p.scdn.co/mp3-preview/f30eb6d1c55afa13ce754559a41ab683a1a76b02?cid=fa6ae353840041ee8af3bd1d21a66783",
type:"track",
},
played_at:new Date().toISOString(),
context:{
type:"album",
}
},
],
}
})
}
}
//Twitter api
if (/api.twitter.com/.test(url)) {
//Get user profile
if ((/users.by.username/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
return ({
status:200,
data:{
data:{
profile_image_url:"https://pbs.twimg.com/profile_images/1338344493234286592/C_ujKIUa_normal.png",
name:"GitHub",
verified:true,
id:"13334762",
username:"github",
},
}
})
}
//Get recent tweets
if ((/tweets.search.recent/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
return ({
status:200,
data:{
data:[
{
id:"1000000000000000001",
created_at:new Date().toISOString(),
entities:{
mentions:[
{start:22, end:33, username:"lowlighter"},
],
},
text:"Checkout metrics from @lowlighter ! #GitHub",
},
{
id:"1000000000000000000",
created_at:new Date().toISOString(),
text:"Hello world !",
}
],
includes:{
users:[
{
id:"100000000000000000",
name:"lowlighter",
username:"lowlighter",
},
]
},
meta:{
newest_id:"1000000000000000001",
oldest_id:"1000000000000000000",
result_count:2,
next_token:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
},
}
})
}
}
return target(...args)
}
})
}
//Return mocked elements
return {graphql, rest}
}

View File

@@ -131,9 +131,9 @@
{headers:{"Content-Type":"application/x-www-form-urlencoded"}}, {headers:{"Content-Type":"application/x-www-form-urlencoded"}},
) )
console.debug(`metrics/compute/${login}/plugins > music > got access token`) console.debug(`metrics/compute/${login}/plugins > music > got access token`)
//Retriev tracks //Retrieve tracks
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`) console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
tracks = (await imports.axios(`https://api.spotify.com/v1/me/player/recently-played?limit=${limit}&after=${timestamp}`, {headers:{ tracks = (await imports.axios.get(`https://api.spotify.com/v1/me/player/recently-played?limit=${limit}&after=${timestamp}`, {headers:{
"Accept":"application/json", "Accept":"application/json",
"Content-Type":"application/json", "Content-Type":"application/json",
"Authorization":`Bearer ${access}`} "Authorization":`Bearer ${access}`}

View File

@@ -39,6 +39,8 @@
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message)
throw error
throw {error:{message:"An error occured", instance:error}} throw {error:{message:"An error occured", instance:error}}
} }
} }

View File

@@ -8,9 +8,6 @@ query Metrics {
websiteUrl websiteUrl
isHireable isHireable
twitterUsername twitterUsername
gists {
totalCount
}
repositories(last: 0, isFork: false, ownerAffiliations: OWNER) { repositories(last: 0, isFork: false, ownerAffiliations: OWNER) {
totalCount totalCount
totalDiskUsage totalDiskUsage

View File

@@ -1,4 +1,4 @@
query Projects { query RepositoryProject {
user(login: "$user") { user(login: "$user") {
repository(name: "$repository") { repository(name: "$repository") {
project(number: $id) { project(number: $id) {

View File

@@ -1,4 +1,4 @@
query Metrics { query Repositories {
user(login: "$login") { user(login: "$login") {
repositories($after first: $repositories, isFork: false, ownerAffiliations: OWNER, orderBy: {field: UPDATED_AT, direction: DESC}) { repositories($after first: $repositories, isFork: false, ownerAffiliations: OWNER, orderBy: {field: UPDATED_AT, direction: DESC}) {
edges { edges {

View File

@@ -1,4 +1,4 @@
query Metrics { query Repository {
user(login: "$login") { user(login: "$login") {
repository(name: "$repo") { repository(name: "$repo") {
name name

View File

@@ -37,73 +37,56 @@
//Load package settings //Load package settings
logger(`metrics/setup > load package.json`) logger(`metrics/setup > load package.json`)
if (fs.existsSync(path.resolve("package.json"))) { conf.package = JSON.parse(`${await fs.promises.readFile(path.resolve("package.json"))}`)
conf.package = JSON.parse(`${await fs.promises.readFile(path.resolve("package.json"))}`) logger(`metrics/setup > load package.json > success`)
logger(`metrics/setup > load package.json > success`)
}
else {
logger(`metrics/setup > load package.json > (missing)`)
conf.package = {version:"<#version>", author:"lowlighter"}
}
//Load templates //Load templates
if (fs.existsSync(path.resolve(templates))) { for (const name of await fs.promises.readdir(templates)) {
for (const name of await fs.promises.readdir(templates)) { //Cache templates
//Cache templates if (/.*[.]mjs$/.test(name))
if (/.*[.]mjs$/.test(name)) continue
continue logger(`metrics/setup > load template [${name}]`)
logger(`metrics/setup > load template [${name}]`) const files = [
const files = [ `${templates}/${name}/image.svg`,
`${templates}/${name}/image.svg`, `${templates}/${name}/style.css`,
`${templates}/${name}/style.css`, `${templates}/${name}/fonts.css`,
`${templates}/${name}/fonts.css`, ].map(file => fs.existsSync(path.resolve(file)) ? file : file.replace(`${templates}/${name}/`, `${templates}/classic/`)).map(file => path.resolve(file))
].map(file => fs.existsSync(path.resolve(file)) ? file : file.replace(`${templates}/${name}/`, `${templates}/classic/`)).map(file => path.resolve(file)) const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`))
const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`)) conf.templates[name] = {image, style, fonts}
conf.templates[name] = {image, style, fonts} logger(`metrics/setup > load template [${name}] > success`)
logger(`metrics/setup > load template [${name}] > success`) //Debug
//Debug if (conf.settings.debug) {
if (conf.settings.debug) { Object.defineProperty(conf.templates, name, {
Object.defineProperty(conf.templates, name, { get() {
get() { logger(`metrics/setup > reload template [${name}]`)
logger(`metrics/setup > reload template [${name}]`) const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`) logger(`metrics/setup > reload template [${name}] > success`)
logger(`metrics/setup > reload template [${name}] > success`) return {image, style, fonts}
return {image, style, fonts} }
} })
}) }
}
}
}
else {
logger(`metrics/setup > load templates from build`)
conf.templates = JSON.parse(Buffer.from(`<#assets>`, "base64").toString("utf8"))
} }
//Load queries //Load queries
if (fs.existsSync(path.resolve(queries))) { for (const query of await fs.promises.readdir(queries)) {
for (const query of await fs.promises.readdir(queries)) { //Cache queries
//Cache queries const name = query.replace(/[.]graphql$/, "")
const name = query.replace(/[.]graphql$/, "") logger(`metrics/setup > load query [${name}]`)
logger(`metrics/setup > load query [${name}]`) conf.queries[`_${name}`] = `${await fs.promises.readFile(path.resolve(`${queries}/${query}`))}`
conf.queries[`_${name}`] = `${await fs.promises.readFile(path.resolve(`${queries}/${query}`))}` logger(`metrics/setup > load query [${name}] > success`)
logger(`metrics/setup > load query [${name}] > success`) //Debug
//Debug if (conf.settings.debug) {
if (conf.settings.debug) { Object.defineProperty(conf.queries, `_${name}`, {
Object.defineProperty(conf.queries, `_${name}`, { get() {
get() { logger(`metrics/setup > reload query [${name}]`)
logger(`metrics/setup > reload query [${name}]`) const raw = `${fs.readFileSync(path.resolve(`${queries}/${query}`))}`
const raw = `${fs.readFileSync(path.resolve(`${queries}/${query}`))}` logger(`metrics/setup > reload query [${name}] > success`)
logger(`metrics/setup > reload query [${name}] > success`) return raw
return raw }
} })
}) }
}
}
}
else {
logger(`metrics/setup > load queries from build`)
conf.queries = JSON.parse(Buffer.from(`<#queries>`, "base64").toString("utf8"))
} }
//Create queries formatters //Create queries formatters
Object.keys(conf.queries).map(name => conf.queries[name.substring(1)] = (vars = {}) => { Object.keys(conf.queries).map(name => conf.queries[name.substring(1)] = (vars = {}) => {
let query = conf.queries[name] let query = conf.queries[name]

View File

@@ -1,27 +0,0 @@
//Imports
import build from "../utils/build.mjs"
import colors from "colors"
//Initialization
process.on("unhandledRejection", error => { throw error })
colors.enable()
/** Test function */
export default async function test() {
//Perform tests
await test.build()
}
/** Build test */
test.build = async function () {
//Ensure that code has been rebuild
console.log("TEST : build".cyan)
await build({actions:["check"]})
}
//Main
if (/metrics.mjs/.test(process.argv[1])) {
//Test
await test()
console.log("Test success !".green)
}

211
tests/metrics.test.js Normal file
View File

@@ -0,0 +1,211 @@
//Imports
const processes = require("child_process")
const yaml = require("js-yaml")
const fs = require("fs")
//Github action
const action = yaml.safeLoad(fs.readFileSync("action.yml", "utf8"))
action.defaults = Object.fromEntries(Object.entries(action.inputs).map(([key, {default:value}]) => [key, /^(yes|no)$/.test(value) ? value === "yes" : value]))
action.input = vars => Object.fromEntries([...Object.entries(action.defaults), ...Object.entries(vars)].map(([key, value]) => [`INPUT_${key.toLocaleUpperCase()}`, value]))
action.run = async (vars) => await new Promise((solve, reject) => {
let [stdout, stderr] = ["", ""]
const env = {...process.env, ...action.input(vars), GITHUB_REPOSITORY:"lowlighter/metrics"}
const child = processes.spawn("node", ["action/index.mjs"], {env})
child.stdout.on("data", data => stdout += data)
child.stderr.on("data", data => stderr += data)
child.on("close", code => {
if (code === 0)
return solve(true)
console.log(stdout, stderr)
reject(stdout)
})
})
//Tests run
describe.each([
["classic", {}],
["terminal", {}],
["repository", {repo:"metrics"}],
])("Template : %s", (template, query) => {
for (const [name, input, {skip = []} = {}] of [
["Base (header)", {
base:"header"
}],
["Base (activity", {
base:"activity"
}],
["Base (community)", {
base:"community"
}],
["Base (repositories)", {
base:"repositories"
}],
["Base (metadata)", {
base:"metadata"
}],
["Base (complete)", {
base:"header, activity, community, repositories, metadata"
}],
["PageSpeed plugin (default)", {
plugin_pagespeed:true,
}, {skip:["repository"]}],
["PageSpeed plugin (detailed)", {
plugin_pagespeed:true,
plugin_pagespeed_detailed:true,
}, {skip:["repository"]}],
["PageSpeed plugin (screenshot)", {
plugin_pagespeed:true,
plugin_pagespeed_screenshot:true,
}, {skip:["repository"]}],
["PageSpeed plugin (complete)", {
plugin_pagespeed:true,
plugin_pagespeed_detailed:true,
plugin_pagespeed_screenshot:true,
}, {skip:["repository"]}],
["Isocalendar plugin (default)", {
plugin_isocalendar: true,
}, {skip:["terminal", "repository"]}],
["Isocalendar plugin (half-year)", {
plugin_isocalendar: true,
plugin_isocalendar_duration: "half-year",
}, {skip:["terminal", "repository"]}],
["Isocalendar plugin (full-year)", {
plugin_isocalendar: true,
plugin_isocalendar_duration: "full-year",
}, {skip:["terminal", "repository"]}],
["Music plugin (playlist - apple)", {
plugin_music:true,
plugin_music_playlist:"https://embed.music.apple.com/fr/playlist/usr-share/pl.u-V9D7m8Etjmjd0D",
}, {skip:["terminal", "repository"]}],
["Music plugin (playlist - spotify)", {
plugin_music:true,
plugin_music_playlist:"https://open.spotify.com/embed/playlist/3nfA87oeJw4LFVcUDjRcqi",
}, {skip:["terminal", "repository"]}],
["Music plugin (recent - spotify)", {
plugin_music:true,
plugin_music_provider: "spotify",
}, {skip:["terminal", "repository"]}],
["Language plugin (default)", {
plugin_languages:true,
}, {skip:["repository"]}],
["Language plugin (ignored languages)", {
plugin_languages:true,
plugin_languages_ignored:"html, css, dockerfile",
}, {skip:["repository"]}],
["Language plugin (skipped repositories)", {
plugin_languages:true,
plugin_languages_skipped:"metrics",
}, {skip:["repository"]}],
["Language plugin (complete)", {
plugin_languages:true,
plugin_languages_ignored:"html, css, dockerfile",
plugin_languages_skipped:"metrics",
}, {skip:["repository"]}],
["Follow-up plugin (default)", {
plugin_followup:true,
}],
["Topics plugin (default)", {
plugin_topics:true,
}, {skip:["terminal", "repository"]}],
["Topics plugin (starred - starred sort)", {
plugin_topics:true,
plugin_topics_mode:"starred",
plugin_topics_sort:"starred",
}, {skip:["terminal", "repository"]}],
["Topics plugin (starred - activity sort)", {
plugin_topics:true,
plugin_topics_mode:"starred",
plugin_topics_sort:"activity",
}, {skip:["terminal", "repository"]}],
["Topics plugin (starred - stars sort)", {
plugin_topics:true,
plugin_topics_mode:"starred",
plugin_topics_sort:"stars",
}, {skip:["terminal", "repository"]}],
["Topics plugin (starred - random sort)", {
plugin_topics:true,
plugin_topics_mode:"starred",
plugin_topics_sort:"random",
}, {skip:["terminal", "repository"]}],
["Topics plugin (mastered - starred sort)", {
plugin_topics:true,
plugin_topics_mode:"mastered",
plugin_topics_sort:"starred",
}, {skip:["terminal", "repository"]}],
["Topics plugin (mastered - activity sort)", {
plugin_topics:true,
plugin_topics_mode:"mastered",
plugin_topics_sort:"activity",
}, {skip:["terminal", "repository"]}],
["Topics plugin (mastered - stars sort)", {
plugin_topics:true,
plugin_topics_mode:"mastered",
plugin_topics_sort:"stars",
}, {skip:["terminal", "repository"]}],
["Topics plugin (mastered - random sort)", {
plugin_topics:true,
plugin_topics_mode:"mastered",
plugin_topics_sort:"random",
}, {skip:["terminal", "repository"]}],
["Projects plugin (default)", {
plugin_projects:true,
}, {skip:["terminal"]}],
["Projects plugin (repositories)", {
plugin_projects:true,
plugin_projects_repositories:"lowlighter/metrics/projects/1",
plugin_projects_limit:0,
}, {skip:["terminal"]}],
["Lines plugin (default)", {
base:"repositories",
plugin_lines:true,
}],
["Traffic plugin (default)", {
base:"repositories",
plugin_traffic:true,
}],
["Tweets plugin (default)", {
plugin_tweets:true,
}, {skip:["terminal", "repository"]}],
["Posts plugin (dev.to)", {
user:"lowlighter",
plugin_posts:true,
plugin_posts_source:"dev.to",
}, {skip:["terminal", "repository"]}],
["Habits plugin (default)", {
plugin_habits:true,
plugin_habits_from:5,
}, {skip:["terminal", "repository"]}],
["Habits plugin (charts)", {
plugin_habits:true,
plugin_habits_from:5,
plugin_habits_charts:true,
}, {skip:["terminal", "repository"]}],
["Habits plugin (facts)", {
plugin_habits:true,
plugin_habits_from:5,
plugin_habits_facts:true,
}, {skip:["terminal", "repository"]}],
["Habits plugin (complete)", {
plugin_habits:true,
plugin_habits_from:5,
plugin_habits_charts:true,
plugin_habits_charts:true,
}, {skip:["terminal", "repository"]}],
["Gists plugin (default)", {
plugin_gists:true,
}, {skip:["terminal"]}],
])
if (skip.includes(template))
test.skip(name, () => null)
else
test(name, async () => expect(await action.run({
token:"MOCKED_TOKEN",
plugin_pagespeed_token:"MOCKED_TOKEN",
plugin_tweets_token:"MOCKED_TOKEN",
plugin_music_token:"MOCKED_CLIENT_ID, MOCKED_CLIENT_SECRET, MOCKED_REFRESH_TOKEN",
template, base:"", query:JSON.stringify(query),
config_timezone:"Europe/Paris",
plugins_errors_fatal:true, dryrun:true, use_mocked_data:true, verify:true,
...input
})).toBe(true), 60*1e3)
})

View File

@@ -2,20 +2,13 @@
import fs from "fs" import fs from "fs"
import path from "path" import path from "path"
import url from "url" import url from "url"
import ncc from "@vercel/ncc"
import minify from "babel-minify"
import colors from "colors" import colors from "colors"
import ejs from "ejs"
//Initialization //Initialization
const __dirname = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..") const __dirname = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..")
const __action = path.join(__dirname, "action")
const __workflows = path.join(__dirname, ".github/workflows")
const __utils = path.join(__dirname, "utils")
const __src = path.join(__dirname, "src") const __src = path.join(__dirname, "src")
const __plugins = path.join(__src, "plugins") const __plugins = path.join(__src, "plugins")
const __templates = path.join(__src, "templates") const __templates = path.join(__src, "templates")
const __queries = path.join(__src, "queries")
process.on("unhandledRejection", error => { throw error }) process.on("unhandledRejection", error => { throw error })
colors.enable() colors.enable()
@@ -61,116 +54,6 @@
} }
//Workflow
{
//Build
const code = await ejs.renderFile(path.join(__utils, "workflow.yml"), {
releases:["master"],
templates:(await fs.promises.readdir(__templates)).filter(name => !/.*[.]mjs$/.test(name)).filter(name => !["repository"].includes(name)).sort(),
testcase(context = {}) {
return [`with:`, ...Object.entries({
token:"${{ secrets.METRICS_TOKEN }}",
dryrun:true,
repositories:0,
template:"${{ matrix.template }}",
base:"",
plugins_errors_fatal:true,
...context
}).map(([key, value]) => `${" ".repeat(5)}${key}: ${
typeof value === "boolean" ? (value ? "yes" : "no") :
typeof value === "string" ? (!value ? `""` : value) :
value
}`)].join("\n")
},
}, {async:true})
console.log(`Generated workflow`.grey)
//Save build
if (actions.includes("build")) {
fs.promises.writeFile(path.join(__workflows, "workflow.yml"), code)
console.log(`Generated workflow saved to ${path.join(__workflows, "dist/index.js")}`.green)
}
//Check build
if (actions.includes("check")) {
const status = `${await fs.promises.readFile(path.join(__workflows, "workflow.yml"))}` === code
if (status)
console.log(`Workflow is up-to-date`.grey)
else {
console.log(`Workflow is outdated`.red)
errors.push(`Workflow is outdated, run "npm run build" to fix it`)
}
}
}
//Action
{
//Build
let {code} = await ncc(`${__action}/index.mjs`, {sourceMap:false, sourceMapRegister:false})
console.log(`Generated action`.grey)
//Perform assets includes
{
const assets = {}
const templates = (await fs.promises.readdir(__templates)).filter(name => !/.*[.]mjs$/.test(name)).sort()
for (const name of templates) {
const files = [
`${__templates}/${name}/image.svg`,
`${__templates}/${name}/style.css`,
`${__templates}/${name}/fonts.css`,
].map(file => fs.existsSync(path.resolve(file)) ? file : file.replace(`${__templates}/${name}/`, `${__templates}/classic/`))
const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(path.resolve(file))}`))
assets[name] = {image, style, fonts}
console.log(`Prepared template ${name}`.grey)
}
code = code.replace(/<#assets>/g, Buffer.from(JSON.stringify(assets)).toString("base64"))
console.log(`Included ${templates.length} templates to generated action`.grey)
}
//Perform queries includes
{
const assets = {}
const queries = (await fs.promises.readdir(__queries)).sort()
for (const query of queries) {
const name = query.replace(/[.]graphql$/, "")
assets[`_${name}`] = `${await fs.promises.readFile(path.resolve(`${__queries}/${query}`))}`
console.log(`Prepared query ${name}`.grey)
}
code = code.replace(/<#queries>/g, Buffer.from(JSON.stringify(assets)).toString("base64"))
console.log(`Included ${queries.length} queries to generated action`.grey)
}
//Perform version include
{
const version = JSON.parse(await fs.promises.readFile(path.join(__dirname, "package.json"))).version
code = code.replace(/<#version>/g, version)
console.log(`Included version number (${version}) to generated action`.grey)
}
//Minify
code = minify(code).code
console.log(`Minified code`.grey)
if (!code)
throw new Error(`Failed to minify code`)
//Save build
if (actions.includes("build")) {
fs.promises.writeFile(path.join(__action, "dist/index.js"), code)
console.log(`Generated action saved to ${path.join(__action, "dist/index.js")}`.green)
}
//Check build
if (actions.includes("check")) {
const status = `${await fs.promises.readFile(path.join(__action, "dist/index.js"))}` === code
if (status)
console.log(`Action is up-to-date`.grey)
else {
console.log(`Action is outdated`.red)
errors.push(`Action is outdated, run "npm run build" to fix it`)
}
}
}
//Throw on errors //Throw on errors
if (errors.length) if (errors.length)
throw new Error(`${errors.length} errors occured :\n${errors.map(error => ` - ${error}`).join("\n")}`) throw new Error(`${errors.length} errors occured :\n${errors.map(error => ` - ${error}`).join("\n")}`)

View File

@@ -1,167 +0,0 @@
name: Build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup
uses: actions/setup-node@v1
with:
node-version: 15.x
- name: Install
run: npm ci
- name: Build
run: npm run build
- name: Test
run: npm test
analyze:
runs-on: ubuntu-latest
needs: <%- JSON.stringify(releases.map(release => `test-${release}`)) %>
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup
uses: github/codeql-action/init@v1
with:
languages: javascript
config-file: ./.github/config/codeql.yml
- name: Analyze
uses: github/codeql-action/analyze@v1
# Tests cases below are auto generated through `npm run build`
# Edit utils/workflow.yml instead if you need to update workflow
<% for (const release of releases) { %>
test-<%- release %>:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
template: <%- JSON.stringify(templates) %>
steps:
- name: ${{ matrix.template }} > Base
uses: lowlighter/metrics@<%- release %>
<%- testcase({
base: "header, activity, community, repositories, metadata",
}) %>
- name: ${{ matrix.template }} > Plugin > PageSpeed
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_pagespeed: true,
plugin_pagespeed_token: "${{ secrets.PAGESPEED_TOKEN }}",
plugin_pagespeed_detailed: true,
plugin_pagespeed_screenshot: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Music (playlist - apple)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_music: true,
plugin_music_playlist: "${{ secrets.MUSIC_PLAYLIST_APPLE }}",
}) %>
- name: ${{ matrix.template }} > Plugin > Music (playlist - spotify)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_music: true,
plugin_music_playlist: "${{ secrets.MUSIC_PLAYLIST_SPOTIFY }}",
}) %>
- name: ${{ matrix.template }} > Plugin > Music (recent - spotify)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_music: true,
plugin_music_provider: "spotify",
plugin_music_token: "${{ secrets.SPOTIFY_TOKENS }}",
}) %>
- name: ${{ matrix.template }} > Plugin > Posts (dev.to)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_posts: true,
plugin_posts_source: "dev.to",
}) %>
- name: ${{ matrix.template }} > Plugin > Isocalendar
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_isocalendar: true,
plugin_isocalendar_duration: "full-year",
}) %>
- name: ${{ matrix.template }} > Plugin > Habits
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_habits: true,
plugin_habits_from: 5,
plugin_habits_charts: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Languages
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_languages: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Follow-up
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_followup: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Lines and Traffic
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_lines: true,
plugin_traffic: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Gists
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_gists: true,
}) %>
- name: ${{ matrix.template }} > Plugin > Topics (starred)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_topics: true,
plugin_topics_mode: "starred",
plugin_topics_sort: "random",
}) %>
- name: ${{ matrix.template }} > Plugin > Topics (mastered)
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_topics: true,
plugin_topics_mode: "mastered",
plugin_topics_sort: "stars",
}) %>
- name: ${{ matrix.template }} > Plugin > Projects
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_projects: true,
plugin_projects_repositories: "lowlighter/metrics/projects/1",
plugin_projects_limit: 2,
}) %>
- name: ${{ matrix.template }} > Plugin > Tweets
uses: lowlighter/metrics@<%- release %>
<%- testcase({
plugin_tweets: true,
plugin_tweets_limit: 2,
plugin_tweets_token: "${{ secrets.TWITTER_TOKEN }}",
}) %>
<% } -%>