From 21fe2669f78306e176601c29a6f91494520f5f8d Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 25 Feb 2025 22:09:16 -0500 Subject: [PATCH 1/6] feat: add entrypoint for action and corresponding tests Signed-off-by: Sebastian Beltran --- __tests__/bin.test.js | 14 +++ package.json | 4 +- src/action.js | 264 +++++++++++++++++++++--------------------- src/bin.js | 6 + 4 files changed, 157 insertions(+), 131 deletions(-) create mode 100644 __tests__/bin.test.js create mode 100644 src/bin.js diff --git a/__tests__/bin.test.js b/__tests__/bin.test.js new file mode 100644 index 0000000..8cfae0c --- /dev/null +++ b/__tests__/bin.test.js @@ -0,0 +1,14 @@ +const { run } = require('../src/action') + +// Mock the action's entrypoint +jest.mock('../src/action', () => ({ + run: jest.fn() +})) + +describe('index', () => { + it('calls run when imported', async () => { + require('../src/bin') + + expect(run).toHaveBeenCalled() + }) +}) \ No newline at end of file diff --git a/package.json b/package.json index a6d1653..c8980f6 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "openssf-scorecard-monitor", "version": "2.0.0-beta8", "description": "A simple way to monitor OpenSSF Scorecard at organization level", - "main": "src/index.js", + "main": "src/action.js", "private": true, "scripts": { - "build": "ncc build src/action.js -o dist", + "build": "ncc build src/bin.js -o dist", "test": "FORCE_COLOR=3 jest --verbose", "test:update": "FORCE_COLOR=3 jest --verbose --u", "test:coverage": "FORCE_COLOR=3 jest --verbose --coverage", diff --git a/src/action.js b/src/action.js index e8beceb..296bdd2 100644 --- a/src/action.js +++ b/src/action.js @@ -10,155 +10,161 @@ const { generateScores, generateScope } = require('./') const { validateDatabaseIntegrity, validateScopeIntegrity } = require('./utils') async function run () { - let octokit - // Context - const context = github.context - // Inputs - const scopePath = core.getInput('scope', { required: true }) - const databasePath = core.getInput('database', { required: true }) - const reportPath = core.getInput('report', { required: true }) - // Options - const maxRequestInParallel = parseInt(core.getInput('max-request-in-parallel') || 10) - const generateIssue = normalizeBoolean(core.getInput('generate-issue')) - const autoPush = normalizeBoolean(core.getInput('auto-push')) - const autoCommit = normalizeBoolean(core.getInput('auto-commit')) - const issueTitle = core.getInput('issue-title') || 'OpenSSF Scorecard Report Updated!' - const issueAssignees = core.getInput('issue-assignees').split(',').filter(x => x !== '').map(x => x.trim()) || [] - const issueLabels = core.getInput('issue-labels').split(',').filter(x => x !== '').map(x => x.trim()) || [] - const githubToken = core.getInput('github-token') - const discoveryEnabled = normalizeBoolean(core.getInput('discovery-enabled')) - const discoveryOrgs = core.getInput('discovery-orgs').split(',').filter(x => x !== '').map(x => x.trim()) || [] - const reportTagsEnabled = normalizeBoolean(core.getInput('report-tags-enabled')) - const startTag = core.getInput('report-start-tag') || '' - const endTag = core.getInput('report-end-tag') || '' - const renderBadge = normalizeBoolean(core.getInput('render-badge')) - const reportTool = core.getInput('report-tool') || 'scorecard-visualizer' - - const availableReportTools = ['scorecard-visualizer', 'deps.dev'] - if (!availableReportTools.includes(reportTool)) { - throw new Error(`The report-tool is not valid, please use: ${availableReportTools.join(', ')}`) - } + try { + let octokit + // Context + const context = github.context + // Inputs + const scopePath = core.getInput('scope', { required: true }) + const databasePath = core.getInput('database', { required: true }) + const reportPath = core.getInput('report', { required: true }) + // Options + const maxRequestInParallel = parseInt(core.getInput('max-request-in-parallel') || 10) + const generateIssue = normalizeBoolean(core.getInput('generate-issue')) + const autoPush = normalizeBoolean(core.getInput('auto-push')) + const autoCommit = normalizeBoolean(core.getInput('auto-commit')) + const issueTitle = core.getInput('issue-title') || 'OpenSSF Scorecard Report Updated!' + const issueAssignees = core.getInput('issue-assignees').split(',').filter(x => x !== '').map(x => x.trim()) || [] + const issueLabels = core.getInput('issue-labels').split(',').filter(x => x !== '').map(x => x.trim()) || [] + const githubToken = core.getInput('github-token') + const discoveryEnabled = normalizeBoolean(core.getInput('discovery-enabled')) + const discoveryOrgs = core.getInput('discovery-orgs').split(',').filter(x => x !== '').map(x => x.trim()) || [] + const reportTagsEnabled = normalizeBoolean(core.getInput('report-tags-enabled')) + const startTag = core.getInput('report-start-tag') || '' + const endTag = core.getInput('report-end-tag') || '' + const renderBadge = normalizeBoolean(core.getInput('render-badge')) + const reportTool = core.getInput('report-tool') || 'scorecard-visualizer' + + const availableReportTools = ['scorecard-visualizer', 'deps.dev'] + if (!availableReportTools.includes(reportTool)) { + throw new Error(`The report-tool is not valid, please use: ${availableReportTools.join(', ')}`) + } - // Error Handling - if (!githubToken && [autoPush, autoCommit, generateIssue, discoveryEnabled].some(value => value)) { - throw new Error('Github token is required for push, commit, create an issue and discovery operations!') - } + // Error Handling + if (!githubToken && [autoPush, autoCommit, generateIssue, discoveryEnabled].some(value => value)) { + throw new Error('Github token is required for push, commit, create an issue and discovery operations!') + } - if (discoveryEnabled && !discoveryOrgs.length) { - throw new Error('Discovery is enabled but no organizations were provided!') - } + if (discoveryEnabled && !discoveryOrgs.length) { + throw new Error('Discovery is enabled but no organizations were provided!') + } - if (githubToken) { - octokit = github.getOctokit(githubToken) - } + if (githubToken) { + octokit = github.getOctokit(githubToken) + } - let database = {} - let scope = { 'github.com': {} } - let originalReportContent = '' + let database = {} + let scope = { 'github.com': {} } + let originalReportContent = '' - // check if scope exists - core.info('Checking if scope file exists...') - const existScopeFile = existsSync(scopePath) - if (!existScopeFile && !discoveryEnabled) { - throw new Error('Scope file does not exist and discovery is not enabled') - } + // check if scope exists + core.info('Checking if scope file exists...') + const existScopeFile = existsSync(scopePath) + if (!existScopeFile && !discoveryEnabled) { + throw new Error('Scope file does not exist and discovery is not enabled') + } - // Use scope file if it exists - if (existScopeFile) { - core.debug('Scope file exists, using it...') - scope = await readFile(scopePath, 'utf8').then(content => JSON.parse(content)) - validateScopeIntegrity(scope) - } + // Use scope file if it exists + if (existScopeFile) { + core.debug('Scope file exists, using it...') + scope = await readFile(scopePath, 'utf8').then(content => JSON.parse(content)) + validateScopeIntegrity(scope) + } - if (discoveryEnabled) { - core.info(`Starting discovery for the organizations ${discoveryOrgs}...`) - scope = await generateScope({ octokit, orgs: discoveryOrgs, scope, maxRequestInParallel }) - } + if (discoveryEnabled) { + core.info(`Starting discovery for the organizations ${discoveryOrgs}...`) + scope = await generateScope({ octokit, orgs: discoveryOrgs, scope, maxRequestInParallel }) + } - // Check if database exists - core.info('Checking if database exists...') - const existDatabaseFile = existsSync(databasePath) - if (existDatabaseFile) { - database = await readFile(databasePath, 'utf8').then(content => JSON.parse(content)) - validateDatabaseIntegrity(database) - } else { - core.info('Database does not exist, creating new database') - } + // Check if database exists + core.info('Checking if database exists...') + const existDatabaseFile = existsSync(databasePath) + if (existDatabaseFile) { + database = await readFile(databasePath, 'utf8').then(content => JSON.parse(content)) + validateDatabaseIntegrity(database) + } else { + core.info('Database does not exist, creating new database') + } - // Check if report exists as the content will be used to update the report with the tags - if (reportTagsEnabled) { - try { - core.info('Checking if report exists...') - await stat(reportPath) - originalReportContent = await readFile(reportPath, 'utf8') - } catch (error) { - core.info('Previous Report does not exist, ignoring previous content for tags...') + // Check if report exists as the content will be used to update the report with the tags + if (reportTagsEnabled) { + try { + core.info('Checking if report exists...') + await stat(reportPath) + originalReportContent = await readFile(reportPath, 'utf8') + } catch (error) { + core.info('Previous Report does not exist, ignoring previous content for tags...') + } } - } - // PROCESS - core.info('Generating scores...') - const { reportContent, issueContent, database: newDatabaseState } = await generateScores({ scope, database, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool }) + // PROCESS + core.info('Generating scores...') + const { reportContent, issueContent, database: newDatabaseState } = await generateScores({ scope, database, maxRequestInParallel, reportTagsEnabled, renderBadge, reportTool }) - core.info('Checking database changes...') - const hasChanges = isDifferent(database, newDatabaseState) + core.info('Checking database changes...') + const hasChanges = isDifferent(database, newDatabaseState) - if (!hasChanges) { - core.info('No changes to database, skipping the rest of the process') - return - } + if (!hasChanges) { + core.info('No changes to database, skipping the rest of the process') + return + } - // Save changes - core.info('Saving changes to database and report') - await writeFile(databasePath, JSON.stringify(newDatabaseState, null, 2)) - await writeFile(reportPath, reportTagsEnabled - ? updateOrCreateSegment({ - original: originalReportContent, - replacementSegment: reportContent, - startTag, - endTag - }) - : reportContent) - - if (discoveryEnabled) { - core.info('Saving changes to scope...') - await writeFile(scopePath, JSON.stringify(scope, null, 2)) - } + // Save changes + core.info('Saving changes to database and report') + await writeFile(databasePath, JSON.stringify(newDatabaseState, null, 2)) + await writeFile(reportPath, reportTagsEnabled + ? updateOrCreateSegment({ + original: originalReportContent, + replacementSegment: reportContent, + startTag, + endTag + }) + : reportContent) - // Commit changes - // @see: https://github.com/actions/checkout#push-a-commit-using-the-built-in-token - if (autoCommit) { - core.info('Committing changes to database and report') - await exec.exec('git config user.name github-actions') - await exec.exec('git config user.email github-actions@github.com') - await exec.exec(`git add ${databasePath}`) - await exec.exec(`git add ${reportPath}`) if (discoveryEnabled) { - core.info('Committing changes to scope...') - await exec.exec(`git add ${scopePath}`) + core.info('Saving changes to scope...') + await writeFile(scopePath, JSON.stringify(scope, null, 2)) + } + + // Commit changes + // @see: https://github.com/actions/checkout#push-a-commit-using-the-built-in-token + if (autoCommit) { + core.info('Committing changes to database and report') + await exec.exec('git config user.name github-actions') + await exec.exec('git config user.email github-actions@github.com') + await exec.exec(`git add ${databasePath}`) + await exec.exec(`git add ${reportPath}`) + if (discoveryEnabled) { + core.info('Committing changes to scope...') + await exec.exec(`git add ${scopePath}`) + } + await exec.exec('git commit -m "Updated Scorecard Report"') } - await exec.exec('git commit -m "Updated Scorecard Report"') - } - // Push changes - if (autoPush) { + // Push changes + if (autoPush) { // @see: https://github.com/actions-js/push/blob/master/start.sh#L43 - core.info('Pushing changes to database and report') - const remoteRepo = `https://${process.env.INPUT_GITHUB_ACTOR}:${githubToken}@github.com/${process.env.INPUT_REPOSITORY}.git` - await exec.exec(`git push origin ${process.env.GITHUB_HEAD_REF} --force --no-verify --repo ${remoteRepo}`) - } + core.info('Pushing changes to database and report') + const remoteRepo = `https://${process.env.INPUT_GITHUB_ACTOR}:${githubToken}@github.com/${process.env.INPUT_REPOSITORY}.git` + await exec.exec(`git push origin ${process.env.GITHUB_HEAD_REF} --force --no-verify --repo ${remoteRepo}`) + } - // Issue creation - if (generateIssue && issueContent) { - core.info('Creating issue...') - await octokit.rest.issues.create({ - ...context.repo, - title: issueTitle, - body: issueContent, - labels: issueLabels, - assignees: issueAssignees - }) + // Issue creation + if (generateIssue && issueContent) { + core.info('Creating issue...') + await octokit.rest.issues.create({ + ...context.repo, + title: issueTitle, + body: issueContent, + labels: issueLabels, + assignees: issueAssignees + }) + } + } catch (error) { + core.setFailed(error.message) } } -run() +module.exports = { + run +} diff --git a/src/bin.js b/src/bin.js new file mode 100644 index 0000000..ef59f47 --- /dev/null +++ b/src/bin.js @@ -0,0 +1,6 @@ +/** + * The entrypoint for the action. + */ +const { run } = require('./action') + +run() \ No newline at end of file From 71570a6e5e6bc8f22e47e43f5b30dc996f0c08d3 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 25 Feb 2025 22:10:21 -0500 Subject: [PATCH 2/6] test: add unit tests for some action input validation Signed-off-by: Sebastian Beltran --- __tests__/action.test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 __tests__/action.test.js diff --git a/__tests__/action.test.js b/__tests__/action.test.js new file mode 100644 index 0000000..e5aea5d --- /dev/null +++ b/__tests__/action.test.js @@ -0,0 +1,34 @@ +const core = require('@actions/core') +const main = require('../src/action') + +// Mock the GitHub Actions core library +const debugMock = jest.spyOn(core, 'debug').mockImplementation() +const getInputMock = jest.spyOn(core, 'getInput').mockImplementation() +const setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation() +const setOutputMock = jest.spyOn(core, 'setOutput').mockImplementation() + +// Mock the action's main function +const runMock = jest.spyOn(main, 'run') + +describe('action', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it.each(["scope", "database", "report"])('should throw an error if the %s is not provided', async (input) => { + getInputMock.mockImplementation(name => { + if (name === input) { + throw new Error(`Input required and not supplied: ${input}`) + } + + return '' + }) + await main.run() + + expect(runMock).toHaveReturned() + expect(setFailedMock).toHaveBeenNthCalledWith( + 1, + `Input required and not supplied: ${input}` + ) + }) +}) From 984223c21a0223ddd888fa3aaf0538a028615ad8 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 25 Feb 2025 22:33:28 -0500 Subject: [PATCH 3/6] test: add validation for report-tool input in action tests Signed-off-by: Sebastian Beltran --- __tests__/action.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/__tests__/action.test.js b/__tests__/action.test.js index e5aea5d..ffed42b 100644 --- a/__tests__/action.test.js +++ b/__tests__/action.test.js @@ -31,4 +31,22 @@ describe('action', () => { `Input required and not supplied: ${input}` ) }) + + it('should throw an error if the available report is not valid', async () => { + getInputMock.mockImplementation(name => { + if (name === 'report-tool') { + return 'invalid' + } + + return '' + }) + await main.run() + + expect(runMock).toHaveReturned() + expect(setFailedMock).toHaveBeenNthCalledWith( + 'The report-tool is not valid, please use: scorecard-visualizer, deps.dev' + ) + }) + + it.todo("should't throw an error if the available report is valid") }) From 2a3b60353f788b1ab4fe0840795ee74e2d7017a3 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 25 Feb 2025 22:52:14 -0500 Subject: [PATCH 4/6] test: enhance input validation for GitHub token requirements in action tests Signed-off-by: Sebastian Beltran --- __tests__/action.test.js | 20 ++++++++++++++++++-- __tests__/bin.test.js | 2 +- src/bin.js | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/__tests__/action.test.js b/__tests__/action.test.js index ffed42b..d227f2c 100644 --- a/__tests__/action.test.js +++ b/__tests__/action.test.js @@ -15,7 +15,7 @@ describe('action', () => { jest.clearAllMocks() }) - it.each(["scope", "database", "report"])('should throw an error if the %s is not provided', async (input) => { + it.each(['scope', 'database', 'report'])('should throw an error if the %s is not provided', async (input) => { getInputMock.mockImplementation(name => { if (name === input) { throw new Error(`Input required and not supplied: ${input}`) @@ -43,10 +43,26 @@ describe('action', () => { await main.run() expect(runMock).toHaveReturned() - expect(setFailedMock).toHaveBeenNthCalledWith( + expect(setFailedMock).toHaveBeenNthCalledWith(1, 'The report-tool is not valid, please use: scorecard-visualizer, deps.dev' ) }) it.todo("should't throw an error if the available report is valid") + + it.each(['auto-push', 'generate-issue', 'auto-commit', 'discovery-enabled'])('should throw an error if the github token is not provided when %s is enabled', async (input) => { + getInputMock.mockImplementation(name => { + if (name === input) { + return 'true' + } + + return '' + }) + await main.run() + + expect(runMock).toHaveReturned() + expect(setFailedMock).toHaveBeenNthCalledWith(1, + 'Github token is required for push, commit, create an issue and discovery operations!' + ) + }) }) diff --git a/__tests__/bin.test.js b/__tests__/bin.test.js index 8cfae0c..ad0662a 100644 --- a/__tests__/bin.test.js +++ b/__tests__/bin.test.js @@ -11,4 +11,4 @@ describe('index', () => { expect(run).toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/src/bin.js b/src/bin.js index ef59f47..fcdb76f 100644 --- a/src/bin.js +++ b/src/bin.js @@ -3,4 +3,4 @@ */ const { run } = require('./action') -run() \ No newline at end of file +run() From 44eb8c4989c6331b8a9e50f448d10734ec893942 Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 25 Feb 2025 22:54:01 -0500 Subject: [PATCH 5/6] test: add todo for default report tool in action tests Signed-off-by: Sebastian Beltran --- __tests__/action.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/__tests__/action.test.js b/__tests__/action.test.js index d227f2c..5ff8999 100644 --- a/__tests__/action.test.js +++ b/__tests__/action.test.js @@ -49,6 +49,7 @@ describe('action', () => { }) it.todo("should't throw an error if the available report is valid") + it.todo("should scorecard-visualizer be the default report tool") it.each(['auto-push', 'generate-issue', 'auto-commit', 'discovery-enabled'])('should throw an error if the github token is not provided when %s is enabled', async (input) => { getInputMock.mockImplementation(name => { From 6ce042fc47458639ad3610e4d609596a107e90cf Mon Sep 17 00:00:00 2001 From: Sebastian Beltran Date: Tue, 25 Feb 2025 22:56:54 -0500 Subject: [PATCH 6/6] test: add validation for missing organizations when discovery is enabled Signed-off-by: Sebastian Beltran --- __tests__/action.test.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/__tests__/action.test.js b/__tests__/action.test.js index 5ff8999..5fda084 100644 --- a/__tests__/action.test.js +++ b/__tests__/action.test.js @@ -66,4 +66,23 @@ describe('action', () => { 'Github token is required for push, commit, create an issue and discovery operations!' ) }) + + it('should throw an error if discovery is enabled but no organizations are provided', async () => { + getInputMock.mockImplementation(name => { + if (name === 'github-token') { + return 'ghp_obj_obj' + } + if (name === 'discovery-enabled') { + return 'true' + } + + return '' + }) + await main.run() + + expect(runMock).toHaveReturned() + expect(setFailedMock).toHaveBeenNthCalledWith(1, + 'Discovery is enabled but no organizations were provided!' + ) + }) })