Skip to content

Commit 1bb59a9

Browse files
author
Peter Bengtsson
authored
script to find unicorn action SHAs (github#33446)
1 parent bcdc32c commit 1bb59a9

File tree

1 file changed

+87
-0
lines changed

1 file changed

+87
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env node
2+
3+
// [start-readme]
4+
//
5+
// In the .github/workflows, We use...
6+
//
7+
// uses: some/action@95cb08cb2672c73d4ffd2f422e6d11953d2a9c70
8+
//
9+
// But sometimes we fail to update the uniformly. This script
10+
// is for finding these unicorns.
11+
//
12+
// [end-readme]
13+
//
14+
//
15+
16+
import fs from 'fs'
17+
18+
import { program } from 'commander'
19+
import walk from 'walk-sync'
20+
import chalk from 'chalk'
21+
22+
program
23+
.description('Finds action shas that are unusual')
24+
.option('-v, --verbose', 'Verbose outputs')
25+
.parse(process.argv)
26+
27+
main(program.opts(), program.args)
28+
29+
async function main(opts, args) {
30+
const files = walk('.github/workflows', { globs: ['*.yml'], includeBasePath: true })
31+
const counts = {}
32+
const places = {}
33+
34+
for (const file of files) {
35+
const content = fs.readFileSync(file, 'utf-8')
36+
let lineNo = 0
37+
for (const line of content.split(/\n/g)) {
38+
lineNo++
39+
if (line.includes('uses:') && /@[a-f0-9]{40}/.test(line)) {
40+
const match = line.match(/\b(?<name>[\w/-]+)@(?<sha>[a-f0-9]{40})/)
41+
const whole = match[0]
42+
if (!places[whole]) {
43+
places[whole] = []
44+
}
45+
places[whole].push({ file, line, lineNo })
46+
const { name, sha } = match.groups
47+
if (!counts[name]) {
48+
counts[name] = {}
49+
}
50+
counts[name][sha] = 1 + (counts[name][sha] || 0)
51+
}
52+
}
53+
}
54+
const suspects = Object.fromEntries(
55+
Object.entries(counts).filter(([, shas]) => Object.keys(shas).length > 1)
56+
)
57+
58+
const countSuspects = Object.keys(suspects).length
59+
if (countSuspects) {
60+
console.log(chalk.yellow(`Found ${countSuspects} suspect${countSuspects === 1 ? '' : 's'}\n`))
61+
62+
for (const [action, shas] of Object.entries(suspects)) {
63+
const total = Object.values(shas).reduce((a, b) => a + b, 0)
64+
const flat = Object.entries(shas)
65+
.map(([sha, count]) => [count, sha])
66+
.sort((a, b) => b[0] - a[0])
67+
68+
const mostPopular = flat[0]
69+
for (const [count, sha] of flat.slice(1)) {
70+
console.log(chalk.bold('Suspect:'), `${action}@${chalk.yellow(sha)}`)
71+
console.log(
72+
`is only used ${count} time${count === 1 ? '' : 's'} (${((100 * count) / total).toFixed(
73+
1
74+
)}%) compared to ${mostPopular[1]} (used ${mostPopular[0]} times)`
75+
)
76+
console.log(chalk.bold(`Consider changing to ${action}@${mostPopular[1]}`))
77+
console.log('in...')
78+
for (const { file, lineNo } of places[`${action}@${sha}`]) {
79+
console.log(`\t${file} (line ${lineNo})`)
80+
}
81+
console.log('\n')
82+
}
83+
}
84+
} else {
85+
console.log(chalk.green('All good! No suspects found 😎'))
86+
}
87+
}

0 commit comments

Comments
 (0)