Skip to content

Commit 150bee3

Browse files
committed
Abstract out reused action code
1 parent 9028657 commit 150bee3

6 files changed

Lines changed: 379 additions & 1 deletion

File tree

.gitignore

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# Created by https://www.toptal.com/developers/gitignore/api/node
2+
# Edit at https://www.toptal.com/developers/gitignore?templates=node
3+
4+
### Node ###
5+
# Logs
6+
logs
7+
*.log
8+
npm-debug.log*
9+
yarn-debug.log*
10+
yarn-error.log*
11+
lerna-debug.log*
12+
.pnpm-debug.log*
13+
14+
# Diagnostic reports (https://nodejs.org/api/report.html)
15+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
16+
17+
# Runtime data
18+
pids
19+
*.pid
20+
*.seed
21+
*.pid.lock
22+
23+
# Directory for instrumented libs generated by jscoverage/JSCover
24+
lib-cov
25+
26+
# Coverage directory used by tools like istanbul
27+
coverage
28+
*.lcov
29+
30+
# nyc test coverage
31+
.nyc_output
32+
33+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
34+
.grunt
35+
36+
# Bower dependency directory (https://bower.io/)
37+
bower_components
38+
39+
# node-waf configuration
40+
.lock-wscript
41+
42+
# Compiled binary addons (https://nodejs.org/api/addons.html)
43+
build/Release
44+
45+
# Dependency directories
46+
node_modules/
47+
jspm_packages/
48+
49+
# Snowpack dependency directory (https://snowpack.dev/)
50+
web_modules/
51+
52+
# TypeScript cache
53+
*.tsbuildinfo
54+
55+
# Optional npm cache directory
56+
.npm
57+
58+
# Optional eslint cache
59+
.eslintcache
60+
61+
# Optional stylelint cache
62+
.stylelintcache
63+
64+
# Microbundle cache
65+
.rpt2_cache/
66+
.rts2_cache_cjs/
67+
.rts2_cache_es/
68+
.rts2_cache_umd/
69+
70+
# Optional REPL history
71+
.node_repl_history
72+
73+
# Output of 'npm pack'
74+
*.tgz
75+
76+
# Yarn Integrity file
77+
.yarn-integrity
78+
79+
# dotenv environment variable files
80+
.env
81+
.env.development.local
82+
.env.test.local
83+
.env.production.local
84+
.env.local
85+
86+
# parcel-bundler cache (https://parceljs.org/)
87+
.cache
88+
.parcel-cache
89+
90+
# Next.js build output
91+
.next
92+
out
93+
94+
# Nuxt.js build / generate output
95+
.nuxt
96+
97+
# Gatsby files
98+
.cache/
99+
# Comment in the public line in if your project uses Gatsby and not Next.js
100+
# https://nextjs.org/blog/next-9-1#public-directory-support
101+
# public
102+
103+
# vuepress build output
104+
.vuepress/dist
105+
106+
# vuepress v2.x temp and cache directory
107+
.temp
108+
109+
# Docusaurus cache and generated files
110+
.docusaurus
111+
112+
# Serverless directories
113+
.serverless/
114+
115+
# FuseBox cache
116+
.fusebox/
117+
118+
# DynamoDB Local files
119+
.dynamodb/
120+
121+
# TernJS port file
122+
.tern-port
123+
124+
# Stores VSCode versions used for testing VSCode extensions
125+
.vscode-test
126+
127+
# yarn v2
128+
.yarn/cache
129+
.yarn/unplugged
130+
.yarn/build-state.yml
131+
.yarn/install-state.gz
132+
.pnp.*
133+
134+
### Node Patch ###
135+
# Serverless Webpack directories
136+
.webpack/
137+
138+
# Optional stylelint cache
139+
140+
# SvelteKit build / generate output
141+
.svelte-kit
142+
143+
# End of https://www.toptal.com/developers/gitignore/api/node

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,44 @@
11
# all-contributors-image
2-
Generates an image of the icons of all users in all-contributors list
2+
3+
Generates a contributors image (`contributors.png`) from the `.all-contributorsrc` file and opens a pull request with the updated image.
4+
5+
## What it does
6+
7+
- Fetches `.all-contributorsrc` from the repository
8+
- Extracts contributor avatars
9+
- Generates a grid image (`contributors.png`)
10+
- Creates/updates a branch
11+
- Commits the image
12+
- Opens a pull request
13+
14+
## Usage
15+
16+
```yaml
17+
name: Generate Contributors Image
18+
19+
on:
20+
push:
21+
paths:
22+
- .all-contributorsrc
23+
24+
jobs:
25+
generate:
26+
runs-on: ubuntu-latest
27+
steps:
28+
- name: Generate contributors image on main branch
29+
uses: processing/all-contributors-image@v1
30+
with:
31+
token: ${{ secrets.GITHUB_TOKEN }}
32+
```
33+
34+
## Inputs
35+
36+
| name | required | description |
37+
| ------ | -------- | ------------------------------------------------------------------------------------ |
38+
| token | yes | github token used for api calls |
39+
40+
## Requirements
41+
42+
Repository must have:
43+
44+
- A valid `.all-contributorsrc` file

action.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Generate Contributors PNG
2+
description: Github action to generate an image of the icons of all users in all-contributors list
3+
author: p5.js contributors & The Processing Foundation
4+
5+
inputs:
6+
token:
7+
description: GitHub token
8+
9+
runs:
10+
using: composite
11+
steps:
12+
- run: npm install
13+
shell: bash
14+
working-directory: ${{ github.action_path }}
15+
16+
- run: node index.js
17+
shell: bash
18+
working-directory: ${{ github.action_path }}
19+
env:
20+
INPUT_TOKEN: ${{ inputs.token }}

generateImage.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { createCanvas, loadImage } from "canvas";
2+
3+
export async function generateImage(contributors) {
4+
const AVATAR_SIZE = 50;
5+
const GAP = 4;
6+
const COLS = 40;
7+
const ROWS = Math.ceil(contributors.length / COLS);
8+
9+
const width = COLS * AVATAR_SIZE + (COLS - 1) * GAP;
10+
const height = ROWS * AVATAR_SIZE + (ROWS - 1) * GAP;
11+
12+
const canvas = createCanvas(width, height);
13+
const ctx = canvas.getContext("2d");
14+
15+
for (let i = 0; i < contributors.length; i++) {
16+
const c = contributors[i];
17+
18+
const col = i % COLS;
19+
const row = Math.floor(i / COLS);
20+
21+
const x = col * (AVATAR_SIZE + GAP);
22+
const y = row * (AVATAR_SIZE + GAP);
23+
24+
const img = await loadAvatar(c.avatar_url);
25+
26+
ctx.save();
27+
ctx.beginPath();
28+
ctx.arc(
29+
x + AVATAR_SIZE / 2,
30+
y + AVATAR_SIZE / 2,
31+
AVATAR_SIZE / 2,
32+
0,
33+
Math.PI * 2,
34+
);
35+
ctx.clip();
36+
37+
if (img) {
38+
ctx.drawImage(img, x, y, AVATAR_SIZE, AVATAR_SIZE);
39+
}
40+
ctx.restore();
41+
}
42+
return canvas.toBuffer("image/png");
43+
}
44+
45+
async function loadAvatar(url) {
46+
try {
47+
const res = await fetch(url);
48+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
49+
50+
const buffer = Buffer.from(await res.arrayBuffer());
51+
return await loadImage(buffer);
52+
} catch (err) {
53+
return null;
54+
}
55+
}

index.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import core from "@actions/core";
2+
import github from "@actions/github";
3+
import { generateImage } from "./generateImage.js";
4+
5+
const context = github.context;
6+
const owner = context.repo.owner;
7+
const repo = context.repo.repo;
8+
9+
const token = core.getInput("token", { required: true });
10+
const baseBranch = "main";
11+
12+
const octokit = github.getOctokit(token);
13+
const newBranch = "update-contributors-png";
14+
const filePath = "contributors.png";
15+
16+
const res = await fetch(
17+
`https://raw.githubusercontent.com/${owner}/${repo}/${baseBranch}/.all-contributorsrc`,
18+
);
19+
if (!res.ok) {
20+
throw new Error("failed to fetch .all-contributorsrc");
21+
}
22+
const data = await res.json();
23+
const contributors = data.contributors;
24+
25+
const contentBuffer = await generateImage(contributors);
26+
const content = contentBuffer.toString("base64");
27+
28+
// get base branch sha
29+
const { data: baseRef } = await octokit.rest.git.getRef({
30+
owner,
31+
repo,
32+
ref: `heads/${baseBranch}`,
33+
});
34+
const baseSha = baseRef.object.sha;
35+
36+
// create branch (or reuse if exists)
37+
try {
38+
await octokit.rest.git.createRef({
39+
owner,
40+
repo,
41+
ref: `refs/heads/${newBranch}`,
42+
sha: baseSha,
43+
});
44+
} catch (e) {
45+
// update branch, if already exists
46+
await octokit.rest.git.updateRef({
47+
owner,
48+
repo,
49+
ref: `heads/${newBranch}`,
50+
sha: baseSha,
51+
force: true,
52+
});
53+
}
54+
55+
// check if file already exists on branch
56+
let existingSha = undefined;
57+
try {
58+
const { data } = await octokit.rest.repos.getContent({
59+
owner,
60+
repo,
61+
path: filePath,
62+
ref: newBranch,
63+
});
64+
65+
if (!Array.isArray(data) && data.sha) {
66+
existingSha = data.sha;
67+
}
68+
} catch {
69+
// file does not exist
70+
}
71+
72+
// create/update file and commit
73+
try {
74+
await octokit.rest.repos.createOrUpdateFileContents({
75+
owner,
76+
repo,
77+
path: filePath,
78+
message: "Update contributors.png from .all-contributorsrc",
79+
content,
80+
branch: newBranch,
81+
...(existingSha && { sha: existingSha }),
82+
});
83+
} catch (e) {
84+
core.setFailed(e.message);
85+
}
86+
87+
// create pull request if not exists
88+
const { data: prs } = await octokit.rest.pulls.list({
89+
owner,
90+
repo,
91+
head: `${owner}:${newBranch}`,
92+
base: baseBranch,
93+
state: "open",
94+
});
95+
if (prs.length === 0) {
96+
await octokit.rest.pulls.create({
97+
owner,
98+
repo,
99+
title: "chore: update contributors.png from .all-contributorsrc",
100+
head: newBranch,
101+
base: baseBranch,
102+
body: "This PR updates the contributors.png to reflect changes in .all-contributorsrc",
103+
});
104+
}

package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "all-contributors-image",
3+
"version": "0.1.0",
4+
"description": "Github action to generate an image of the icons of all users in all-contributors list",
5+
"type": "module",
6+
"main": "index.js",
7+
"author": "p5.js contributors & The Processing Foundation",
8+
"license": "MIT",
9+
"dependencies": {
10+
"@actions/core": "^1.11.1",
11+
"@actions/github": "^6.0.1",
12+
"canvas": "^3.2.3"
13+
}
14+
}

0 commit comments

Comments
 (0)