|
| 1 | +#!/usr/bin/env bun |
| 2 | +import { audit } from '../src/engine' |
| 3 | +import { formatGitHub } from '../src/reporters/github' |
| 4 | +import { formatJson } from '../src/reporters/json' |
| 5 | +import { formatPretty } from '../src/reporters/pretty' |
| 6 | + |
| 7 | +interface Args { |
| 8 | + root: string |
| 9 | + format: 'pretty' | 'json' | 'github' |
| 10 | + ignore: string[] |
| 11 | + noColor: boolean |
| 12 | + showHelp: boolean |
| 13 | + showVersion: boolean |
| 14 | +} |
| 15 | + |
| 16 | +function parseArgs(argv: string[]): Args { |
| 17 | + const args: Args = { |
| 18 | + root: '.', |
| 19 | + format: detectDefaultFormat(), |
| 20 | + ignore: [], |
| 21 | + noColor: !!process.env.NO_COLOR, |
| 22 | + showHelp: false, |
| 23 | + showVersion: false, |
| 24 | + } |
| 25 | + |
| 26 | + for (let i = 0; i < argv.length; i++) { |
| 27 | + const a = argv[i] |
| 28 | + if (a === '--help' || a === '-h') { |
| 29 | + args.showHelp = true |
| 30 | + } |
| 31 | + else if (a === '--version' || a === '-v') { |
| 32 | + args.showVersion = true |
| 33 | + } |
| 34 | + else if (a === '--format' || a === '-f') { |
| 35 | + const next = argv[++i] |
| 36 | + if (next !== 'pretty' && next !== 'json' && next !== 'github') { |
| 37 | + throw new Error(`Unknown --format ${next}. Use pretty | json | github.`) |
| 38 | + } |
| 39 | + args.format = next |
| 40 | + } |
| 41 | + else if (a.startsWith('--format=')) { |
| 42 | + const v = a.slice('--format='.length) |
| 43 | + if (v !== 'pretty' && v !== 'json' && v !== 'github') |
| 44 | + throw new Error(`Unknown --format ${v}. Use pretty | json | github.`) |
| 45 | + args.format = v |
| 46 | + } |
| 47 | + else if (a === '--ignore') { |
| 48 | + args.ignore.push(...argv[++i].split(',')) |
| 49 | + } |
| 50 | + else if (a.startsWith('--ignore=')) { |
| 51 | + args.ignore.push(...a.slice('--ignore='.length).split(',')) |
| 52 | + } |
| 53 | + else if (a === '--no-color') { |
| 54 | + args.noColor = true |
| 55 | + } |
| 56 | + else if (!a.startsWith('-')) { |
| 57 | + args.root = a |
| 58 | + } |
| 59 | + else { |
| 60 | + throw new Error(`Unknown argument: ${a}`) |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + return args |
| 65 | +} |
| 66 | + |
| 67 | +function detectDefaultFormat(): 'pretty' | 'json' | 'github' { |
| 68 | + // When invoked from a GitHub Actions runner, default to the |
| 69 | + // workflow-command format so findings surface as inline annotations. |
| 70 | + if (process.env.GITHUB_ACTIONS === 'true') |
| 71 | + return 'github' |
| 72 | + return 'pretty' |
| 73 | +} |
| 74 | + |
| 75 | +function help(): string { |
| 76 | + return `gh-audit — static analysis for GitHub Actions workflows |
| 77 | +
|
| 78 | +USAGE |
| 79 | + gh-audit [path] [options] |
| 80 | +
|
| 81 | +OPTIONS |
| 82 | + -f, --format <pretty|json|github> Reporter (default: github when on a runner, pretty otherwise) |
| 83 | + --ignore <id,id,...> Skip specific rule ids |
| 84 | + --no-color Disable ANSI colours in pretty output |
| 85 | + -h, --help Show this help |
| 86 | + -v, --version Show version |
| 87 | +
|
| 88 | +EXAMPLES |
| 89 | + gh-audit Audit .github/workflows in CWD |
| 90 | + gh-audit ../other-repo Audit a different repo |
| 91 | + gh-audit --format json Machine-readable output |
| 92 | + gh-audit --ignore missing-timeout Disable a noisy rule |
| 93 | +` |
| 94 | +} |
| 95 | + |
| 96 | +async function main(): Promise<void> { |
| 97 | + let args: Args |
| 98 | + try { |
| 99 | + args = parseArgs(process.argv.slice(2)) |
| 100 | + } |
| 101 | + catch (err) { |
| 102 | + process.stderr.write(`${(err as Error).message}\n\n${help()}`) |
| 103 | + process.exit(2) |
| 104 | + return |
| 105 | + } |
| 106 | + |
| 107 | + if (args.showHelp) { |
| 108 | + process.stdout.write(help()) |
| 109 | + return |
| 110 | + } |
| 111 | + if (args.showVersion) { |
| 112 | + const pkg = await Bun.file(`${import.meta.dir}/../package.json`).json() as { version: string } |
| 113 | + process.stdout.write(`${pkg.version}\n`) |
| 114 | + return |
| 115 | + } |
| 116 | + |
| 117 | + const result = await audit(args.root, { ignore: args.ignore }) |
| 118 | + |
| 119 | + let output: string |
| 120 | + switch (args.format) { |
| 121 | + case 'json': |
| 122 | + output = formatJson(result) |
| 123 | + break |
| 124 | + case 'github': |
| 125 | + output = formatGitHub(result) |
| 126 | + break |
| 127 | + default: |
| 128 | + output = formatPretty(result, { color: !args.noColor }) |
| 129 | + } |
| 130 | + if (output) |
| 131 | + process.stdout.write(`${output}\n`) |
| 132 | + |
| 133 | + process.exit(result.failed ? 1 : 0) |
| 134 | +} |
| 135 | + |
| 136 | +main().catch((err) => { |
| 137 | + process.stderr.write(`gh-audit: ${(err as Error).message}\n`) |
| 138 | + process.exit(2) |
| 139 | +}) |
0 commit comments