Repository: remorses/actions-cli Branch: master Commit: 2e5d17c380e2 Files: 20 Total size: 27.7 KB Directory structure: gitextract_frtp2xx9/ ├── .dockerignore ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── nothing.yml │ └── package.yml ├── .gitignore ├── .vscode/ │ └── launch.json ├── Dockerfile ├── README.md ├── VERSION ├── package.json ├── src/ │ ├── constants.ts │ ├── fetch.ts │ ├── index.ts │ ├── login.ts │ ├── logout.ts │ └── support.ts ├── tests/ │ ├── init.js │ ├── simple.ts │ └── spinner.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules dist esm example ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/workflows/nothing.yml ================================================ name: Nothing on: push: branches: - master jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: node-version: 12 registry-url: https://registry.npmjs.org/ - run: yarn - run: yarn test - run: tsc ================================================ FILE: .github/workflows/package.yml ================================================ name: Npm Package on: push: branches: - master jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/setup-node@v1 with: node-version: 12 registry-url: https://registry.npmjs.org/ - run: yarn - run: yarn test - run: tsc - run: tsc -m es6 --outDir esm - name: Bump version uses: remorses/bump-version@js with: version_file: VERSION env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: yarn publish env: NODE_AUTH_TOKEN: ${{ secrets.npm_token }} ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* .DS_Store play* dist/* esm # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # parcel-bundler cache (https://parceljs.org/) .cache # next.js build output .next # nuxt.js build output .nuxt # vuepress build output .vuepress/dist # Serverless directories .serverless # FuseBox cache .fusebox/ ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "Launch Program", "skipFiles": [ "/**" ], "program": "${workspaceFolder}/dist/index.js", "args": ["-w", "package.yml", "-n"], "outFiles": [ "${workspaceFolder}/**/*.js" ] } ] } ================================================ FILE: Dockerfile ================================================ FROM node:12-alpine RUN apk add --no-cache dumb-init # build-base WORKDIR /workdir COPY *.json *.lock /workdir/ RUN yarn COPY . /workdir/ ENTRYPOINT ["dumb-init", "--"] CMD yarn dev ================================================ FILE: README.md ================================================


actions-cli

Monitor your GitHub Actions from the command line




``` npm install -g actions-cli ``` ## Usage ``` $ actions-cli login $ actions-cli ./ ``` ``` actions-cli Fetch the current hash job status and logs Commands: actions-cli login Logins to cli actions-cli Fetch the current hash job status and logs [predefinito] Positionals: path The github repo path [stringa] [predefinito: ""] Options: --version Mostra il numero di versione [booleano] --verbose, -v [booleano] [predefinito: false] -h Mostra la schermata di aiuto [booleano] --sha The sha to look for actions, at least 7 characters long [stringa] [richiesto] [predefinito: ""] --workflow, -w The workflow file name [stringa] [predefinito: ""] --job, -j The job name, defaults to the first listed job [stringa] [predefinito: ""] ``` ### TODOS - multiple jobs per workflow - multiple workflows - ~~use latest commit pushed instead of latest commit~~ use -p - ~~don't use the commit pushed from github actions~~ use -n ================================================ FILE: VERSION ================================================ 0.0.36 ================================================ FILE: package.json ================================================ { "name": "actions-cli", "_": "[bump]", "version": "0.0.36", "description": "", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { "actions-cli": "./dist/index.js" }, "mocha": { "require": "tests/init.js", "spec": "tests/**.ts", "timeout": 9999999999 }, "files": [ "/dist/*", "/esm/*" ], "scripts": { "test": "NODE_ENV=test mocha --colors --exit", "example": "parcel serve --no-autoinstall example/index.html", "compile": "sucrase -q ./src -d ./dist --transforms typescript,imports", "dev": "yarn compile && node dist", "play": "tsc --incremental && node dist" }, "keywords": [], "author": "Tommaso De Rossi, morse ", "license": "ISC", "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.0.7", "dotenv": "^8.2.0", "mocha": "^6.1.4", "sucrase": "^3.12.1", "typescript": "^3.8.3" }, "dependencies": { "@octokit/rest": "^17.10.0", "@types/chalk": "^2.2.0", "@types/lodash": "^4.14.150", "@types/memoizee": "^0.4.4", "@types/node-fetch": "^2.5.7", "@types/yargs": "^15.0.4", "appdata-path": "^1.0.0", "await-to-js": "^2.1.1", "chalk": "^4.0.0", "cli-social-login": "^1.0.1", "cli-spinners": "^2.3.0", "conf": "^6.2.4", "encoding": "^0.1.12", "git-remote-origin-url": "^3.1.0", "lodash": "^4.17.15", "log-symbols": "^4.0.0", "log-update": "^4.0.0", "memoizee": "^0.4.14", "multispinner": "^0.2.1", "node-fetch": "^2.6.0", "ora": "^4.0.4", "parse-github-url": "^1.0.2", "simple-git": "^2.2.0", "winston": "^3.2.1", "yargs": "^15.3.1" } } ================================================ FILE: src/constants.ts ================================================ import getAppDataPath from 'appdata-path' import path from 'path' const APP_DATA_FOLDER = 'actions-cli' export const USER_TOKEN_CONFIG_KEY = 'token' export const APP_DATA_PATH = path.resolve(getAppDataPath(APP_DATA_FOLDER)) export const firebaseConfig = { apiKey: 'AIzaSyAdM1fGOP_2bxM72M-Tf91wI76WiSe1ajE', authDomain: 'actions-cli-85f6f.firebaseapp.com', databaseURL: 'https://actions-cli-85f6f.firebaseio.com', projectId: 'actions-cli-85f6f', storageBucket: 'actions-cli-85f6f.appspot.com', messagingSenderId: '846622072078', appId: '1:846622072078:web:d7f258caf4cc93e6090b5f', measurementId: 'G-01QPNTKXC1', } ================================================ FILE: src/fetch.ts ================================================ import memoize from 'memoizee' import { RestEndpointMethodTypes, Octokit } from '@octokit/rest' import { flatten } from 'lodash' import simpleGit from 'simple-git/promise' import to from 'await-to-js' import chalk from 'chalk' import { execSync } from 'child_process' import { dots as cliSpinner } from 'cli-spinners' import getRepoUrl from 'git-remote-origin-url' import * as logSymbols from 'log-symbols' import Multispinner from 'multispinner' import Spinners from 'multispinner/lib/spinners' import ora from 'ora' import logUpdate from 'log-update' import path from 'path' import { Argv } from 'yargs' import { initOctokit, parseGithubUrl, printGreen, printRed, sleep, catchAll, print, } from './support' const DEBUG = process.env.DEBUG const FetchCommand = { command: '$0', describe: 'Fetch the current hash job status and logs', builder: (argv: Argv) => { argv.positional('path', { type: 'string', default: '', required: true, description: 'The github repo path', }) argv.option('sha', { type: 'string', default: '', required: true, description: 'The sha to look for actions, at least 7 characters long', }) argv.option('workflow', { type: 'string', default: '', alias: 'w', description: 'The workflow file name', }) argv.option('job', { type: 'string', default: '', alias: 'j', description: 'The job name, defaults to the first listed job', }) argv.option('non-actions-commit', { type: 'boolean', default: false, alias: 'n', description: 'Double checks that the used sha was not committed by github-actions, only works for recent actions commits', }) argv.option('pushed-commit', { type: 'boolean', default: false, alias: 'p', description: 'Use latest pushed commit instead of using latest commit in HEAD', }) }, handler: catchAll(async (argv) => { const octokit = initOctokit() const jobToFetch = argv.job const useLatestPushedCommit = argv['pushed-commit'] const skipActionsCheck = !argv['non-actions-commit'] const workflowToFetch = argv.workflow const currentPath = path.resolve(argv.path || process.cwd()) const { owner, repo } = await getRepoInfo(currentPath) const spinner = ora(`getting last commit sha`).start() let sha = argv.sha || (await getLastCommit({ octokit, owner, repo, cwd: currentPath, skipActionsCheck, useLatestPushedCommit, })) const prettySha = sha.slice(0, 7) changeSpinnerText({ text: `fetching state for sha '${prettySha}'`, spinner, }) while (true) { let workflowRuns = [] if (workflowToFetch) { const [err, data] = await to( octokit.actions.listWorkflowRuns({ owner, repo, workflow_id: workflowToFetch, }), ) if (err) { spinner.stop() printRed(`Workflow '${workflowToFetch}' not found`) return } workflowRuns = data.data.workflow_runs } else { const data = await octokit.actions.listWorkflowRunsForRepo({ owner, repo, }) workflowRuns = data.data.workflow_runs } const lastRun = workflowRuns.find((x) => { const { head_sha, status, id, conclusion, workflow_url } = x // console.log({ workflow_url }) if (head_sha.slice(0, 7) === sha.slice(0, 7)) { // console.log('found') return true } return false }) if (!lastRun) { changeSpinnerText({ spinner, text: `waiting job handling last commit '${prettySha}'`, }) await sleep(3000) if (!argv.sha) { sha = await getLastCommit({ octokit, owner, repo, cwd: currentPath, skipActionsCheck, useLatestPushedCommit, }) } continue } const { head_sha, status, id, conclusion, url, html_url } = lastRun // console.log( // 'unexpected values', // JSON.stringify({ head_sha, status, id, conclusion }, null, 4) // ) if (status === 'queued') { changeSpinnerText({ spinner, text: 'queued' }) await sleep(3000) continue } if (status === 'in_progress') { changeSpinnerText({ spinner, text: 'in progress' }) spinner.info() spinner.stop() await pollJobs({ repo, owner, id, jobToFetch }) return } if (status === 'completed') { changeSpinnerText({ spinner, text: 'completed' }) spinner.info() spinner.stop() // if (conclusion === 'success') { // spinner.succeed(chalk.green('Success')) // } // if (conclusion === 'failure') { // spinner.fail(chalk.red('Failure')) // } await pollJobs({ repo, owner, id, jobToFetch }) return } console.log( 'unexpected state', JSON.stringify({ head_sha, status, id, conclusion }, null, 4), ) spinner.fail('Wtf?') return } }), } // as CommandModule export default FetchCommand export async function pollJobs({ owner, repo, id, jobToFetch }) { DEBUG && console.log('pollJobs') const octokit = initOctokit() let spinners = null while (true) { const data = await octokit.actions.listJobsForWorkflowRun({ owner, repo, run_id: id, }) const job = jobToFetch ? data.data.jobs.find((job) => { return job.name === jobToFetch }) : data.data.jobs?.[0] if (!job) { printRed( `Job '${jobToFetch}' not found, make sure your yaml is valid`, ) return } if ( spinners === null || // if the steps changed during build Object.keys(spinners.spinners).length !== job.steps.length ) { const obj = Object.assign( {}, ...job.steps.map((x) => ({ [x.number]: x.name, })), ) if (!spinners) { // init spinners spinners = new Multispinner(obj, { // clear: false, // update: logUpdate, ...cliSpinner, }) } else { // add a new spinner const spinnersKeys = Object.keys(spinners.spinners) Object.keys(obj).map((k) => { if (!spinnersKeys.includes(k)) { addSpinner({ spinners, key: k, value: obj[k] }) } }) } DEBUG && console.log(JSON.stringify(job, null, 4)) } displayJobsTree({ spinners, job }) if (job.status !== 'completed') { await sleep(2000) continue } spinners.update.clear() // TODO bug on multispinners if (job.conclusion === 'failure') { job.steps.forEach((step) => { const spinner = spinners.spinners[step.number] if (spinner && spinner.state === 'incomplete') { spinners.error(step.number) } }) } if (job.conclusion === 'failure') { printRed( `${ logSymbols.error } Failed, read the logs at ${chalk.whiteBright(job.html_url)}`, ) return } if (job.conclusion === 'success') { printGreen( `${ logSymbols.success } Success, read the logs at ${chalk.whiteBright(job.html_url)}`, ) return } console.log(JSON.stringify(job, null, 4)) return } } export function addSpinner({ spinners, key, value }) { spinners.spinners[key] = Spinners.prototype.spinnerObj(value) } export function displayJobsTree({ job = null as RestEndpointMethodTypes['actions']['listJobsForWorkflowRun']['response']['data']['jobs'][0], spinners, }) { // console.log(JSON.stringify(job, null, 4)) for (let step of job.steps) { try { if ( !Object.keys(spinners.spinners).includes(step.number.toString()) ) { continue } if (step.status === 'queued') { // spinner.info(step.name) // return { ok: true } } if (step.status === 'in_progress') { // spinner.info(step.name) // spinners.success(step.number) // return { ok: true } } if (step.status === 'completed') { if (step.conclusion === 'success') { // spinner.info(step.name) spinners.success(step.number) // return { ok: true, completed: true } } if (step.conclusion === 'failure') { spinners.error(step.number) // return { ok: false, completed: true } } } } catch (e) { // console.log('wtf', step) console.log(JSON.stringify(job, null, 4)) throw e // console.error(step.name, step.number, e) } } } function changeSpinnerText({ spinner, text }) { if (spinner.text !== text) { spinner.info() } spinner.start(text) } // export async function isCommitFromActions({ sha, repo, owner }) { // const octokit = initOctokit() // const data = await octokit.repos.getCommit({ owner, repo, ref: sha }) // console.log(JSON.stringify(data.data, null, 4)) // return false // } export async function getRepoInfo(currentPath) { const gitRepoUrl = await getRepoUrl(currentPath) const { name: repo, owner } = parseGithubUrl(gitRepoUrl) return { repo, owner } } const GITHUB_ACTIONS_BOT_LOGIN = 'github-actions[bot]' const getRepoCommits = memoize( async ({ octokit, owner, repo, }): Promise<{ actor: string; refs: string[] }[]> => { const data = await octokit.activity.listRepoEvents({ owner, repo, per_page: 50, }) const commits = data.data .filter((event) => { return event.type === 'PushEvent' }) .map((event) => { return { actor: event.actor.login, refs: event.payload.commits.map((x) => x.sha), } }) return commits }, ) export async function getLastCommit(args: { octokit: Octokit owner repo cwd skipActionsCheck?: boolean useLatestPushedCommit: boolean }): Promise { if (args.useLatestPushedCommit) { return execSync(`git rev-parse origin/${getCurrentBranch()}`) .toString() .trim() } const { owner, repo, octokit } = args const git = simpleGit(args.cwd) const lastLocalCommits = await git.log() if (args.skipActionsCheck) { lastLocalCommits.all.find( (x) => !['[no ci]', '[skip ci]', '[ci skip]'].some((m) => x.message.toLocaleLowerCase().includes(m), ), ) } // console.log(JSON.stringify(data.data, null, 4)) const commits = await getRepoCommits({ owner, repo, octokit }) const githubActionsCommits: string[] = flatten( commits .filter((x) => { if (process.env.DEBUG) { console.log('commit actor is ' + x.actor) } return x.actor === GITHUB_ACTIONS_BOT_LOGIN }) .map((x) => x.refs), ).map((x) => x.slice(0, 7)) const lastNonActionsCommit = lastLocalCommits.all.find((commit) => { return !githubActionsCommits.includes(commit.hash.slice(0, 7)) }) // console.log( // '\n' + // cliSpinner.frames[0] + // ` found a non actions commit made from ` + // lastNonActionsCommit.author_name || // lastNonActionsCommit.author_email, // ) return lastNonActionsCommit.hash } function getCurrentBranch() { return execSync('git branch --show-current').toString().trim() } ================================================ FILE: src/index.ts ================================================ #!/usr/bin/env node import yargs from 'yargs' import winston from 'winston' import loginCommand from './login' import logoutCommand from './logout' import { winstonConf } from './support' import FetchCommand from './fetch' yargs .option('verbose', { alias: 'v', type: 'boolean', default: false, }) .middleware([ (argv) => { if (argv.verbose) { winston.configure({ ...winstonConf, level: 'debug', }) return } winston.configure({ ...winstonConf, silent: true, level: 'error' }) }, ]) .command(loginCommand as any) .command(logoutCommand as any) .command(FetchCommand as any) // .demandCommand() .help('help').argv ================================================ FILE: src/login.ts ================================================ import yargs, { CommandModule, Argv } from 'yargs' import { loginOnLocalhost } from 'cli-social-login' import fs from 'fs' import { USER_TOKEN_CONFIG_KEY, firebaseConfig } from './constants' import { initStore, printRed } from './support' const welcomeMessage = 'Run `actions-cli` to see the actions status for the current commit' export default { command: 'login', describe: 'Logins to cli', builder: (argv: Argv) => { argv.option('token', { type: 'string', default: '', description: "Pass the token directly, necessary if you can't login via localhost and browser", }) }, handler: async (argv) => { const store = initStore() if (argv.token) { store.set(USER_TOKEN_CONFIG_KEY, argv.token) console.log(`Token Saved`) console.log(welcomeMessage) return } // starts a server on localhost to login the user const { credentials, user } = await loginOnLocalhost({ firebaseConfig, providers: ['github'], scopes: { github: ['notifications', 'repo'], // TODO organizations dont work }, }) const githubToken = credentials.oauthAccessToken if (!githubToken) { printRed('cannot get token') return } store.set(USER_TOKEN_CONFIG_KEY, githubToken) console.log(`Token Saved`) console.log(welcomeMessage) }, } // as CommandModule ================================================ FILE: src/logout.ts ================================================ import yargs, { CommandModule, Argv } from 'yargs' import { loginOnLocalhost } from 'cli-social-login' import fs from 'fs' import { USER_TOKEN_CONFIG_KEY, firebaseConfig } from './constants' import { initStore, printRed } from './support' export default { command: 'logout', describe: 'Deletes the saved token', builder: (argv: Argv) => {}, handler: async (argv) => { const store = initStore() store.delete(USER_TOKEN_CONFIG_KEY) console.log(`Deleted Token`) }, } // as CommandModule ================================================ FILE: src/support.ts ================================================ import Conf from 'conf' import _parseGithubUrl from 'parse-github-url' import chalk from 'chalk' import path from 'path' import { APP_DATA_PATH, USER_TOKEN_CONFIG_KEY } from './constants' import winston from 'winston' import { LoggerOptions } from 'winston' import { Octokit } from '@octokit/rest' let conf export function initStore(): Conf { if (!conf) { conf = new Conf({ cwd: APP_DATA_PATH }) } return conf } const { format, transports } = winston const logFormat = format.printf((info) => { const msg = typeof info.message === 'object' ? JSON.stringify(info.message, null, 4) : info.message return `${info.timestamp} ${msg}` }) export const winstonConf: LoggerOptions = { format: format.combine( format.label({ label: path.basename( (process.mainModule && process.mainModule.filename) || '', ), }), format.timestamp({ format: 'YYYY-MM-DD HH' }), // Format the metadata object format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'], }), ), transports: [ new transports.Console({ format: format.combine(format.colorize({}), logFormat), }), ], } export const print = console.log export const printRed = (x) => console.log(chalk.red(x)) export const printGreen = (x) => console.log(chalk.green(x)) export function getGithubToken() { const store = initStore() const token = store.get(USER_TOKEN_CONFIG_KEY) // console.log(token) if (!token) { printRed('cannot find github token, run `actions-cli login` first') process.exit(1) } return token } export function initOctokit(): Octokit { const token = getGithubToken() const octokit = new Octokit({ auth: token }) return octokit } export function parseGithubUrl(githubUrl): { name; owner } { if (!githubUrl) { throw new Error(`cannot parse null github url `) } const parsedUrl = _parseGithubUrl(githubUrl) if (!parsedUrl) { throw new Error('cannot parse github url ' + githubUrl) } const { owner, name: repo } = parsedUrl if (!owner || !repo) { throw new Error('cannot parse github url ' + githubUrl) } return parsedUrl } export const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) export function catchAll(fun) { return async (...args) => { try { return await fun(...args) } catch (e) { console.log() printRed(e) if (process.env.DEBUG) console.log(e) process.exit(1) } } } ================================================ FILE: tests/init.js ================================================ const { config } = require('dotenv') config({ path: 'test.env', }) // require('ts-node/register') require('sucrase/register') ================================================ FILE: tests/simple.ts ================================================ import { getRepoInfo, getLastCommit } from '../src/fetch' import { strict as assert } from 'assert' import { Octokit } from '@octokit/rest' it('ready', () => { assert.ok(true) }) it('getLastCommit', async () => { const cwd = process.cwd() const res = await getLastCommit({ ...await getRepoInfo(cwd), cwd, octokit: new Octokit(), }) console.log(res) }) ================================================ FILE: tests/spinner.ts ================================================ import ora from 'ora' import Multispinner from 'multispinner' import { sleep, printRed } from '../src/support' import { addSpinner } from '../src/fetch' it('spinner', async () => { let spinner = ora('xxx').start() await sleep(1000) spinner.info() spinner.start('ciao') await sleep(1000) spinner.info() spinner.start('bye') }) it('multispinner', async () => { let xs = new Multispinner({ a: 'x', b: 'y' }, {}) await sleep(1000) xs.success('a') await sleep(1000) }) it('addSpinner', async () => { let xs = new Multispinner({ a: 'x', b: 'y' }, {}) await sleep(1000) xs.success('a') addSpinner({ spinners: xs, key: 'z', value: 'z' }) await sleep(1000) printRed('ciao') }) ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "module": "commonjs", "moduleResolution": "Node", "lib": [ "es2017", "es7", "es6" // "dom" ], "declaration": true, "outDir": "dist", "strict": false, "esModuleInterop": true, "noImplicitAny": false, "jsx": "react", "sourceMap": true, "skipLibCheck": true }, "exclude": ["node_modules", "dist", "esm", "example", "tests"] }