From 150bee38192f1e90307958d93d4c938fbb3b59c8 Mon Sep 17 00:00:00 2001 From: skyash-dev Date: Sat, 11 Apr 2026 23:20:33 +0530 Subject: [PATCH 1/2] Abstract out reused action code --- .gitignore | 143 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 44 ++++++++++++++- action.yml | 20 +++++++ generateImage.js | 55 ++++++++++++++++++ index.js | 104 ++++++++++++++++++++++++++++++++++ package.json | 14 +++++ 6 files changed, 379 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 action.yml create mode 100644 generateImage.js create mode 100644 index.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e7e99d --- /dev/null +++ b/.gitignore @@ -0,0 +1,143 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node diff --git a/README.md b/README.md index d833046..154d2cb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ # all-contributors-image -Generates an image of the icons of all users in all-contributors list + +Generates a contributors image (`contributors.png`) from the `.all-contributorsrc` file and opens a pull request with the updated image. + +## What it does + +- Fetches `.all-contributorsrc` from the repository +- Extracts contributor avatars +- Generates a grid image (`contributors.png`) +- Creates/updates a branch +- Commits the image +- Opens a pull request + +## Usage + +```yaml +name: Generate Contributors Image + +on: + push: + paths: + - .all-contributorsrc + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - name: Generate contributors image on main branch + uses: processing/all-contributors-image@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} +``` + +## Inputs + +| name | required | description | +| ------ | -------- | ------------------------------------------------------------------------------------ | +| token | yes | github token used for api calls | + +## Requirements + +Repository must have: + +- A valid `.all-contributorsrc` file diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..d6fc908 --- /dev/null +++ b/action.yml @@ -0,0 +1,20 @@ +name: Generate Contributors PNG +description: Github action to generate an image of the icons of all users in all-contributors list +author: p5.js contributors & The Processing Foundation + +inputs: + token: + description: GitHub token + +runs: + using: composite + steps: + - run: npm install + shell: bash + working-directory: ${{ github.action_path }} + + - run: node index.js + shell: bash + working-directory: ${{ github.action_path }} + env: + INPUT_TOKEN: ${{ inputs.token }} diff --git a/generateImage.js b/generateImage.js new file mode 100644 index 0000000..2979695 --- /dev/null +++ b/generateImage.js @@ -0,0 +1,55 @@ +import { createCanvas, loadImage } from "canvas"; + +export async function generateImage(contributors) { + const AVATAR_SIZE = 50; + const GAP = 4; + const COLS = 40; + const ROWS = Math.ceil(contributors.length / COLS); + + const width = COLS * AVATAR_SIZE + (COLS - 1) * GAP; + const height = ROWS * AVATAR_SIZE + (ROWS - 1) * GAP; + + const canvas = createCanvas(width, height); + const ctx = canvas.getContext("2d"); + + for (let i = 0; i < contributors.length; i++) { + const c = contributors[i]; + + const col = i % COLS; + const row = Math.floor(i / COLS); + + const x = col * (AVATAR_SIZE + GAP); + const y = row * (AVATAR_SIZE + GAP); + + const img = await loadAvatar(c.avatar_url); + + ctx.save(); + ctx.beginPath(); + ctx.arc( + x + AVATAR_SIZE / 2, + y + AVATAR_SIZE / 2, + AVATAR_SIZE / 2, + 0, + Math.PI * 2, + ); + ctx.clip(); + + if (img) { + ctx.drawImage(img, x, y, AVATAR_SIZE, AVATAR_SIZE); + } + ctx.restore(); + } + return canvas.toBuffer("image/png"); +} + +async function loadAvatar(url) { + try { + const res = await fetch(url); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + + const buffer = Buffer.from(await res.arrayBuffer()); + return await loadImage(buffer); + } catch (err) { + return null; + } +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..561ab59 --- /dev/null +++ b/index.js @@ -0,0 +1,104 @@ +import core from "@actions/core"; +import github from "@actions/github"; +import { generateImage } from "./generateImage.js"; + +const context = github.context; +const owner = context.repo.owner; +const repo = context.repo.repo; + +const token = core.getInput("token", { required: true }); +const baseBranch = "main"; + +const octokit = github.getOctokit(token); +const newBranch = "update-contributors-png"; +const filePath = "contributors.png"; + +const res = await fetch( + `https://raw.githubusercontent.com/${owner}/${repo}/${baseBranch}/.all-contributorsrc`, +); +if (!res.ok) { + throw new Error("failed to fetch .all-contributorsrc"); +} +const data = await res.json(); +const contributors = data.contributors; + +const contentBuffer = await generateImage(contributors); +const content = contentBuffer.toString("base64"); + +// get base branch sha +const { data: baseRef } = await octokit.rest.git.getRef({ + owner, + repo, + ref: `heads/${baseBranch}`, +}); +const baseSha = baseRef.object.sha; + +// create branch (or reuse if exists) +try { + await octokit.rest.git.createRef({ + owner, + repo, + ref: `refs/heads/${newBranch}`, + sha: baseSha, + }); +} catch (e) { + // update branch, if already exists + await octokit.rest.git.updateRef({ + owner, + repo, + ref: `heads/${newBranch}`, + sha: baseSha, + force: true, + }); +} + +// check if file already exists on branch +let existingSha = undefined; +try { + const { data } = await octokit.rest.repos.getContent({ + owner, + repo, + path: filePath, + ref: newBranch, + }); + + if (!Array.isArray(data) && data.sha) { + existingSha = data.sha; + } +} catch { + // file does not exist +} + +// create/update file and commit +try { + await octokit.rest.repos.createOrUpdateFileContents({ + owner, + repo, + path: filePath, + message: "Update contributors.png from .all-contributorsrc", + content, + branch: newBranch, + ...(existingSha && { sha: existingSha }), + }); +} catch (e) { + core.setFailed(e.message); +} + +// create pull request if not exists +const { data: prs } = await octokit.rest.pulls.list({ + owner, + repo, + head: `${owner}:${newBranch}`, + base: baseBranch, + state: "open", +}); +if (prs.length === 0) { + await octokit.rest.pulls.create({ + owner, + repo, + title: "chore: update contributors.png from .all-contributorsrc", + head: newBranch, + base: baseBranch, + body: "This PR updates the contributors.png to reflect changes in .all-contributorsrc", + }); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ea3c1f2 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "all-contributors-image", + "version": "0.1.0", + "description": "Github action to generate an image of the icons of all users in all-contributors list", + "type": "module", + "main": "index.js", + "author": "p5.js contributors & The Processing Foundation", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.11.1", + "@actions/github": "^6.0.1", + "canvas": "^3.2.3" + } +} From 89fc476947b89935b6e66aed346938bb82a9c821 Mon Sep 17 00:00:00 2001 From: skyash-dev Date: Sat, 11 Apr 2026 23:24:34 +0530 Subject: [PATCH 2/2] update readme for workflow requirements --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 154d2cb..227101e 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,6 @@ jobs: Repository must have: - A valid `.all-contributorsrc` file +- `GITHUB_TOKEN` must be configured with the appropriate workflow permissions: + - enable read and write permissions + - enable allow github actions to create and approve pull requests