mirror of
https://github.com/gradle/actions
synced 2024-11-23 18:02:13 +00: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:
parent
a6ad1901be
commit
4c9c435d2f
11 changed files with 153 additions and 65 deletions
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue