Compare commits
3 Commits
v3.2
...
usage-stat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b5258d549 | ||
|
|
f3820e8edc | ||
|
|
d9d2fa1b52 |
32
.github/workflows/main.yml
vendored
32
.github/workflows/main.yml
vendored
@@ -7,23 +7,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
cache: yarn
|
cache: yarn
|
||||||
node-version: 20
|
node-version: 16
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- run: npm run type
|
- run: yarn type
|
||||||
- run: npm run lint
|
- run: yarn lint
|
||||||
- run: npm run test --ci
|
- run: yarn test --ci
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
test-action:
|
test-action:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: update action.yml to use image from local Dockerfile
|
- name: update action.yml to use image from local Dockerfile
|
||||||
run: |
|
run: |
|
||||||
@@ -56,16 +54,16 @@ jobs:
|
|||||||
test-action-svg-only:
|
test-action-svg-only:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
cache: yarn
|
cache: yarn
|
||||||
node-version: 20
|
node-version: 16
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: build svg-only action
|
- name: build svg-only action
|
||||||
run: |
|
run: |
|
||||||
npm run build:action
|
yarn build:action
|
||||||
rm -r svg-only/dist
|
rm -r svg-only/dist
|
||||||
mv packages/action/dist svg-only/dist
|
mv packages/action/dist svg-only/dist
|
||||||
|
|
||||||
@@ -94,18 +92,18 @@ jobs:
|
|||||||
deploy-ghpages:
|
deploy-ghpages:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
cache: yarn
|
cache: yarn
|
||||||
node-version: 20
|
node-version: 16
|
||||||
- run: yarn install --frozen-lockfile
|
- run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- run: npm run build:demo
|
- run: yarn build:demo
|
||||||
env:
|
env:
|
||||||
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/
|
GITHUB_USER_CONTRIBUTION_API_ENDPOINT: https://snk-one.vercel.app/api/github-user-contribution/
|
||||||
|
|
||||||
- uses: crazy-max/ghaction-github-pages@v3.1.0
|
- uses: crazy-max/ghaction-github-pages@v2.6.0
|
||||||
if: success() && github.ref == 'refs/heads/main'
|
if: success() && github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
target_branch: gh-pages
|
target_branch: gh-pages
|
||||||
|
|||||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -21,19 +21,19 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v2
|
- uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v2
|
- uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
- uses: docker/login-action@v2
|
- uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: build and publish the docker image
|
- name: build and publish the docker image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v2
|
||||||
id: docker-build
|
id: docker-build
|
||||||
with:
|
with:
|
||||||
push: true
|
push: true
|
||||||
@@ -45,15 +45,15 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
|
sed -i "s/image: .*/image: docker:\/\/platane\/snk@${{ steps.docker-build.outputs.digest }}/" action.yml
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
cache: yarn
|
cache: yarn
|
||||||
node-version: 20
|
node-version: 16
|
||||||
|
|
||||||
- name: build svg-only action
|
- name: build svg-only action
|
||||||
run: |
|
run: |
|
||||||
yarn install --frozen-lockfile
|
yarn install --frozen-lockfile
|
||||||
npm run build:action
|
yarn build:action
|
||||||
rm -r svg-only/dist
|
rm -r svg-only/dist
|
||||||
mv packages/action/dist svg-only/dist
|
mv packages/action/dist svg-only/dist
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ jobs:
|
|||||||
echo "prerelease=true" >> $GITHUB_OUTPUT
|
echo "prerelease=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: ncipollo/release-action@v1.12.0
|
- uses: ncipollo/release-action@v1.11.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,5 +3,4 @@ npm-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
dist
|
dist
|
||||||
!svg-only/dist
|
!svg-only/dist
|
||||||
build
|
build
|
||||||
.env
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-slim as builder
|
FROM node:16-slim as builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -18,12 +18,12 @@ RUN yarn build:action
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
FROM node:20-slim
|
FROM node:16-slim
|
||||||
|
|
||||||
WORKDIR /action-release
|
WORKDIR /action-release
|
||||||
|
|
||||||
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
RUN export YARN_CACHE_FOLDER="$(mktemp -d)" \
|
||||||
&& yarn add canvas@2.11.2 gifsicle@5.3.0 --no-lockfile \
|
&& yarn add canvas@2.10.2 gifsicle@5.3.0 --no-lockfile \
|
||||||
&& rm -r "$YARN_CACHE_FOLDER"
|
&& rm -r "$YARN_CACHE_FOLDER"
|
||||||
|
|
||||||
COPY --from=builder /app/packages/action/dist/ /action-release/
|
COPY --from=builder /app/packages/action/dist/ /action-release/
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -8,20 +8,7 @@
|
|||||||
|
|
||||||
Generates a snake game from a github user contributions graph
|
Generates a snake game from a github user contributions graph
|
||||||
|
|
||||||
<picture>
|

|
||||||
<source
|
|
||||||
media="(prefers-color-scheme: dark)"
|
|
||||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake-dark.svg"
|
|
||||||
/>
|
|
||||||
<source
|
|
||||||
media="(prefers-color-scheme: light)"
|
|
||||||
srcset="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
|
||||||
/>
|
|
||||||
<img
|
|
||||||
alt="github contribution grid snake animation"
|
|
||||||
src="https://raw.githubusercontent.com/platane/snk/output/github-contribution-grid-snake.svg"
|
|
||||||
/>
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
Pull a github user's contribution graph.
|
Pull a github user's contribution graph.
|
||||||
Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
|
Make it a snake Game, generate a snake path where the cells get eaten in an orderly fashion.
|
||||||
@@ -35,7 +22,7 @@ Available as github action. It can automatically generate a new image each day.
|
|||||||
**github action**
|
**github action**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: Platane/snk@v3
|
- uses: Platane/snk@v2
|
||||||
with:
|
with:
|
||||||
# github user name to read the contribution graph from (**required**)
|
# github user name to read the contribution graph from (**required**)
|
||||||
# using action context var `github.repository_owner` or specified user
|
# using action context var `github.repository_owner` or specified user
|
||||||
@@ -54,26 +41,19 @@ Available as github action. It can automatically generate a new image each day.
|
|||||||
dist/github-snake.svg
|
dist/github-snake.svg
|
||||||
dist/github-snake-dark.svg?palette=github-dark
|
dist/github-snake-dark.svg?palette=github-dark
|
||||||
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
dist/ocean.gif?color_snake=orange&color_dots=#bfd6f6,#8dbdff,#64a1f4,#4b91f1,#3c7dd9
|
||||||
|
|
||||||
env:
|
|
||||||
# a github token is required to fetch the contribution calendar from github API
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L26-L35)
|
[example with cron job](https://github.com/Platane/Platane/blob/master/.github/workflows/main.yml#L24-L29)
|
||||||
|
|
||||||
If you are only interested in generating a svg, consider using this faster action: `uses: Platane/snk/svg-only@v3`
|
If you are only interested in generating a svg, consider using this faster action: `uses: Platane/snk/svg-only@v2`
|
||||||
|
|
||||||
**dark mode**
|
**dark mode**
|
||||||
|
|
||||||
For **dark mode** support on github, use this [special syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to) in your readme.
|
For **dark mode** support on github, use this [special syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to=) in your readme.
|
||||||
|
|
||||||
```html
|
```md
|
||||||
<picture>
|

|
||||||
<source media="(prefers-color-scheme: dark)" srcset="github-snake-dark.svg" />
|

|
||||||
<source media="(prefers-color-scheme: light)" srcset="github-snake.svg" />
|
|
||||||
<img alt="github-snake" src="github-snake.svg" />
|
|
||||||
</picture>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**interactive demo**
|
**interactive demo**
|
||||||
|
|||||||
@@ -4,16 +4,12 @@ author: "platane"
|
|||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: docker
|
using: docker
|
||||||
image: docker://platane/snk@sha256:1c8a0b51a75ad8cf36b7defddd2187bdbb92bbbb5521a9e6cc5df795b00fc590
|
image: docker://platane/snk@sha256:dcb351bdad223f2a2161fa5d6e3c9102e6ebe9fbde99a10fa3bf443d69f61a0f
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
github_user_name:
|
github_user_name:
|
||||||
description: "github user name"
|
description: "github user name"
|
||||||
required: true
|
required: true
|
||||||
github_token:
|
|
||||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
|
||||||
required: false
|
|
||||||
default: ${{ github.token }}
|
|
||||||
outputs:
|
outputs:
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
|
|||||||
24
package.json
24
package.json
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "snk",
|
"name": "snk",
|
||||||
"description": "Generates a snake game from a github user contributions grid",
|
"description": "Generates a snake game from a github user contributions grid",
|
||||||
"version": "3.2.0",
|
"version": "2.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"repository": "github:platane/snk",
|
"repository": "github:platane/snk",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sucrase/jest-plugin": "3.0.0",
|
"@sucrase/jest-plugin": "3.0.0",
|
||||||
"@types/jest": "29.5.5",
|
"@types/jest": "29.2.1",
|
||||||
"@types/node": "20.6.3",
|
"@types/node": "16.11.7",
|
||||||
"jest": "29.7.0",
|
"jest": "29.2.2",
|
||||||
"prettier": "2.8.8",
|
"prettier": "2.7.1",
|
||||||
"sucrase": "3.34.0",
|
"sucrase": "3.28.0",
|
||||||
"typescript": "5.2.2"
|
"typescript": "4.8.4"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/**"
|
"packages/**"
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"type": "tsc --noEmit",
|
"type": "tsc --noEmit",
|
||||||
"lint": "prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
|
"lint": "yarn prettier -c '**/*.{ts,js,json,md,yml,yaml}' '!packages/*/dist/**' '!svg-only/dist/**'",
|
||||||
"test": "jest --verbose --no-cache",
|
"test": "jest --verbose --passWithNoTests --no-cache",
|
||||||
"dev:demo": "( cd packages/demo ; npm run dev )",
|
"dev:demo": "( cd packages/demo ; yarn dev )",
|
||||||
"build:demo": "( cd packages/demo ; npm run build )",
|
"build:demo": "( cd packages/demo ; yarn build )",
|
||||||
"build:action": "( cd packages/action ; npm run build )"
|
"build:action": "( cd packages/action ; yarn build )"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,81 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`should parse /out.svg {"color_snake":"yellow"} 1`] = `
|
||||||
|
{
|
||||||
|
"animationOptions": {
|
||||||
|
"frameDuration": 100,
|
||||||
|
"step": 1,
|
||||||
|
},
|
||||||
|
"drawOptions": {
|
||||||
|
"colorDotBorder": "#1b1f230a",
|
||||||
|
"colorDots": [
|
||||||
|
"#ebedf0",
|
||||||
|
"#9be9a8",
|
||||||
|
"#40c463",
|
||||||
|
"#30a14e",
|
||||||
|
"#216e39",
|
||||||
|
],
|
||||||
|
"colorEmpty": "#ebedf0",
|
||||||
|
"colorSnake": "yellow",
|
||||||
|
"dark": {
|
||||||
|
"colorDotBorder": "#1b1f230a",
|
||||||
|
"colorDots": [
|
||||||
|
"#161b22",
|
||||||
|
"#01311f",
|
||||||
|
"#034525",
|
||||||
|
"#0f6d31",
|
||||||
|
"#00c647",
|
||||||
|
],
|
||||||
|
"colorEmpty": "#161b22",
|
||||||
|
"colorSnake": "purple",
|
||||||
|
},
|
||||||
|
"sizeCell": 16,
|
||||||
|
"sizeDot": 12,
|
||||||
|
"sizeDotBorderRadius": 2,
|
||||||
|
},
|
||||||
|
"filename": "/out.svg",
|
||||||
|
"format": "svg",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`should parse /out.svg?.gif.svg?color_snake=orange 1`] = `
|
||||||
|
{
|
||||||
|
"animationOptions": {
|
||||||
|
"frameDuration": 100,
|
||||||
|
"step": 1,
|
||||||
|
},
|
||||||
|
"drawOptions": {
|
||||||
|
"colorDotBorder": "#1b1f230a",
|
||||||
|
"colorDots": [
|
||||||
|
"#ebedf0",
|
||||||
|
"#9be9a8",
|
||||||
|
"#40c463",
|
||||||
|
"#30a14e",
|
||||||
|
"#216e39",
|
||||||
|
],
|
||||||
|
"colorEmpty": "#ebedf0",
|
||||||
|
"colorSnake": "orange",
|
||||||
|
"dark": {
|
||||||
|
"colorDotBorder": "#1b1f230a",
|
||||||
|
"colorDots": [
|
||||||
|
"#161b22",
|
||||||
|
"#01311f",
|
||||||
|
"#034525",
|
||||||
|
"#0f6d31",
|
||||||
|
"#00c647",
|
||||||
|
],
|
||||||
|
"colorEmpty": "#161b22",
|
||||||
|
"colorSnake": "purple",
|
||||||
|
},
|
||||||
|
"sizeCell": 16,
|
||||||
|
"sizeDot": 12,
|
||||||
|
"sizeDotBorderRadius": 2,
|
||||||
|
},
|
||||||
|
"filename": "/out.svg?.gif.svg",
|
||||||
|
"format": "svg",
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
|
exports[`should parse /out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]} 1`] = `
|
||||||
{
|
{
|
||||||
"animationOptions": {
|
"animationOptions": {
|
||||||
@@ -72,7 +148,6 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
|
|||||||
"colorEmpty": "#000",
|
"colorEmpty": "#000",
|
||||||
"colorSnake": "orange",
|
"colorSnake": "orange",
|
||||||
"dark": {
|
"dark": {
|
||||||
"colorDotBorder": "#1b1f230a",
|
|
||||||
"colorDots": [
|
"colorDots": [
|
||||||
"#a00",
|
"#a00",
|
||||||
"#a11",
|
"#a11",
|
||||||
@@ -81,7 +156,6 @@ exports[`should parse /out.svg?color_snake=orange&color_dots=#000,#111,#222,#333
|
|||||||
"#a44",
|
"#a44",
|
||||||
],
|
],
|
||||||
"colorEmpty": "#a00",
|
"colorEmpty": "#a00",
|
||||||
"colorSnake": "orange",
|
|
||||||
},
|
},
|
||||||
"sizeCell": 16,
|
"sizeCell": 16,
|
||||||
"sizeDot": 12,
|
"sizeDot": 12,
|
||||||
@@ -109,7 +183,18 @@ exports[`should parse path/to/out.gif 1`] = `
|
|||||||
],
|
],
|
||||||
"colorEmpty": "#ebedf0",
|
"colorEmpty": "#ebedf0",
|
||||||
"colorSnake": "purple",
|
"colorSnake": "purple",
|
||||||
"dark": undefined,
|
"dark": {
|
||||||
|
"colorDotBorder": "#1b1f230a",
|
||||||
|
"colorDots": [
|
||||||
|
"#161b22",
|
||||||
|
"#01311f",
|
||||||
|
"#034525",
|
||||||
|
"#0f6d31",
|
||||||
|
"#00c647",
|
||||||
|
],
|
||||||
|
"colorEmpty": "#161b22",
|
||||||
|
"colorSnake": "purple",
|
||||||
|
},
|
||||||
"sizeCell": 16,
|
"sizeCell": 16,
|
||||||
"sizeDot": 12,
|
"sizeDot": 12,
|
||||||
"sizeDotBorderRadius": 2,
|
"sizeDotBorderRadius": 2,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { generateContributionSnake } from "../generateContributionSnake";
|
import { generateContributionSnake } from "../generateContributionSnake";
|
||||||
import { parseOutputsOption } from "../outputsOptions";
|
import { parseOutputsOption } from "../outputsOptions";
|
||||||
import { config } from "dotenv";
|
|
||||||
config({ path: __dirname + "/../../../.env" });
|
|
||||||
|
|
||||||
jest.setTimeout(2 * 60 * 1000);
|
jest.setTimeout(2 * 60 * 1000);
|
||||||
|
|
||||||
@@ -32,9 +30,7 @@ it(
|
|||||||
|
|
||||||
const outputs = parseOutputsOption(entries);
|
const outputs = parseOutputsOption(entries);
|
||||||
|
|
||||||
const results = await generateContributionSnake("platane", outputs, {
|
const results = await generateContributionSnake("platane", outputs);
|
||||||
githubToken: process.env.GITHUB_TOKEN!,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(results[0]).toBeDefined();
|
expect(results[0]).toBeDefined();
|
||||||
expect(results[1]).toBeDefined();
|
expect(results[1]).toBeDefined();
|
||||||
|
|||||||
@@ -1,58 +1,17 @@
|
|||||||
import { parseEntry } from "../outputsOptions";
|
import { parseEntry } from "../outputsOptions";
|
||||||
|
|
||||||
it("should parse options as json", () => {
|
|
||||||
expect(
|
|
||||||
parseEntry(`/out.svg {"color_snake":"yellow"}`)?.drawOptions
|
|
||||||
).toHaveProperty("colorSnake", "yellow");
|
|
||||||
|
|
||||||
expect(
|
|
||||||
parseEntry(`/out.svg?{"color_snake":"yellow"}`)?.drawOptions
|
|
||||||
).toHaveProperty("colorSnake", "yellow");
|
|
||||||
|
|
||||||
expect(
|
|
||||||
parseEntry(`/out.svg?{"color_dots":["#000","#111","#222","#333","#444"]}`)
|
|
||||||
?.drawOptions.colorDots
|
|
||||||
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse options as searchparams", () => {
|
|
||||||
expect(parseEntry(`/out.svg?color_snake=yellow`)?.drawOptions).toHaveProperty(
|
|
||||||
"colorSnake",
|
|
||||||
"yellow"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
parseEntry(`/out.svg?color_dots=#000,#111,#222,#333,#444`)?.drawOptions
|
|
||||||
.colorDots
|
|
||||||
).toEqual(["#000", "#111", "#222", "#333", "#444"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse filename", () => {
|
|
||||||
expect(parseEntry(`/a/b/c.svg?{"color_snake":"yellow"}`)).toHaveProperty(
|
|
||||||
"filename",
|
|
||||||
"/a/b/c.svg"
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
parseEntry(`/a/b/out.svg?.gif.svg?{"color_snake":"yellow"}`)
|
|
||||||
).toHaveProperty("filename", "/a/b/out.svg?.gif.svg");
|
|
||||||
|
|
||||||
expect(
|
|
||||||
parseEntry(`/a/b/{[-1].svg?.gif.svg?{"color_snake":"yellow"}`)
|
|
||||||
).toHaveProperty("filename", "/a/b/{[-1].svg?.gif.svg");
|
|
||||||
});
|
|
||||||
|
|
||||||
[
|
[
|
||||||
// default
|
|
||||||
"path/to/out.gif",
|
"path/to/out.gif",
|
||||||
|
|
||||||
// overwrite colors (search params)
|
|
||||||
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444",
|
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444",
|
||||||
|
|
||||||
// overwrite colors (json)
|
|
||||||
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
|
`/out.svg?{"color_snake":"yellow","color_dots":["#000","#111","#222","#333","#444"]}`,
|
||||||
|
|
||||||
// overwrite dark colors
|
`/out.svg {"color_snake":"yellow"}`,
|
||||||
|
|
||||||
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44",
|
"/out.svg?color_snake=orange&color_dots=#000,#111,#222,#333,#444&dark_color_dots=#a00,#a11,#a22,#a33,#a44",
|
||||||
|
|
||||||
|
"/out.svg?.gif.svg?color_snake=orange",
|
||||||
].forEach((entry) =>
|
].forEach((entry) =>
|
||||||
it(`should parse ${entry}`, () => {
|
it(`should parse ${entry}`, () => {
|
||||||
expect(parseEntry(entry)).toMatchSnapshot();
|
expect(parseEntry(entry)).toMatchSnapshot();
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ export const generateContributionSnake = async (
|
|||||||
format: "svg" | "gif";
|
format: "svg" | "gif";
|
||||||
drawOptions: DrawOptions;
|
drawOptions: DrawOptions;
|
||||||
animationOptions: AnimationOptions;
|
animationOptions: AnimationOptions;
|
||||||
} | null)[],
|
} | null)[]
|
||||||
options: { githubToken: string }
|
|
||||||
) => {
|
) => {
|
||||||
console.log("🎣 fetching github user contribution");
|
console.log("🎣 fetching github user contribution");
|
||||||
const cells = await getGithubUserContribution(userName, options);
|
const cells = await getGithubUserContribution(userName);
|
||||||
|
|
||||||
const grid = userContributionToGrid(cells);
|
const grid = userContributionToGrid(cells);
|
||||||
const snake = snake4;
|
const snake = snake4;
|
||||||
|
|||||||
@@ -12,15 +12,11 @@ import { parseOutputsOption } from "./outputsOptions";
|
|||||||
core.getInput("svg_out_path"),
|
core.getInput("svg_out_path"),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const githubToken =
|
|
||||||
process.env.GITHUB_TOKEN ?? core.getInput("github_token");
|
|
||||||
|
|
||||||
const { generateContributionSnake } = await import(
|
const { generateContributionSnake } = await import(
|
||||||
"./generateContributionSnake"
|
"./generateContributionSnake"
|
||||||
);
|
);
|
||||||
const results = await generateContributionSnake(userName, outputs, {
|
const results = await generateContributionSnake(userName, outputs);
|
||||||
githubToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
outputs.forEach((out, i) => {
|
outputs.forEach((out, i) => {
|
||||||
const result = results[i];
|
const result = results[i];
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export const parseEntry = (entry: string) => {
|
|||||||
sizeCell: 16,
|
sizeCell: 16,
|
||||||
sizeDot: 12,
|
sizeDot: 12,
|
||||||
...palettes["default"],
|
...palettes["default"],
|
||||||
dark: palettes["default"].dark && { ...palettes["default"].dark },
|
|
||||||
};
|
};
|
||||||
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
|
const animationOptions: AnimationOptions = { step: 1, frameDuration: 100 };
|
||||||
|
|
||||||
@@ -44,14 +43,6 @@ export const parseEntry = (entry: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
|
||||||
const dark_palette = palettes[sp.get("dark_palette")!];
|
|
||||||
if (dark_palette) {
|
|
||||||
const clone = { ...dark_palette, dark: undefined };
|
|
||||||
drawOptions.dark = clone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sp.has("color_snake")) drawOptions.colorSnake = sp.get("color_snake")!;
|
if (sp.has("color_snake")) drawOptions.colorSnake = sp.get("color_snake")!;
|
||||||
if (sp.has("color_dots")) {
|
if (sp.has("color_dots")) {
|
||||||
const colors = sp.get("color_dots")!.split(/[,;]/);
|
const colors = sp.get("color_dots")!.split(/[,;]/);
|
||||||
@@ -65,8 +56,6 @@ export const parseEntry = (entry: string) => {
|
|||||||
if (sp.has("dark_color_dots")) {
|
if (sp.has("dark_color_dots")) {
|
||||||
const colors = sp.get("dark_color_dots")!.split(/[,;]/);
|
const colors = sp.get("dark_color_dots")!.split(/[,;]/);
|
||||||
drawOptions.dark = {
|
drawOptions.dark = {
|
||||||
colorDotBorder: drawOptions.colorDotBorder,
|
|
||||||
colorSnake: drawOptions.colorSnake,
|
|
||||||
...drawOptions.dark,
|
...drawOptions.dark,
|
||||||
colorDots: colors,
|
colorDots: colors,
|
||||||
colorEmpty: colors[0],
|
colorEmpty: colors[0],
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "@snk/action",
|
"name": "@snk/action",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "1.10.1",
|
"@actions/core": "1.10.0",
|
||||||
"@snk/gif-creator": "1.0.0",
|
"@snk/gif-creator": "1.0.0",
|
||||||
"@snk/github-user-contribution": "1.0.0",
|
"@snk/github-user-contribution": "1.0.0",
|
||||||
"@snk/solver": "1.0.0",
|
"@snk/solver": "1.0.0",
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
"@snk/types": "1.0.0"
|
"@snk/types": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vercel/ncc": "0.38.0",
|
"@vercel/ncc": "0.34.0"
|
||||||
"dotenv": "16.3.1"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
"build": "ncc build --external canvas --external gifsicle --out dist ./index.ts",
|
||||||
|
|||||||
@@ -23,5 +23,8 @@ export const basePalettes: Record<
|
|||||||
|
|
||||||
// aliases
|
// aliases
|
||||||
export const palettes = { ...basePalettes };
|
export const palettes = { ...basePalettes };
|
||||||
palettes["github"] = palettes["github-light"];
|
palettes["github"] = {
|
||||||
|
...palettes["github-light"],
|
||||||
|
dark: { ...palettes["github-dark"] },
|
||||||
|
};
|
||||||
palettes["default"] = palettes["github"];
|
palettes["default"] = palettes["github"];
|
||||||
|
|||||||
@@ -10,15 +10,14 @@
|
|||||||
"@snk/types": "1.0.0"
|
"@snk/types": "1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv": "16.3.1",
|
"@types/dat.gui": "0.7.7",
|
||||||
"@types/dat.gui": "0.7.10",
|
|
||||||
"dat.gui": "0.7.9",
|
"dat.gui": "0.7.9",
|
||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.5.0",
|
||||||
"ts-loader": "9.4.4",
|
"ts-loader": "9.4.1",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"webpack": "5.88.2",
|
"webpack": "5.74.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "4.10.0",
|
||||||
"webpack-dev-server": "4.15.1"
|
"webpack-dev-server": "4.11.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import HtmlWebpackPlugin from "html-webpack-plugin";
|
import HtmlWebpackPlugin from "html-webpack-plugin";
|
||||||
import webpack from "webpack";
|
|
||||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
|
||||||
import { config } from "dotenv";
|
|
||||||
import type { Configuration as WebpackConfiguration } from "webpack";
|
import type { Configuration as WebpackConfiguration } from "webpack";
|
||||||
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
import type { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server";
|
||||||
config({ path: __dirname + "/../../.env" });
|
import webpack from "webpack";
|
||||||
|
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||||
|
|
||||||
const demos: string[] = require("./demo.json");
|
const demos: string[] = require("./demo.json");
|
||||||
|
|
||||||
@@ -14,11 +13,7 @@ const webpackDevServerConfiguration: WebpackDevServerConfiguration = {
|
|||||||
onAfterSetupMiddleware: ({ app }) => {
|
onAfterSetupMiddleware: ({ app }) => {
|
||||||
app!.get("/api/github-user-contribution/:userName", async (req, res) => {
|
app!.get("/api/github-user-contribution/:userName", async (req, res) => {
|
||||||
const userName: string = req.params.userName;
|
const userName: string = req.params.userName;
|
||||||
res.send(
|
res.send(await getGithubUserContribution(userName));
|
||||||
await getGithubUserContribution(userName, {
|
|
||||||
githubToken: process.env.GITHUB_TOKEN!,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export const drawWorld = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const drawLerpWorld = (
|
export const drawLerpWorld = (
|
||||||
ctx: CanvasRenderingContext2D | CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
grid: Grid,
|
grid: Grid,
|
||||||
cells: Point[] | null,
|
cells: Point[] | null,
|
||||||
snake0: Snake,
|
snake0: Snake,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const createGif = async (
|
|||||||
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
const { width, height } = getCanvasWorldSize(grid0, drawOptions);
|
||||||
|
|
||||||
const canvas = createCanvas(width, height);
|
const canvas = createCanvas(width, height);
|
||||||
const ctx = canvas.getContext("2d") as any as CanvasRenderingContext2D;
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
|
||||||
const grid = copyGrid(grid0);
|
const grid = copyGrid(grid0);
|
||||||
const stack: Color[] = [];
|
const stack: Color[] = [];
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@snk/draw": "1.0.0",
|
"@snk/draw": "1.0.0",
|
||||||
"@snk/solver": "1.0.0",
|
"@snk/solver": "1.0.0",
|
||||||
"canvas": "2.11.2",
|
"canvas": "2.10.2",
|
||||||
"gif-encoder-2": "1.0.5",
|
"gif-encoder-2": "1.0.5",
|
||||||
"gifsicle": "5.3.0",
|
"gifsicle": "5.3.0",
|
||||||
"tmp": "0.2.1"
|
"tmp": "0.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/gifsicle": "5.2.0",
|
"@types/gifsicle": "5.2.0",
|
||||||
"@types/tmp": "0.2.4",
|
"@types/tmp": "0.2.3",
|
||||||
"@vercel/ncc": "0.38.0"
|
"@vercel/ncc": "0.34.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
"benchmark": "ncc run __tests__/benchmark.ts --quiet"
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
import { getGithubUserContribution } from "@snk/github-user-contribution";
|
||||||
import { VercelRequest, VercelResponse } from "@vercel/node";
|
import { VercelRequest, VercelResponse } from "@vercel/node";
|
||||||
import nodeFetch from "node-fetch";
|
|
||||||
|
|
||||||
(global as any).fetch = nodeFetch;
|
|
||||||
|
|
||||||
export default async (req: VercelRequest, res: VercelResponse) => {
|
export default async (req: VercelRequest, res: VercelResponse) => {
|
||||||
const { userName } = req.query;
|
const { userName } = req.query;
|
||||||
@@ -10,11 +7,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
|
|||||||
try {
|
try {
|
||||||
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
|
res.setHeader("Access-Control-Allow-Origin", "https://platane.github.io");
|
||||||
res.statusCode = 200;
|
res.statusCode = 200;
|
||||||
res.json(
|
res.json(await getGithubUserContribution(userName as string));
|
||||||
await getGithubUserContribution(userName as string, {
|
|
||||||
githubToken: process.env.GITHUB_TOKEN!,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.statusCode = 500;
|
res.statusCode = 500;
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
"name": "@snk/github-user-contribution-service",
|
"name": "@snk/github-user-contribution-service",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-fetch": "2.7.0",
|
|
||||||
"@snk/github-user-contribution": "1.0.0",
|
"@snk/github-user-contribution": "1.0.0",
|
||||||
"@vercel/node": "3.0.6"
|
"@vercel/node": "2.6.1"
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node-fetch": "2.6.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
|||||||
|
import { formatParams } from "../formatParams";
|
||||||
|
|
||||||
|
const params = [
|
||||||
|
//
|
||||||
|
[{}, ""],
|
||||||
|
[{ year: 2017 }, "from=2017-01-01&to=2017-12-31"],
|
||||||
|
[{ from: "2017-12-03" }, "from=2017-12-03"],
|
||||||
|
[{ to: "2017-12-03" }, "to=2017-12-03"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
params.forEach(([params, res]) =>
|
||||||
|
it(`should format ${JSON.stringify(params)}`, () => {
|
||||||
|
expect(formatParams(params)).toBe(res);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should fail if the date is in the future", () => {
|
||||||
|
expect(() => formatParams({ to: "9999-01-01" })).toThrow(Error);
|
||||||
|
});
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import { getGithubUserContribution } from "..";
|
import { getGithubUserContribution } from "..";
|
||||||
import { config } from "dotenv";
|
|
||||||
config({ path: __dirname + "/../../../.env" });
|
|
||||||
|
|
||||||
describe("getGithubUserContribution", () => {
|
describe("getGithubUserContribution", () => {
|
||||||
const promise = getGithubUserContribution("platane", {
|
const promise = getGithubUserContribution("platane");
|
||||||
githubToken: process.env.GITHUB_TOKEN!,
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should resolve", async () => {
|
it("should resolve", async () => {
|
||||||
await promise;
|
await promise;
|
||||||
@@ -31,3 +27,9 @@ describe("getGithubUserContribution", () => {
|
|||||||
expect(undefinedDays).toEqual([]);
|
expect(undefinedDays).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
xit("should match snapshot for year=2019", async () => {
|
||||||
|
expect(
|
||||||
|
await getGithubUserContribution("platane", { year: 2019 })
|
||||||
|
).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|||||||
38
packages/github-user-contribution/formatParams.ts
Normal file
38
packages/github-user-contribution/formatParams.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export type Options = { from?: string; to?: string } | { year: number };
|
||||||
|
|
||||||
|
export const formatParams = (options: Options = {}) => {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
|
||||||
|
const o: any = { ...options };
|
||||||
|
|
||||||
|
if ("year" in options) {
|
||||||
|
o.from = `${options.year}-01-01`;
|
||||||
|
o.to = `${options.year}-12-31`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const s of ["from", "to"])
|
||||||
|
if (o[s]) {
|
||||||
|
const value = o[s];
|
||||||
|
|
||||||
|
if (value >= formatDate(new Date()))
|
||||||
|
throw new Error(
|
||||||
|
"Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day."
|
||||||
|
);
|
||||||
|
|
||||||
|
sp.set(s, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sp.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (d: Date) => {
|
||||||
|
const year = d.getUTCFullYear();
|
||||||
|
const month = d.getUTCMonth() + 1;
|
||||||
|
const date = d.getUTCDate();
|
||||||
|
|
||||||
|
return [
|
||||||
|
year,
|
||||||
|
month.toString().padStart(2, "0"),
|
||||||
|
date.toString().padStart(2, "0"),
|
||||||
|
].join("-");
|
||||||
|
};
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import fetch from "node-fetch";
|
||||||
|
import { formatParams, Options } from "./formatParams";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the contribution grid from a github user page
|
* get the contribution grid from a github user page
|
||||||
*
|
*
|
||||||
@@ -16,83 +19,61 @@
|
|||||||
*/
|
*/
|
||||||
export const getGithubUserContribution = async (
|
export const getGithubUserContribution = async (
|
||||||
userName: string,
|
userName: string,
|
||||||
o: { githubToken: string }
|
options: Options = {}
|
||||||
) => {
|
) => {
|
||||||
const query = /* GraphQL */ `
|
// either use github.com/users/xxxx/contributions for previous years
|
||||||
query ($login: String!) {
|
// or github.com/xxxx ( which gives the latest update to today result )
|
||||||
user(login: $login) {
|
const url =
|
||||||
contributionsCollection {
|
"year" in options || "from" in options || "to" in options
|
||||||
contributionCalendar {
|
? `https://github.com/users/${userName}/contributions?` +
|
||||||
weeks {
|
formatParams(options)
|
||||||
contributionDays {
|
: `https://github.com/${userName}`;
|
||||||
contributionCount
|
|
||||||
contributionLevel
|
|
||||||
weekday
|
|
||||||
date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const variables = { login: userName };
|
|
||||||
|
|
||||||
const res = await fetch("https://api.github.com/graphql", {
|
const res = await fetch(url);
|
||||||
headers: {
|
|
||||||
Authorization: `bearer ${o.githubToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ variables, query }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error(res.statusText);
|
if (!res.ok) throw new Error(res.statusText);
|
||||||
|
|
||||||
const { data, errors } = (await res.json()) as {
|
const resText = await res.text();
|
||||||
data: GraphQLRes;
|
|
||||||
errors?: { message: string }[];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (errors?.[0]) throw errors[0];
|
return parseUserPage(resText);
|
||||||
|
|
||||||
return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(
|
|
||||||
({ contributionDays }, x) =>
|
|
||||||
contributionDays.map((d) => ({
|
|
||||||
x,
|
|
||||||
y: d.weekday,
|
|
||||||
date: d.date,
|
|
||||||
count: d.contributionCount,
|
|
||||||
level:
|
|
||||||
(d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
|
|
||||||
(d.contributionLevel === "THIRD_QUARTILE" && 3) ||
|
|
||||||
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
|
|
||||||
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
|
|
||||||
0,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type GraphQLRes = {
|
const parseUserPage = (content: string) => {
|
||||||
user: {
|
// take roughly the svg block
|
||||||
contributionsCollection: {
|
const block = content
|
||||||
contributionCalendar: {
|
.split(`class="js-calendar-graph-svg"`)[1]
|
||||||
weeks: {
|
.split("</svg>")[0];
|
||||||
contributionDays: {
|
|
||||||
contributionCount: number;
|
let x = 0;
|
||||||
contributionLevel:
|
let lastYAttribute = 0;
|
||||||
| "FOURTH_QUARTILE"
|
|
||||||
| "THIRD_QUARTILE"
|
const rects = Array.from(block.matchAll(/<rect[^>]*>[^<]*<\/rect>/g)).map(
|
||||||
| "SECOND_QUARTILE"
|
([m]) => {
|
||||||
| "FIRST_QUARTILE"
|
const date = m.match(/data-date="([^"]+)"/)![1];
|
||||||
| "NONE";
|
const level = +m.match(/data-level="([^"]+)"/)![1];
|
||||||
date: string;
|
const yAttribute = +m.match(/y="([^"]+)"/)![1];
|
||||||
weekday: number;
|
|
||||||
}[];
|
const literalCount = m.match(/(No|\d+) contributions? on/)![1];
|
||||||
}[];
|
const count = literalCount === "No" ? 0 : +literalCount;
|
||||||
};
|
|
||||||
};
|
if (lastYAttribute > yAttribute) x++;
|
||||||
};
|
|
||||||
|
lastYAttribute = yAttribute;
|
||||||
|
|
||||||
|
return { date, count, level, x, yAttribute };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const yAttributes = Array.from(
|
||||||
|
new Set(rects.map((c) => c.yAttribute)).keys()
|
||||||
|
).sort();
|
||||||
|
|
||||||
|
const cells = rects.map(({ yAttribute, ...c }) => ({
|
||||||
|
y: yAttributes.indexOf(yAttribute),
|
||||||
|
...c,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return cells;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
export type Res = Awaited<ReturnType<typeof getGithubUserContribution>>;
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "@snk/github-user-contribution",
|
"name": "@snk/github-user-contribution",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"cheerio": "1.0.0-rc.10",
|
||||||
|
"node-fetch": "2.6.7"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv": "16.3.1"
|
"@types/node-fetch": "2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
packages/usage-stats/.gitignore
vendored
Normal file
2
packages/usage-stats/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
.env
|
||||||
|
cache
|
||||||
53
packages/usage-stats/getDependentInfo-api.ts
Normal file
53
packages/usage-stats/getDependentInfo-api.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Octokit } from "octokit";
|
||||||
|
import { httpGet } from "./httpGet";
|
||||||
|
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||||
|
|
||||||
|
export const getLastRunInfo = async (repo_: string) => {
|
||||||
|
const [owner, repo] = repo_.split("/");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { workflow_runs },
|
||||||
|
} = await octokit.request(
|
||||||
|
"GET /repos/{owner}/{repo}/actions/runs{?actor,branch,event,status,per_page,page,created,exclude_pull_requests,check_suite_id,head_sha}",
|
||||||
|
{ owner, repo }
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const r of workflow_runs) {
|
||||||
|
const {
|
||||||
|
run_started_at: date,
|
||||||
|
head_sha,
|
||||||
|
path,
|
||||||
|
conclusion,
|
||||||
|
} = r as {
|
||||||
|
run_started_at: string;
|
||||||
|
head_sha: string;
|
||||||
|
path: string;
|
||||||
|
conclusion: "failure" | "success";
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflow_url = `https://raw.githubusercontent.com/${owner}/${repo}/${head_sha}/${path}`;
|
||||||
|
|
||||||
|
const workflow_code = await httpGet(workflow_url);
|
||||||
|
|
||||||
|
const [_, dependency] =
|
||||||
|
workflow_code.match(/uses\s*:\s*(Platane\/snk(\/svg-only)?@\w*)/) ?? [];
|
||||||
|
|
||||||
|
const cronMatch = workflow_code.match(/cron\s*:([^\n]*)/);
|
||||||
|
|
||||||
|
if (dependency)
|
||||||
|
return {
|
||||||
|
dependency,
|
||||||
|
success: conclusion === "success",
|
||||||
|
date,
|
||||||
|
cron: cronMatch?.[1].replace(/["|']/g, "").trim(),
|
||||||
|
workflow_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
56
packages/usage-stats/getDependentInfo.ts
Normal file
56
packages/usage-stats/getDependentInfo.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { load as CheerioLoad } from "cheerio";
|
||||||
|
import { httpGet } from "./httpGet";
|
||||||
|
|
||||||
|
export const getDependentInfo = async (repo: string) => {
|
||||||
|
const pageText = await httpGet(`https://github.com/${repo}/actions`).catch(
|
||||||
|
() => null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!pageText) return;
|
||||||
|
|
||||||
|
const $ = CheerioLoad(pageText);
|
||||||
|
|
||||||
|
const runs = $("#partial-actions-workflow-runs [data-url]")
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
const success =
|
||||||
|
$(el).find('[aria-label="completed successfully"]').toArray().length ===
|
||||||
|
1;
|
||||||
|
|
||||||
|
const workflow_file_href = $(el)
|
||||||
|
.find("a")
|
||||||
|
.toArray()
|
||||||
|
.map((el) => $(el).attr("href")!)
|
||||||
|
.find((href) => href.match(/\/actions\/runs\/\d+\/workflow/))!;
|
||||||
|
|
||||||
|
const workflow_file_url = workflow_file_href
|
||||||
|
? new URL(workflow_file_href, "https://github.com").toString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const date = $(el).find("relative-time").attr("datetime");
|
||||||
|
|
||||||
|
return { success, workflow_file_url, date };
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { workflow_file_url, success, date } of runs) {
|
||||||
|
if (!workflow_file_url) continue;
|
||||||
|
|
||||||
|
const $ = CheerioLoad(await httpGet(workflow_file_url));
|
||||||
|
|
||||||
|
const workflow_code = $("table[data-hpc]").text();
|
||||||
|
|
||||||
|
const [_, dependency] =
|
||||||
|
workflow_code.match(/uses\s*:\s*(Platane\/snk(\/svg-only)?@\w*)/) ?? [];
|
||||||
|
|
||||||
|
const cronMatch = workflow_code.match(/cron\s*:([^\n]*)/);
|
||||||
|
|
||||||
|
if (dependency)
|
||||||
|
return {
|
||||||
|
dependency,
|
||||||
|
success,
|
||||||
|
date,
|
||||||
|
cron: cronMatch?.[1].replace(/["|']/g, "").trim(),
|
||||||
|
workflow_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
67
packages/usage-stats/getDependents.ts
Normal file
67
packages/usage-stats/getDependents.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { load as CheerioLoad } from "cheerio";
|
||||||
|
import { httpGet } from "./httpGet";
|
||||||
|
|
||||||
|
const getPackages = async (repo: string) => {
|
||||||
|
const pageText = await httpGet(
|
||||||
|
`https://github.com/${repo}/network/dependents`
|
||||||
|
);
|
||||||
|
const $ = CheerioLoad(pageText);
|
||||||
|
|
||||||
|
return $("#dependents .select-menu-list a")
|
||||||
|
.toArray()
|
||||||
|
.map((el) => {
|
||||||
|
const name = $(el).text().trim();
|
||||||
|
const href = $(el).attr("href");
|
||||||
|
const u = new URL(href!, "http://example.com");
|
||||||
|
|
||||||
|
return { name, id: u.searchParams.get("package_id")! };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDependentByPackage = async (repo: string, packageId: string) => {
|
||||||
|
const repos = [] as string[];
|
||||||
|
|
||||||
|
const pages = [];
|
||||||
|
|
||||||
|
let url:
|
||||||
|
| string
|
||||||
|
| null = `https://github.com/${repo}/network/dependents?package_id=${packageId}`;
|
||||||
|
|
||||||
|
while (url) {
|
||||||
|
const $ = CheerioLoad(await httpGet(url));
|
||||||
|
|
||||||
|
console.log(repos.length);
|
||||||
|
|
||||||
|
const reposOnPage = $(`#dependents [data-hovercard-type="repository"]`)
|
||||||
|
.toArray()
|
||||||
|
.map((el) => $(el).attr("href")!.slice(1));
|
||||||
|
|
||||||
|
repos.push(...reposOnPage);
|
||||||
|
|
||||||
|
const nextButton = $(`#dependents a`)
|
||||||
|
.filter((_, el) => $(el).text().trim().toLowerCase() === "next")
|
||||||
|
.eq(0);
|
||||||
|
|
||||||
|
const href = nextButton ? nextButton.attr("href") : null;
|
||||||
|
|
||||||
|
pages.push({ url, reposOnPage, next: href });
|
||||||
|
|
||||||
|
url = href ? new URL(href, "https://github.com").toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { repos, pages };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDependents = async (repo: string) => {
|
||||||
|
const packages = await getPackages(repo);
|
||||||
|
|
||||||
|
const ps: (typeof packages[number] & { dependents: string[] })[] = [];
|
||||||
|
|
||||||
|
for (const p of packages)
|
||||||
|
ps.push({
|
||||||
|
...p,
|
||||||
|
dependents: (await getDependentByPackage(repo, p.id)).repos,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ps;
|
||||||
|
};
|
||||||
125
packages/usage-stats/getRunInfo-api-copy.ts
Normal file
125
packages/usage-stats/getRunInfo-api-copy.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { Octokit } from "octokit";
|
||||||
|
|
||||||
|
require("dotenv").config();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import packages from "./out.json";
|
||||||
|
|
||||||
|
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||||
|
|
||||||
|
const getLastRunInfo = async (repo_: string) => {
|
||||||
|
const [owner, repo] = repo_.split("/");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { workflow_runs },
|
||||||
|
} = await octokit.request(
|
||||||
|
"GET /repos/{owner}/{repo}/actions/runs{?actor,branch,event,status,per_page,page,created,exclude_pull_requests,check_suite_id,head_sha}",
|
||||||
|
{ owner, repo }
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const r of workflow_runs) {
|
||||||
|
const { run_started_at, head_sha, path, conclusion } = r as {
|
||||||
|
run_started_at: string;
|
||||||
|
head_sha: string;
|
||||||
|
path: string;
|
||||||
|
conclusion: "failure" | "success";
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflow_url = `https://raw.githubusercontent.com/${owner}/${repo}/${head_sha}/${path}`;
|
||||||
|
|
||||||
|
const workflow_file = await fetch(workflow_url).then((res) => res.text());
|
||||||
|
|
||||||
|
const [_, dependency, __, version] =
|
||||||
|
workflow_file.match(/uses\s*:\s*(Platane\/snk(\/svg-only)?@(\w*))/) ??
|
||||||
|
[];
|
||||||
|
|
||||||
|
const cronMatch = workflow_file.match(/cron\s*:([^\n]*)/);
|
||||||
|
|
||||||
|
if (dependency)
|
||||||
|
return {
|
||||||
|
dependency,
|
||||||
|
version,
|
||||||
|
run_started_at,
|
||||||
|
conclusion,
|
||||||
|
cron: cronMatch?.[1].replace(/["|']/g, "").trim(),
|
||||||
|
workflow_file,
|
||||||
|
workflow_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const wait = (delay = 0) => new Promise((r) => setTimeout(r, delay));
|
||||||
|
|
||||||
|
const getRepos = () => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(__dirname + "/cache/out.json").toString())
|
||||||
|
.map((p: any) => p.dependents)
|
||||||
|
.flat() as string[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReposInfo = () => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(
|
||||||
|
fs.readFileSync(__dirname + "/cache/stats.json").toString()
|
||||||
|
) as any[];
|
||||||
|
} catch (err) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const saveRepoInfo = (rr: any[]) => {
|
||||||
|
fs.writeFileSync(__dirname + "/cache/stats.json", JSON.stringify(rr));
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const repos = getRepos();
|
||||||
|
const total = repos.length;
|
||||||
|
|
||||||
|
const reposInfo = getReposInfo().slice(0, -20);
|
||||||
|
for (const { repo } of reposInfo) {
|
||||||
|
const i = repos.indexOf(repo);
|
||||||
|
if (i >= 0) repos.splice(i, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (repos.length) {
|
||||||
|
const {
|
||||||
|
data: { rate },
|
||||||
|
} = await octokit.request("GET /rate_limit", {});
|
||||||
|
|
||||||
|
console.log(rate);
|
||||||
|
if (rate.remaining < 100) {
|
||||||
|
const delay = rate.reset - Math.floor(Date.now() / 1000);
|
||||||
|
console.log(
|
||||||
|
`waiting ${delay} second (${(delay / 60).toFixed(
|
||||||
|
1
|
||||||
|
)} minutes) for reset `
|
||||||
|
);
|
||||||
|
await wait(Math.max(0, delay) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rs = repos.splice(0, 20);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
rs.map(async (repo) => {
|
||||||
|
reposInfo.push({ repo, ...(await getLastRunInfo(repo)) });
|
||||||
|
|
||||||
|
saveRepoInfo(reposInfo);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
reposInfo.length.toString().padStart(5, " "),
|
||||||
|
"/",
|
||||||
|
total,
|
||||||
|
repo
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
84
packages/usage-stats/httpGet.ts
Normal file
84
packages/usage-stats/httpGet.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import fetch from "node-fetch";
|
||||||
|
import * as path from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
const CACHE_DIR = path.join(__dirname, "cache", "http");
|
||||||
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const createMutex = () => {
|
||||||
|
let locked = false;
|
||||||
|
const q: any[] = [];
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
if (locked) return;
|
||||||
|
|
||||||
|
if (q[0]) {
|
||||||
|
locked = true;
|
||||||
|
q.shift()(() => {
|
||||||
|
locked = false;
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = () =>
|
||||||
|
new Promise<() => void>((resolve) => {
|
||||||
|
q.push(resolve);
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mutex = createMutex();
|
||||||
|
|
||||||
|
export const httpGet = async (url: string | URL): Promise<string> => {
|
||||||
|
const cacheKey = url
|
||||||
|
.toString()
|
||||||
|
.replace(/https?:\/\//, "")
|
||||||
|
.replace(/[^\w=&\?\.]/g, "_");
|
||||||
|
|
||||||
|
const cacheFilename = path.join(CACHE_DIR, cacheKey);
|
||||||
|
|
||||||
|
if (fs.existsSync(cacheFilename))
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
fs.readFile(cacheFilename, (err, data) =>
|
||||||
|
err ? reject(err) : resolve(data.toString())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const release = await mutex();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 429 || res.statusText === "Too Many Requests") {
|
||||||
|
const delay = +(res.headers.get("retry-after") ?? 300) * 1000;
|
||||||
|
|
||||||
|
console.log("Too Many Requests", delay);
|
||||||
|
|
||||||
|
await wait(delay);
|
||||||
|
|
||||||
|
console.log("waited long enough");
|
||||||
|
|
||||||
|
return httpGet(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(url, res.status, res.statusText);
|
||||||
|
throw new Error("res not ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = await res.text();
|
||||||
|
|
||||||
|
fs.writeFileSync(cacheFilename, text);
|
||||||
|
|
||||||
|
// await wait(Math.random() * 200 + 100);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const wait = (delay = 0) => new Promise((r) => setTimeout(r, delay));
|
||||||
51
packages/usage-stats/index.ts
Normal file
51
packages/usage-stats/index.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { getDependentInfo } from "./getDependentInfo";
|
||||||
|
import { getDependents } from "./getDependents";
|
||||||
|
import ParkMiller from "park-miller";
|
||||||
|
|
||||||
|
const toChunk = <T>(arr: T[], n = 1) =>
|
||||||
|
Array.from({ length: Math.ceil(arr.length / n) }, (_, i) =>
|
||||||
|
arr.slice(i * n, (i + 1) * n)
|
||||||
|
);
|
||||||
|
|
||||||
|
const random = new ParkMiller(10);
|
||||||
|
|
||||||
|
const shuffle = <T>(array: T[]) => {
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(random.float() * (i + 1));
|
||||||
|
const temp = array[i];
|
||||||
|
array[i] = array[j];
|
||||||
|
array[j] = temp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const packages = await getDependents("Platane/snk");
|
||||||
|
|
||||||
|
const repos = packages.map((p) => p.dependents).flat();
|
||||||
|
|
||||||
|
shuffle(repos);
|
||||||
|
repos.splice(0, repos.length - 5000);
|
||||||
|
|
||||||
|
console.log(repos);
|
||||||
|
|
||||||
|
const infos: any[] = [];
|
||||||
|
|
||||||
|
// for (const chunk of toChunk(repos, 10))
|
||||||
|
// await Promise.all(
|
||||||
|
// chunk.map(async (repo) => {
|
||||||
|
// console.log(
|
||||||
|
// infos.length.toString().padStart(5, " "),
|
||||||
|
// "/",
|
||||||
|
// repos.length
|
||||||
|
// );
|
||||||
|
|
||||||
|
// infos.push({ repo, ...(await getDependentInfo(repo)) });
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
|
||||||
|
for (const repo of repos) {
|
||||||
|
console.log(infos.length.toString().padStart(5, " "), "/", repos.length);
|
||||||
|
|
||||||
|
infos.push({ repo, ...(await getDependentInfo(repo)) });
|
||||||
|
}
|
||||||
|
})();
|
||||||
16
packages/usage-stats/package.json
Normal file
16
packages/usage-stats/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "@snk/usage-stats",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"sucrase": "3.29.0",
|
||||||
|
"cheerio": "1.0.0-rc.12",
|
||||||
|
"node-fetch": "2.6.7",
|
||||||
|
"octokit": "2.0.11",
|
||||||
|
"dotenv": "16.0.3",
|
||||||
|
"park-miller": "1.1.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "sucrase-node index.ts"
|
||||||
|
}
|
||||||
|
}
|
||||||
62
packages/usage-stats/stats.ts
Normal file
62
packages/usage-stats/stats.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
type R = { repo: string } & Partial<{
|
||||||
|
dependency: string;
|
||||||
|
version: string;
|
||||||
|
run_started_at: string;
|
||||||
|
conclusion: "failure" | "success";
|
||||||
|
cron?: string;
|
||||||
|
workflow_file: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const repos: R[] = JSON.parse(
|
||||||
|
fs.readFileSync(__dirname + "/cache/stats.json").toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const total = repos.length;
|
||||||
|
|
||||||
|
const recent_repos = repos.filter(
|
||||||
|
(r) =>
|
||||||
|
new Date(r.run_started_at!).getTime() >
|
||||||
|
Date.now() - 7 * 24 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
const recent_successful_repos = recent_repos.filter(
|
||||||
|
(r) => r?.conclusion === "success"
|
||||||
|
);
|
||||||
|
|
||||||
|
const versions = new Map();
|
||||||
|
for (const { dependency } of recent_successful_repos) {
|
||||||
|
versions.set(dependency, (versions.get(dependency) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`total ${total}`);
|
||||||
|
console.log(
|
||||||
|
`recent_repos ${recent_repos.length} (${(
|
||||||
|
(recent_repos.length / total) *
|
||||||
|
100
|
||||||
|
).toFixed(2)}%)`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`recent_successful_repos ${recent_successful_repos.length} (${(
|
||||||
|
(recent_successful_repos.length / total) *
|
||||||
|
100
|
||||||
|
).toFixed(2)}%)`
|
||||||
|
);
|
||||||
|
console.log("versions");
|
||||||
|
for (const [name, count] of Array.from(versions.entries()).sort(
|
||||||
|
([, a], [, b]) => b - a
|
||||||
|
))
|
||||||
|
console.log(
|
||||||
|
`${(name as string).split("Platane/")[1].padEnd(20, " ")} ${(
|
||||||
|
(count / recent_successful_repos.length) *
|
||||||
|
100
|
||||||
|
)
|
||||||
|
.toFixed(2)
|
||||||
|
.padStart(6, " ")}% ${count} `
|
||||||
|
);
|
||||||
|
|
||||||
|
const gif_repos = repos.filter((r) => r.workflow_file?.includes(".gif"));
|
||||||
|
console.log("repo with git ouput", gif_repos.length);
|
||||||
|
})();
|
||||||
@@ -3,17 +3,13 @@ description: "Generates a snake game from a github user contributions grid. Outp
|
|||||||
author: "platane"
|
author: "platane"
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: node20
|
using: node16
|
||||||
main: dist/index.js
|
main: dist/index.js
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
github_user_name:
|
github_user_name:
|
||||||
description: "github user name"
|
description: "github user name"
|
||||||
required: true
|
required: true
|
||||||
github_token:
|
|
||||||
description: "github token used to fetch the contribution calendar. Default to the action token if empty."
|
|
||||||
required: false
|
|
||||||
default: ${{ github.token }}
|
|
||||||
outputs:
|
outputs:
|
||||||
required: false
|
required: false
|
||||||
default: null
|
default: null
|
||||||
|
|||||||
3889
svg-only/dist/197.index.js
vendored
Normal file
3889
svg-only/dist/197.index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,9 +1,9 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
exports.id = 407;
|
exports.id = 317;
|
||||||
exports.ids = [407];
|
exports.ids = [317];
|
||||||
exports.modules = {
|
exports.modules = {
|
||||||
|
|
||||||
/***/ 407:
|
/***/ 5317:
|
||||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
// ESM COMPAT FLAG
|
// ESM COMPAT FLAG
|
||||||
@@ -14,7 +14,40 @@ __webpack_require__.d(__webpack_exports__, {
|
|||||||
"generateContributionSnake": () => (/* binding */ generateContributionSnake)
|
"generateContributionSnake": () => (/* binding */ generateContributionSnake)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// EXTERNAL MODULE: ../../node_modules/node-fetch/lib/index.js
|
||||||
|
var lib = __webpack_require__(2197);
|
||||||
|
var lib_default = /*#__PURE__*/__webpack_require__.n(lib);
|
||||||
|
;// CONCATENATED MODULE: ../github-user-contribution/formatParams.ts
|
||||||
|
const formatParams = (options = {}) => {
|
||||||
|
const sp = new URLSearchParams();
|
||||||
|
const o = { ...options };
|
||||||
|
if ("year" in options) {
|
||||||
|
o.from = `${options.year}-01-01`;
|
||||||
|
o.to = `${options.year}-12-31`;
|
||||||
|
}
|
||||||
|
for (const s of ["from", "to"])
|
||||||
|
if (o[s]) {
|
||||||
|
const value = o[s];
|
||||||
|
if (value >= formatDate(new Date()))
|
||||||
|
throw new Error("Cannot get contribution for a date in the future.\nPlease limit your range to the current UTC day.");
|
||||||
|
sp.set(s, value);
|
||||||
|
}
|
||||||
|
return sp.toString();
|
||||||
|
};
|
||||||
|
const formatDate = (d) => {
|
||||||
|
const year = d.getUTCFullYear();
|
||||||
|
const month = d.getUTCMonth() + 1;
|
||||||
|
const date = d.getUTCDate();
|
||||||
|
return [
|
||||||
|
year,
|
||||||
|
month.toString().padStart(2, "0"),
|
||||||
|
date.toString().padStart(2, "0"),
|
||||||
|
].join("-");
|
||||||
|
};
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ../github-user-contribution/index.ts
|
;// CONCATENATED MODULE: ../github-user-contribution/index.ts
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the contribution grid from a github user page
|
* get the contribution grid from a github user page
|
||||||
*
|
*
|
||||||
@@ -31,50 +64,43 @@ __webpack_require__.d(__webpack_exports__, {
|
|||||||
* getGithubUserContribution("platane", { year: 2019 })
|
* getGithubUserContribution("platane", { year: 2019 })
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const getGithubUserContribution = async (userName, o) => {
|
const getGithubUserContribution = async (userName, options = {}) => {
|
||||||
const query = /* GraphQL */ `
|
// either use github.com/users/xxxx/contributions for previous years
|
||||||
query ($login: String!) {
|
// or github.com/xxxx ( which gives the latest update to today result )
|
||||||
user(login: $login) {
|
const url = "year" in options || "from" in options || "to" in options
|
||||||
contributionsCollection {
|
? `https://github.com/users/${userName}/contributions?` +
|
||||||
contributionCalendar {
|
formatParams(options)
|
||||||
weeks {
|
: `https://github.com/${userName}`;
|
||||||
contributionDays {
|
const res = await lib_default()(url);
|
||||||
contributionCount
|
|
||||||
contributionLevel
|
|
||||||
weekday
|
|
||||||
date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
const variables = { login: userName };
|
|
||||||
const res = await fetch("https://api.github.com/graphql", {
|
|
||||||
headers: {
|
|
||||||
Authorization: `bearer ${o.githubToken}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({ variables, query }),
|
|
||||||
});
|
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error(res.statusText);
|
throw new Error(res.statusText);
|
||||||
const { data, errors } = (await res.json());
|
const resText = await res.text();
|
||||||
if (errors?.[0])
|
return parseUserPage(resText);
|
||||||
throw errors[0];
|
};
|
||||||
return data.user.contributionsCollection.contributionCalendar.weeks.flatMap(({ contributionDays }, x) => contributionDays.map((d) => ({
|
const parseUserPage = (content) => {
|
||||||
x,
|
// take roughly the svg block
|
||||||
y: d.weekday,
|
const block = content
|
||||||
date: d.date,
|
.split(`class="js-calendar-graph-svg"`)[1]
|
||||||
count: d.contributionCount,
|
.split("</svg>")[0];
|
||||||
level: (d.contributionLevel === "FOURTH_QUARTILE" && 4) ||
|
let x = 0;
|
||||||
(d.contributionLevel === "THIRD_QUARTILE" && 3) ||
|
let lastYAttribute = 0;
|
||||||
(d.contributionLevel === "SECOND_QUARTILE" && 2) ||
|
const rects = Array.from(block.matchAll(/<rect[^>]*>[^<]*<\/rect>/g)).map(([m]) => {
|
||||||
(d.contributionLevel === "FIRST_QUARTILE" && 1) ||
|
const date = m.match(/data-date="([^"]+)"/)[1];
|
||||||
0,
|
const level = +m.match(/data-level="([^"]+)"/)[1];
|
||||||
})));
|
const yAttribute = +m.match(/y="([^"]+)"/)[1];
|
||||||
|
const literalCount = m.match(/(No|\d+) contributions? on/)[1];
|
||||||
|
const count = literalCount === "No" ? 0 : +literalCount;
|
||||||
|
if (lastYAttribute > yAttribute)
|
||||||
|
x++;
|
||||||
|
lastYAttribute = yAttribute;
|
||||||
|
return { date, count, level, x, yAttribute };
|
||||||
|
});
|
||||||
|
const yAttributes = Array.from(new Set(rects.map((c) => c.yAttribute)).keys()).sort();
|
||||||
|
const cells = rects.map(({ yAttribute, ...c }) => ({
|
||||||
|
y: yAttributes.indexOf(yAttribute),
|
||||||
|
...c,
|
||||||
|
}));
|
||||||
|
return cells;
|
||||||
};
|
};
|
||||||
|
|
||||||
// EXTERNAL MODULE: ../types/grid.ts
|
// EXTERNAL MODULE: ../types/grid.ts
|
||||||
@@ -629,9 +655,9 @@ const getPathToPose = (snake0, target, grid) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const generateContributionSnake = async (userName, outputs, options) => {
|
const generateContributionSnake = async (userName, outputs) => {
|
||||||
console.log("🎣 fetching github user contribution");
|
console.log("🎣 fetching github user contribution");
|
||||||
const cells = await getGithubUserContribution(userName, options);
|
const cells = await getGithubUserContribution(userName);
|
||||||
const grid = userContributionToGrid(cells);
|
const grid = userContributionToGrid(cells);
|
||||||
const snake = snake4;
|
const snake = snake4;
|
||||||
console.log("📡 computing best route");
|
console.log("📡 computing best route");
|
||||||
@@ -663,14 +689,14 @@ const generateContributionSnake = async (userName, outputs, options) => {
|
|||||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||||
/* harmony export */ "Dy": () => (/* binding */ setColorEmpty),
|
|
||||||
/* harmony export */ "HJ": () => (/* binding */ isInsideLarge),
|
|
||||||
/* harmony export */ "Lq": () => (/* binding */ getColor),
|
|
||||||
/* harmony export */ "V0": () => (/* binding */ isInside),
|
/* harmony export */ "V0": () => (/* binding */ isInside),
|
||||||
|
/* harmony export */ "HJ": () => (/* binding */ isInsideLarge),
|
||||||
/* harmony export */ "VJ": () => (/* binding */ copyGrid),
|
/* harmony export */ "VJ": () => (/* binding */ copyGrid),
|
||||||
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid),
|
/* harmony export */ "Lq": () => (/* binding */ getColor),
|
||||||
|
/* harmony export */ "xb": () => (/* binding */ isEmpty),
|
||||||
/* harmony export */ "vk": () => (/* binding */ setColor),
|
/* harmony export */ "vk": () => (/* binding */ setColor),
|
||||||
/* harmony export */ "xb": () => (/* binding */ isEmpty)
|
/* harmony export */ "Dy": () => (/* binding */ setColorEmpty),
|
||||||
|
/* harmony export */ "u1": () => (/* binding */ createEmptyGrid)
|
||||||
/* harmony export */ });
|
/* harmony export */ });
|
||||||
/* unused harmony exports isGridEmpty, gridEquals */
|
/* unused harmony exports isGridEmpty, gridEquals */
|
||||||
const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height;
|
const isInside = (grid, x, y) => x >= 0 && y >= 0 && x < grid.width && y < grid.height;
|
||||||
@@ -707,13 +733,13 @@ const createEmptyGrid = (width, height) => ({
|
|||||||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
|
||||||
|
|
||||||
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
||||||
/* harmony export */ "IP": () => (/* binding */ getHeadY),
|
|
||||||
/* harmony export */ "If": () => (/* binding */ getHeadX),
|
/* harmony export */ "If": () => (/* binding */ getHeadX),
|
||||||
|
/* harmony export */ "IP": () => (/* binding */ getHeadY),
|
||||||
/* harmony export */ "JJ": () => (/* binding */ getSnakeLength),
|
/* harmony export */ "JJ": () => (/* binding */ getSnakeLength),
|
||||||
/* harmony export */ "Ks": () => (/* binding */ snakeToCells),
|
|
||||||
/* harmony export */ "kE": () => (/* binding */ snakeEquals),
|
/* harmony export */ "kE": () => (/* binding */ snakeEquals),
|
||||||
/* harmony export */ "kv": () => (/* binding */ nextSnake),
|
/* harmony export */ "kv": () => (/* binding */ nextSnake),
|
||||||
/* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide),
|
/* harmony export */ "nJ": () => (/* binding */ snakeWillSelfCollide),
|
||||||
|
/* harmony export */ "Ks": () => (/* binding */ snakeToCells),
|
||||||
/* harmony export */ "xG": () => (/* binding */ createSnakeFromCells)
|
/* harmony export */ "xG": () => (/* binding */ createSnakeFromCells)
|
||||||
/* harmony export */ });
|
/* harmony export */ });
|
||||||
/* unused harmony export copySnake */
|
/* unused harmony export copySnake */
|
||||||
51
svg-only/dist/index.js
vendored
51
svg-only/dist/index.js
vendored
@@ -558,7 +558,7 @@ class OidcClient {
|
|||||||
.catch(error => {
|
.catch(error => {
|
||||||
throw new Error(`Failed to get ID Token. \n
|
throw new Error(`Failed to get ID Token. \n
|
||||||
Error Code : ${error.statusCode}\n
|
Error Code : ${error.statusCode}\n
|
||||||
Error Message: ${error.message}`);
|
Error Message: ${error.result.message}`);
|
||||||
});
|
});
|
||||||
const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value;
|
const id_token = (_a = res.result) === null || _a === void 0 ? void 0 : _a.value;
|
||||||
if (!id_token) {
|
if (!id_token) {
|
||||||
@@ -2784,6 +2784,14 @@ module.exports = require("path");
|
|||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 5477:
|
||||||
|
/***/ ((module) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
module.exports = require("punycode");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ 2781:
|
/***/ 2781:
|
||||||
/***/ ((module) => {
|
/***/ ((module) => {
|
||||||
|
|
||||||
@@ -2800,12 +2808,28 @@ module.exports = require("tls");
|
|||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 7310:
|
||||||
|
/***/ ((module) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
module.exports = require("url");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
/***/ 3837:
|
/***/ 3837:
|
||||||
/***/ ((module) => {
|
/***/ ((module) => {
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
module.exports = require("util");
|
module.exports = require("util");
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 9796:
|
||||||
|
/***/ ((module) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
module.exports = require("zlib");
|
||||||
|
|
||||||
/***/ })
|
/***/ })
|
||||||
|
|
||||||
/******/ });
|
/******/ });
|
||||||
@@ -2967,7 +2991,7 @@ var external_path_ = __nccwpck_require__(1017);
|
|||||||
// EXTERNAL MODULE: ../../node_modules/@actions/core/lib/core.js
|
// EXTERNAL MODULE: ../../node_modules/@actions/core/lib/core.js
|
||||||
var core = __nccwpck_require__(7117);
|
var core = __nccwpck_require__(7117);
|
||||||
;// CONCATENATED MODULE: ./palettes.ts
|
;// CONCATENATED MODULE: ./palettes.ts
|
||||||
const basePalettes = {
|
const palettes = {
|
||||||
"github-light": {
|
"github-light": {
|
||||||
colorDotBorder: "#1b1f230a",
|
colorDotBorder: "#1b1f230a",
|
||||||
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
colorDots: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
|
||||||
@@ -2982,8 +3006,10 @@ const basePalettes = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
// aliases
|
// aliases
|
||||||
const palettes = { ...basePalettes };
|
palettes["github"] = {
|
||||||
palettes["github"] = palettes["github-light"];
|
...palettes["github-light"],
|
||||||
|
dark: { ...palettes["github-dark"] },
|
||||||
|
};
|
||||||
palettes["default"] = palettes["github"];
|
palettes["default"] = palettes["github"];
|
||||||
|
|
||||||
;// CONCATENATED MODULE: ./outputsOptions.ts
|
;// CONCATENATED MODULE: ./outputsOptions.ts
|
||||||
@@ -3013,7 +3039,6 @@ const parseEntry = (entry) => {
|
|||||||
sizeCell: 16,
|
sizeCell: 16,
|
||||||
sizeDot: 12,
|
sizeDot: 12,
|
||||||
...palettes["default"],
|
...palettes["default"],
|
||||||
dark: palettes["default"].dark && { ...palettes["default"].dark },
|
|
||||||
};
|
};
|
||||||
const animationOptions = { step: 1, frameDuration: 100 };
|
const animationOptions = { step: 1, frameDuration: 100 };
|
||||||
{
|
{
|
||||||
@@ -3023,13 +3048,6 @@ const parseEntry = (entry) => {
|
|||||||
drawOptions.dark = palette.dark && { ...palette.dark };
|
drawOptions.dark = palette.dark && { ...palette.dark };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
|
||||||
const dark_palette = palettes[sp.get("dark_palette")];
|
|
||||||
if (dark_palette) {
|
|
||||||
const clone = { ...dark_palette, dark: undefined };
|
|
||||||
drawOptions.dark = clone;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sp.has("color_snake"))
|
if (sp.has("color_snake"))
|
||||||
drawOptions.colorSnake = sp.get("color_snake");
|
drawOptions.colorSnake = sp.get("color_snake");
|
||||||
if (sp.has("color_dots")) {
|
if (sp.has("color_dots")) {
|
||||||
@@ -3043,8 +3061,6 @@ const parseEntry = (entry) => {
|
|||||||
if (sp.has("dark_color_dots")) {
|
if (sp.has("dark_color_dots")) {
|
||||||
const colors = sp.get("dark_color_dots").split(/[,;]/);
|
const colors = sp.get("dark_color_dots").split(/[,;]/);
|
||||||
drawOptions.dark = {
|
drawOptions.dark = {
|
||||||
colorDotBorder: drawOptions.colorDotBorder,
|
|
||||||
colorSnake: drawOptions.colorSnake,
|
|
||||||
...drawOptions.dark,
|
...drawOptions.dark,
|
||||||
colorDots: colors,
|
colorDots: colors,
|
||||||
colorEmpty: colors[0],
|
colorEmpty: colors[0],
|
||||||
@@ -3074,11 +3090,8 @@ const parseEntry = (entry) => {
|
|||||||
core.getInput("gif_out_path"),
|
core.getInput("gif_out_path"),
|
||||||
core.getInput("svg_out_path"),
|
core.getInput("svg_out_path"),
|
||||||
]);
|
]);
|
||||||
const githubToken = process.env.GITHUB_TOKEN ?? core.getInput("github_token");
|
const { generateContributionSnake } = await Promise.all(/* import() */[__nccwpck_require__.e(197), __nccwpck_require__.e(317)]).then(__nccwpck_require__.bind(__nccwpck_require__, 5317));
|
||||||
const { generateContributionSnake } = await __nccwpck_require__.e(/* import() */ 407).then(__nccwpck_require__.bind(__nccwpck_require__, 407));
|
const results = await generateContributionSnake(userName, outputs);
|
||||||
const results = await generateContributionSnake(userName, outputs, {
|
|
||||||
githubToken,
|
|
||||||
});
|
|
||||||
outputs.forEach((out, i) => {
|
outputs.forEach((out, i) => {
|
||||||
const result = results[i];
|
const result = results[i];
|
||||||
if (out?.filename && result) {
|
if (out?.filename && result) {
|
||||||
|
|||||||
Reference in New Issue
Block a user