update-badge: rewrite to use GraphQL and process multiple files

This commit is contained in:
ndunnett committed 2024-04-06 22:22:05 +00:00
1 parent e2a3ca41d8
commit 0db8c385f6
5 files changed
+169 -114

No files matched your search

+2 -6
View File
@@ -8,13 +8,9 @@ This action fetches last commit time of repositories and generates the badge for
**Required** GitHub token
### `inputFile`
### `fileNames`
**Required** File path of README.md
### `outputFile`
**Required** File path of updated README.md
**Required** Names of files to process (space delimited list)
## Outputs
+4 -8
View File
@@ -4,14 +4,10 @@ inputs:
ghToken:
description: 'GitHub token'
required: true
inputFile:
description: 'File path of README.md'
fileNames:
description: 'Names of files to process (space delimited list)'
required: true
default: 'README.md'
outputFile:
description: 'File path of output'
required: true
default: 'README-badge.md'
runs:
using: 'node12'
main: 'index.js'
using: 'node20'
main: 'index.js'
@@ -1,45 +1,47 @@
// modifiy based on https://runkit.com/melnikow/version-from-requirements-txt
const axios = require('axios');
const moment = require('moment');
const moment = require('moment')
const badgeDefaults = {
schemaVersion: 1,
label: 'last commit',
message: 'not available',
color: 'red'
}
async function handle(octokit, { owner, repo }) {
let lastCommitDayStr
try {
const { data: commitList } = await Promise.race([
new Promise(resolve => setTimeout(resolve, 10 * 1000)),
octokit.repos.listCommits({
owner,
repo
})
])
lastCommitDayStr = commitList[0].commit.author.date
} catch (e) {
if (e.message.indexOf('rate limit exceeded') !== -1) {
throw e;
}
module.exports = function generateBadge (repo) {
const { label, message, color } = badgeProperties(repo)
return (
'https://img.shields.io/badge/' +
[label, message, color]
.map(s => encodeURIComponent(s.replace(/\-/g, '--')))
.join('-')
)
}
console.log(`[${owner }/${repo}] ${e.message}`)
return { message: 'resource not available', color: 'red' }
}
const message = moment(lastCommitDayStr).format('YYYY-MM-DD')
return {
message,
color: age(message),
function badgeProperties (repo) {
if (repo.lastCommit === undefined) {
return badgeDefaults
} else {
const message = moment(repo.lastCommit).format('YYYY-MM-DD')
return Object.assign({}, badgeDefaults, { message, color: age(message) })
}
}
module.exports = async function invokeHandler(octokit, query) {
return Object.assign({}, badgeDefaults, await handle(octokit, query))
function age (date) {
if (!date.add) {
return 'brightgreen'
}
const now = moment()
const then = moment(date)
if (now.diff(date) < 0) date.add(-1, 'y')
const daysElapsed = now.diff(moment(date), 'days')
const colorByAge = colorScale([0, 5, 10, 20, 25], undefined, false)
return colorByAge(daysElapsed)
}
// copy from https://github.com/badges/shields
function colorScale(steps, colors, reversed) {
function colorScale (steps, colors, reversed) {
if (steps === undefined) {
throw Error('When invoking colorScale, steps should be provided.')
}
@@ -78,17 +80,3 @@ function colorScale(steps, colors, reversed) {
return colors.slice(stepIndex)[0]
}
}
function age(date) {
if (!date.add) {
return "brightgreen"
}
const now = moment()
const then = moment(date)
if (now.diff(date) < 0) date.add(-1, 'y')
const daysElapsed = now.diff(moment(date), 'days')
const colorByAge = colorScale([0, 5, 10, 20, 25], undefined, false)
return colorByAge(daysElapsed)
}
+57 -57
View File
@@ -1,62 +1,79 @@
const fs = require('fs')
const core = require('@actions/core')
const github = require('@actions/github')
const endpoint = require('./endpoint')
const generateBadge = require('./generateBadge')
const updateRepos = require('./updateRepos')
let octokit
;(async () => {
const ghToken = core.getInput('ghToken')
const fileNames = core.getInput('fileNames').split(' ')
let octokit = github.getOctokit(ghToken)
(async function() {
for (const fileName of fileNames) {
await processFile(fileName, octokit)
}
})()
async function processFile (fileName, octokit) {
try {
const ghToken = core.getInput('ghToken')
const inputFile = core.getInput('inputFile')
const outputFile = core.getInput('outputFile')
const lines = await core.group(`Reading from '${fileName}'...`, () =>
fs.readFileSync(fileName, 'utf8').split('\n')
)
let repos = await core.group('Parsing repositories...', () =>
parseGithubRepos(lines)
)
repos = await core.group('Querying repositories...', async () => {
return await updateRepos(octokit, repos)
})
const content = fs.readFileSync(inputFile, 'utf8')
const lines = content.split('\n')
const repos = await core.group('Extracting repos...', () => extractRepositories(lines))
core.info(`count=${repos.length}`)
octokit = github.getOctokit(ghToken)
await core.group('Fetching repositories & updating lines...', async () => {
await core.group('Updating badges...', async () => {
for (const repo of repos) {
const line = await generateLine(repo.repoStr)
if (otherDomain(lines[repo.index])) {
core.info(`...line refers to non-GH domain: '${lines[repo.index]}'`)
} else if (shouldUpdate(lines[repo.index], line)) {
lines[repo.index] = line
core.info(`...updated line: '${line}'`)
const line = generateLine(repo)
if (line === lines[repo.index]) {
core.info(`...skipped line ${repo.index + 1}: '${line}'`)
} else {
core.info(`...skipped line: '${line}'`)
lines[repo.index] = line
core.info(`...updated line ${repo.index + 1}: '${line}'`)
}
}
})
await core.group('Writing README...', () => {
fs.writeFileSync(outputFile, lines.join('\n'))
core.info(`Finished writing to ${outputFile}`)
await core.group('Writing file...', () => {
fs.writeFileSync(fileName, lines.join('\n'))
core.info(`Finished writing to '${fileName}'`)
})
} catch (error) {
core.setFailed(error.message)
}
})()
}
function extractRepositories(lines) {
function parseGithubRepos (lines) {
const repos = []
let collect = false
lines.some((line, index) => {
if (line === '### Solutions') {
collect = true
} else if (line === '### Live Streams') {
collect = false
} else if (collect) {
const idx1 = line.indexOf('[')
const idx2 = line.indexOf(']')
if (idx1 >= 0 && idx2 >= 0) {
repos.push({
index,
repoStr: line.slice(idx1 + 1, idx2)
})
if (otherDomain(line)) {
core.info(`...line refers to non-GH domain: '${line}'`)
} else {
const idx1 = line.indexOf('[')
const idx2 = line.indexOf(']')
if (idx1 >= 0 && idx2 >= 0) {
const repoStr = line.slice(idx1 + 1, idx2)
const [owner, name] = repoStr.split('/')
repos.push({ index, owner, name })
core.info(`...parsed repo: '${owner}/${name}'`)
}
}
}
@@ -66,31 +83,14 @@ function extractRepositories(lines) {
return repos
}
async function generateLine(repoStr) {
const badge = await generateBadge(repoStr)
function otherDomain (line) {
return (
line.indexOf('gitlab.com') !== -1 || line.indexOf('gist.github.com') !== -1
)
}
function generateLine (repo) {
const badge = generateBadge(repo)
const repoStr = `${repo.owner}/${repo.name}`
return `* [${repoStr}](https://github.com/${repoStr}) ![Last Commit on GitHub](${badge})`
}
async function generateBadge(repoStr) {
const [owner, repo] = repoStr.split('/')
const { label, message, color } = await endpoint(octokit, { owner, repo })
core.info(`...fetched repo ${repoStr}`)
return 'https://img.shields.io/badge/' + [label, message, color]
.map(s => encodeURIComponent(s.replace(/\-/g, '--')))
.join('-')
}
function shouldUpdate(oldLine, newLine) {
const lastReg = /Last Commit on GitHub/;
const badDateReg = /red\)$/;
return !lastReg.test(oldLine) ||
!badDateReg.test(newLine) ||
badDateReg.test(oldLine);
}
function otherDomain(line) {
return line.indexOf("gitlab.com") !== -1 ||
line.indexOf("gist.github.com") !== -1;
}
@@ -0,0 +1,75 @@
const core = require('@actions/core')
// limit size of each request to avoid getting timed out
const chunkSize = 200
module.exports = async function updateRepos (octokit, repos) {
let responses = []
for (let i = 0; i < repos.length; i += chunkSize) {
const chunk = repos.slice(i, i + chunkSize)
responses.push(await queryRepos(octokit, chunk))
}
return responses.flatMap(response => {
return Object.entries(response).map(([key, obj]) => {
const index = parseInt(key.replace('index', ''))
if (obj === null) {
return repos.find(repo => repo.index === index)
} else {
return {
index,
owner: obj.owner.login,
name: obj.name,
lastCommit: obj.defaultBranchRef.target.committedDate
}
}
})
})
}
async function queryRepos (octokit, repos) {
const query = buildQuery(repos)
try {
return await octokit.graphql(query)
} catch (error) {
if (error.errors !== undefined) {
const softFail = error.errors.every(
e => e.message.indexOf('Could not resolve to a Repository') !== -1
)
if (softFail) {
core.info(error)
return error.data
}
}
throw error
}
}
function buildQuery (repos) {
const frag = `fragment props on Repository {
owner {
login
}
name
defaultBranchRef {
target {
... on Commit {
committedDate
}
}
}
}`
const query = repos
.map(repo => {
return `index${repo.index}: repository(owner: "${repo.owner}", name: "${repo.name}") { ...props }`
})
.join('\n')
return `${frag}\n\n{\n${query}\n}`
}