diff --git a/.gitignore b/.gitignore index 433ec740..1ccf4fbf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ \.DS_Store scripts/node_modules scripts/learningpath +scripts/.env \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md index 058afdd2..decd70b7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -19,6 +19,11 @@ node substitute_article_urls.js isc A node script to generate markdown files required for hosting Learning Path on innersourcecommons.org. +This script requires a [GitHub access token](https://docs.github.com/en/free-pro-team@latest/github/authenticating-to-github/creating-a-personal-access-token), as it uses the GitHub API to get Learning Path contributors. Your token does not require any scopes, as the Learning Path is Open Source. To provide this, create a `.env` file in this directory in the following format: +``` +TOKEN= +``` + ### Usage: ``` npm ci @@ -36,7 +41,7 @@ If so, then do both of the following: * update the ["sections" config](https://github.com/InnerSourceCommons/InnerSourceLearningPath/blob/master/scripts/generate_learning_path_markdown.js#L37) with the language code of the articles for the appropriate section. Open a pull request for the change. * update the [Learning Path landing page](https://github.com/InnerSourceCommons/innersourcecommons.org/blob/master/resources/learningpath/index.md) with a link to your new language pages. - + 3. Run **generate_learning_path_markdown.js** as described above. 3. `cp -r learningpath/* /resources/learningpath/`. 3. Open a pull request with the modified files in the [InnerSourceCommons/innersourcecommons.org] repo. diff --git a/scripts/generate_learning_path_markdown.js b/scripts/generate_learning_path_markdown.js index 3c380586..9bef7370 100644 --- a/scripts/generate_learning_path_markdown.js +++ b/scripts/generate_learning_path_markdown.js @@ -1,120 +1,127 @@ -const fs = require('fs') -const YAML = require('yaml') -const { EOL } = require('os') -const { join } = require('path') +(async() => { + const fs = require('fs') + const YAML = require('yaml') + const { EOL } = require('os') + const { join } = require('path') + const getContributors = require('./get_contributors') -const mkdirSync = (dir) => { - try { - fs.mkdirSync(dir) - } catch (e) { - if (e.code !== 'EEXIST') { - console.log(e) + const mkdirSync = (dir) => { + try { + fs.mkdirSync(dir) + } catch (e) { + if (e.code !== 'EEXIST') { + console.log(e) + } } } -} -const getArticleFiles = (path) => { - return fs.readdirSync(path).reduce((articles, filename) => { - const filePath = `${path}/${filename}` - if (filePath.match(/\d\d/) && !filePath.includes('-script.asciidoc')) { - return [...articles, { - filePath, - asciiDoc: fs.readFileSync(filePath, 'utf-8') - }] - } else { - return articles - } - }, []) -} + const getArticleFiles = (path) => { + return fs.readdirSync(path).reduce((articles, filename) => { + const filePath = `${path}/${filename}` + if (filePath.match(/\d\d/) && !filePath.includes('-script.asciidoc')) { + return [...articles, { + filePath, + asciiDoc: fs.readFileSync(filePath, 'utf-8') + }] + } else { + return articles + } + }, []) + } -const writeMarkdownFile = (filePath, frontMatter) => { - const frontMatterTerminator = '---' - const originStatement = '' - const output = [frontMatterTerminator, YAML.stringify(frontMatter).trim(), frontMatterTerminator, originStatement].join(EOL) - fs.writeFileSync(filePath, output) -} + const writeMarkdownFile = (filePath, frontMatter) => { + const frontMatterTerminator = '---' + const originStatement = '' + const output = [frontMatterTerminator, YAML.stringify(frontMatter).trim(), frontMatterTerminator, originStatement].join(EOL) + fs.writeFileSync(filePath, output) + } -const sections = [ - { - learning_path_group: 'Introduction', - dirName: 'introduction', - workbook: '01-introduction.asciidoc', - translations: ['de', 'it', 'ja', 'zh'], - renderArticles: true - }, - { - learning_path_group: 'Trusted Committer', - dirName: 'trusted-committer', - workbook: '02-trusted-committer.asciidoc', - translations: ['de', 'zh'], - renderArticles: true - }, - { - learning_path_group: 'Contributor', - dirName: 'contributor', - workbook: '04-contributor.asciidoc', - translations: ['ja', 'zh'], - renderArticles: true - }, - { - learning_path_group: 'Product Owner', - dirName: 'product-owner', - workbook: '03-product-owner.asciidoc', - translations: ['zh'], - renderArticles: true - }, -] + const sections = [ + { + learning_path_group: 'Introduction', + dirName: 'introduction', + workbook: '01-introduction.asciidoc', + translations: ['de', 'it', 'ja', 'zh'], + renderArticles: true + }, + { + learning_path_group: 'Trusted Committer', + dirName: 'trusted-committer', + workbook: '02-trusted-committer.asciidoc', + translations: ['de', 'zh'], + renderArticles: true + }, + { + learning_path_group: 'Contributor', + dirName: 'contributor', + workbook: '04-contributor.asciidoc', + translations: ['ja', 'zh'], + renderArticles: true + }, + { + learning_path_group: 'Product Owner', + dirName: 'product-owner', + workbook: '03-product-owner.asciidoc', + translations: ['zh'], + renderArticles: true + }, + ] mkdirSync('./learningpath') -sections.forEach(({ learning_path_group, dirName, workbook, translations, renderArticles }) => { - const baseReadPath = `../${dirName}` - const baseWritePath = `./learningpath/${dirName}` - mkdirSync(baseWritePath) + sections.forEach(({ learning_path_group, dirName, workbook, translations, renderArticles }) => { + const baseReadPath = `../${dirName}` + const baseWritePath = `./learningpath/${dirName}` + mkdirSync(baseWritePath) - translations.concat('' /* The English original */).forEach((translation) => { - const isTranslation = translation !== '' - const writePath = join(baseWritePath, translation) - mkdirSync(writePath) + translations.concat('' /* The English original */).forEach(async (translation) => { + const isTranslation = translation !== '' + const writePath = join(baseWritePath, translation) + mkdirSync(writePath) - const readPath = join(baseReadPath, translation) - const articles = getArticleFiles(readPath) - articles.forEach((article) => { - const articleTitle = article.asciiDoc.match(/== (.*)/)[1] - const articleNumber = article.filePath.split('/').pop().split('-')[0] - const fileName = articleNumber === '01' ? `${writePath}/index.md` : `${writePath}/${articleNumber}.md` - const frontMatter = { - layout: 'learning-path-page', - show_meta: false, - title: `Learning Path - ${learning_path_group} - ${articleTitle}`, - learning_path_article: renderArticles ? article.filePath.replace('../', '') : undefined, - learning_path_group, - learning_path_menu_title: `${articleNumber} - ${articleTitle}`, - learning_path_position: parseInt(articleNumber), - learning_path_translation: translation, - no_video: isTranslation // Videos not available translated. - } + const readPath = join(baseReadPath, translation) + const articles = getArticleFiles(readPath) + articles.forEach(async (article) => { + const articleTitle = article.asciiDoc.match(/== (.*)/)[1] + const articleNumber = article.filePath.split('/').pop().split('-')[0] + const fileName = articleNumber === '01' ? `${writePath}/index.md` : `${writePath}/${articleNumber}.md` + const contributors = await getContributors(article.filePath.replace('../', '')) + const frontMatter = { + layout: 'learning-path-page', + show_meta: false, + title: `Learning Path - ${learning_path_group} - ${articleTitle}`, + learning_path_article: renderArticles ? article.filePath.replace('../', '') : undefined, + learning_path_group, + learning_path_menu_title: `${articleNumber} - ${articleTitle}`, + learning_path_position: parseInt(articleNumber), + learning_path_translation: translation, + no_video: isTranslation, // Videos not available translated. + contributors + } - writeMarkdownFile(fileName, frontMatter) - }) + writeMarkdownFile(fileName, frontMatter) + }) - // Workbooks not translated. - if (!isTranslation) { - const workbookFileName = `${writePath}/workbook.md` - console.log('workbookFileName', workbookFileName) - const workbookFrontMatter = { - layout: 'learning-path-page', - show_meta: false, - title: `Learning Path - ${learning_path_group} - Workbook`, - learning_path_article: `workbook/${workbook}`, - learning_path_group, - learning_path_menu_title: `${learning_path_group} Workbook`, - learning_path_position: articles.length - articles.filter(Array.isArray).length + 1, - learning_path_translation: translation, - no_video: true - } + // Workbooks not translated. + if (!isTranslation) { + const workbookFileName = `${writePath}/workbook.md` + const contributors = await getContributors(`workbook/${workbook}`) + console.log('workbookFileName', workbookFileName) + const workbookFrontMatter = { + layout: 'learning-path-page', + show_meta: false, + title: `Learning Path - ${learning_path_group} - Workbook`, + learning_path_article: `workbook/${workbook}`, + learning_path_group, + learning_path_menu_title: `${learning_path_group} Workbook`, + learning_path_position: articles.length - articles.filter(Array.isArray).length + 1, + learning_path_translation: translation, + no_video: true, + contributors + } - writeMarkdownFile(workbookFileName, workbookFrontMatter) - } + writeMarkdownFile(workbookFileName, workbookFrontMatter) + } + }) }) -}) +})() diff --git a/scripts/get_contributors.js b/scripts/get_contributors.js new file mode 100644 index 00000000..491bc85a --- /dev/null +++ b/scripts/get_contributors.js @@ -0,0 +1,48 @@ +require('dotenv').config() +const { graphql } = require("@octokit/graphql") + +const graphqlWithAuth = graphql.defaults({ + headers: { + authorization: `token ${process.env.TOKEN}` + } +}) + +module.exports = async function (filepath) { + const contributors = await graphqlWithAuth( + `{ + repository(owner: "InnerSourceCommons", name: "InnerSourceLearningPath") { + object(expression: "master") { + ... on Commit { + history(first: 100, path: "${filepath}") { + totalCount + nodes { + author { + name + user { + name + url + } + } + } + } + } + } + } + }` + ) + + const history = contributors.repository.object.history + + if (history.totalCount > 100) { + throw Error('This script needs updating to handle >100 commits') + } + + return Object.values(history.nodes.reduce((acc, { author }) => { + const name = (author.user && author.user.name) || author.name + acc[name] = { + name, + url: author.user && author.user.url + } + return acc + }, {})) +} \ No newline at end of file diff --git a/scripts/package-lock.json b/scripts/package-lock.json index aed0f697..9d57dfb2 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -4,6 +4,102 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@octokit/endpoint": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.9.tgz", + "integrity": "sha512-3VPLbcCuqji4IFTclNUtGdp9v7g+nspWdiCUbK3+iPMjJCZ6LEhn1ts626bWLOn0GiDb6j+uqGvPpqLnY7pBgw==", + "requires": { + "@octokit/types": "^5.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/graphql": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.7.tgz", + "integrity": "sha512-Gk0AR+DcwIK/lK/GX+OQ99UqtenQhcbrhHHfOYlrCQe17ADnX3EKAOKRsAZ9qZvpi5MuwWm/Nm+9aO2kTDSdyA==", + "requires": { + "@octokit/request": "^5.3.0", + "@octokit/types": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request": { + "version": "5.4.10", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.10.tgz", + "integrity": "sha512-egA49HkqEORVGDZGav1mh+VD+7uLgOxtn5oODj6guJk0HCy+YBSYapFkSLFgeYj3Fr18ZULKGURkjyhkAChylw==", + "requires": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.0.0", + "@octokit/types": "^5.0.0", + "deprecation": "^2.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.1", + "once": "^1.4.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/request-error": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.3.tgz", + "integrity": "sha512-GgD5z8Btm301i2zfvJLk/mkhvGCdjQ7wT8xF9ov5noQY8WbKZDH9cOBqXzoeKd1mLr1xH2FwbtGso135zGBgTA==", + "requires": { + "@octokit/types": "^5.0.1", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/types": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz", + "integrity": "sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ==", + "requires": { + "@types/node": ">= 8" + } + }, + "@types/node": { + "version": "14.14.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz", + "integrity": "sha512-6QlRuqsQ/Ox/aJEQWBEJG7A9+u7oSYl3mem/K8IzxXG/kAGbV1YPD9Bg9Zw3vyxC/YP+zONKwy8hGkSt1jxFMw==" + }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, "yaml": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz", diff --git a/scripts/package.json b/scripts/package.json index 47878207..d436ee54 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -4,6 +4,8 @@ "description": "", "main": "substitute_article_urls.js", "dependencies": { + "@octokit/graphql": "^4.5.7", + "dotenv": "^8.2.0", "yaml": "^1.10.0" }, "devDependencies": {},