From d6b427fa87ca2b55a103f0e9f1a7c28687233a9c Mon Sep 17 00:00:00 2001 From: mihay42 Date: Mon, 5 Feb 2024 19:58:41 -0800 Subject: [PATCH 01/10] Coded actions install routine plus cleanups --- cli/actions/.gitignore | 2 + .../actions/basic-reporting/action.yml | 9 + cli/actions/actions/basic-reporting/github.js | 200 +++++++++ cli/actions/actions/basic-reporting/index.js | 53 +++ .../actions/basic-reporting/package-lock.json | 233 +++++++++++ .../actions/basic-reporting/package.json | 17 + .../actions/basic-reporting/reports.js | 300 ++++++++++++++ cli/actions/actions/prune-branches/action.yml | 9 + cli/actions/actions/prune-branches/index.js | 47 +++ .../actions/prune-branches/package-lock.json | 227 +++++++++++ .../actions/prune-branches/package.json | 16 + cli/actions/workflows/basic-reporting.yml | 20 + cli/actions/workflows/prune-branches.yml | 20 + cli/mrcli-github.js | 144 ------- cli/mrcli-setup-mrserver.js | 384 ------------------ cli/mrcli-setup.js | 107 ++++- package.json | 2 +- src/api/github.js | 21 +- src/cli/filesystem.js | 9 + src/cli/installInstructions.js | 2 + src/cli/interactionWizard.js | 2 +- 21 files changed, 1279 insertions(+), 545 deletions(-) create mode 100644 cli/actions/.gitignore create mode 100644 cli/actions/actions/basic-reporting/action.yml create mode 100644 cli/actions/actions/basic-reporting/github.js create mode 100644 cli/actions/actions/basic-reporting/index.js create mode 100644 cli/actions/actions/basic-reporting/package-lock.json create mode 100644 cli/actions/actions/basic-reporting/package.json create mode 100644 cli/actions/actions/basic-reporting/reports.js create mode 100644 cli/actions/actions/prune-branches/action.yml create mode 100644 cli/actions/actions/prune-branches/index.js create mode 100644 cli/actions/actions/prune-branches/package-lock.json create mode 100644 cli/actions/actions/prune-branches/package.json create mode 100644 cli/actions/workflows/basic-reporting.yml create mode 100644 cli/actions/workflows/prune-branches.yml delete mode 100755 cli/mrcli-github.js delete mode 100755 cli/mrcli-setup-mrserver.js diff --git a/cli/actions/.gitignore b/cli/actions/.gitignore new file mode 100644 index 0000000..28f1ba7 --- /dev/null +++ b/cli/actions/.gitignore @@ -0,0 +1,2 @@ +node_modules +.DS_Store \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/action.yml b/cli/actions/actions/basic-reporting/action.yml new file mode 100644 index 0000000..01ee5b5 --- /dev/null +++ b/cli/actions/actions/basic-reporting/action.yml @@ -0,0 +1,9 @@ +name: 'Basic Reporting' +description: 'Generates basic reports for companies plus wrapping README.md files in the repository' +inputs: + github-token: + description: 'GitHub token' + required: true +runs: + using: 'node20' + main: 'index.js' \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/github.js b/cli/actions/actions/basic-reporting/github.js new file mode 100644 index 0000000..6c483e1 --- /dev/null +++ b/cli/actions/actions/basic-reporting/github.js @@ -0,0 +1,200 @@ +const github = require('@actions/github') +const core = require('@actions/core') + +async function readObjects (fileName) { + const token = core.getInput('github-token', { required: true }) + const octokit = github.getOctokit(token) + + const { data: file } = await octokit.rest.repos.getContent({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + path: fileName, + ref: 'main' + }) + + // check to see if the file is empty and if it is return an empty object + if (file.size === 0) { + return [] + } + + // Return the file content as an object + return JSON.parse(Buffer.from(file.content, 'base64').toString()) +} + +async function readWorkflows () { + const token = core.getInput('github-token', { required: true }) + const octokit = github.getOctokit(token) + + const workflows = await octokit.rest.actions.listWorkflowRunsForRepo({ + owner: github.context.repo.owner, + repo: github.context.repo.repo + }) + + const workflowList = [] + let totalRunTimeThisMonth = 0 + for (const workflow of workflows.data.workflow_runs) { + // Get the current month + const currentMonth = new Date().getMonth() + + // Compute the runtime and if the time is less than 60s round it to 1m + const runTime = Math.ceil((new Date(workflow.updated_at) - new Date(workflow.created_at)) / 1000 / 60) < 1 ? 1 : Math.ceil((new Date(workflow.updated_at) - new Date(workflow.created_at)) / 1000 / 60) + + // If the month of the workflow is not the current month, then skip it + if (new Date(workflow.updated_at).getMonth() !== currentMonth) { + continue + } + totalRunTimeThisMonth += runTime + + // Add the workflow to the workflowList + workflowList.push({ + run_time_minutes: runTime, + path: workflow.path, + name: workflow.path.replace('.github/workflows/', '').replace('.yml', ''), + run_time_name: workflow.name, + conclusion: workflow.conclusion, + created_at: workflow.created_at, + updated_at: workflow.updated_at, + html_url: workflow.html_url + }) + } + + // Sort the worflowList to put the most recent workflows first + workflowList.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) + + // log the length of the workflowList + console.log(`Number of workflows: ${workflowList.length}`) + + return {allWorkFlows: workflowList, runTime: totalRunTimeThisMonth} +} + +async function readBranches () { + const token = core.getInput('github-token', { required: true }) + const octokit = github.getOctokit(token) + + let { data: branches } = await octokit.rest.repos.listBranches({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + }) + + // Loop through the branches to obtain the latest commit comment, author, and date added to the branch, and then add them to the branches array + for (const branch of branches) { + const { data: commit } = await octokit.rest.repos.getBranch({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + branch: branch.name + }) + branch.commit = commit.commit.sha + branch.author = commit.commit.author.name + branch.date = commit.commit.author.date + } + + return branches +} + +// Create a function that compares the current list of companies and the associated markdown files for each company present in the repository. Should there be a markdown file for a company that is not in the current list of companies, then the file should be deleted. +async function reconcileCompanyFiles (companies) { + const token = core.getInput('github-token', { required: true }) + const octokit = github.getOctokit(token) + + // Get the list of company files + const { data: companyFiles } = await octokit.rest.repos.getContent({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + path: 'Companies', + ref: 'main' + }) + // Filter in only the markdown files that aren't the README.md + const companyFilesFiltered = companyFiles.filter(file => file.name.endsWith('.md') && file.name !== 'README.md') + + // Using the supplied companies create a new object with the key being the company markdown and the value being the company name + let markdownToCompanies = companies.reduce((acc, company) => { + const markdown = company.name.replace(/[\s,.\?!]/g, '') + acc[`${markdown}.md`] = company.name + return acc + } , {}) + + // Check to see if if each companyFilesFiltered is in the markdownToCompanies object, if it isn't then delete the file + const deleteFiles = [] + for (const file of companyFilesFiltered) { + if (!markdownToCompanies[file.name]) { + await octokit.rest.repos.deleteFile({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + path: `Companies/${file.name}`, + message: `Delete ${file.name} report`, + branch: 'main' + }) + // Add the file to the deleteFiles array + deleteFiles.push(file.name) + } + } + // Return the deleteFiles array + return deleteFiles +} + +async function saveReports (reports, inputs) { + // Reconile the company files + const prunedMarkdowns = await reconcileCompanyFiles(inputs.companies) + + const token = core.getInput('github-token', { required: true }) + const octokit = github.getOctokit(token) + + // Create or update each file in a single commit + for (const report of reports) { + // Get the sha of the file if it exists + try { + const { data: file } = await octokit.rest.repos.getContent({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + path: report.path, + ref: 'main' + }) + // If the file exists then get the sha + if (file) { + report.sha = file.sha + } + } catch (error) { + // If the file doesn't exist then set the sha to null + report.sha = null + } + + // Create or update the file + // NOTE: The file must be encoded as a base64 string + // NOTE: Unsure how to handle the case where the file exists vs doesn't exist + + // If the file doesn't exist then create then report.sha will be null, so create the object to create the file without the sha + let octokitContent = { + owner: github.context.repo.owner, + repo: github.context.repo.repo, + // If the file exists then update it, otherwise create it + path: report.path, + message: `Update ${report.name} report`, + content: Buffer.from(report.content).toString('base64'), + sha: report.sha, // The blob SHA of the file being replaced + branch: 'main', + committer: { + name: github.context.actor, + email: `${github.context.actor}@users.noreply.github.com` + }, + author: { + name: github.context.actor, + email: `${github.context.actor}@users.noreply.github.com` + } + } + + // if report.sha is null delete the sha property from the object + if (!report.sha) { + delete octokitContent.sha + } + + await octokit.rest.repos.createOrUpdateFileContents(octokitContent) + } +} + + +module.exports = { + readObjects, + readWorkflows, + readBranches, + saveReports +} \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/index.js b/cli/actions/actions/basic-reporting/index.js new file mode 100644 index 0000000..4b67e20 --- /dev/null +++ b/cli/actions/actions/basic-reporting/index.js @@ -0,0 +1,53 @@ +// Load the modules +const { readObjects, readBranches, readWorkflows, saveReports } = require('./github.js') +const { createCompaniesReport, createCompanyReports, createMainReport } = require('./reports.js') + + +// Create the run function that creates the reports +async function run () { + // Define inputs + const inputs = { + companies: await readObjects('/Companies/Companies.json'), + interactions: await readObjects('/Interactions/Interactions.json'), + branches: await readBranches(), + workflows: await readWorkflows() + } + + // If there are no companies then return + if (inputs.companies.length === 0) { + return + } + + // Define reports + const reports = { + companies: `Companies/README.md`, + company: `Companies/`, + main: `README.md`, + } + + // Create the company files + const companyFiles = await createCompanyReports(inputs.companies, inputs.interactions, reports) + // Create the companies file + const companiesFile = createCompaniesReport(inputs.companies) + // Create the main file + const mainFile = createMainReport(inputs) + // Create the reports array + const markdownReports = [ + { + name: 'Companies', + path: reports.companies, + content: companiesFile + }, + { + name: 'Main', + path: reports.main, + content: mainFile + }, + ...companyFiles + ] + + // Write the reports + await saveReports(markdownReports, inputs) +} + +run() \ No newline at end of file diff --git a/cli/actions/actions/basic-reporting/package-lock.json b/cli/actions/actions/basic-reporting/package-lock.json new file mode 100644 index 0000000..c86788f --- /dev/null +++ b/cli/actions/actions/basic-reporting/package-lock.json @@ -0,0 +1,233 @@ +{ + "name": "basic-reporting", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "basic-reporting", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "mr_markdown_builder": "^0.9.3" + } + }, + "node_modules/@actions/core": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", + "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/github": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", + "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", + "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "dependencies": { + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "dependencies": { + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", + "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", + "dependencies": { + "@octokit/types": "^12.4.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz", + "integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==", + "dependencies": { + "@octokit/types": "^12.3.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/request": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", + "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", + "dependencies": { + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "dependencies": { + "@octokit/types": "^12.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", + "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", + "dependencies": { + "@octokit/openapi-types": "^19.1.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/mr_markdown_builder": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/mr_markdown_builder/-/mr_markdown_builder-0.9.3.tgz", + "integrity": "sha512-jdXy1B/FR9FqnSAHgsmBMQ6V5Y9/34Cqt0pSwTwI8IZZmtGoi+YTycn+m3V2u2x57ogfHvJa6iyBO6pLBtMIiA==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.28.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", + "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/cli/actions/actions/basic-reporting/package.json b/cli/actions/actions/basic-reporting/package.json new file mode 100644 index 0000000..f3edad3 --- /dev/null +++ b/cli/actions/actions/basic-reporting/package.json @@ -0,0 +1,17 @@ +{ + "name": "basic-reporting", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0", + "mr_markdown_builder": "^0.9.3" + } +} diff --git a/cli/actions/actions/basic-reporting/reports.js b/cli/actions/actions/basic-reporting/reports.js new file mode 100644 index 0000000..61a55e1 --- /dev/null +++ b/cli/actions/actions/basic-reporting/reports.js @@ -0,0 +1,300 @@ +const mrMarkdownBuilder = require('mr_markdown_builder') + +// Globals +const MAPS_WARNING = `**Notice:** If you are using Safari a geojson map may not display correctly unless you disable the \`Prevent cross-site tracking\` feature in the \`Privacy tab\` of Safari's preferences. Disabling cross-site tracking in Safari is a workaround and the GitHub team is working on a fix to this issue. You can add any additional information on this matter in the GitHub Community discussion [GeoJSON rendering is broken #19258](https://github.com/orgs/community/discussions/19258). However, geojson rendering has been tested and works with Chrome and Edge, but if there is only a single point on the map you will need to zoom-out to see the point's context.\n` + +const ACTION_WARNING = `**Notice:** This page is automatically generated by the custom GitHub action \`basic-reporting\`. It is scheduled to run at 12:00 AM everyday and will update \`README.md\` files in the \`/Companies\` directory, generate a \`.md\` file for each company in the \`/Companies\` directory, and update the main \`README.md\` file in the root of the repository. Manual updates to these files are not recommended as they will be overwritten the next time the action runs.\n` + +async function createBadges (company) { + // Create a badge for the company role + const badgesRow = [ + await mrMarkdownBuilder.badge(encodeURIComponent('Role'), company.role), + await mrMarkdownBuilder.badge(encodeURIComponent('Type'), encodeURIComponent(company.company_type)), + await mrMarkdownBuilder.badge(encodeURIComponent('Region'), company.region), + await mrMarkdownBuilder.badge(encodeURIComponent('Creator'), encodeURIComponent(company.creator_name)) + ] + return "\n" + badgesRow.join('  ') + "\n" +} + +function createIndustryList (company) { + const industryDataList = [ + `**Industry** → ${company.industry} (Code: ${company.industry_code})`, + `**Industry Group** → ${company.industry_group_description} (Code: ${company.industry_group_code})`, + `**Major Group** → ${company.major_group_description} (Code: ${company.major_group_code})` + ] + // Create a list of industries + const industryList = `${mrMarkdownBuilder.h2('Standard Industry Code (SIC) Details')}\n${mrMarkdownBuilder.ul(industryDataList)}` + return industryList +} + +function createCompanyWebLinkList (company) { + + // Create the table rows + let wikipediaURL + company.wikipedia_url === 'Unknown' ? + wikipediaURL = `The Wikipedia URL is ${company.wikipedia_url}` : + wikipediaURL = mrMarkdownBuilder.link(`Wikipedia for ${company.name}`, company.wikipedia_url) + let listItems = [ + + [wikipediaURL], + [mrMarkdownBuilder.link(`${company.name} on Google News`, company.google_news_url)], + [mrMarkdownBuilder.link(`Map for ${company.name}`, company.google_maps_url)], + [mrMarkdownBuilder.link(`${company.name} Patents`, company.google_patents_url)] + ] + // If the company is public then add the public properties + if (company.company_type === 'Public') { + listItems.push( + [mrMarkdownBuilder.link(`Google Finance`, company.google_finance_url)], + [mrMarkdownBuilder.link(`Most Recent 10-K Filing`, company.recent10k_url)], + [mrMarkdownBuilder.link(`Most Recent 10-Q Filing`, company.recent10q_url)], + [mrMarkdownBuilder.link(`SEC EDGAR Firmographics`, company.firmographics_url)], + [mrMarkdownBuilder.link(`All Filings for ${company.name}`, company.filings_url)], + [mrMarkdownBuilder.link(`Shareholder Transactions`, company.owner_transactions_url)] + ) + } + // Create the table + return mrMarkdownBuilder.h2('Key Web Links') + "\n" + mrMarkdownBuilder.ul(listItems) +} + +function createInteractionList (company, interactions) { + // Create a list of interactions + const interactionNames = Object.keys(company.linked_interactions) + const interactionList = interactionNames.map((interactionName) => { + // Find the interaction object that matches the interaction name + const interaction = interactions.find((interaction) => interaction.name === interactionName) + // Create link internal link to the interaction file + const interactionLink = mrMarkdownBuilder.link(interaction.name, `/${encodeURI(interaction.url)}`) + return interactionLink + }) + + return `${mrMarkdownBuilder.h2('Interactions')} \n ${mrMarkdownBuilder.ul(interactionList)}` +} + +function createCompanyMap (company) { + let geoJsonMarkdown = mrMarkdownBuilder.h2('Location') + geoJsonMarkdown += MAPS_WARNING + // Create the Industry List + const geoJson = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [company.longitude, company.latitude] + }, + properties: { + name: company.name, + description: company.description, + role: company.role, + url: company.url + } + } + // Add the geojson object to the company file + geoJsonMarkdown += mrMarkdownBuilder.geojson(geoJson) + return geoJsonMarkdown +} + +function createCompaniesMap (companies) { + // Create the map + let map = mrMarkdownBuilder.h1('Company Locations') + map += MAPS_WARNING + map += mrMarkdownBuilder.geojson({ + type: 'FeatureCollection', + features: companies.map((company) => { + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [company.longitude, company.latitude] + }, + properties: { + name: company.name, + description: company.description, + role: company.role, + url: company.url + } + } + }) + }) + // return the map + return map +} + +async function createCompanyReports (companies, interactions, reports) { + let companiesReports = [] + const suffix = '.md' + const prefix = reports.company + for (let company of companies) { + // Get the name of the company and remove spaces, commas, periods, question marks, and exclamation points + const companyFileName = company.name.replace(/[\s,.\?!]/g, '') + // Using the emphasis module create a bolded version of the string "Company Name:" + const companyLogo = mrMarkdownBuilder.imageWithSize(`${company.name} Logo`, company.logo_url, 25, company.name) + // Call the h1 method from the headers module + let companyFile = `[${mrMarkdownBuilder.link('Back to Company Directory', './README.md')}]\n` + companyFile += mrMarkdownBuilder.hr() + companyFile += mrMarkdownBuilder.h1(`${companyLogo} ${mrMarkdownBuilder.link(company.name, company.url)}`) + // Add a line break + companyFile += "\n" + // Add the company badges + companyFile += await createBadges(company) + // Add a line break + companyFile += "\n" + // Add the company description + companyFile += `${mrMarkdownBuilder.b('Description:')} ${company.description}` + // Add a line break + companyFile += "\n" + // Add a horizontal rule + companyFile += mrMarkdownBuilder.hr() + // Add a line break + companyFile += "\n" + // Create the Industry List + companyFile += createIndustryList(company) + // If there are linked_interactions in the company then create the interaction list + if (Object.keys(company.linked_interactions).length > 0) { + companyFile += createInteractionList(company, interactions) + } + + // Add a line break + companyFile += "\n" + + // Create the company table + companyFile += createCompanyWebLinkList(company) + + // Add a line break + companyFile += "\n" + + // Add an h2 for the company's location + companyFile += createCompanyMap(company) + + // Add a line break + companyFile += "\n" + + // Add a horizontal rule + companyFile += mrMarkdownBuilder.hr() + + // Add the creation date + companyFile += `[ ${mrMarkdownBuilder.b('Created:')} ${company.creation_date} by ${company.creator_name} | ${mrMarkdownBuilder.b('Modified:')} ${company.modification_date} ]` + + // Return the file content + companiesReports.push({ + name: company.name, + path: `${prefix}${companyFileName}${suffix}`, + content: companyFile + }) + } + return companiesReports +} + +function createCompaniesReport (companies) { + let readme = `[${mrMarkdownBuilder.link('Back to main README', '../README.md')}]\n` + readme += mrMarkdownBuilder.hr() + readme += mrMarkdownBuilder.h1('Introduction') + readme += `There are currently \`${companies.length}\` companies in the repository. The table below lists all available companies and some of their firmographics. Click on the company name to view the company's profile. Below the table is a map of all companies in the repository. Click on a company's marker to view additional company information in context.` + readme += mrMarkdownBuilder.h1('Table of Companies') + // Create the table header + const tableHeader = mrMarkdownBuilder.tableHeader(['Company Name', 'Company Type', 'Company Role', 'Company Region']) + // Create the table rows + const tableRows = companies.map((company) => { + const companyRow = [ + mrMarkdownBuilder.link(company.name, `./${encodeURI(company.name.replace(/[\s,.\?!]/g, ''))}.md`), + company.company_type, + company.role, + company.region + ] + return companyRow + }) + // Create the table + const companyTable = tableHeader + "\n" + mrMarkdownBuilder.tableRows(tableRows) + // Create the README.md file + readme += companyTable + // Add a line break + readme += "\n" + // Call the createMap function + readme += createCompaniesMap(companies) + // Return the file content + return readme +} + +// Create a function that generates the main report +function createMainReport (inputs) { + // Loop through all of the inputs.companies to find the one company with the role of "Owner" + const owner = inputs.companies.find((company) => company.role === 'Owner') + // Create the owner logo + const ownerLogo = mrMarkdownBuilder.imageWithSize(`${owner.name} Logo`, owner.logo_url, 25, owner.name) + + // Create the main report + let readme = mrMarkdownBuilder.h1(`${ownerLogo} Product and Service Discovery Repository for ${owner.name}`) + readme += `Welcome to your discovery repository meant to help you build products and services via an evidence based approach. Presently, the repository contains \`${inputs.companies.length}\` companies and \`${inputs.interactions.length}\` interactions. This repository means to be the warehouse for all evidence needed to generate the why\'s and what\'s for your product or service plans. It is intentionally integrated into GitHub to help you leverage the power of GitHub's ecosystem.` + + // Create a paragraph about companies, the title should be h2 and contain a link to the Companies/README.md file + readme += mrMarkdownBuilder.h2(`Companies [${mrMarkdownBuilder.link('View Companies', './Companies/README.md')}]`) + readme += `Companies are presently the primary object used and the repository contains \`${inputs.companies.length}\` of them. This means you'll be able to find information about these companies in the repository. Each company has a profile page containing information about the company including its name, description, industry, and location. Additionally, each company has a list of interactions that are linked to it.` + + // Create a paragraph about interactions, the title should be h2 and not contain a link to the Interactions/README.md file + readme += mrMarkdownBuilder.h2(`Interactions`) + readme += `Interaction objects are essentially content related to a company. They can include meeting notes, emails, product documentation, blog posts, audio transcripts, and more. While each interaction is linked to a company access to the interaction is presented handled by the company that owns it.` + + readme += mrMarkdownBuilder.h2(`Studies`) + readme += `Study objects will be a part of a future release of Mediumroast and will be a part of a paid subscription. Stay tuned for more information on this feature and a way to gain access to it.` + + readme += mrMarkdownBuilder.h2('Navigation and Modification') + readme +=`Direct navigation and modifcation of repository contents is not recommended. Instead this README file, and accompanying markdown files, will guide you through its contents. Additionally, the open source node module and CLI \`mediumroast_js\` [${mrMarkdownBuilder.link('GitHub', 'https://github.com/mediumroast/mediumroast_js')}, ${mrMarkdownBuilder.link('NPM', 'https://www.npmjs.com/package/mediumroast_js')}] can be used to create, update, and delete content in the repository.` + + // Add the notice + readme += mrMarkdownBuilder.h2('Notice') + readme += ACTION_WARNING + + // Create a paragraph that focuses on listing the active workflows and their last status + readme += mrMarkdownBuilder.h2('Workflows') + // Get the current month and year and put them in a single string + const date = new Date() + const month = date.toLocaleString('default', { month: 'long' }) + const year = date.getFullYear() + + readme += `The repository contains \`2\` active workflows. As of \`${month}-${year}\` \`${inputs.workflows.runTime} minutes\` have been consumed. A GitHub free plan has \`2000 minutes\` available per month meaning there is \`${2000 - inputs.workflows.runTime}\` remaining minutes for the month. Assuming a repository with 10s of company objects, each workflow runs about a minute at midnight everyday. This means a good hueristic for how many minutes are consumed in a month is 2 workflows/day x 1 min/workflow x 30 days/month or \`${2*1*30} min/month\`. To get an accurate view of your consumed minutes for your planning please run \`mrcli billing\`. The statuses of five most recent workflow runs are provided below, links are included to enable more information on the workflows.\n` + // Create the table header + const workflowTableHeader = mrMarkdownBuilder.tableHeader(['Workflow Name', 'Last Status', 'Run Time Message', 'Run Time']) + // Create the table rows + const myWorkflows = inputs.workflows.allWorkFlows.slice(1, 6) + const workflowTableRows = myWorkflows.map((workflow) => { + const workflowRow = [ + mrMarkdownBuilder.link(workflow.name, `./.github/workflows/${workflow.name}.yml`), + mrMarkdownBuilder.link(workflow.conclusion, workflow.html_url), + workflow.run_time_name, + `${workflow.run_time_minutes} minute(s)` + ] + return workflowRow + }) + // Create the table + const workflowTable = workflowTableHeader + "\n" + mrMarkdownBuilder.tableRows(workflowTableRows) + // Add the table to the README.md file + readme += workflowTable + + // // Add a line break + // readme += "\n" + + // Create a paragraph that lists the branches and their last commit + // readme += mrMarkdownBuilder.h2('Branches') + // readme += `The repository contains \`${inputs.branches.length}\` branches. The last commit of each branch is listed below. Click on the branch name to view the branch.\n` + // // Create the table header + // const branchTableHeader = mrMarkdownBuilder.tableHeader(['Branch Name', 'Last Commit']) + // // Create the table rows + // const branchTableRows = inputs.branches.map((branch) => { + // const branchRow = [ + // mrMarkdownBuilder.link(branch.name, `./tree/${branch.name}`), + // branch.last_commit + // ] + // return branchRow + // }) + // // Create the table + // const branchTable = branchTableHeader + "\n" + mrMarkdownBuilder.tableRows(branchTableRows) + // // Add the table to the README.md file + // readme += branchTable + + // Return the report content + return readme +} + +module.exports = { + createCompanyReports, + createCompaniesReport, + createMainReport +} \ No newline at end of file diff --git a/cli/actions/actions/prune-branches/action.yml b/cli/actions/actions/prune-branches/action.yml new file mode 100644 index 0000000..5824556 --- /dev/null +++ b/cli/actions/actions/prune-branches/action.yml @@ -0,0 +1,9 @@ +name: 'Prune Branches' +description: 'Keeps a set number of branches available for the user to recover from' +inputs: + github-token: + description: 'GitHub token' + required: true +runs: + using: 'node20' + main: 'index.js' \ No newline at end of file diff --git a/cli/actions/actions/prune-branches/index.js b/cli/actions/actions/prune-branches/index.js new file mode 100644 index 0000000..8cff8d1 --- /dev/null +++ b/cli/actions/actions/prune-branches/index.js @@ -0,0 +1,47 @@ +const core = require('@actions/core') +const github = require('@actions/github') + +async function run(maxBranches) { + try { + const token = core.getInput('github-token', { required: true }) + const octokit = github.getOctokit(token) + + let { data: branches } = await octokit.rest.repos.listBranches({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + }) + + // Create a variable to keep track of the number of non-number branches + let nonNumberBranchCount = 0 + for (const branch of branches) { + // Skip any branch is that is not a number (e.g. master, main, etc.), but keep a count of them. + if (isNaN(branch.name)) { + nonNumberBranchCount++ + continue + } + + // If the branch count is greater than the maxBranches, assuming this timestamp format 1706153906529, find all of the oldest branches greater than the maxBranches and put them in an array + if (branches.length - nonNumberBranchCount > maxBranches) { + const branchTimestamp = parseInt(branch.name) + const branchesToDelete = branches.filter(b => parseInt(b.name) < branchTimestamp) + for (const branchToDelete of branchesToDelete) { + // Log the branch name to be deleted + console.log(`Deleting branch ${branchToDelete.name}`) + await octokit.rest.git.deleteRef({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + ref: `heads/${branchToDelete.name}`, + }) + // Remove the branch from the branches array + branches.splice(branches.indexOf(branchToDelete), 1) + } + } + } + } catch (error) { + core.setFailed(error.message) + } + } + +// Set a variable maxBranches to 15 +const maxBranches = 15 +run(maxBranches) \ No newline at end of file diff --git a/cli/actions/actions/prune-branches/package-lock.json b/cli/actions/actions/prune-branches/package-lock.json new file mode 100644 index 0000000..04f5cf9 --- /dev/null +++ b/cli/actions/actions/prune-branches/package-lock.json @@ -0,0 +1,227 @@ +{ + "name": "prune-branches", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prune-branches", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0" + } + }, + "node_modules/@actions/core": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", + "integrity": "sha512-3lBR9EDAY+iYIpTnTIXmWcNbX3T2kCkAEQGIQx4NVQ0575nk2k3GRZDTPQG+vVtS2izSLmINlxXf0uLtnrTP+g==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/github": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-6.0.0.tgz", + "integrity": "sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g==", + "dependencies": { + "@actions/http-client": "^2.2.0", + "@octokit/core": "^5.0.1", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" + } + }, + "node_modules/@actions/http-client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", + "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", + "dependencies": { + "tunnel": "^0.0.6", + "undici": "^5.25.4" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "dependencies": { + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "dependencies": { + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", + "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", + "dependencies": { + "@octokit/types": "^12.4.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz", + "integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==", + "dependencies": { + "@octokit/types": "^12.3.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/request": { + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", + "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", + "dependencies": { + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "dependencies": { + "@octokit/types": "^12.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", + "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", + "dependencies": { + "@octokit/openapi-types": "^19.1.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/undici": { + "version": "5.28.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.2.tgz", + "integrity": "sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + } + } +} diff --git a/cli/actions/actions/prune-branches/package.json b/cli/actions/actions/prune-branches/package.json new file mode 100644 index 0000000..a25468b --- /dev/null +++ b/cli/actions/actions/prune-branches/package.json @@ -0,0 +1,16 @@ +{ + "name": "prune-branches", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@actions/core": "^1.10.1", + "@actions/github": "^6.0.0" + } +} diff --git a/cli/actions/workflows/basic-reporting.yml b/cli/actions/workflows/basic-reporting.yml new file mode 100644 index 0000000..efd37f3 --- /dev/null +++ b/cli/actions/workflows/basic-reporting.yml @@ -0,0 +1,20 @@ +name: basic-reporting +run-name: ${{ github.actor }} executed basic-reporting action +on: + schedule: + - cron: '0 0 * * *' # Runs at midnight every day +jobs: + prune-branches: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '20' + - name: Install dependencies + run: npm install + working-directory: ./.github/actions/basic-reporting + - name: Generate basic reports for companies + uses: ./.github/actions/basic-reporting + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/cli/actions/workflows/prune-branches.yml b/cli/actions/workflows/prune-branches.yml new file mode 100644 index 0000000..e993545 --- /dev/null +++ b/cli/actions/workflows/prune-branches.yml @@ -0,0 +1,20 @@ +name: prune-branches +run-name: ${{ github.actor }} executed prune-branches action +on: + schedule: + - cron: '0 0 * * *' # Runs at midnight every day +jobs: + prune-branches: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '20' + - name: Install dependencies + run: npm install + working-directory: ./.github/actions/prune-branches + - name: Prune stale branches + uses: ./.github/actions/prune-branches + with: + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/cli/mrcli-github.js b/cli/mrcli-github.js deleted file mode 100755 index f759bbc..0000000 --- a/cli/mrcli-github.js +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env node - -import { Companies, Interactions, Studies } from '../src/api/gitHubServer.js' -import Environmentals from '../src/cli/env.js' -import CLIOutput from '../src/cli/output.js' -import FilesystemOperators from '../src/cli/filesystem.js' -import chalk from 'chalk' -import GitHubFunctions from '../src/api/github.js' -import * as progress from 'cli-progress' - -console.log(chalk.bold.yellow('NOTICE: This CLI is being deprecated and will be removed in a future release.')) - -/* - ----------------------------------------------------------------------- - - MAIN - Steps below represent the main function of the program - - ----------------------------------------------------------------------- -*/ - -// Related object type -const objectType = 'Interactions' - -// Environmentals object -const environment = new Environmentals( - '3.0', - `${objectType}`, - `Command line interface for mediumroast.io ${objectType} objects.`, - objectType -) - -// Filesystem object -const fileSystem = new FilesystemOperators() - -// Create the environmental settings -const myArgs = environment.parseCLIArgs() -const myConfig = environment.readConfig(myArgs.conf_file) -const myEnv = environment.getEnv(myArgs, myConfig) -const accessToken = await environment.verifyAccessToken() -const processName = 'mrcli-interaction' - -// Output object -const output = new CLIOutput(myEnv, objectType) - -// Construct the controller objects -const companyCtl = new Companies(accessToken, myEnv.gitHubOrg, processName) -const studyCtl = new Studies(accessToken, myEnv.gitHubOrg, processName) -const interactionCtl = new Interactions(accessToken, myEnv.gitHubOrg, processName) -const gitHubCtl = new GitHubFunctions(accessToken, myEnv.gitHubOrg, processName) - -let gitHubResp - -gitHubResp = await gitHubCtl.checkForLock(objectType) -if(gitHubResp[0]) { - console.log(`The ${objectType} container is locked, please remove the lock and try again.`) - process.exit(1) -} - - -gitHubResp = await gitHubCtl.lockContainer(objectType) -// Save the sha for the unlock -const lockSha = gitHubResp[2].data.content.sha - -// doSetup = await wizardUtils.operationOrNot( -// `Did the lock occur?` -// ) - -// Create a new Branch -gitHubResp = await gitHubCtl.createBranchFromMain() - -// console.log(gitHubResp[2].data) - -// doSetup = await wizardUtils.operationOrNot( -// `Did the Branch create?` -// ) - -const branchName = gitHubResp[2].data.ref -const branchSha = gitHubResp[2].data.object.sha - - -// Read the blob -const fileName = './sample doc_one 1.pdf' -const dirName = './sample_pdf' -// const fileData = fileSystem.readBlobFile(fileName) -const progressBar = new progress.SingleBar( - {format: '\tProgress [{bar}] {percentage}% | ETA: {eta}s | {value}/{total}'}, - progress.Presets.rect -) - -let myFiles = [] -const allFiles = fileSystem.listAllFiles(dirName) -progressBar.start(allFiles[2].length - 1, 0) -for(const myIdx in allFiles[2]) { - // Set the file name for easier readability - const fileName = allFiles[2][myIdx] - // Skip files that start with . including present and parent working directories - if(fileName.indexOf('.') === 0) { continue } - const fileData = fileSystem.readBlobFile(`${dirName}/${fileName}`) - const insteractionResp = await gitHubCtl.writeBlob(objectType, fileName, fileData[2], branchName, branchSha) - myFiles.push(insteractionResp[2].data.commit) - // Increment the progress bar - progressBar.increment() -} -progressBar.stop() -console.log('Finished writing files to the repository.') - -// Create the object -// const insteractionResp = await gitHubCtl.writeBlob(objectType, fileName, fileData[2], branchName, branchSha) - -// console.log(insteractionResp[2].data.commit) - -// Read objects from the repository -// gitHubResp = await gitHubCtl.readObjects(objectType) -// console.log(gitHubResp[2].mrJson) -// doSetup = await wizardUtils.operationOrNot( -// `Objects read?` -// ) -// const objectSha = gitHubResp[2].data.sha -// gitHubResp = await gitHubCtl.writeObject(objectType, companies[2], branchName, objectSha) -// console.log(gitHubResp[2]) - -// doSetup = await wizardUtils.operationOrNot( -// `Objects write?` -// ) - -// Merge branch into main -gitHubResp = await gitHubCtl.mergeBranchToMain(branchName, branchSha) -// console.log(gitHubResp[2]) - - -// doSetup = await wizardUtils.operationOrNot( -// `Did the Branch merge?` -// ) - -gitHubResp = await gitHubCtl.unlockContainer(objectType, lockSha, branchName) -// console.log(gitHubResp[2]) -gitHubResp = await gitHubCtl.unlockContainer(objectType, lockSha) -// console.log(gitHubResp[2]) - -// doSetup = await wizardUtils.operationOrNot( -// `Did the repo unlock merge?` -// ) - - diff --git a/cli/mrcli-setup-mrserver.js b/cli/mrcli-setup-mrserver.js deleted file mode 100755 index 4efd4e5..0000000 --- a/cli/mrcli-setup-mrserver.js +++ /dev/null @@ -1,384 +0,0 @@ -#!/usr/bin/env node - -/** - * A CLI utility to setup the configuration file to talk to the mediumroast.io - * @author Michael Hay - * @file mr_setup.js - * @copyright 2023 Mediumroast, Inc. All rights reserved. - * @license Apache-2.0 - * @version 2.1.0 - */ - -console.log(chalk.bold.yellow('NOTICE: This CLI is presently a work in progress and will not operate, exiting.')) -process.exit(0) - -// Import required modules -import { Utilities } from '../src/helpers.js' -import { Auth, Companies, Studies } from '../src/api/mrServer.js' -import CLIOutput from '../src/cli/output.js' -import WizardUtils from '../src/cli/commonWizard.js' -import AddCompany from '../src/cli/companyWizard.js' -import s3Utilities from '../src/cli/s3.js' -import demoEulaText from '../src/cli/demoEula.js' -import Authenticate from '../src/api/authorize.js' -import FilesystemOperators from '../src/cli/filesystem.js' -import MinioUtilities from '../src/cli/minio.js' - -import program from 'commander' -import chalk from 'chalk' -import ConfigParser from 'configparser' -import inquirer from "inquirer" -import { Users } from 'mediumroast_js' -import AddUser from '../src/cli/userWizard.js' - -/* - ----------------------------------------------------------------------- - - FUNCTIONS - Key functions needed for MAIN - - ----------------------------------------------------------------------- -*/ - -function parseCLIArgs() { - // Define commandline options - program - .name("mr_setup") - .version('2.1.0') - .description('A utility for setting up the mediumroast.io CLI.') - - program - // System command line switches - .requiredOption( - '-s --splash ', - 'Whether or not to include the splash screen at startup.', - 'yes', - 'no' - ) - - program.parse(process.argv) - return program.opts() -} - -// Define the key environmental variables to create the appropriate settings -function getEnv () { - return { - DEFAULT: { - mr_server: "https://app.mediumroast.io/api", - company_dns: "https://www.mediumroast.io/company_dns", - company_logos: "https://logo-server.mediumroast.io:7000/allicons.json?url=", - echarts: "https://chart-server.mediumroast.io:11000", - nominatim: 'https://nominatim.openstreetmap.org/search?addressdetails=1&q=', - user_agent: 'mediumroast-cli', - working_directory: "working", - report_output_dir: "Documents", - theme: "coffee", - access_token: "", - access_token_expiry: "", - token_type: "", - device_code: "", - accepted_eula: false, - user_first_name: "", - user_email_address: "", - live: false - }, - s3_settings: { - user: "medium_roast_io", - // api_key: "b7d1ac5ec5c2193a7d6dd61e7a8a76451885da5bd754b2b776632afd413d53e7", - api_key: "", - server: "https://s3.mediumroast.io:9000", - region: "scripps-dc", - // source: "Unknown" // TODO this is deprecated remove after testing - } - } -} - -// Check to see if the directory for the configuration exists, and -// if not create it. Also return the full path to the configuration -// file. -function checkConfigDir(configDir='/.mediumroast', configFile='config.ini') { - utils.safeMakedir(process.env.HOME + configDir) - return process.env.HOME + configDir + '/' + configFile -} - -// Save the configuration file -function writeConfigFile(myConfig, configFile) { - // Write the config file - const configurator = new ConfigParser() - for(const section in myConfig){ - configurator.addSection(section) - for(const setting in myConfig[section]){ - configurator.set(section, setting, myConfig[section][setting]) - } - } - // This won't return anything so we'll need to see if we can find another way to determine success/failure - configurator.write(configFile) -} - -// Verify the configuration was written -function verifyConfiguration(myConfig, configFile) { - const configurator = new ConfigParser() - // Read in the config file and check to see if things are ok by confirming the rest_server value matches - configurator.read(configFile) - const newRestServer = configurator.get('DEFAULT', 'rest_server') - let success = false - if(newRestServer === myConfig.DEFAULT.rest_server) { success = true } - return success -} - -async function getS3APIKey(prompt) { - let apiKey = await wizardUtils.doManual( - prompt, // Object that we should send to doManual - ['key'], // Set of attributes to prompt for - true, // Should we prompt only for the whitelisted attributtes - true // Use an alternative message than the default supplied - ) - if(!apiKey.key) { - apiKey = await getS3APIKey(prompt) - } - return apiKey -} - -/* - ----------------------------------------------------------------------- - - MAIN - Steps below represent the main function of the program - - ----------------------------------------------------------------------- -*/ - -// Parse the commandline arguements -const myArgs = parseCLIArgs() - -// Get the key settings to create the configuration file -let myEnv = getEnv() - -// Define the basic structure of the new object to store to the config file -let myConfig = { - DEFAULT: null, - s3_settings: null -} - -// Assign the env data to the configuration -myConfig.DEFAULT = myEnv.DEFAULT -myConfig.s3_settings = myEnv.s3_settings - -// Construct needed classes -const cliOutput = new CLIOutput(myEnv) -const wizardUtils = new WizardUtils('all') -const utils = new Utilities("all") - -// Unless we suppress this print out the splash screen. -if (myArgs.splash === 'yes') { - cliOutput.splashScreen( - "mediumroast.io Setup Wizard", - "version 2.1.0", - "CLI prompt based setup and registration for the mediumroast.io service." - ) -} - - - -// Are we going to proceed or not? -// const doSetup = await wizardUtils.operationOrNot('You\'d like to setup the mediumroast.io CLI, right?') -// if (!doSetup) { -// console.log(chalk.red.bold('\t-> Ok exiting CLI setup.')) -// process.exit() -// } - - -// // Ask the user to accept the EULA, if they do not the function will exit -// const acceptEula = await wizardUtils.doEula(demoEulaText) -// myConfig.DEFAULT.accepted_eula = acceptEula // Keep the acceptance visible -// cliOutput.printLine() - -// // Perform device flow authorization -const authenticator = new Authenticate() -// // ----------------------- DEVICE CODE ---------------------------- -// const [result, data] = await authenticator.getDeviceCode() -// myConfig.DEFAULT.device_code = data.device_code -// const userCode = data.user_code -// const verificationUri = data.verification_uri -// // const verificationUriComplete = data.verification_uri_complete - -// // Verify the client authorization -// console.log(chalk.blue.bold(`Opening your browser to authorize this client, copy or type this code in your browser [${userCode}].`)) -// await authenticator.verifyClientAuth(verificationUri) -// let authorized = null -// // Prompt the user and await their login and approval -// while (!authorized) { -// authorized = await wizardUtils.operationOrNot('Has the web authorization completed?') -// } - -// // Obtain the token and save to the environmental object -// const theTokens = await authenticator.getTokensDeviceCode(myConfig.DEFAULT.device_code) -// myConfig.DEFAULT.access_token = theTokens[1].access_token -// myConfig.DEFAULT.token_type = theTokens[1].token_type -// myConfig.DEFAULT.access_token_expiry = theTokens[1].expires_in -// cliOutput.printLine() - - -// Create the first user -// TODO user email address and first_name should be added to config file -// Why add these, I don't remember? - -// Generate the needed controllers to interact with the backend -const credential = authenticator.login(myEnv) -const companyCtl = new Companies(credential) -const studyCtl = new Studies(credential) -const userCtl = new Users(credential) - -// Obtain user attributes -console.log(chalk.blue.bold('Learning a little more about you...')) -const uWizard = new AddUser( - myConfig, - userCtl // NOTE: User creation is commented out -) -// TODO: We do not yet know the name of the company so have to update the user later on. -let myUser = await uWizard.wizard(true, false) -myConfig.DEFAULT.company = myUser[2].company_name -cliOutput.printLine() - - -// Create the owning company for the initial user -console.log(chalk.blue.bold('Creating your owning company...')) -myEnv.splash = false -const cWizard = new AddCompany( - myConfig, - companyCtl, // NOTE: Company creation is commented out - myConfig.DEFAULT.company_dns -) -let owningCompany = await cWizard.wizard(true, myConfig.DEFAULT.live) -// console.log(`Firmographics summary for ${owningCompany[2].name}`) -// console.log(`\tWebsite: ${owningCompany[2].url}`) -// console.log(`\tLogo URL: ${owningCompany[2].logo_url}`) -// console.log(`\tIndustry: ${owningCompany[2].industry}`) -// console.log(`\tIndustry code: ${owningCompany[2].industry_code}`) -// console.log(`\tCompany type: ${owningCompany[2].company_type}`) -// console.log(`\tRegion: ${owningCompany[2].region}`) -// console.log(`\tRole: ${owningCompany[2].role}`) -// console.log(`\tLongitude: ${owningCompany[2].longitude}`) -// console.log(`\tLatitude: ${owningCompany[2].latitude}`) -// console.log(`\tMaps URL: ${owningCompany[2].google_maps_url}`) - - -// Set company user name to user name set in the company wizard -myUser.company = owningCompany[2].name - - - -// Create an S3 bucket to store interactions -console.log(chalk.blue.bold(`Establishing the storage container for [${myConfig.DEFAULT.company}] ...`)) - -// Get the key from the command line -const s3PromptObj = { - key: {consoleString: "the provided API Key from mediumroast.io", value: null, altMessage: 'Please input'}, -} -const apiKey = await getS3APIKey(s3PromptObj) -myConfig.s3_settings.api_key = apiKey.key -const myAdvisoryS3 = new s3Utilities(myConfig.s3_settings) - -// Create the s3Name name -// NOTES: -// 1. containerName = userName = s3Name -// 2. userName can only access a container named userName -// 3. Permissions for the container are GET, PUT and LIST, others may be added over time -// 4. -const s3Name = myAdvisoryS3.generateBucketName(myConfig.DEFAULT.company) - -// Create the bucket -const s3Resp = await myAdvisoryS3.s3CreateBucket(s3Name) -if(s3Resp[0]) { - console.log(chalk.blue.bold(`For ${owningCompany[2].name} added storage container [${s3Resp[2].Location}].`)) -} else if (s3Resp[2].code === 'BucketAlreadyOwnedByYou') { - console.log(chalk.blue.red(`Storage container for [${owningCompany[2].name}] already exists, nothing to do.`)) -} else { - console.log(chalk.blue.red(`Cannot add storage container for [${owningCompany[2].name}], exiting.`)) - // TODO: Need to be more graceful in the case where the bucket already exists - process.exit(-1) -} - -// Create the user -// TODO: When we support generic S3 ww must ensure that there are switches that -// shift between Minio and generic S3. Note that this may become a support -// nightmare since to support every cloud variation could be bespoke. -console.log(chalk.blue.bold(`Establishing the storage container credential for [${myConfig.DEFAULT.company}] ...`)) -const minioCtl = new MinioUtilities(myEnv) -const userS3Key = await minioCtl.addMinioUser(s3Name, myConfig.DEFAULT.company) - -// Set the S3 credential information into the env -myConfig.s3_settings.api_key = userS3Key -myConfig.s3_settings.bucket = s3Name -myConfig.s3_settings.user = s3Name -cliOutput.printLine() - -// Persist and verify the config file -// Check for and create the directory process.env.HOME/.mediumroast -const configFile = checkConfigDir() -console.log(chalk.blue.bold('Writing configuration file [' + configFile + '].')) -// Write the config file -writeConfigFile(myConfig, configFile) -// Verify the config file -console.log(chalk.blue.bold('Verifying existence and contents of configuration file [' + configFile + '].')) -const success = verifyConfiguration(myConfig, configFile) -success ? - console.log(chalk.blue.bold('SUCCESS: Verified configuration file [' + configFile + '].')) : - console.log(chalk.red.bold('ERROR: Unable to verify configuration file [' + configFile + '].')) -cliOutput.printLine() - -// Create the first company -// Reset company user name to user name set in the company wizard -myConfig.DEFAULT.company = 'Unknown' -const firstComp = new AddCompany( - myConfig, - companyCtl, // NOTE: Company creation is commented out - myConfig.DEFAULT.company_dns -) -console.log(chalk.blue.bold('Creating the first company ...')) -let firstCompanyResp = await firstComp.wizard(false, myConfig.DEFAULT.live) -const firstCompany = firstCompanyResp[1].data -cliOutput.printLine() - -// Create a default study for interactions and companies to use -console.log(chalk.blue.bold(`Adding default study ...`)) -const myStudy = { - name: 'Default Study', - description: 'A placeholder study to ensure that interactions are able to have something to link to', - public: false, - groups: 'default:default', - document: {} -} -// const studyResp = await studyCtl.createObj(myStudy) -cliOutput.printLine() - -// TODO perform linkages between company and study objects -// cliOutput.printLine() - - -// List all created objects to the console -// console.log(chalk.blue.bold(`Fetching and listing all created objects...`)) -// console.log(chalk.blue.bold(`Default study:`)) -// const myStudies = await studyCtl.getAll() -// cliOutput.outputCLI(myStudies[2]) -// cliOutput.printLine() -// console.log(chalk.blue.bold(`Owning and first companies:`)) -// const myCompanies = await companyCtl.getAll() -// cliOutput.outputCLI(myCompanies[2]) -// cliOutput.printLine() - -// TEMP save objects to /tmp/.json -const fsOps = new FilesystemOperators() -console.log(chalk.blue.bold(`Saving user and company information to /tmp...`)) -fsOps.saveTextFile(`/tmp/user.json`, JSON.stringify(myUser)) -fsOps.saveTextFile(`/tmp/owning_company.json`, JSON.stringify(owningCompany[2])) -fsOps.saveTextFile(`/tmp/first_company.json`, JSON.stringify(firstCompany)) -cliOutput.printLine() - -// Print out the next steps -console.log(`Now that you\'ve performed the initial registration here\'s what\'s next.`) -console.log(chalk.blue.bold(`\t1. Create and register additional companies with mrcli company --add_wizard.`)) -console.log(chalk.blue.bold(`\t2. Register and add interactions with mrcli interaction --add_wizard.`)) -console.log('\nWith additional companies and new interactions registered the mediumroast.io caffeine\nservice will perform basic company comparisons.') -cliOutput.printLine() - - - diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index 878a818..092d4fc 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -27,9 +27,11 @@ import Environmentals from '../src/cli/env.js' import { GitHubAuth } from '../src/api/authorize.js' import { Companies, Users } from '../src/api/gitHubServer.js' import GitHubFunctions from "../src/api/github.js" -import Table from 'cli-table' import ora from "ora" +import * as fs from 'fs' +import * as path from 'path' + /* ----------------------------------------------------------------------- @@ -101,6 +103,29 @@ function printNextSteps() { cliOutput.printLine() } +// Create a function that reads all contents from the actions directory and copies them to the GitHub repository +function installActionsToGitHub(fsUtils, gitHubCtl, myConfig, myEnv, actionsDir) { + // Use the fsUtils to read the contents of the actions directory recursively + const actionFiles = fsUtils.readDirRecursive(actionsDir) + + // Use readBlobFile to read the contents of each file into an object that mirrors the actions directory + const actionObjects = [] + actionFiles.forEach((file) => { + const action = fsUtils.readBlobFile(file) + if(action[0]) { + actionObjects.push({ + name: file, + data: action[2] + }) + } + }) + + // Copy the contents into the GitHub repository into the .guthub directory which should include both actions and workflows subdirectories + const actionsPath = '.github/actions' + const workflowsPath = '.github/workflows' + +} + // NOTE: Commented out until we can confirm it is no longer needed // function printOrgTable(gitHubOrg) { // const table = new Table({ @@ -173,6 +198,70 @@ function verifyConfiguration(myConfig, configFile) { return success } +// Use fs to read all the files in the actions directory recursively +function generateActionsManifest(dir='./actions') { + const files = fs.readdirSync(dir) + let filelist + files.forEach((file) => { + // Skip .DS_Store files and node_modules directories + if (file === '.DS_Store' || file === 'node_modules') { + return + } + if (fs.statSync(path.join(dir, file)).isDirectory()) { + filelist = generateActionsManifest (path.join(dir, file), filelist) + } + else { + // Substitute .github for the first part of the path, in the variable dir + // Log dir to the console including if there are any special characters + if (dir.includes('./')) { + dir = dir.replace('./', '') + } + // This will be the name of the target in the repository + let dotGitHub = dir.replace(/^(\.\/)?actions\//, '.github/') + + filelist.push({ + tgtPath: path.join(dotGitHub, file), + fileName: file, + containerName: dotGitHub, + srcUrl: new URL(path.join(dir, file), import.meta.url) + }) + } + }) + return filelist +} + +async function installActions(actionsManifest) { + // Set up the spinner + let spinner = ora(chalk.bold.blue('Installing GitHub Workflows and Actions')) + spinner.start() // Start the spinner + // Loop through the actionsManifest and install each action + await actionsManifest.forEach(async (action) => { + // Read in the blob file + const [status, msg, blobData] = fsUtils.readBlobFile(action.srcUrl) + if(status) { + // Install the action + const installResp = await gitHubCtl.writeBlob( + action.container, + action.fileName, + blobData, + 'main' + ) + if(installResp[0]) { + spinner.text = `Installed item [${action.fileName}] ` + } else { + spinner.text = `Failed to install item [${action.fileName}]` + return [false, installResp[1], null] + } + } else { + spinner.text = `Failed to read item [${action.fileName}] ... ` + return [false, msg, null] + } + + }) + spinner.stop() // Stop the spinner + return [true, 'All actions installed', null] +} + /* ----------------------------------------------------------------------- @@ -243,11 +332,6 @@ if(configExists[0]) { const installed = await wizardUtils.doInstallInstructions(installText) cliOutput.printLine() -// Ask the user to accept the EULA, if they do not the function will exit -const acceptEula = await wizardUtils.doEula(demoEulaText) -myConfig.DEFAULT.accepted_eula = acceptEula // Keep the acceptance visible -cliOutput.printLine() - /* --------- End check start setup --------- */ /* ----------------------------------------- */ @@ -386,6 +470,17 @@ cliOutput.printLine() /* --------- End create containers --------- */ /* ----------------------------------------- */ +/* ----------------------------------------- */ +/* ------------ Install actions ------------ */ +process.log(chalk.bold.blue(`Installing GitHub Workflows and Actions ... `)) +const actionsManifest = generateActionsManifest() +const installResp = await installActions(actionsManifest) +if(installResp[0]) { + console.log(chalk.bold.green('Installed all Workflows and Actions')) +} else { + console.log(chalk.bold.red(`Failed, exiting with error: [${installResp[1]}]`)) + process.exit(-1) +} /* ----------------------------------------- */ /* ---- Begin initial objects creation ----- */ diff --git a/package.json b/package.json index 5379c45..588cfc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.35", + "version": "0.4.36", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/api/github.js b/src/api/github.js index 18ff474..2226a95 100644 --- a/src/api/github.js +++ b/src/api/github.js @@ -392,16 +392,19 @@ class GitHubFunctions { const fileBits = fileName.split('/') const shortFilename = fileBits[fileBits.length - 1] // Using the github API write a file to the container + let octoObj = { + owner: this.orgName, + repo: this.repoName, + path: `${containerName}/${shortFilename}`, + message: `Create object [${shortFilename}]`, + content: blob, + branch: branchName + } + if(sha) { + octoObj.sha = sha + } try { - const writeResponse = await this.octCtl.rest.repos.createOrUpdateFileContents({ - owner: this.orgName, - repo: this.repoName, - path: `${containerName}/${shortFilename}`, - message: `Create object [${shortFilename}]`, - content: blob, - branch: branchName, - sha: sha - }) + const writeResponse = await this.octCtl.rest.repos.createOrUpdateFileContents(octoObj) // Return the write response if the write was successful or an error if not return [true, `SUCCESS: wrote object [${fileName}] to container [${containerName}]`, writeResponse] } catch (err) { diff --git a/src/cli/filesystem.js b/src/cli/filesystem.js index b2ef6b3..6d084b8 100644 --- a/src/cli/filesystem.js +++ b/src/cli/filesystem.js @@ -191,6 +191,15 @@ class FilesystemOperators { } } + // Create a function that will recursively list all files in a directory + /** + * @function globAllFilesRecursive + * @description List all contents of the directory recursively + * @param {String} dirName - full path of the directory to list the contents of + * @returns {Array} containing the status of the rmdir operation, status message and either the file contents or null + */ + + /** * @function checkFilesystemObjectType * @description Check the type of file system object diff --git a/src/cli/installInstructions.js b/src/cli/installInstructions.js index 2b4fc93..b1293a9 100644 --- a/src/cli/installInstructions.js +++ b/src/cli/installInstructions.js @@ -8,6 +8,8 @@ tool will fail. You can learn more about and install the GitHub Application by visiting the following URL: https://github.com/apps/mediumroast-for-github A more detailed product description, help and other related documentation ca be found here: https://www.mediumroast.io/ + +WARNING: This setup installs two GitHub Actions, and their associated workflows, into the repository that it creates. These Actions consume GitHub Actions minutes and may incur charges to your GitHub account if you are over the allowed actions for your plan. Please consider reviewing the GitHub Actions pricing and usage documentation before continuing. ` export default installText diff --git a/src/cli/interactionWizard.js b/src/cli/interactionWizard.js index 5b7aac0..2c86faf 100755 --- a/src/cli/interactionWizard.js +++ b/src/cli/interactionWizard.js @@ -129,7 +129,7 @@ class AddInteraction { // List all files in the directory and process them one at a time const allFiles = this.fileSystem.listAllFiles(myPath) // Start the progress bar - const totalInteractions = allFiles[2].length - 1 + const totalInteractions = allFiles[2].length this.progressBar.start(totalInteractions, 0) // Iterate through each file in the directory for(const myIdx in allFiles[2]) { From ed3adfffb641899d5adacab5619e42d141ce5841 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Thu, 8 Feb 2024 21:13:02 -0800 Subject: [PATCH 02/10] Actions installation complete --- cli/mrcli-setup.js | 53 +++++++++++++++------------------- package.json | 2 +- src/cli/installInstructions.js | 12 ++++++-- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index 092d4fc..b2f35e5 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -199,16 +199,17 @@ function verifyConfiguration(myConfig, configFile) { } // Use fs to read all the files in the actions directory recursively -function generateActionsManifest(dir='./actions') { +function generateActionsManifest(dir, filelist) { + dir = dir || './actions' const files = fs.readdirSync(dir) - let filelist + filelist = filelist || [] files.forEach((file) => { // Skip .DS_Store files and node_modules directories if (file === '.DS_Store' || file === 'node_modules') { return } if (fs.statSync(path.join(dir, file)).isDirectory()) { - filelist = generateActionsManifest (path.join(dir, file), filelist) + filelist = generateActionsManifest(path.join(dir, file), filelist) } else { // Substitute .github for the first part of the path, in the variable dir @@ -216,49 +217,43 @@ function generateActionsManifest(dir='./actions') { if (dir.includes('./')) { dir = dir.replace('./', '') } - // This will be the name of the target in the repository + // This will be the repository name let dotGitHub = dir.replace(/^(\.\/)?actions\//, '.github/') filelist.push({ - tgtPath: path.join(dotGitHub, file), fileName: file, containerName: dotGitHub, - srcUrl: new URL(path.join(dir, file), import.meta.url) + srcURL: new URL(path.join(dir, file), import.meta.url) }) } }) return filelist -} +} async function installActions(actionsManifest) { - // Set up the spinner - let spinner = ora(chalk.bold.blue('Installing GitHub Workflows and Actions')) - spinner.start() // Start the spinner // Loop through the actionsManifest and install each action await actionsManifest.forEach(async (action) => { - // Read in the blob file - const [status, msg, blobData] = fsUtils.readBlobFile(action.srcUrl) + let status = false + let blobData + try { + // Read in the blob file + blobData = fs.readFileSync(action.srcURL, 'base64') + status = true + } catch (err) { + return [false, 'Unable to read file [' + action.fileName + '] because: ' + err, null] + } if(status) { // Install the action const installResp = await gitHubCtl.writeBlob( - action.container, + action.containerName, action.fileName, blobData, 'main' ) - if(installResp[0]) { - spinner.text = `Installed item [${action.fileName}] ` - } else { - spinner.text = `Failed to install item [${action.fileName}]` - return [false, installResp[1], null] - } } else { - spinner.text = `Failed to read item [${action.fileName}] ... ` - return [false, msg, null] + return [false, 'Failed to read item [' + action.fileName + ']', null] } - }) - spinner.stop() // Stop the spinner return [true, 'All actions installed', null] } @@ -472,15 +467,18 @@ cliOutput.printLine() /* ----------------------------------------- */ /* ------------ Install actions ------------ */ -process.log(chalk.bold.blue(`Installing GitHub Workflows and Actions ... `)) +process.stdout.write(chalk.bold.blue(`Installing actions and workflows ... `)) const actionsManifest = generateActionsManifest() const installResp = await installActions(actionsManifest) if(installResp[0]) { - console.log(chalk.bold.green('Installed all Workflows and Actions')) + console.log(chalk.bold.green('Ok')) } else { console.log(chalk.bold.red(`Failed, exiting with error: [${installResp[1]}]`)) process.exit(-1) } +cliOutput.printLine() +/* ---------- End Install actions ---------- */ +/* ----------------------------------------- */ /* ----------------------------------------- */ /* ---- Begin initial objects creation ----- */ @@ -509,11 +507,8 @@ console.log(chalk.blue.bold('Creating the first company ...')) let firstCompanyResp = await firstComp.wizard(false, false) const firstCompany = firstCompanyResp[2] -// Set up the spinner -let spinner - // Save the companies to GitHub -spinner = ora(chalk.bold.blue('Saving companies to GitHub ... ')) +let spinner = ora(chalk.bold.blue('Saving companies to GitHub ... ')) spinner.start() // Start the spinner const companyResp = await companyCtl.createObj([owningCompany, firstCompany]) spinner.stop() // Stop the spinner diff --git a/package.json b/package.json index 588cfc9..33e3e86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.36", + "version": "0.4.37", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/cli/installInstructions.js b/src/cli/installInstructions.js index b1293a9..ed98deb 100644 --- a/src/cli/installInstructions.js +++ b/src/cli/installInstructions.js @@ -5,11 +5,17 @@ work at all. Please exit this tool until you can confirm the application is ins pressing \'Control-C\'. If you attempt to continue without the application installed this tool will fail. -You can learn more about and install the GitHub Application by visiting the following URL: https://github.com/apps/mediumroast-for-github +You can learn more about and install the GitHub Application by visiting the following +URL: https://github.com/apps/mediumroast-for-github -A more detailed product description, help and other related documentation ca be found here: https://www.mediumroast.io/ +A more detailed product description, help and other related documentation ca be found +here: https://www.mediumroast.io/ -WARNING: This setup installs two GitHub Actions, and their associated workflows, into the repository that it creates. These Actions consume GitHub Actions minutes and may incur charges to your GitHub account if you are over the allowed actions for your plan. Please consider reviewing the GitHub Actions pricing and usage documentation before continuing. +WARNING: This setup installs two GitHub Actions, and their associated workflows, into +the repository that it creates. These Actions consume GitHub Actions minutes and may incur +charges to your GitHub account if you are over the allowed actions for your plan. +Please consider reviewing the GitHub Actions pricing and usage documentation before +continuing. ` export default installText From 438c046cc9e31382038a6a2b66a3cffaa57bf593 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Fri, 9 Feb 2024 19:12:56 -0800 Subject: [PATCH 03/10] Removed extra action artifact, fixed interaction oddity --- cli/actions/.gitignore | 2 -- cli/mrcli-company.js | 1 + src/cli/interactionWizard.js | 7 +++++-- src/cli/output.js | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 cli/actions/.gitignore diff --git a/cli/actions/.gitignore b/cli/actions/.gitignore deleted file mode 100644 index 28f1ba7..0000000 --- a/cli/actions/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -.DS_Store \ No newline at end of file diff --git a/cli/mrcli-company.js b/cli/mrcli-company.js index 5a95fcb..9f57601 100755 --- a/cli/mrcli-company.js +++ b/cli/mrcli-company.js @@ -288,6 +288,7 @@ if (myArgs.report) { } else { [success, stat, results] = await companyCtl.getAll() results = results.mrJson + // console.log(JSON.stringify(obj, null, 2)) } // Emit the output diff --git a/src/cli/interactionWizard.js b/src/cli/interactionWizard.js index 2c86faf..d31ab2a 100755 --- a/src/cli/interactionWizard.js +++ b/src/cli/interactionWizard.js @@ -18,6 +18,7 @@ import FilesystemOperators from "./filesystem.js" import * as progress from 'cli-progress' import ora from 'ora' import crypto from 'crypto' +import { resolve } from 'path' class AddInteraction { /** @@ -103,13 +104,15 @@ class AddInteraction { path: {consoleString: 'full path to the directory (e.g., /dir/subdir)', value:this.defaultValue} } let myPath = await this.wutils.doManual(pathPrototype) + // const [success, message, result] = this.fileSystem.checkFilesystemObject(myPath.path) + // new URL(path.join(dir, file), import.meta.url) const myObjectType = this.fileSystem.checkFilesystemObjectType(myPath.path) if(!success || myObjectType[2].isFile()) { console.log(chalk.red.bold(`The directory path wasn\'t resolved correctly. Here\'s your input [${myPath.path}]. Let\'s try again.`)) - myPath = await this.getValidPath() + myPath.path = await this.getValidPath() } - return myPath.path + return resolve(myPath.path) } // Create a function that computes the hash of base64 encoded data and returns the hash diff --git a/src/cli/output.js b/src/cli/output.js index 3894951..085d563 100644 --- a/src/cli/output.js +++ b/src/cli/output.js @@ -39,7 +39,7 @@ class CLIOutput { if (outputType === 'table') { this.outputTable(results) } else if (outputType === 'json') { - console.dir(results) + console.log(JSON.stringify(results, null, 2)) } else if (outputType === 'csv') { this.outputCSV(results) } else if (outputType === 'xls') { From f8283154d5ba266dac36afb555c8a21f17da0e1f Mon Sep 17 00:00:00 2001 From: mihay42 Date: Fri, 9 Feb 2024 19:25:17 -0800 Subject: [PATCH 04/10] Resolve path for install --- cli/mrcli-setup.js | 2 +- cli/mrcli.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index b2f35e5..d4bacb4 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -200,7 +200,7 @@ function verifyConfiguration(myConfig, configFile) { // Use fs to read all the files in the actions directory recursively function generateActionsManifest(dir, filelist) { - dir = dir || './actions' + dir = dir || path.resolve(path.join(__dirname, './actions') ) const files = fs.readdirSync(dir) filelist = filelist || [] files.forEach((file) => { diff --git a/cli/mrcli.js b/cli/mrcli.js index ea67ad9..632118d 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.4.35') + .version('0.4.38') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index 33e3e86..e9da39a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.37", + "version": "0.4.38", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { From 97870cccfc0f49e9c81343b2a7790325b80353fc Mon Sep 17 00:00:00 2001 From: mihay42 Date: Fri, 9 Feb 2024 19:30:49 -0800 Subject: [PATCH 05/10] Actually less action packed --- cli/mrcli-setup.js | 3 +++ cli/mrcli.js | 2 +- package.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index d4bacb4..eb4d616 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -31,6 +31,7 @@ import ora from "ora" import * as fs from 'fs' import * as path from 'path' +import { fileURLToPath, URL } from 'url' /* ----------------------------------------------------------------------- @@ -200,6 +201,8 @@ function verifyConfiguration(myConfig, configFile) { // Use fs to read all the files in the actions directory recursively function generateActionsManifest(dir, filelist) { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename) dir = dir || path.resolve(path.join(__dirname, './actions') ) const files = fs.readdirSync(dir) filelist = filelist || [] diff --git a/cli/mrcli.js b/cli/mrcli.js index 632118d..cbc30ab 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.4.38') + .version('0.4.39') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index e9da39a..c6c2938 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.38", + "version": "0.4.30", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { From 3fd09733af72c211441c1481ad56292e0df9ef0c Mon Sep 17 00:00:00 2001 From: mihay42 Date: Fri, 9 Feb 2024 19:35:10 -0800 Subject: [PATCH 06/10] No actions packed --- cli/mrcli-setup.js | 2 +- cli/mrcli.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index eb4d616..e8b9aaa 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -202,7 +202,7 @@ function verifyConfiguration(myConfig, configFile) { // Use fs to read all the files in the actions directory recursively function generateActionsManifest(dir, filelist) { const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename) + const __dirname = path.dirname(__filename) dir = dir || path.resolve(path.join(__dirname, './actions') ) const files = fs.readdirSync(dir) filelist = filelist || [] diff --git a/cli/mrcli.js b/cli/mrcli.js index cbc30ab..1734845 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.4.39') + .version('0.4.40') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index c6c2938..4afbb93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.30", + "version": "0.4.40", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { From 7d236c43fcf7bf49769453a0885be270edf6135e Mon Sep 17 00:00:00 2001 From: mihay42 Date: Fri, 9 Feb 2024 19:44:01 -0800 Subject: [PATCH 07/10] Actions unpacked --- cli/mrcli-setup.js | 1 + cli/mrcli.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index e8b9aaa..b0c35b7 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -472,6 +472,7 @@ cliOutput.printLine() /* ------------ Install actions ------------ */ process.stdout.write(chalk.bold.blue(`Installing actions and workflows ... `)) const actionsManifest = generateActionsManifest() +console.log(actionsManifest) const installResp = await installActions(actionsManifest) if(installResp[0]) { console.log(chalk.bold.green('Ok')) diff --git a/cli/mrcli.js b/cli/mrcli.js index 1734845..8e4dfd7 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.4.40') + .version('0.4.41') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index 4afbb93..722e1b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.40", + "version": "0.4.41", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { From fd584b5549319244b015e61c9750c1b53edb01bc Mon Sep 17 00:00:00 2001 From: mihay42 Date: Fri, 9 Feb 2024 19:52:12 -0800 Subject: [PATCH 08/10] Maybe actions packed --- cli/mrcli-setup.js | 2 +- cli/mrcli.js | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/mrcli-setup.js b/cli/mrcli-setup.js index b0c35b7..f3a3d19 100755 --- a/cli/mrcli-setup.js +++ b/cli/mrcli-setup.js @@ -221,7 +221,7 @@ function generateActionsManifest(dir, filelist) { dir = dir.replace('./', '') } // This will be the repository name - let dotGitHub = dir.replace(/^(\.\/)?actions\//, '.github/') + let dotGitHub = dir.replace(/.*(workflows|actions)/, '.github/$1') filelist.push({ fileName: file, diff --git a/cli/mrcli.js b/cli/mrcli.js index 8e4dfd7..0d3e786 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.4.41') + .version('0.4.42') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index 722e1b0..1e86763 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.41", + "version": "0.4.42", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { From cc078ac36a93433f4de9b61e360b6e9a6f22805e Mon Sep 17 00:00:00 2001 From: mihay42 Date: Sun, 11 Feb 2024 18:07:24 -0800 Subject: [PATCH 09/10] Added billings --- cli/mrcli-billing.js | 101 ++++++++++++++++++++++++++++++++++++++++ cli/mrcli-user.js | 12 ++--- cli/mrcli.js | 5 +- package.json | 2 +- src/api/gitHubServer.js | 50 +++++++++++++++++++- src/api/github.js | 39 +++++++++++++++- src/cli/output.js | 36 ++++++++++++++ 7 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 cli/mrcli-billing.js diff --git a/cli/mrcli-billing.js b/cli/mrcli-billing.js new file mode 100644 index 0000000..f457e8c --- /dev/null +++ b/cli/mrcli-billing.js @@ -0,0 +1,101 @@ +#!/usr/bin/env node + +/** + * A CLI utility used for accessing and reporting on mediumroast.io user objects + * @author Michael Hay + * @file billing.js + * @copyright 2024 Mediumroast, Inc. All rights reserved. + * @license Apache-2.0 + * @verstion 2.0.0 + */ + +// Import required modules +import { Billings } from '../src/api/gitHubServer.js' +import Environmentals from '../src/cli/env.js' +import CLIOutput from '../src/cli/output.js' +import chalk from 'chalk' + +// Related object type +const objectType = 'Billings' + +// Environmentals object +const environment = new Environmentals( + '2.0', + `${objectType}`, + `Command line interface to report on consumed units of GitHub actions and storage.`, + objectType +) + +/* + ----------------------------------------------------------------------- + + FUNCTIONS - Key functions needed for MAIN + + ----------------------------------------------------------------------- +*/ + + +/* + ----------------------------------------------------------------------- + + MAIN - Steps below represent the main function of the program + + ----------------------------------------------------------------------- +*/ + +// Create the environmental settings +let myProgram = environment.parseCLIArgs(true) +myProgram + .option('-s, --storage', 'Return all storage billing information for the GitHub organization') + .option('-a, --actions', 'Return all actions billing information for the GitHub organization') + +// Remove command line options for reset_by_type, delete, update, and add_wizard by calling the removeArgByName method in the environmentals class +myProgram = environment.removeArgByName(myProgram, '--delete') +myProgram = environment.removeArgByName(myProgram, '--update') +myProgram = environment.removeArgByName(myProgram, '--add_wizard') +myProgram = environment.removeArgByName(myProgram, '--reset_by_type') +myProgram = environment.removeArgByName(myProgram, '--report') +myProgram = environment.removeArgByName(myProgram, '--find_by_name') +myProgram = environment.removeArgByName(myProgram, '--find_by_x') +myProgram = environment.removeArgByName(myProgram, '--find_by_id') +myProgram = environment.removeArgByName(myProgram, '--update') +myProgram = environment.removeArgByName(myProgram, '--delete') +myProgram = environment.removeArgByName(myProgram, '--report') +myProgram = environment.removeArgByName(myProgram, '--package') +myProgram = environment.removeArgByName(myProgram, '--splash') + +// Parse the command line arguments into myArgs and obtain the options +let myArgs = myProgram.parse(process.argv) +myArgs = myArgs.opts() + +const myConfig = environment.readConfig(myArgs.conf_file) +let myEnv = environment.getEnv(myArgs, myConfig) +const accessToken = await environment.verifyAccessToken() +const processName = 'mrcli-billing' + +// Output object +const output = new CLIOutput(myEnv, objectType) + +// Construct the controller objects +const billingsCtl = new Billings(accessToken, myEnv.gitHubOrg, processName) + +// Predefine the results variable +let [success, stat, results] = [null, null, null] + +if (myArgs.actions) { + [success, stat, results] = await billingsCtl.getActionsBilling() + const myUserOutput = new CLIOutput(myEnv, 'ActionsBilling') + myUserOutput.outputCLI([results], myArgs.output) + process.exit() +} else if (myArgs.storage) { + [success, stat, results] = await billingsCtl.getStorageBilling() + const myUserOutput = new CLIOutput(myEnv, 'StorageBilling') + myUserOutput.outputCLI([results], myArgs.output) + process.exit() +} else { + [success, stat, results] = await billingsCtl.getAll() + const myUserOutput = new CLIOutput(myEnv, 'AllBilling') + myUserOutput.outputCLI(results, myArgs.output) + process.exit() +} + diff --git a/cli/mrcli-user.js b/cli/mrcli-user.js index 9864a94..1c1a8ca 100755 --- a/cli/mrcli-user.js +++ b/cli/mrcli-user.js @@ -4,7 +4,7 @@ * A CLI utility used for accessing and reporting on mediumroast.io user objects * @author Michael Hay * @file user.js - * @copyright 2022 Mediumroast, Inc. All rights reserved. + * @copyright 2024 Mediumroast, Inc. All rights reserved. * @license Apache-2.0 * @verstion 2.0.0 */ @@ -53,6 +53,10 @@ myProgram = environment.removeArgByName(myProgram, '--delete') myProgram = environment.removeArgByName(myProgram, '--update') myProgram = environment.removeArgByName(myProgram, '--add_wizard') myProgram = environment.removeArgByName(myProgram, '--reset_by_type') +myProgram = environment.removeArgByName(myProgram, '--report') +myProgram = environment.removeArgByName(myProgram, '--package') +myProgram = environment.removeArgByName(myProgram, '--find_by_id') +myProgram = environment.removeArgByName(myProgram, '--splash') // Parse the command line arguments into myArgs and obtain the options let myArgs = myProgram.parse(process.argv) @@ -72,10 +76,7 @@ const userCtl = new Users(accessToken, myEnv.gitHubOrg, processName) // Predefine the results variable let [success, stat, results] = [null, null, null] -if (myArgs.report) { - console.log(chalk.bold.yellow(`WARNING: Generating a report for users is not yet implemented in this CLI.`)) - process.exit() -} else if (myArgs.my_user) { +if (myArgs.my_user) { [success, stat, results] = await userCtl.getMyself() const myUserOutput = new CLIOutput(myEnv, 'MyUser') myUserOutput.outputCLI([results], myArgs.output) @@ -93,7 +94,6 @@ if (myArgs.report) { results = foundObjects[2] } else { [success, stat, results] = await userCtl.getAll() - } // Emit the output diff --git a/cli/mrcli.js b/cli/mrcli.js index 0d3e786..dc12cde 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,12 +14,13 @@ import program from 'commander' program .name('mrcli') - .version('0.4.42') + .version('0.4.43') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') .command('company', 'manage and report on mediumroast.io company objects').alias('c') .command('study', 'manage and report on mediumroast.io study objects').alias('s') - .command('user', 'manage and report on mediumroast.io users').alias('u') + .command('user', 'report on mediumroast.io users in GitHub').alias('u') + .command('billing', 'report on GitHub actions and storage units consumed').alias('b') program.parse(process.argv) \ No newline at end of file diff --git a/package.json b/package.json index 1e86763..a57199a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.42", + "version": "0.4.43", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/api/gitHubServer.js b/src/api/gitHubServer.js index 6dbb4ec..2f4664f 100644 --- a/src/api/gitHubServer.js +++ b/src/api/gitHubServer.js @@ -272,6 +272,54 @@ class Users extends baseObjects { } +// Create a subclass called Users that inherits from baseObjects +class Billings extends baseObjects { + /** + * @constructor + * @classdesc A subclass of baseObjects that construct the user objects + * @param {String} token - the token for the GitHub application + * @param {String} org - the organization for the GitHub application + * @param {String} processName - the process name for the GitHub application + */ + constructor (token, org, processName) { + super(token, org, processName, 'Billings') + } + + // Create a new method for getAll that is specific to the Billings class using getBillings() in github.js + async getAll() { + const storageBillingsResp = await this.serverCtl.getStorageBillings() + const actionsBillingsResp = await this.serverCtl.getActionsBillings() + const allBillings = [ + { + resourceType: 'Storage', + includedUnits: Math.abs( + storageBillingsResp[2].estimated_paid_storage_for_month - + storageBillingsResp[2].estimated_storage_for_month + ) + ' GiB', + paidUnitsUsed: storageBillingsResp[2].estimated_paid_storage_for_month + ' GiB', + totalUnitsUsed: storageBillingsResp[2].estimated_storage_for_month + ' GiB' + }, + { + resourceType: 'Actions', + includedUnits: actionsBillingsResp[2].total_minutes_used + ' min', + paidUnitsUsed: actionsBillingsResp[2].total_paid_minutes_used + ' min', + totalUnitsUsed: actionsBillingsResp[2].total_minutes_used + actionsBillingsResp[2].total_paid_minutes_used + ' min' + } + ] + return [true, {status_code: 200, status_msg: `found all billings`}, allBillings] + } + + // Create a new method of to get the actions billing status only + async getActionsBilling() { + return await this.serverCtl.getActionsBillings() + } + + // Create a new method of to get the storage billing status only + async getStorageBilling() { + return await this.serverCtl.getStorageBillings() + } +} + class Companies extends baseObjects { /** * @constructor @@ -415,4 +463,4 @@ class Interactions extends baseObjects { } // Export classes for consumers -export { Studies, Companies, Interactions, Users } \ No newline at end of file +export { Studies, Companies, Interactions, Users, Billings } \ No newline at end of file diff --git a/src/api/github.js b/src/api/github.js index 2226a95..0b4b6cb 100644 --- a/src/api/github.js +++ b/src/api/github.js @@ -48,7 +48,8 @@ class GitHubFunctions { Studies: 'Studies.json', Companies: 'Companies.json', Interactions: 'Interactions.json', - Users: null + Users: null, + Billings: null } } @@ -114,6 +115,42 @@ class GitHubFunctions { } } + /** + * @async + * @function getActionsBillings + * @description Gets the complete billing status for actions from the GitHub API + * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the user info or error message. + */ + async getActionsBillings() { + // using try and catch to handle errors get info for all billings data + try { + const response = await this.octCtl.rest.billing.getGithubActionsBillingOrg({ + org: this.orgName, + }) + return [true, `SUCCESS: able to capture info for actions billing`, response.data] + } catch (err) { + return [false, {status_code: 404, status_msg: `unable to capture info for actions billing due to [${err}]`}, err.message] + } + } + + /** + * @async + * @function getStorageBillings + * @description Gets the complete billing status for actions from the GitHub API + * @returns {Array} An array with position 0 being boolean to signify success/failure and position 1 being the user info or error message. + */ + async getStorageBillings() { + // using try and catch to handle errors get info for all billings data + try { + const response = await this.octCtl.rest.billing.getSharedStorageBillingOrg({ + org: this.orgName, + }) + return [true, `SUCCESS: able to capture info for storage billing`, response.data] + } catch (err) { + return [false, {status_code: 404, status_msg: `unable to capture info for storage billing due to [${err}]`}, err.message] + } + } + /** * @function createRepository * @description Creates a repository, at the organization level, for keeping track of all mediumroast.io assets diff --git a/src/cli/output.js b/src/cli/output.js index 085d563..d37ac7c 100644 --- a/src/cli/output.js +++ b/src/cli/output.js @@ -133,6 +133,42 @@ class CLIOutput { linkedCompany ]) } + } else if (this.objectType === 'ActionsBilling') { + table = new Table({ + head: ['Minutes Used', 'Paid Minutes Used', 'Minutes Remaining', 'Included Minutes'], + }) + for (const myObj in objects) { + table.push([ + objects[myObj].total_minutes_used + ' min', + objects[myObj].total_paid_minutes_used + ' min', + objects[myObj].included_minutes - objects[myObj].total_minutes_used + objects[myObj].total_paid_minutes_used + ' min', + objects[myObj].included_minutes + ' min', + ]) + } + } else if (this.objectType === 'StorageBilling') { + table = new Table({ + head: ['Storage Used', 'Paid Storage Used', 'Estimated Storage Used', 'Days Left in Cycle'], + }) + for (const myObj in objects) { + table.push([ + Math.abs(objects[myObj].estimated_paid_storage_for_month - objects[myObj].estimated_storage_for_month) + ' GiB', + objects[myObj].estimated_storage_for_month + ' GiB', + objects[myObj].estimated_paid_storage_for_month + ' GiB', + objects[myObj].days_left_in_billing_cycle + ' days', + ]) + } + } else if (this.objectType === 'AllBilling') { + table = new Table({ + head: ['Resource Type', 'Included Units Used', 'Paid Units Used', 'Total Units Used'], + }) + for (const myObj in objects) { + table.push([ + objects[myObj].resourceType, + objects[myObj].includedUnits, + objects[myObj].paidUnitsUsed, + objects[myObj].totalUnitsUsed, + ]) + } } else { table = new Table({ head: ['Name', 'Description'], From a7ae0c600b25adf2f49f0f13e039ec5f77019a90 Mon Sep 17 00:00:00 2001 From: mihay42 Date: Mon, 12 Feb 2024 18:28:51 -0800 Subject: [PATCH 10/10] User formatting and help update --- cli/mrcli.js | 2 +- package.json | 2 +- src/cli/env.js | 2 +- src/cli/output.js | 2 -- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cli/mrcli.js b/cli/mrcli.js index dc12cde..acc457a 100755 --- a/cli/mrcli.js +++ b/cli/mrcli.js @@ -14,7 +14,7 @@ import program from 'commander' program .name('mrcli') - .version('0.4.43') + .version('0.4.44') .description('mediumroast.io command line interface') .command('setup', 'setup the mediumroast.io system via the command line').alias('f') .command('interaction', 'manage and report on mediumroast.io interaction objects').alias('i') diff --git a/package.json b/package.json index a57199a..edd6288 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediumroast_js", - "version": "0.4.43", + "version": "0.4.44", "description": "A Command Line Interface (CLI) and Javascript SDK to interact with mediumroast.io.", "main": "cli/mrcli.js", "scripts": { diff --git a/src/cli/env.js b/src/cli/env.js index 72d65c6..2b8c341 100644 --- a/src/cli/env.js +++ b/src/cli/env.js @@ -71,7 +71,7 @@ class Environmentals { // Operational command line switches .option( '--find_by_name ', - 'Find an individual Interaction by name' + 'Find an individual object by name' ) .option( '--find_by_id ', diff --git a/src/cli/output.js b/src/cli/output.js index d37ac7c..0e0fe35 100644 --- a/src/cli/output.js +++ b/src/cli/output.js @@ -57,7 +57,6 @@ class CLIOutput { if (this.objectType === 'Users') { table = new Table({ head: ['GitHub Id', 'Login', 'User Type', 'Role Name', 'Site Admin'], - colWidths: [12, 20, 15, 20, 30] }) // NOTE: In this alpha version users aren't yet operable for (const myObj in objects) { @@ -82,7 +81,6 @@ class CLIOutput { } else if (this.objectType === 'MyUser') { table = new Table({ head: ['GitHub Id', 'Login', 'Name', 'Type', 'Company', 'GitHub Website'], - colWidths: [12, 20, 30, 10, 25, 50] }) for (const myObj in objects) { table.push([