2023-06-05 15:14:31 -06:00
|
|
|
import * as core from '@actions/core'
|
|
|
|
import * as github from '@actions/github'
|
|
|
|
import * as glob from '@actions/glob'
|
2023-12-23 18:39:37 -07:00
|
|
|
import {DefaultArtifactClient} from '@actions/artifact'
|
2023-09-21 18:55:26 +02:00
|
|
|
import {GitHub} from '@actions/github/lib/utils'
|
2023-09-30 08:37:51 -06:00
|
|
|
import {RequestError} from '@octokit/request-error'
|
2023-09-26 05:14:11 -06:00
|
|
|
import type {PullRequestEvent} from '@octokit/webhooks-types'
|
2023-06-05 15:14:31 -06:00
|
|
|
|
|
|
|
import * as path from 'path'
|
|
|
|
import fs from 'fs'
|
|
|
|
|
2024-04-11 11:56:55 -06:00
|
|
|
import {JobFailure} from './errors'
|
2024-04-09 13:47:18 -06:00
|
|
|
import {DependencyGraphConfig, DependencyGraphOption, getGithubToken, getWorkspaceDirectory} from './configuration'
|
2023-06-05 15:14:31 -06:00
|
|
|
|
2023-12-23 18:39:37 -07:00
|
|
|
const DEPENDENCY_GRAPH_PREFIX = 'dependency-graph_'
|
2023-07-01 19:00:28 -06:00
|
|
|
|
2024-04-06 20:37:46 -06:00
|
|
|
export async function setup(config: DependencyGraphConfig): Promise<void> {
|
2024-04-07 11:54:02 -06:00
|
|
|
const option = config.getDependencyGraphOption()
|
|
|
|
if (option === DependencyGraphOption.Disabled) {
|
2024-04-06 19:03:51 -06:00
|
|
|
core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', 'false')
|
2023-09-29 20:36:16 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
// Download and submit early, for compatability with dependency review.
|
2024-04-07 11:54:02 -06:00
|
|
|
if (option === DependencyGraphOption.DownloadAndSubmit) {
|
2024-04-25 10:37:03 +01:00
|
|
|
maybeExportVariable('DEPENDENCY_GRAPH_REPORT_DIR', config.getReportDirectory())
|
2024-04-06 20:37:46 -06:00
|
|
|
await downloadAndSubmitDependencyGraphs(config)
|
2023-07-05 12:33:47 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
core.info('Enabling dependency graph generation')
|
2024-04-06 19:03:51 -06:00
|
|
|
core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', 'true')
|
2024-04-07 11:54:02 -06:00
|
|
|
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_CONTINUE_ON_FAILURE', config.getDependencyGraphContinueOnFailure())
|
2024-04-06 20:37:46 -06:00
|
|
|
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR', config.getJobCorrelator())
|
2024-07-19 17:07:41 -06:00
|
|
|
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_ID', github.context.runId.toString())
|
2024-01-16 09:43:56 -07:00
|
|
|
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_REF', github.context.ref)
|
|
|
|
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_SHA', getShaFromContext())
|
2024-04-08 14:04:29 -06:00
|
|
|
maybeExportVariable('GITHUB_DEPENDENCY_GRAPH_WORKSPACE', getWorkspaceDirectory())
|
2024-04-18 13:40:41 -06:00
|
|
|
maybeExportVariable('DEPENDENCY_GRAPH_REPORT_DIR', config.getReportDirectory())
|
2024-07-19 17:07:41 -06:00
|
|
|
|
|
|
|
maybeExportVariable('DEPENDENCY_GRAPH_EXCLUDE_PROJECTS', config.getExcludeProjects())
|
|
|
|
maybeExportVariable('DEPENDENCY_GRAPH_INCLUDE_PROJECTS', config.getIncludeProjects())
|
|
|
|
maybeExportVariable('DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS', config.getExcludeConfigurations())
|
|
|
|
maybeExportVariable('DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS', config.getIncludeConfigurations())
|
2023-07-01 19:00:28 -06:00
|
|
|
}
|
2023-06-05 15:14:31 -06:00
|
|
|
|
2024-07-19 17:07:41 -06:00
|
|
|
function maybeExportVariable(variableName: string, value: string | boolean | undefined): void {
|
2024-01-16 09:43:56 -07:00
|
|
|
if (!process.env[variableName]) {
|
2024-07-19 17:07:41 -06:00
|
|
|
if (value !== undefined) {
|
|
|
|
core.exportVariable(variableName, value)
|
|
|
|
}
|
2024-01-16 09:43:56 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-06 20:37:46 -06:00
|
|
|
export async function complete(config: DependencyGraphConfig): Promise<void> {
|
2024-04-07 11:54:02 -06:00
|
|
|
const option = config.getDependencyGraphOption()
|
2023-12-23 18:39:37 -07:00
|
|
|
try {
|
2024-04-07 11:54:02 -06:00
|
|
|
switch (option) {
|
2023-12-23 18:39:37 -07:00
|
|
|
case DependencyGraphOption.Disabled:
|
|
|
|
case DependencyGraphOption.Generate: // Performed via init-script: nothing to do here
|
|
|
|
case DependencyGraphOption.DownloadAndSubmit: // Performed in setup
|
|
|
|
return
|
|
|
|
case DependencyGraphOption.GenerateAndSubmit:
|
2024-07-15 12:47:06 -06:00
|
|
|
await findAndSubmitDependencyGraphs(config)
|
2023-12-23 18:39:37 -07:00
|
|
|
return
|
|
|
|
case DependencyGraphOption.GenerateAndUpload:
|
2024-07-15 12:47:06 -06:00
|
|
|
await findAndUploadDependencyGraphs(config)
|
2023-12-23 18:39:37 -07:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2024-04-07 11:54:02 -06:00
|
|
|
warnOrFail(config, option, e)
|
2023-07-05 12:33:47 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-06 20:37:46 -06:00
|
|
|
async function downloadAndSubmitDependencyGraphs(config: DependencyGraphConfig): Promise<void> {
|
2024-04-06 16:35:03 -06:00
|
|
|
if (isRunningInActEnvironment()) {
|
2024-07-15 12:47:06 -06:00
|
|
|
core.info('Dependency graph not supported in the ACT environment.')
|
2024-04-06 16:35:03 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-23 18:39:37 -07:00
|
|
|
try {
|
2024-07-16 13:45:14 -06:00
|
|
|
await submitDependencyGraphs(await downloadDependencyGraphs(config))
|
2023-12-23 18:39:37 -07:00
|
|
|
} catch (e) {
|
2024-04-07 11:54:02 -06:00
|
|
|
warnOrFail(config, DependencyGraphOption.DownloadAndSubmit, e)
|
2023-12-23 18:39:37 -07:00
|
|
|
}
|
2023-07-01 19:00:28 -06:00
|
|
|
}
|
|
|
|
|
2024-07-15 12:47:06 -06:00
|
|
|
async function findAndSubmitDependencyGraphs(config: DependencyGraphConfig): Promise<void> {
|
2024-04-07 12:27:51 -06:00
|
|
|
if (isRunningInActEnvironment()) {
|
2024-07-15 12:47:06 -06:00
|
|
|
core.info('Dependency graph not supported in the ACT environment.')
|
2024-04-07 12:27:51 -06:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-07-15 12:47:06 -06:00
|
|
|
const dependencyGraphFiles = await findDependencyGraphFiles()
|
|
|
|
try {
|
|
|
|
await submitDependencyGraphs(dependencyGraphFiles)
|
|
|
|
} catch (e) {
|
2023-09-30 08:37:51 -06:00
|
|
|
try {
|
2024-07-15 12:47:06 -06:00
|
|
|
await uploadDependencyGraphs(dependencyGraphFiles, config)
|
|
|
|
} catch (uploadError) {
|
|
|
|
core.info(String(uploadError))
|
2023-09-30 08:37:51 -06:00
|
|
|
}
|
2024-07-15 12:47:06 -06:00
|
|
|
throw e
|
2023-09-30 08:37:51 -06:00
|
|
|
}
|
|
|
|
}
|
2023-06-05 15:14:31 -06:00
|
|
|
|
2024-07-15 12:47:06 -06:00
|
|
|
async function findAndUploadDependencyGraphs(config: DependencyGraphConfig): Promise<void> {
|
|
|
|
if (isRunningInActEnvironment()) {
|
|
|
|
core.info('Dependency graph not supported in the ACT environment.')
|
|
|
|
return
|
2023-12-19 13:55:20 -07:00
|
|
|
}
|
2023-06-05 15:14:31 -06:00
|
|
|
|
2024-07-15 12:47:06 -06:00
|
|
|
await uploadDependencyGraphs(await findDependencyGraphFiles(), config)
|
2023-06-05 15:14:31 -06:00
|
|
|
}
|
|
|
|
|
2024-07-16 13:45:14 -06:00
|
|
|
async function downloadDependencyGraphs(config: DependencyGraphConfig): Promise<string[]> {
|
2023-12-23 18:39:37 -07:00
|
|
|
const findBy = github.context.payload.workflow_run
|
|
|
|
? {
|
|
|
|
token: getGithubToken(),
|
|
|
|
workflowRunId: github.context.payload.workflow_run.id,
|
|
|
|
repositoryName: github.context.repo.repo,
|
|
|
|
repositoryOwner: github.context.repo.owner
|
|
|
|
}
|
|
|
|
: undefined
|
2023-06-05 15:14:31 -06:00
|
|
|
|
2023-12-23 18:39:37 -07:00
|
|
|
const artifactClient = new DefaultArtifactClient()
|
2023-06-05 15:14:31 -06:00
|
|
|
|
2024-07-16 13:45:14 -06:00
|
|
|
let dependencyGraphArtifacts = (
|
2023-12-23 18:39:37 -07:00
|
|
|
await artifactClient.listArtifacts({
|
|
|
|
latest: true,
|
|
|
|
findBy
|
|
|
|
})
|
2024-04-18 13:40:41 -06:00
|
|
|
).artifacts.filter(artifact => artifact.name.startsWith(DEPENDENCY_GRAPH_PREFIX))
|
2023-12-23 18:39:37 -07:00
|
|
|
|
2024-07-16 13:45:14 -06:00
|
|
|
const artifactName = config.getDownloadArtifactName()
|
|
|
|
if (artifactName) {
|
|
|
|
core.info(`Filtering for artifacts ending with ${artifactName}`)
|
|
|
|
dependencyGraphArtifacts = dependencyGraphArtifacts.filter(artifact => artifact.name.includes(artifactName))
|
|
|
|
}
|
|
|
|
|
2023-12-23 18:39:37 -07:00
|
|
|
for (const artifact of dependencyGraphArtifacts) {
|
|
|
|
const downloadedArtifact = await artifactClient.downloadArtifact(artifact.id, {
|
|
|
|
findBy
|
|
|
|
})
|
|
|
|
core.info(`Downloading dependency-graph artifact ${artifact.name} to ${downloadedArtifact.downloadPath}`)
|
2023-06-05 15:14:31 -06:00
|
|
|
}
|
|
|
|
|
2024-04-18 13:40:41 -06:00
|
|
|
return findDependencyGraphFiles()
|
2023-06-05 15:14:31 -06:00
|
|
|
}
|
|
|
|
|
2024-04-18 13:40:41 -06:00
|
|
|
async function findDependencyGraphFiles(): Promise<string[]> {
|
|
|
|
const globber = await glob.create(`${getReportDirectory()}/**/*.json`)
|
2024-04-07 12:27:51 -06:00
|
|
|
const allFiles = await globber.glob()
|
|
|
|
const unprocessedFiles = allFiles.filter(file => !isProcessed(file))
|
|
|
|
unprocessedFiles.forEach(markProcessed)
|
2024-04-18 13:40:41 -06:00
|
|
|
core.info(`Found dependency graph files: ${unprocessedFiles.join(', ')}`)
|
2024-04-07 12:27:51 -06:00
|
|
|
return unprocessedFiles
|
|
|
|
}
|
|
|
|
|
2024-07-15 12:47:06 -06:00
|
|
|
async function uploadDependencyGraphs(dependencyGraphFiles: string[], config: DependencyGraphConfig): Promise<void> {
|
|
|
|
if (dependencyGraphFiles.length === 0) {
|
|
|
|
core.info('No dependency graph files found to upload.')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const workspaceDirectory = getWorkspaceDirectory()
|
|
|
|
|
|
|
|
const artifactClient = new DefaultArtifactClient()
|
|
|
|
for (const dependencyGraphFile of dependencyGraphFiles) {
|
|
|
|
const relativePath = getRelativePathFromWorkspace(dependencyGraphFile)
|
|
|
|
core.info(`Uploading dependency graph file: ${relativePath}`)
|
|
|
|
const artifactName = `${DEPENDENCY_GRAPH_PREFIX}${path.basename(dependencyGraphFile)}`
|
|
|
|
await artifactClient.uploadArtifact(artifactName, [dependencyGraphFile], workspaceDirectory, {
|
|
|
|
retentionDays: config.getArtifactRetentionDays()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function submitDependencyGraphs(dependencyGraphFiles: string[]): Promise<void> {
|
|
|
|
if (dependencyGraphFiles.length === 0) {
|
|
|
|
core.info('No dependency graph files found to submit.')
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const dependencyGraphFile of dependencyGraphFiles) {
|
|
|
|
try {
|
|
|
|
await submitDependencyGraphFile(dependencyGraphFile)
|
|
|
|
} catch (error) {
|
|
|
|
if (error instanceof RequestError) {
|
|
|
|
error.message = translateErrorMessage(dependencyGraphFile, error)
|
|
|
|
}
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function translateErrorMessage(jsonFile: string, error: RequestError): string {
|
|
|
|
const relativeJsonFile = getRelativePathFromWorkspace(jsonFile)
|
|
|
|
const mainWarning = `Dependency submission failed for ${relativeJsonFile}.\n${error.message}`
|
|
|
|
if (error.message === 'Resource not accessible by integration') {
|
|
|
|
return `${mainWarning}
|
|
|
|
Please ensure that the 'contents: write' permission is available for the workflow job.
|
|
|
|
Note that this permission is never available for a 'pull_request' trigger from a repository fork.
|
|
|
|
`
|
|
|
|
}
|
|
|
|
return mainWarning
|
|
|
|
}
|
|
|
|
|
|
|
|
async function submitDependencyGraphFile(jsonFile: string): Promise<void> {
|
|
|
|
const octokit = getOctokit()
|
|
|
|
const jsonContent = fs.readFileSync(jsonFile, 'utf8')
|
|
|
|
|
|
|
|
const jsonObject = JSON.parse(jsonContent)
|
|
|
|
jsonObject.owner = github.context.repo.owner
|
|
|
|
jsonObject.repo = github.context.repo.repo
|
|
|
|
const response = await octokit.request('POST /repos/{owner}/{repo}/dependency-graph/snapshots', jsonObject)
|
|
|
|
|
|
|
|
const relativeJsonFile = getRelativePathFromWorkspace(jsonFile)
|
|
|
|
core.notice(`Submitted ${relativeJsonFile}: ${response.data.message}`)
|
|
|
|
}
|
2024-04-18 13:40:41 -06:00
|
|
|
function getReportDirectory(): string {
|
|
|
|
return process.env.DEPENDENCY_GRAPH_REPORT_DIR!
|
|
|
|
}
|
|
|
|
|
2024-04-07 12:27:51 -06:00
|
|
|
function isProcessed(dependencyGraphFile: string): boolean {
|
|
|
|
const markerFile = `${dependencyGraphFile}.processed`
|
|
|
|
return fs.existsSync(markerFile)
|
|
|
|
}
|
|
|
|
|
|
|
|
function markProcessed(dependencyGraphFile: string): void {
|
|
|
|
const markerFile = `${dependencyGraphFile}.processed`
|
|
|
|
fs.writeFileSync(markerFile, '')
|
2023-07-01 19:00:28 -06:00
|
|
|
}
|
|
|
|
|
2024-04-07 11:54:02 -06:00
|
|
|
function warnOrFail(config: DependencyGraphConfig, option: String, error: unknown): void {
|
|
|
|
if (!config.getDependencyGraphContinueOnFailure()) {
|
2024-04-11 11:56:55 -06:00
|
|
|
throw new JobFailure(error)
|
2024-01-12 11:15:01 -07:00
|
|
|
}
|
|
|
|
|
2024-04-07 11:54:02 -06:00
|
|
|
core.warning(`Failed to ${option} dependency graph. Will continue.\n${String(error)}`)
|
2024-01-12 11:15:01 -07:00
|
|
|
}
|
|
|
|
|
2023-09-21 18:55:26 +02:00
|
|
|
function getOctokit(): InstanceType<typeof GitHub> {
|
|
|
|
return github.getOctokit(getGithubToken())
|
2023-06-05 15:14:31 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
function getRelativePathFromWorkspace(file: string): string {
|
2024-04-08 14:04:29 -06:00
|
|
|
const workspaceDirectory = getWorkspaceDirectory()
|
2023-06-05 15:14:31 -06:00
|
|
|
return path.relative(workspaceDirectory, file)
|
|
|
|
}
|
2023-07-01 19:00:28 -06:00
|
|
|
|
2023-09-26 05:14:11 -06:00
|
|
|
function getShaFromContext(): string {
|
|
|
|
const context = github.context
|
|
|
|
const pullRequestEvents = [
|
|
|
|
'pull_request',
|
|
|
|
'pull_request_comment',
|
|
|
|
'pull_request_review',
|
|
|
|
'pull_request_review_comment'
|
|
|
|
// Note that pull_request_target is omitted here.
|
|
|
|
// That event runs in the context of the base commit of the PR,
|
|
|
|
// so the snapshot should not be associated with the head commit.
|
|
|
|
]
|
|
|
|
if (pullRequestEvents.includes(context.eventName)) {
|
|
|
|
const pr = (context.payload as PullRequestEvent).pull_request
|
|
|
|
return pr.head.sha
|
|
|
|
} else {
|
|
|
|
return context.sha
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-06 16:35:03 -06:00
|
|
|
function isRunningInActEnvironment(): boolean {
|
|
|
|
return process.env.ACT !== undefined
|
|
|
|
}
|