mirror of
https://github.com/gradle/gradle-build-action.git
synced 2024-11-22 00:01:05 -05: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
|
required: false
|
||||||
default: true
|
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
|
# EXPERIMENTAL & INTERNAL ACTION INPUTS
|
||||||
# The following action properties allow fine-grained tweaking of the action caching behaviour.
|
# 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`.
|
# 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 = [
|
const initScriptFilenames = [
|
||||||
'build-result-capture.init.gradle',
|
'build-result-capture.init.gradle',
|
||||||
'build-result-capture-service.plugin.groovy',
|
'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) {
|
for (const initScriptFilename of initScriptFilenames) {
|
||||||
const initScriptContent = this.readInitScriptAsString(initScriptFilename)
|
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
|
// 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.
|
// 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])
|
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 {
|
function getCacheKeyJobExecution(): string {
|
||||||
// Used to associate a cache key with a particular execution (default is bound to the git commit sha)
|
// 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
|
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> {
|
export async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Retrieve the dependency graph artifact and submit via Dependency Submission API
|
// Retrieve the dependency graph artifact and submit via Dependency Submission API
|
||||||
await dependencyGraph.submitDependencyGraph()
|
await dependencyGraph.downloadAndSubmitDependencyGraphs()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(String(error))
|
core.setFailed(String(error))
|
||||||
if (error instanceof Error && error.stack) {
|
if (error instanceof Error && error.stack) {
|
||||||
|
|
|
@ -10,57 +10,50 @@ import fs from 'fs'
|
||||||
|
|
||||||
import * as execution from './execution'
|
import * as execution from './execution'
|
||||||
import * as layout from './repository-layout'
|
import * as layout from './repository-layout'
|
||||||
|
import * as params from './input-params'
|
||||||
|
|
||||||
const DEPENDENCY_GRAPH_ARTIFACT = 'dependency-graph'
|
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> {
|
export async function generateDependencyGraph(executable: string | undefined): Promise<void> {
|
||||||
const workspaceDirectory = layout.workspaceDirectory()
|
|
||||||
const buildRootDirectory = layout.buildRootDirectory()
|
const buildRootDirectory = layout.buildRootDirectory()
|
||||||
const buildPath = getRelativePathFromWorkspace(buildRootDirectory)
|
|
||||||
|
|
||||||
const initScript = path.resolve(
|
const args = [':GitHubDependencyGraphPlugin_generateDependencyGraph']
|
||||||
__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'
|
|
||||||
]
|
|
||||||
|
|
||||||
await execution.executeGradleBuild(executable, buildRootDirectory, args)
|
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()
|
const artifactClient = artifact.create()
|
||||||
artifactClient.uploadArtifact(DEPENDENCY_GRAPH_ARTIFACT, [dependencyGraphJson], workspaceDirectory)
|
artifactClient.uploadArtifact(DEPENDENCY_GRAPH_ARTIFACT, graphFiles, workspaceDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyDependencyGraphToBuildRoot(buildRootDirectory: string): string {
|
export async function downloadAndSubmitDependencyGraphs(): Promise<void> {
|
||||||
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> {
|
|
||||||
const workspaceDirectory = layout.workspaceDirectory()
|
const workspaceDirectory = layout.workspaceDirectory()
|
||||||
|
submitDependencyGraphs(await retrieveDependencyGraphs(workspaceDirectory))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitDependencyGraphs(dependencyGraphFiles: string[]): Promise<void> {
|
||||||
const octokit: Octokit = getOctokit()
|
const octokit: Octokit = getOctokit()
|
||||||
|
|
||||||
for (const jsonFile of await retrieveDependencyGraphs(octokit, workspaceDirectory)) {
|
for (const jsonFile of dependencyGraphFiles) {
|
||||||
const jsonContent = fs.readFileSync(jsonFile, 'utf8')
|
const jsonContent = fs.readFileSync(jsonFile, 'utf8')
|
||||||
|
|
||||||
const jsonObject = JSON.parse(jsonContent)
|
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 response = await octokit.request('POST /repos/{owner}/{repo}/dependency-graph/snapshots', jsonObject)
|
||||||
|
|
||||||
const relativeJsonFile = getRelativePathFromWorkspace(jsonFile)
|
const relativeJsonFile = getRelativePathFromWorkspace(jsonFile)
|
||||||
core.info(`Submitted ${relativeJsonFile}: ${JSON.stringify(response)}`)
|
|
||||||
core.notice(`Submitted ${relativeJsonFile}: ${response.data.message}`)
|
core.notice(`Submitted ${relativeJsonFile}: ${response.data.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function findDependencyGraphFiles(dir: string): Promise<string[]> {
|
async function retrieveDependencyGraphs(workspaceDirectory: 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[]> {
|
|
||||||
if (github.context.payload.workflow_run) {
|
if (github.context.payload.workflow_run) {
|
||||||
return await retrieveDependencyGraphsForWorkflowRun(
|
return await retrieveDependencyGraphsForWorkflowRun(github.context.payload.workflow_run.id, workspaceDirectory)
|
||||||
github.context.payload.workflow_run.id,
|
|
||||||
octokit,
|
|
||||||
workspaceDirectory
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return retrieveDependencyGraphsForCurrentWorkflow(workspaceDirectory)
|
return retrieveDependencyGraphsForCurrentWorkflow(workspaceDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retrieveDependencyGraphsForWorkflowRun(
|
async function retrieveDependencyGraphsForWorkflowRun(runId: number, workspaceDirectory: string): Promise<string[]> {
|
||||||
runId: number,
|
const octokit: Octokit = getOctokit()
|
||||||
octokit: Octokit,
|
|
||||||
workspaceDirectory: string
|
|
||||||
): Promise<string[]> {
|
|
||||||
// Find the workflow run artifacts named "dependency-graph"
|
// Find the workflow run artifacts named "dependency-graph"
|
||||||
const artifacts = await octokit.rest.actions.listWorkflowRunArtifacts({
|
const artifacts = await octokit.rest.actions.listWorkflowRunArtifacts({
|
||||||
owner: github.context.repo.owner,
|
owner: github.context.repo.owner,
|
||||||
|
@ -139,6 +118,12 @@ async function retrieveDependencyGraphsForCurrentWorkflow(workspaceDirectory: st
|
||||||
return await findDependencyGraphFiles(downloadPath)
|
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 {
|
function getOctokit(): Octokit {
|
||||||
return new Octokit({
|
return new Octokit({
|
||||||
auth: getGithubToken()
|
auth: getGithubToken()
|
||||||
|
@ -153,3 +138,26 @@ function getRelativePathFromWorkspace(file: string): string {
|
||||||
const workspaceDirectory = layout.workspaceDirectory()
|
const workspaceDirectory = layout.workspaceDirectory()
|
||||||
return path.relative(workspaceDirectory, file)
|
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
|
// Internal parameters
|
||||||
export function getJobContext(): string {
|
export function getJobMatrix(): string {
|
||||||
return core.getInput('workflow-job-context')
|
return core.getInput('workflow-job-context')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +63,10 @@ export function isJobSummaryEnabled(): boolean {
|
||||||
return getBooleanInput('generate-job-summary', true)
|
return getBooleanInput('generate-job-summary', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDependencyGraphEnabled(): boolean {
|
||||||
|
return getBooleanInput('generate-dependency-graph', true)
|
||||||
|
}
|
||||||
|
|
||||||
function getBooleanInput(paramName: string, paramDefault = false): boolean {
|
function getBooleanInput(paramName: string, paramDefault = false): boolean {
|
||||||
const paramValue = core.getInput(paramName)
|
const paramValue = core.getInput(paramName)
|
||||||
switch (paramValue.toLowerCase().trim()) {
|
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.
|
if (System.env.GITHUB_DEPENDENCY_GRAPH_ENABLED != "true") {
|
||||||
initscript {
|
return
|
||||||
dependencies {
|
|
||||||
classpath files("github-dependency-graph-gradle-plugin-0.0.3.jar")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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 caches from './caches'
|
||||||
import * as layout from './repository-layout'
|
import * as layout from './repository-layout'
|
||||||
import * as params from './input-params'
|
import * as params from './input-params'
|
||||||
|
import * as dependencyGraph from './dependency-graph'
|
||||||
|
|
||||||
import {logJobSummary, writeJobSummary} from './job-summary'
|
import {logJobSummary, writeJobSummary} from './job-summary'
|
||||||
import {loadBuildResults} from './build-results'
|
import {loadBuildResults} from './build-results'
|
||||||
|
@ -36,6 +37,10 @@ export async function setup(): Promise<void> {
|
||||||
await caches.restore(gradleUserHome, cacheListener)
|
await caches.restore(gradleUserHome, cacheListener)
|
||||||
|
|
||||||
core.saveState(CACHE_LISTENER, cacheListener.stringify())
|
core.saveState(CACHE_LISTENER, cacheListener.stringify())
|
||||||
|
|
||||||
|
if (params.isDependencyGraphEnabled()) {
|
||||||
|
dependencyGraph.prepare()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function complete(): Promise<void> {
|
export async function complete(): Promise<void> {
|
||||||
|
@ -58,6 +63,10 @@ export async function complete(): Promise<void> {
|
||||||
} else {
|
} else {
|
||||||
logJobSummary(buildResults, cacheListener)
|
logJobSummary(buildResults, cacheListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.isDependencyGraphEnabled()) {
|
||||||
|
dependencyGraph.uploadDependencyGraphs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function determineGradleUserHome(): Promise<string> {
|
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