Skip to content

Commit a881223

Browse files
justaugustusclaude
andcommitted
feat: add local results mode for Scorecard data
Add a `local-results-path` Action input that enables reading Scorecard results from a local JSON file instead of the public Scorecard API. This enables integration with tools like Allstar that produce Scorecard results locally. When `local-results-path` is set: - Scores are read from the specified file (Scorecard JSON v2 format) - The `scope` input is not required (repos are discovered from results) - Database enrichment (history, deltas) works identically to API mode When `local-results-path` is not set: - Existing API-based behavior is preserved (no changes) This replaces the experimental hardcoded local results hack (51b8e77) with a proper, configurable implementation that supports both modes. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Stephen Augustus <foo@auggie.dev>
1 parent a0bc9ec commit a881223

File tree

4 files changed

+208
-160
lines changed

4 files changed

+208
-160
lines changed

action.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ author: 'OpenSSF Scorecard Authors'
44

55
inputs:
66
scope:
7-
description: 'File that includes the list of repositories to monitor'
8-
required: true
7+
description: 'File that includes the list of repositories to monitor. Required when not using local-results-path.'
8+
required: false
99
database:
1010
description: 'File that stores the state of the scorecard'
1111
required: true
@@ -63,7 +63,15 @@ inputs:
6363
description: 'Tool to be included as link in the report'
6464
required: false
6565
default: "scorecard-visualizer"
66-
66+
local-results-path:
67+
description: >-
68+
Path to a local Scorecard results JSON file. When provided, scores
69+
are read from this file instead of the public Scorecard API. The file
70+
should contain an array of Scorecard JSON v2 result objects (the
71+
output of scorecard --format=json2). When set, the scope input is
72+
not required.
73+
required: false
74+
6775
outputs:
6876
scores:
6977
description: 'Score data in JSON format'

dist/index.js

Lines changed: 98 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -47266,63 +47266,98 @@ const generateScope = async ({ octokit, orgs, scope, maxRequestInParallel }) =>
4726647266
return newScope
4726747267
}
4726847268

47269-
const generateScores = async ({ scope, database: currentDatabase, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool }) => {
47269+
/**
47270+
* Parse local Scorecard results (Scorecard JSON v2 format) into the
47271+
* internal score format used by scorecard-monitor.
47272+
* @param {Array} results - Array of Scorecard JSON v2 result objects
47273+
* @returns {Array} - Array of {score, date, commit, platform, org, repo}
47274+
*/
47275+
const parseLocalResults = (results) => {
47276+
return results.map((x) => {
47277+
const parts = x.repo.name.split('/')
47278+
return {
47279+
score: x.score,
47280+
date: x.date,
47281+
commit: x.repo.commit,
47282+
platform: parts[0],
47283+
org: parts[1],
47284+
repo: parts[2]
47285+
}
47286+
})
47287+
}
47288+
47289+
const generateScores = async ({ scope, database: currentDatabase, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool, localResultsPath }) => {
4727047290
// @TODO: Improve deep clone logic
4727147291
const database = JSON.parse(JSON.stringify(currentDatabase))
47272-
const platform = 'github.com'
4727347292

47274-
// @TODO: End the action if there are no projects in scope?
47293+
let rawScores = []
4727547294

47276-
const orgs = Object.keys(scope[platform])
47277-
core.debug(`Total Orgs/Users in scope: ${orgs.length}`)
47295+
if (localResultsPath) {
47296+
// Local results mode: read scores from a Scorecard JSON v2 file
47297+
const { readFile } = (__nccwpck_require__(7147).promises)
47298+
const content = await readFile(localResultsPath, 'utf8')
47299+
const results = JSON.parse(content)
47300+
rawScores = parseLocalResults(Array.isArray(results) ? results : [results])
47301+
core.debug(`Loaded ${rawScores.length} scores from local results file: ${localResultsPath}`)
47302+
} else {
47303+
// API mode: fetch scores from the public Scorecard API
47304+
const platform = 'github.com'
4727847305

47279-
// Structure Projects
47280-
const projects = []
47306+
// @TODO: End the action if there are no projects in scope?
4728147307

47282-
orgs.forEach((org) => {
47283-
const repos = scope[platform][org].included
47284-
repos.forEach((repo) => projects.push({ org, repo }))
47285-
})
47308+
const orgs = Object.keys(scope[platform])
47309+
core.debug(`Total Orgs/Users in scope: ${orgs.length}`)
4728647310

47287-
core.debug(`Total Projects in scope: ${projects.length}`)
47311+
// Structure Projects
47312+
const projects = []
4728847313

47289-
const chunks = chunkArray(projects, maxRequestInParallel)
47290-
core.debug(`Total chunks: ${chunks.length}`)
47314+
orgs.forEach((org) => {
47315+
const repos = scope[platform][org].included
47316+
repos.forEach((repo) => projects.push({ org, repo }))
47317+
})
4729147318

47292-
const scores = []
47319+
core.debug(`Total Projects in scope: ${projects.length}`)
4729347320

47294-
for (let index = 0; index < chunks.length; index++) {
47295-
const chunk = chunks[index]
47296-
core.debug(`Processing chunk ${index + 1}/${chunks.length}`)
47321+
const chunks = chunkArray(projects, maxRequestInParallel)
47322+
core.debug(`Total chunks: ${chunks.length}`)
4729747323

47298-
const chunkScores = await Promise.all(chunk.map(async ({ org, repo }) => {
47299-
const { score, date, commit } = await getProjectScore({ platform, org, repo })
47300-
core.debug(`Got project score for ${platform}/${org}/${repo}: ${score} (${date})`)
47324+
for (let index = 0; index < chunks.length; index++) {
47325+
const chunk = chunks[index]
47326+
core.debug(`Processing chunk ${index + 1}/${chunks.length}`)
4730147327

47302-
const storedScore = getScore({ database, platform, org, repo })
47328+
const chunkScores = await Promise.all(chunk.map(async ({ org, repo }) => {
47329+
const { score, date, commit } = await getProjectScore({ platform, org, repo })
47330+
core.debug(`Got project score for ${platform}/${org}/${repo}: ${score} (${date})`)
47331+
return { score, date, commit, platform, org, repo }
47332+
}))
4730347333

47304-
const scoreData = { platform, org, repo, score, date, commit }
47305-
// If no stored score then record if score is different then:
47306-
if (!storedScore || storedScore.score !== score) {
47307-
saveScore({ database, platform, org, repo, score, date, commit })
47308-
}
47334+
rawScores.push(...chunkScores)
47335+
}
47336+
}
4730947337

47310-
// Add previous score and date if available to the report
47311-
if (storedScore) {
47312-
scoreData.prevScore = storedScore.score
47313-
scoreData.prevDate = storedScore.date
47314-
scoreData.prevCommit = storedScore.commit
47338+
// Common path: enrich scores with database history
47339+
const scores = rawScores.map(({ score, date, commit, platform, org, repo }) => {
47340+
const storedScore = getScore({ database, platform, org, repo })
47341+
const scoreData = { platform, org, repo, score, date, commit }
4731547342

47316-
if (storedScore.score !== score) {
47317-
scoreData.currentDiff = parseFloat((score - storedScore.score).toFixed(1))
47318-
}
47319-
}
47343+
// If no stored score then record if score is different then:
47344+
if (!storedScore || storedScore.score !== score) {
47345+
saveScore({ database, platform, org, repo, score, date, commit })
47346+
}
4732047347

47321-
return scoreData
47322-
}))
47348+
// Add previous score and date if available to the report
47349+
if (storedScore) {
47350+
scoreData.prevScore = storedScore.score
47351+
scoreData.prevDate = storedScore.date
47352+
scoreData.prevCommit = storedScore.commit
4732347353

47324-
scores.push(...chunkScores)
47325-
}
47354+
if (storedScore.score !== score) {
47355+
scoreData.currentDiff = parseFloat((score - storedScore.score).toFixed(1))
47356+
}
47357+
}
47358+
47359+
return scoreData
47360+
})
4732647361

4732747362
core.debug('All the scores are already collected')
4732847363

@@ -49452,9 +49487,10 @@ async function run () {
4945249487
// Context
4945349488
const context = github.context
4945449489
// Inputs
49455-
const scopePath = core.getInput('scope', { required: true })
49490+
const scopePath = core.getInput('scope')
4945649491
const databasePath = core.getInput('database', { required: true })
4945749492
const reportPath = core.getInput('report', { required: true })
49493+
const localResultsPath = core.getInput('local-results-path')
4945849494
// Options
4945949495
const maxRequestInParallel = parseInt(core.getInput('max-request-in-parallel') || 10)
4946049496
const generateIssue = normalizeBoolean(core.getInput('generate-issue'))
@@ -49494,23 +49530,28 @@ async function run () {
4949449530
let scope = { 'github.com': {} }
4949549531
let originalReportContent = ''
4949649532

49497-
// check if scope exists
49498-
core.info('Checking if scope file exists...')
49499-
const existScopeFile = existsSync(scopePath)
49500-
if (!existScopeFile && !discoveryEnabled) {
49501-
throw new Error('Scope file does not exist and discovery is not enabled')
49502-
}
49533+
// In local results mode, scope is discovered from the results file
49534+
if (!localResultsPath) {
49535+
// check if scope exists
49536+
core.info('Checking if scope file exists...')
49537+
const existScopeFile = existsSync(scopePath)
49538+
if (!existScopeFile && !discoveryEnabled) {
49539+
throw new Error('Scope file does not exist and discovery is not enabled')
49540+
}
4950349541

49504-
// Use scope file if it exists
49505-
if (existScopeFile) {
49506-
core.debug('Scope file exists, using it...')
49507-
scope = await readFile(scopePath, 'utf8').then(content => JSON.parse(content))
49508-
validateScopeIntegrity(scope)
49509-
}
49542+
// Use scope file if it exists
49543+
if (existScopeFile) {
49544+
core.debug('Scope file exists, using it...')
49545+
scope = await readFile(scopePath, 'utf8').then(content => JSON.parse(content))
49546+
validateScopeIntegrity(scope)
49547+
}
4951049548

49511-
if (discoveryEnabled) {
49512-
core.info(`Starting discovery for the organizations ${discoveryOrgs}...`)
49513-
scope = await generateScope({ octokit, orgs: discoveryOrgs, scope, maxRequestInParallel })
49549+
if (discoveryEnabled) {
49550+
core.info(`Starting discovery for the organizations ${discoveryOrgs}...`)
49551+
scope = await generateScope({ octokit, orgs: discoveryOrgs, scope, maxRequestInParallel })
49552+
}
49553+
} else {
49554+
core.info(`Using local results from: ${localResultsPath}`)
4951449555
}
4951549556

4951649557
// Check if database exists and load it
@@ -49529,7 +49570,7 @@ async function run () {
4952949570

4953049571
// PROCESS
4953149572
core.info('Generating scores...')
49532-
const { reportContent, issueContent, database: newDatabaseState } = await generateScores({ scope, database, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool })
49573+
const { reportContent, issueContent, database: newDatabaseState } = await generateScores({ scope, database, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool, localResultsPath })
4953349574

4953449575
core.info('Checking database changes...')
4953549576
const hasChanges = isDifferent(database, newDatabaseState)

src/action.js

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,10 @@ async function run () {
7171
// Context
7272
const context = github.context
7373
// Inputs
74-
const scopePath = 'scope.json'
75-
const databasePath = 'databasefile.json'
76-
const reportPath = 'reportfile.md'
77-
// const scopePath = core.getInput('scope', { required: true })
78-
// const databasePath = core.getInput('database', { required: true })
79-
// const reportPath = core.getInput('report', { required: true })
74+
const scopePath = core.getInput('scope')
75+
const databasePath = core.getInput('database', { required: true })
76+
const reportPath = core.getInput('report', { required: true })
77+
const localResultsPath = core.getInput('local-results-path')
8078
// Options
8179
const maxRequestInParallel = parseInt(core.getInput('max-request-in-parallel') || 10)
8280
const generateIssue = normalizeBoolean(core.getInput('generate-issue'))
@@ -116,23 +114,28 @@ async function run () {
116114
let scope = { 'github.com': {} }
117115
let originalReportContent = ''
118116

119-
// check if scope exists
120-
core.info('Checking if scope file exists...')
121-
const existScopeFile = existsSync(scopePath)
122-
if (!existScopeFile && !discoveryEnabled) {
123-
throw new Error('Scope file does not exist and discovery is not enabled')
124-
}
117+
// In local results mode, scope is discovered from the results file
118+
if (!localResultsPath) {
119+
// check if scope exists
120+
core.info('Checking if scope file exists...')
121+
const existScopeFile = existsSync(scopePath)
122+
if (!existScopeFile && !discoveryEnabled) {
123+
throw new Error('Scope file does not exist and discovery is not enabled')
124+
}
125125

126-
// Use scope file if it exists
127-
if (existScopeFile) {
128-
core.debug('Scope file exists, using it...')
129-
scope = await readFile(scopePath, 'utf8').then(content => JSON.parse(content))
130-
validateScopeIntegrity(scope)
131-
}
126+
// Use scope file if it exists
127+
if (existScopeFile) {
128+
core.debug('Scope file exists, using it...')
129+
scope = await readFile(scopePath, 'utf8').then(content => JSON.parse(content))
130+
validateScopeIntegrity(scope)
131+
}
132132

133-
if (discoveryEnabled) {
134-
core.info(`Starting discovery for the organizations ${discoveryOrgs}...`)
135-
scope = await generateScope({ octokit, orgs: discoveryOrgs, scope, maxRequestInParallel })
133+
if (discoveryEnabled) {
134+
core.info(`Starting discovery for the organizations ${discoveryOrgs}...`)
135+
scope = await generateScope({ octokit, orgs: discoveryOrgs, scope, maxRequestInParallel })
136+
}
137+
} else {
138+
core.info(`Using local results from: ${localResultsPath}`)
136139
}
137140

138141
// Check if database exists and load it
@@ -151,7 +154,7 @@ async function run () {
151154

152155
// PROCESS
153156
core.info('Generating scores...')
154-
const { reportContent, issueContent, database: newDatabaseState } = await generateScores({ scope, database, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool })
157+
const { reportContent, issueContent, database: newDatabaseState } = await generateScores({ scope, database, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool, localResultsPath })
155158

156159
core.info('Checking database changes...')
157160
const hasChanges = isDifferent(database, newDatabaseState)

0 commit comments

Comments
 (0)