mirror of
				https://github.com/gradle/gradle-build-action.git
				synced 2025-10-31 07:08:56 +08:00 
			
		
		
		
	Configure Gradle User Home for dependency-graph
Instead of requiring an action step to generate the graph, configure Gradle User Home so that subsequent Gradle invocations can generate a graph. Any generated graph files are uploaded as artifacts on job completion. - Construct job.correlator from workflow/job/matrix - Export job.correlator as an environment var - Upload artifacts at job completion in post-action step - Specify the location of dependency graph report - Only apply dependency graph init script when explicitly enabled
This commit is contained in:
		| @@ -58,6 +58,11 @@ inputs: | ||||
|     required: false | ||||
|     default: true | ||||
|  | ||||
|   generate-dependency-graph: | ||||
|     description: When 'true', a dependency graph snapshot will be generated for Gradle builds. | ||||
|     required: false | ||||
|     default: false | ||||
|  | ||||
|   # EXPERIMENTAL & INTERNAL ACTION INPUTS | ||||
|   # The following action properties allow fine-grained tweaking of the action caching behaviour. | ||||
|   # These properties are experimental and not (yet) designed for production use, and may change without notice in a subsequent release of `gradle-build-action`. | ||||
|   | ||||
| @@ -175,7 +175,8 @@ export class GradleStateCache { | ||||
|         const initScriptFilenames = [ | ||||
|             'build-result-capture.init.gradle', | ||||
|             'build-result-capture-service.plugin.groovy', | ||||
|             'github-dependency-graph.init.gradle' | ||||
|             'github-dependency-graph.init.gradle', | ||||
|             'github-dependency-graph-gradle-plugin-apply.groovy' | ||||
|         ] | ||||
|         for (const initScriptFilename of initScriptFilenames) { | ||||
|             const initScriptContent = this.readInitScriptAsString(initScriptFilename) | ||||
|   | ||||
| @@ -125,10 +125,25 @@ function getCacheKeyJobInstance(): string { | ||||
|  | ||||
|     // By default, we hash the full `matrix` data for the run, to uniquely identify this job invocation | ||||
|     // The only way we can obtain the `matrix` data is via the `workflow-job-context` parameter in action.yml. | ||||
|     const workflowJobContext = params.getJobContext() | ||||
|     const workflowJobContext = params.getJobMatrix() | ||||
|     return hashStrings([workflowJobContext]) | ||||
| } | ||||
|  | ||||
| export function getUniqueLabelForJobInstance(): string { | ||||
|     return getUniqueLabelForJobInstanceValues(github.context.workflow, github.context.job, params.getJobMatrix()) | ||||
| } | ||||
|  | ||||
| export function getUniqueLabelForJobInstanceValues(workflow: string, jobId: string, matrixJson: string): string { | ||||
|     const matrix = JSON.parse(matrixJson) | ||||
|     const matrixString = Object.values(matrix).join('-') | ||||
|     const label = matrixString ? `${workflow}-${jobId}-${matrixString}` : `${workflow}-${jobId}` | ||||
|     return sanitize(label) | ||||
| } | ||||
|  | ||||
| function sanitize(value: string): string { | ||||
|     return value.replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase() | ||||
| } | ||||
|  | ||||
| function getCacheKeyJobExecution(): string { | ||||
|     // Used to associate a cache key with a particular execution (default is bound to the git commit sha) | ||||
|     return process.env[CACHE_KEY_JOB_EXECUTION_VAR] || github.context.sha | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import * as dependencyGraph from './dependency-graph' | ||||
| export async function run(): Promise<void> { | ||||
|     try { | ||||
|         // Retrieve the dependency graph artifact and submit via Dependency Submission API | ||||
|         await dependencyGraph.submitDependencyGraph() | ||||
|         await dependencyGraph.downloadAndSubmitDependencyGraphs() | ||||
|     } catch (error) { | ||||
|         core.setFailed(String(error)) | ||||
|         if (error instanceof Error && error.stack) { | ||||
|   | ||||
| @@ -10,57 +10,50 @@ import fs from 'fs' | ||||
|  | ||||
| import * as execution from './execution' | ||||
| import * as layout from './repository-layout' | ||||
| import * as params from './input-params' | ||||
|  | ||||
| const DEPENDENCY_GRAPH_ARTIFACT = 'dependency-graph' | ||||
| const DEPENDENCY_GRAPH_FILE = 'dependency-graph.json' | ||||
|  | ||||
| export function prepare(): void { | ||||
|     core.info('Enabling dependency graph') | ||||
|     const jobCorrelator = getJobCorrelator() | ||||
|     core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', 'true') | ||||
|     core.exportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR', jobCorrelator) | ||||
|     core.exportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_ID', github.context.runId) | ||||
|     core.exportVariable( | ||||
|         'GITHUB_DEPENDENCY_GRAPH_REPORT_DIR', | ||||
|         path.resolve(layout.workspaceDirectory(), 'dependency-graph-reports') | ||||
|     ) | ||||
| } | ||||
|  | ||||
| export async function generateDependencyGraph(executable: string | undefined): Promise<void> { | ||||
|     const workspaceDirectory = layout.workspaceDirectory() | ||||
|     const buildRootDirectory = layout.buildRootDirectory() | ||||
|     const buildPath = getRelativePathFromWorkspace(buildRootDirectory) | ||||
|  | ||||
|     const initScript = path.resolve( | ||||
|         __dirname, | ||||
|         '..', | ||||
|         '..', | ||||
|         'src', | ||||
|         'resources', | ||||
|         'init-scripts', | ||||
|         'github-dependency-graph.init.gradle' | ||||
|     ) | ||||
|     const args = [ | ||||
|         `-Dorg.gradle.github.env.GRADLE_BUILD_PATH=${buildPath}`, | ||||
|         '--init-script', | ||||
|         initScript, | ||||
|         ':GitHubDependencyGraphPlugin_generateDependencyGraph' | ||||
|     ] | ||||
|     const args = [':GitHubDependencyGraphPlugin_generateDependencyGraph'] | ||||
|  | ||||
|     await execution.executeGradleBuild(executable, buildRootDirectory, args) | ||||
|     const dependencyGraphJson = copyDependencyGraphToBuildRoot(buildRootDirectory) | ||||
| } | ||||
|  | ||||
| export async function uploadDependencyGraphs(): Promise<void> { | ||||
|     const workspaceDirectory = layout.workspaceDirectory() | ||||
|     const graphFiles = await findDependencyGraphFiles(workspaceDirectory) | ||||
|  | ||||
|     const relativeGraphFiles = graphFiles.map(x => getRelativePathFromWorkspace(x)) | ||||
|     core.info(`Uploading dependency graph files: ${relativeGraphFiles}`) | ||||
|  | ||||
|     const artifactClient = artifact.create() | ||||
|     artifactClient.uploadArtifact(DEPENDENCY_GRAPH_ARTIFACT, [dependencyGraphJson], workspaceDirectory) | ||||
|     artifactClient.uploadArtifact(DEPENDENCY_GRAPH_ARTIFACT, graphFiles, workspaceDirectory) | ||||
| } | ||||
|  | ||||
| function copyDependencyGraphToBuildRoot(buildRootDirectory: string): string { | ||||
|     const sourceFile = path.resolve( | ||||
|         buildRootDirectory, | ||||
|         'build', | ||||
|         'reports', | ||||
|         'github-dependency-graph-plugin', | ||||
|         'github-dependency-snapshot.json' | ||||
|     ) | ||||
|  | ||||
|     const destFile = path.resolve(buildRootDirectory, DEPENDENCY_GRAPH_FILE) | ||||
|     fs.copyFileSync(sourceFile, destFile) | ||||
|     return destFile | ||||
| } | ||||
|  | ||||
| export async function submitDependencyGraph(): Promise<void> { | ||||
| export async function downloadAndSubmitDependencyGraphs(): Promise<void> { | ||||
|     const workspaceDirectory = layout.workspaceDirectory() | ||||
|     submitDependencyGraphs(await retrieveDependencyGraphs(workspaceDirectory)) | ||||
| } | ||||
|  | ||||
| async function submitDependencyGraphs(dependencyGraphFiles: string[]): Promise<void> { | ||||
|     const octokit: Octokit = getOctokit() | ||||
|  | ||||
|     for (const jsonFile of await retrieveDependencyGraphs(octokit, workspaceDirectory)) { | ||||
|     for (const jsonFile of dependencyGraphFiles) { | ||||
|         const jsonContent = fs.readFileSync(jsonFile, 'utf8') | ||||
|  | ||||
|         const jsonObject = JSON.parse(jsonContent) | ||||
| @@ -69,34 +62,20 @@ export async function submitDependencyGraph(): Promise<void> { | ||||
|         const response = await octokit.request('POST /repos/{owner}/{repo}/dependency-graph/snapshots', jsonObject) | ||||
|  | ||||
|         const relativeJsonFile = getRelativePathFromWorkspace(jsonFile) | ||||
|         core.info(`Submitted ${relativeJsonFile}: ${JSON.stringify(response)}`) | ||||
|         core.notice(`Submitted ${relativeJsonFile}: ${response.data.message}`) | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function findDependencyGraphFiles(dir: string): Promise<string[]> { | ||||
|     const globber = await glob.create(`${dir}/**/${DEPENDENCY_GRAPH_FILE}`) | ||||
|     const graphFiles = globber.glob() | ||||
|     core.info(`Found graph files in ${dir}: ${graphFiles}`) | ||||
|     return graphFiles | ||||
| } | ||||
|  | ||||
| async function retrieveDependencyGraphs(octokit: Octokit, workspaceDirectory: string): Promise<string[]> { | ||||
| async function retrieveDependencyGraphs(workspaceDirectory: string): Promise<string[]> { | ||||
|     if (github.context.payload.workflow_run) { | ||||
|         return await retrieveDependencyGraphsForWorkflowRun( | ||||
|             github.context.payload.workflow_run.id, | ||||
|             octokit, | ||||
|             workspaceDirectory | ||||
|         ) | ||||
|         return await retrieveDependencyGraphsForWorkflowRun(github.context.payload.workflow_run.id, workspaceDirectory) | ||||
|     } | ||||
|     return retrieveDependencyGraphsForCurrentWorkflow(workspaceDirectory) | ||||
| } | ||||
|  | ||||
| async function retrieveDependencyGraphsForWorkflowRun( | ||||
|     runId: number, | ||||
|     octokit: Octokit, | ||||
|     workspaceDirectory: string | ||||
| ): Promise<string[]> { | ||||
| async function retrieveDependencyGraphsForWorkflowRun(runId: number, workspaceDirectory: string): Promise<string[]> { | ||||
|     const octokit: Octokit = getOctokit() | ||||
|  | ||||
|     // Find the workflow run artifacts named "dependency-graph" | ||||
|     const artifacts = await octokit.rest.actions.listWorkflowRunArtifacts({ | ||||
|         owner: github.context.repo.owner, | ||||
| @@ -139,6 +118,12 @@ async function retrieveDependencyGraphsForCurrentWorkflow(workspaceDirectory: st | ||||
|     return await findDependencyGraphFiles(downloadPath) | ||||
| } | ||||
|  | ||||
| async function findDependencyGraphFiles(dir: string): Promise<string[]> { | ||||
|     const globber = await glob.create(`${dir}/dependency-graph-reports/*.json`) | ||||
|     const graphFiles = globber.glob() | ||||
|     return graphFiles | ||||
| } | ||||
|  | ||||
| function getOctokit(): Octokit { | ||||
|     return new Octokit({ | ||||
|         auth: getGithubToken() | ||||
| @@ -153,3 +138,26 @@ function getRelativePathFromWorkspace(file: string): string { | ||||
|     const workspaceDirectory = layout.workspaceDirectory() | ||||
|     return path.relative(workspaceDirectory, file) | ||||
| } | ||||
|  | ||||
| export function getJobCorrelator(): string { | ||||
|     return constructJobCorrelator(github.context.workflow, github.context.job, params.getJobMatrix()) | ||||
| } | ||||
|  | ||||
| export function constructJobCorrelator(workflow: string, jobId: string, matrixJson: string): string { | ||||
|     const matrixString = describeMatrix(matrixJson) | ||||
|     const label = matrixString ? `${workflow}-${jobId}-${matrixString}` : `${workflow}-${jobId}` | ||||
|     return sanitize(label) | ||||
| } | ||||
|  | ||||
| function describeMatrix(matrixJson: string): string { | ||||
|     core.info(`Got matrix json: ${matrixJson}`) | ||||
|     const matrix = JSON.parse(matrixJson) | ||||
|     if (matrix) { | ||||
|         return Object.values(matrix).join('-') | ||||
|     } | ||||
|     return '' | ||||
| } | ||||
|  | ||||
| function sanitize(value: string): string { | ||||
|     return value.replace(/[^a-zA-Z0-9_-]/g, '').toLowerCase() | ||||
| } | ||||
|   | ||||
| @@ -51,7 +51,7 @@ export function getArguments(): string[] { | ||||
| } | ||||
|  | ||||
| // Internal parameters | ||||
| export function getJobContext(): string { | ||||
| export function getJobMatrix(): string { | ||||
|     return core.getInput('workflow-job-context') | ||||
| } | ||||
|  | ||||
| @@ -63,6 +63,10 @@ export function isJobSummaryEnabled(): boolean { | ||||
|     return getBooleanInput('generate-job-summary', true) | ||||
| } | ||||
|  | ||||
| export function isDependencyGraphEnabled(): boolean { | ||||
|     return getBooleanInput('generate-dependency-graph', true) | ||||
| } | ||||
|  | ||||
| function getBooleanInput(paramName: string, paramDefault = false): boolean { | ||||
|     const paramValue = core.getInput(paramName) | ||||
|     switch (paramValue.toLowerCase().trim()) { | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -0,0 +1,6 @@ | ||||
| buildscript { | ||||
|   dependencies { | ||||
|     classpath files("github-dependency-graph-gradle-plugin-0.0.3.jar") | ||||
|   } | ||||
| } | ||||
| apply plugin: org.gradle.github.GitHubDependencyGraphPlugin | ||||
| @@ -1,7 +1,17 @@ | ||||
| // TODO:DAZ This should be conditionally applied, since the script may be present when not required. | ||||
| initscript { | ||||
|   dependencies { | ||||
|     classpath files("github-dependency-graph-gradle-plugin-0.0.3.jar") | ||||
|   } | ||||
| if (System.env.GITHUB_DEPENDENCY_GRAPH_ENABLED != "true") { | ||||
|   return | ||||
| } | ||||
| apply plugin: org.gradle.github.GitHubDependencyGraphPlugin | ||||
|  | ||||
| def reportDir = System.env.GITHUB_DEPENDENCY_GRAPH_REPORT_DIR | ||||
| def jobCorrelator = System.env.GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR | ||||
| def reportFile = new File(reportDir, jobCorrelator + ".json") | ||||
|  | ||||
| if (reportFile.exists()) { | ||||
|   println "::warning::No dependency report generated for step: report file for '${jobCorrelator}' created in earlier step. Each build invocation requires a unique job correlator: specify GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR var for this step." | ||||
|   return | ||||
| } | ||||
|  | ||||
| println "Generating dependency graph for '${jobCorrelator}'" | ||||
|  | ||||
| // TODO:DAZ This should be conditionally applied, since the script may be present when not required. | ||||
| apply from: 'github-dependency-graph-gradle-plugin-apply.groovy' | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import * as os from 'os' | ||||
| import * as caches from './caches' | ||||
| import * as layout from './repository-layout' | ||||
| import * as params from './input-params' | ||||
| import * as dependencyGraph from './dependency-graph' | ||||
|  | ||||
| import {logJobSummary, writeJobSummary} from './job-summary' | ||||
| import {loadBuildResults} from './build-results' | ||||
| @@ -36,6 +37,10 @@ export async function setup(): Promise<void> { | ||||
|     await caches.restore(gradleUserHome, cacheListener) | ||||
|  | ||||
|     core.saveState(CACHE_LISTENER, cacheListener.stringify()) | ||||
|  | ||||
|     if (params.isDependencyGraphEnabled()) { | ||||
|         dependencyGraph.prepare() | ||||
|     } | ||||
| } | ||||
|  | ||||
| export async function complete(): Promise<void> { | ||||
| @@ -58,6 +63,10 @@ export async function complete(): Promise<void> { | ||||
|     } else { | ||||
|         logJobSummary(buildResults, cacheListener) | ||||
|     } | ||||
|  | ||||
|     if (params.isDependencyGraphEnabled()) { | ||||
|         dependencyGraph.uploadDependencyGraphs() | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function determineGradleUserHome(): Promise<string> { | ||||
|   | ||||
							
								
								
									
										30
									
								
								test/jest/dependency-graph.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								test/jest/dependency-graph.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import * as dependencyGraph from '../../src/dependency-graph' | ||||
|  | ||||
| describe('dependency-graph', () => { | ||||
|     describe('constructs job correlator', () => { | ||||
|         it('removes commas from workflow name', () => { | ||||
|             const id = dependencyGraph.constructJobCorrelator('Workflow, with,commas', 'jobid', '{}') | ||||
|             expect(id).toBe('workflowwithcommas-jobid') | ||||
|         }) | ||||
|         it('removes non word characters', () => { | ||||
|             const id = dependencyGraph.constructJobCorrelator('Workflow!_with()characters', 'job-*id', '{"foo": "bar!@#$%^&*("}') | ||||
|             expect(id).toBe('workflow_withcharacters-job-id-bar') | ||||
|         }) | ||||
|         it('without matrix', () => { | ||||
|             const id = dependencyGraph.constructJobCorrelator('workflow', 'jobid', 'null') | ||||
|             expect(id).toBe('workflow-jobid') | ||||
|         }) | ||||
|         it('with dashes in values', () => { | ||||
|             const id = dependencyGraph.constructJobCorrelator('workflow-name', 'job-id', '{"os": "ubuntu-latest"}') | ||||
|             expect(id).toBe('workflow-name-job-id-ubuntu-latest') | ||||
|         }) | ||||
|         it('with single matrix value', () => { | ||||
|             const id = dependencyGraph.constructJobCorrelator('workflow', 'jobid', '{"os": "windows"}') | ||||
|             expect(id).toBe('workflow-jobid-windows') | ||||
|         }) | ||||
|         it('with composite matrix value', () => { | ||||
|             const id = dependencyGraph.constructJobCorrelator('workflow', 'jobid', '{"os": "windows", "java-version": "21.1", "other": "Value, with COMMA"}') | ||||
|             expect(id).toBe('workflow-jobid-windows-211-valuewithcomma') | ||||
|         }) | ||||
|     }) | ||||
| }) | ||||
		Reference in New Issue
	
	Block a user