Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _scripts/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Deny from all
2 changes: 2 additions & 0 deletions _scripts/contributors/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
keys.txt
8 changes: 8 additions & 0 deletions _scripts/contributors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Contributors
Generate the /about/contributors.md file from contributors on Github and translators on crowdin.

## Usage
```sh
node . --crowdin-key <crowdin-key> --github-key <github-key>
```
Run `node . --help` for more
58 changes: 58 additions & 0 deletions _scripts/contributors/contributors.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Command } from "commander";
import * as fs from "node:fs";
import { getGithub, genGithub } from "./github.mjs";
import { getCrowdin, genCrowdin } from "./crowdin.mjs";

// Parse CLI input
const program = new Command();
program.name("node .")
.description("Script to generate https://keyman.com/about/contributors")
.version("0.1.0")
.option("-o, --output <filename>", "output file", "contributors.md")
.option("-g, --github-key <key>", "github api authentication key (https://github.com/settings/tokens)")
.option("-c, --crowdin-key <key>", "crowdin api authentication key (https://crowdin.com/setting#api-key)")
.action(main);
program.parse(process.argv);

async function main() {
// Header for the markdown file. Can include explaination, title, etc.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Header for the markdown file. Can include explaination, title, etc.
// Header for the markdown file. Can include explanation, title, etc.

let headers = "---\ntitle: Contributors\n---";

let githubSeg = null;
// If the user provided a key for github
if (program.opts().githubKey) {
// Fetch a list users that have made contributions to "keymanapp" on github, transform it, then generate markdown.
let githubData = await getGithub(program.opts().githubKey);
let [gMajor, gMinor] = genGithub(githubData);
githubSeg = genMarkdownSegment("Contributors", gMajor, gMinor);
}

let crowdinSeg = null;
if (program.opts().crowdinKey) {
// Fetch a list users that have made contributions to "keyman" on crowdin, transform it, then generate markdown.
let crowdinData = await getCrowdin(program.opts().crowdinKey);
let [cMajor, cMinor] = genCrowdin(crowdinData);
crowdinSeg = genMarkdownSegment("Translators", cMajor, cMinor);
}

// To the users input file for output (default: contributors.md), write a join of the header, the github segment.
fs.writeFileSync(program.opts().output, `${headers}\n${githubSeg != null ? githubSeg : ""}\n${crowdinSeg != null ? crowdinSeg : ""}`);
}

function genMarkdownSegment(name, major, minor) {
// Initiate the markdown variable as a level 2 header of the section's name.
let markDown = `## ${name}\n`;

// Add the Major contributors, one at a time
markDown += `### Major ${name}\n`;
major.forEach((user) => {
markDown += `[<img src="${user.avatar_url}" alt="${user.login}'s profile picture" width="50"/> ${user.login}](${user.html_url} "${user.login}")\n\n`;
});

// Add the "Minor" contributors, one at a time
markDown += `### Other ${name}\n`;
minor.forEach((user) => {
markDown += `[<img src="${user.avatar_url}" alt="${user.login}'s profile picture" width="50"/>](${user.html_url} "${user.login}") `;
});
return markDown;
}
62 changes: 62 additions & 0 deletions _scripts/contributors/crowdin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Client } from "@crowdin/crowdin-api-client";

// Initalize an empty crowdin api variable.
let crowdin = null;

export async function getCrowdin(crowdinKey) {
// Set the aforementioned Crowdin vegtable, with the provided key.
crowdin = new Client({
token: crowdinKey,
});

console.log("Generating Crowdin report");

// Generate a Crowdin report
const crowdata = await crowdin.reportsApi.generateReport(386703, { name: "top-members", schema: { format: "json" } });
if (crowdata.data.status !== "finished") {
// Wait for the report to complete it's generation
console.log("Waiting");
let cdc = await crowdin.reportsApi.checkReportStatus(386703, crowdata.data.identifier);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coded values should be consts at top of file

while (cdc.data.status !== "finished") {
await new Promise(resolve => setTimeout(resolve, 1000));
cdc = await crowdin.reportsApi.checkReportStatus(386703, crowdata.data.identifier);
}
}

// Fetch generated report.
console.log("Downloading Crowdin report");
const crowdmembers = await crowdin.reportsApi.downloadReport(386703, crowdata.data.identifier);

const data = await (await fetch(crowdmembers.data.url)).json();
return data;
}

export function genCrowdin(data) {
const MAJOR_CONTUBUTION_THRESHOLD = 1000;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contribution

// Initialize empty arrays, to be added to later
let major = [];
let minor = [];

// Mondify crowdin data to appear like the github data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Mondify crowdin data to appear like the github data
// Modify crowdin data to appear like the github data

const crowdinMembers = data.data.filter(member => member.translated > 10).map(member => ({
login: member.user.username,
avatar_url: member.user.avatarUrl,
html_url: `https://crowdin.com/profile/${member.user.username}`,
contributions: member.translated,
}));

// Sort the data into major and minor categories.
crowdinMembers.forEach((user) => {
if (user.contributions > MAJOR_CONTUBUTION_THRESHOLD) {
major.push(user);
}
else {
minor.push(user);
};
});

// Sort the major and minor categories by contributions
major.sort((a, b) => b.contributions - a.contributions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort alphabetically please by name

minor.sort((a, b) => b.contributions - a.contributions);
return [major, minor];
}
14 changes: 14 additions & 0 deletions _scripts/contributors/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import js from "@eslint/js";
import globals from "globals";
import { defineConfig } from "eslint/config";
import stylistic from "@stylistic/eslint-plugin";

export default defineConfig([
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: globals.node } },
stylistic.configs.customize({
indent: 2,
quotes: "double",
semi: true,
jsx: false,
}),
]);
107 changes: 107 additions & 0 deletions _scripts/contributors/github.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Octokit } from "octokit";

// Initiatlize an empty Octokit variable, to be set later.
let octokit = null;

export async function getGithub(githubKey) {
// Set the aforementioned Octokit variable, with the provided key.
octokit = new Octokit({
auth: githubKey,
});

// Get a list of every repo in the keymanapp github organization
let allRepos = await getReposByOrg("keymanapp");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Const hard-coded values please


// Get the data of each of those repositories
let allRepoData = await getAllRepos(allRepos);
return allRepoData;
}

export function genGithub(repo) {
const MAJOR_CONTUBUTION_THRESHOLD = 100;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto


// Initialize empty arrays, to be added to later
let major = [];
let minor = [];
let all = [];

// For each item in the provided data,
repo.forEach((user) => {
// ... check for bots, and skip over if one is found,
if (/^(keyman-server|keyman-status|.+\[bot\])$/.test(user.login)) {
return;
}

// ... if we have not already added this user to the user list, add them,
if (!all.map(user => user.login).includes(user.login)) {
all.push(user);
return;
}

// ... Otherwise, increase their contributions score.
let i = all.findIndex(oldUser => oldUser.login == user.login);
all[i].contributions += user.contributions;
});

// Sort into the major and minor categories
all.forEach((user) => {
if (user.contributions > MAJOR_CONTUBUTION_THRESHOLD) {
major.push(user);
}
else {
minor.push(user);
};
});

// Sort the major and minor categories by contributions
major.sort((a, b) => b.contributions - a.contributions);
minor.sort((a, b) => b.contributions - a.contributions);
return [major, minor];
}

async function getReposByOrg(org) {
// Fetch a list of all repositories belonging to this organization.
console.log("TODO: Github Api Pagination");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be fixed up? Octokit.paginate


console.log("Retrieving list of Repositories");
let repos = await octokit.request(`GET /orgs/${org}/repos`, {
org,
per_page: 100,
type: "sources",
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
});

// Map the repository data to make mearly a list of names.
return repos.data.map(repo => repo.name);
}

async function getAllRepos(allRepos) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you rename this to make it clearer what it is doing?

// Fetch the data for the given list of repositories.
console.log("TODO: Github Api Pagination");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dittto


let allRepoData = [];

// Using for instead of foreach, as foreach does not support asynchrony.
for (let repo of allRepos) {
// Get data, then concat it to all previously fetched data.
console.log(`Fetching repository contributors: ${repo}`);
let repoData = await getRepo(repo);
allRepoData = allRepoData.concat(repoData);
};
return allRepoData;
}

async function getRepo(repoName) {
// Fetch data for repo of the inputted name.
let repo = await octokit.request(`GET /repos/keymanapp/${repoName}/contributors`, {
owner: "keymanapp",
repo: repoName,
per_page: 100,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
});
return repo.data;
}
Loading