Add basic support for GitHub Dependency Graph (#782)

This commit is contained in:
Daz DeBoer 2023-07-08 04:57:02 +02:00 committed by GitHub
commit 7a67f395d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 18145 additions and 160 deletions

View file

@ -12,6 +12,7 @@
"import/no-namespace": "off", "import/no-namespace": "off",
"i18n-text/no-en": "off", "i18n-text/no-en": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-shadow": "off",
"sort-imports": "off", "sort-imports": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
@ -30,6 +31,7 @@
"@typescript-eslint/no-misused-new": "error", "@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-namespace": "error", "@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-shadow": "error",
"@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-qualifier": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/no-useless-constructor": "error", "@typescript-eslint/no-useless-constructor": "error",

View file

@ -29,6 +29,11 @@ jobs:
with: with:
cache-key-prefix: ${{github.run_number}}- cache-key-prefix: ${{github.run_number}}-
dependency-graph:
uses: ./.github/workflows/integ-test-dependency-graph.yml
with:
cache-key-prefix: ${{github.run_number}}-
execution-with-caching: execution-with-caching:
uses: ./.github/workflows/integ-test-execution-with-caching.yml uses: ./.github/workflows/integ-test-execution-with-caching.yml
with: with:

View file

@ -50,6 +50,13 @@ jobs:
runner-os: '["ubuntu-latest"]' runner-os: '["ubuntu-latest"]'
download-dist: true download-dist: true
dependency-graph:
needs: build-distribution
uses: ./.github/workflows/integ-test-dependency-graph.yml
with:
runner-os: '["ubuntu-latest"]'
download-dist: true
execution-with-caching: execution-with-caching:
needs: build-distribution needs: build-distribution
uses: ./.github/workflows/integ-test-execution-with-caching.yml uses: ./.github/workflows/integ-test-execution-with-caching.yml

View file

@ -0,0 +1,68 @@
name: Test execution with caching
on:
workflow_call:
inputs:
cache-key-prefix:
type: string
runner-os:
type: string
default: '["ubuntu-latest", "windows-latest", "macos-latest"]'
download-dist:
type: boolean
default: false
env:
DOWNLOAD_DIST: ${{ inputs.download-dist }}
GRADLE_BUILD_ACTION_CACHE_KEY_PREFIX: dependency-graph-${{ inputs.cache-key-prefix }}
GRADLE_BUILD_ACTION_CACHE_DEBUG_ENABLED: true
jobs:
groovy-generate:
strategy:
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Download distribution if required
uses: ./.github/actions/download-dist
- name: Setup Gradle for dependency-graph generate
uses: ./
with:
dependency-graph: generate
- name: Run gradle build
run: ./gradlew build
working-directory: .github/workflow-samples/groovy-dsl
kotlin-generate:
strategy:
matrix:
os: ${{fromJSON(inputs.runner-os)}}
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Download distribution if required
uses: ./.github/actions/download-dist
- name: Setup Gradle for dependency-graph generate
uses: ./
with:
dependency-graph: generate-and-submit
- name: Run gradle build
run: ./gradlew build
working-directory: .github/workflow-samples/kotlin-dsl
submit:
needs: [groovy-generate, kotlin-generate]
runs-on: "ubuntu-latest"
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Download distribution if required
uses: ./.github/actions/download-dist
- name: Submit dependency graphs
uses: ./
with:
dependency-graph: download-and-submit

117
README.md
View file

@ -408,3 +408,120 @@ You can use the `gradle-build-action` on GitHub Enterprise Server, and benefit f
- Easily run your build with different versions of Gradle - Easily run your build with different versions of Gradle
- Save/restore of Gradle User Home (requires GHES v3.5+ : GitHub Actions cache was introduced in GHES 3.5) - Save/restore of Gradle User Home (requires GHES v3.5+ : GitHub Actions cache was introduced in GHES 3.5)
- Support for GitHub Actions Job Summary (requires GHES 3.6+ : GitHub Actions Job Summary support was introduced in GHES 3.6). In earlier versions of GHES the build-results summary and caching report will be written to the workflow log, as part of the post-action step. - Support for GitHub Actions Job Summary (requires GHES 3.6+ : GitHub Actions Job Summary support was introduced in GHES 3.6). In earlier versions of GHES the build-results summary and caching report will be written to the workflow log, as part of the post-action step.
# GitHub Dependency Graph support (Experimental)
The `gradle-build-action` has experimental support for submitting a [GitHub Dependency Graph](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph) snapshot via the [GitHub Dependency Submission API](https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28).
The dependency graph snapshot is generated via integration with the [GitHub Dependency Graph Gradle Plugin](https://plugins.gradle.org/plugin/org.gradle.github-dependency-graph-gradle-plugin), and saved as a workflow artifact. The generated snapshot files can be submitted either in the same job, or in a subsequent job (in the same or a dependent workflow).
You enable GitHub Dependency Graph support by setting the `dependency-graph` action parameter. Valid values are:
|<div style="width:290px">Option</div> | Behaviour |
| --- |---|
| `disabled` | Do not generate a dependency graph for any build invocations.<p>This is the default. |
| `generate` | Generate a dependency graph snapshot for each build invocation, saving as a workflow artifact. |
| `generate-and-submit` | As per `generate`, but any generated dependency graph snapshots will be submitted at the end of the job. |
| `download-and-submit` | Download any previously saved dependency graph snapshots, submitting them via the Dependency Submission API. This can be useful to collect all snapshots in a matrix of builds and submit them in one step. |
- 'disabled': Do not generate a dependency graph for any build invocations. This is the default.
- 'generate': Generate a dependency graph snapshot for each build invocation, saving as a workflow artifact.
- 'generate-and-submit': As per 'generate', but any generated dependency graph snapshots will be submitted at the end of the job.
- 'download-and-submit': Download any previously saved dependency graph snapshots, submitting them via the Dependency Submission API. This can be useful to collect all snapshots in a matrix of builds and submit them in one step.
Dependency Graph _submission_ (but not generation) requires the `contents: write` permission, which may need to be explicitly enabled in the workflow file.
Example of a simple workflow that generates and submits a dependency graph:
```yaml
name: Submit dependency graph
on:
push:
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Gradle to generate and submit dependency graphs
uses: gradle/gradle-build-action@dependency-graph
with:
dependency-graph: generate-and-submit
- name: Run a build, generating the dependency graph snapshot which will be submitted
run: ./gradlew build
```
### Running multiple builds in a single Job
GitHub tracks dependency snapshots based on the `job.correlator` value that is embedded in the snapshot. When a newer snapshot for an existing correlator is submitted, the previous snapshot is replaced. Snapshots with different `job.correlator` values are additive to the overall dependency graph for the repository.
The `gradle-build-action` will generate a `job.correlator` value based on the workflow name, job id and matrix values. However, if your job steps contains multiple Gradle invocations, then a unique correlator value must be assigned to each. You assign a correlator by setting the `GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR` environment variable.
```yaml
name: dependency-graph
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Gradle to generate and submit dependency graphs
uses: gradle/gradle-build-action@dependency-graph
with:
dependency-graph: generate-and-submit
- name: Run first build using the default job correlator 'dependency-graph-build'
run: ./gradlew build
- name: Run second build providing a unique job correlator
run: ./gradlew test
env:
GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR: dependency-graph-test
```
### Dependency snapshots generated for pull requests
This `contents: write` permission is not available for any workflow that is triggered by a pull request submitted from a forked repository, since it would permit a malicious pull request to make repository changes.
Because of this restriction, it is not possible to `generate-and-submit` a dependency graph generated for a pull-request that comes from a repository fork. In order to do so, 2 workflows will be required:
1. The first workflow runs directly against the pull request sources and will generate the dependency graph snapshot.
2. The second workflow is triggered on `workflow_run` of the first workflow, and will submit the previously saved dependency snapshots.
Note: when `download-and-submit` is used in a workflow triggered via [workflow_run](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run), the action will download snapshots saved in the triggering workflow.
***Main workflow file***
```yaml
name: run-build-and-generate-dependency-snapshot
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Gradle to generate and submit dependency graphs
uses: gradle/gradle-build-action@v2
with:
dependency-graph: generate # Only generate in this job
- name: Run a build, generating the dependency graph snapshot which will be submitted
run: ./gradlew build
```
***Dependent workflow file***
```yaml
name: submit-dependency-snapshot
on:
workflow_run:
workflows: ['run-build-and-generate-dependency-snapshot']
types: [completed]
jobs:
submit-snapshots:
runs-on: ubuntu-latest
steps:
- name: Retrieve dependency graph artifact and submit
uses: gradle/gradle-build-action@v2
with:
dependency-graph: download-and-submit
```

View file

@ -58,6 +58,11 @@ inputs:
required: false required: false
default: true default: true
dependency-graph:
description: Specifies if a GitHub dependency snapshot should be generated for each Gradle build, and if so, how. Valid values are 'disabled' (default), 'generate', 'generate-and-submit' and 'download-and-submit'.
required: false
default: 'disabled'
# 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`.
@ -75,6 +80,11 @@ inputs:
required: false required: false
default: false default: false
github-token:
description: The GitHub token used to authenticate when submitting via the Dependency Submission API.
default: ${{ github.token }}
required: false
outputs: outputs:
build-scan-url: build-scan-url:
description: Link to the build scan if any description: Link to the build scan if any

View file

@ -0,0 +1,24 @@
name: 'Clear dependency graph for a correlator'
inputs:
job-correlator:
required: true
runs:
using: "composite"
steps:
- name: Set current timestamp as env variable
shell: bash
run: echo "NOW=$(date -Iseconds)" >> $GITHUB_ENV
- name: Submit empty dependency graph
shell: bash
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ github.token }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/dependency-graph/snapshots \
-d '{ "version" : 0, "job" : { "id" : "${{ github.run_id }}", "correlator" : "${{ inputs.job-correlator }} " }, "sha" : "${{ github.sha }}", "ref" : "${{ github.ref }}", "detector" : { "name" : "GitHub Dependency Graph Gradle Plugin", "version" : "0.0.3", "url" : "https://github.com/gradle/github-dependency-graph-gradle-plugin" }, "manifests" : {}, "scanned" : "${{ env.NOW }}" }'
- run: echo "::notice ::Cleared dependency graph for job correlator '${{ inputs.job-correlator }}'"
shell: bash

View file

@ -1,19 +0,0 @@
name: "Dependency Graph Generate"
description: Calculates the complete dependency graph for the repository, saving it as a JSON artifact.
inputs:
gradle-version:
description: Gradle version to use. If specified, this Gradle version will be downloaded, added to the PATH and used for invoking Gradle.
required: false
gradle-executable:
description: Path to the Gradle executable. If specified, this executable will be added to the PATH and used for invoking Gradle.
required: false
build-root-directory:
description: Path to the root directory of the build. Default is the root of the GitHub workspace.
required: false
runs:
using: 'node16'
main: '../../dist/dependency-graph-generate/index.js'

View file

@ -1,12 +0,0 @@
name: "Dependency Graph Submit"
description: Retrieves a previously created dependency graph JSON and submits via the GitHub Dependency Submission API.
inputs:
github-token:
description: The GitHub token used to authenticate when submitting via the Dependency Submission API.
default: ${{ github.token }}
required: false
runs:
using: 'node16'
main: '../../dist/dependency-graph-submit/index.js'

8367
dist/main/index.js vendored

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

9264
dist/post/index.js vendored

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -11,9 +11,7 @@
"compile-main": "ncc build src/main.ts --out dist/main --source-map --no-source-map-register", "compile-main": "ncc build src/main.ts --out dist/main --source-map --no-source-map-register",
"compile-post": "ncc build src/post.ts --out dist/post --source-map --no-source-map-register", "compile-post": "ncc build src/post.ts --out dist/post --source-map --no-source-map-register",
"compile-dependency-graph-generate": "ncc build src/dependency-graph-generate.ts --out dist/dependency-graph-generate --source-map --no-source-map-register", "compile": "npm run compile-main && npm run compile-post",
"compile-dependency-graph-submit": "ncc build src/dependency-graph-submit.ts --out dist/dependency-graph-submit --source-map --no-source-map-register",
"compile": "npm run compile-main && npm run compile-post && npm run compile-dependency-graph-generate && npm run compile-dependency-graph-submit",
"test": "jest", "test": "jest",
"check": "npm run format && npm run lint", "check": "npm run format && npm run lint",

View file

@ -172,7 +172,12 @@ export class GradleStateCache {
} }
private initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void { private initializeGradleUserHome(gradleUserHome: string, initScriptsDir: string): void {
const initScriptFilenames = ['build-result-capture.init.gradle', 'build-result-capture-service.plugin.groovy'] const initScriptFilenames = [
'build-result-capture.init.gradle',
'build-result-capture-service.plugin.groovy',
'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)
const initScriptPath = path.resolve(initScriptsDir, initScriptFilename) const initScriptPath = path.resolve(initScriptsDir, initScriptFilename)

View file

@ -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

View file

@ -1,24 +0,0 @@
import * as core from '@actions/core'
import * as provisioner from './provision'
import * as dependencyGraph from './dependency-graph'
/**
* The main entry point for the action, called by Github Actions for the step.
*/
export async function run(): Promise<void> {
try {
// Download and install Gradle if required
const executable = await provisioner.provisionGradle()
// Generate and upload dependency graph artifact
await dependencyGraph.generateDependencyGraph(executable)
} catch (error) {
core.setFailed(String(error))
if (error instanceof Error && error.stack) {
core.info(error.stack)
}
}
}
run()

View file

@ -1,16 +0,0 @@
import * as core from '@actions/core'
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()
} catch (error) {
core.setFailed(String(error))
if (error instanceof Error && error.stack) {
core.info(error.stack)
}
}
}
run()

View file

@ -8,59 +8,64 @@ import {Octokit} from '@octokit/rest'
import * as path from 'path' import * as path from 'path'
import fs from 'fs' import fs from 'fs'
import * as execution from './execution'
import * as layout from './repository-layout' import * as layout from './repository-layout'
import {DependencyGraphOption, getJobMatrix} from './input-params'
const DEPENDENCY_GRAPH_ARTIFACT = 'dependency-graph' const DEPENDENCY_GRAPH_ARTIFACT = 'dependency-graph'
const DEPENDENCY_GRAPH_FILE = 'dependency-graph.json'
export async function generateDependencyGraph(executable: string | undefined): Promise<void> { export function setup(option: DependencyGraphOption): void {
const workspaceDirectory = layout.workspaceDirectory() if (option === DependencyGraphOption.Disabled || option === DependencyGraphOption.DownloadAndSubmit) {
const buildRootDirectory = layout.buildRootDirectory() return
const buildPath = getRelativePathFromWorkspace(buildRootDirectory) }
const initScript = path.resolve( core.info('Enabling dependency graph generation')
__dirname, const jobCorrelator = getJobCorrelator()
'..', core.exportVariable('GITHUB_DEPENDENCY_GRAPH_ENABLED', 'true')
'..', core.exportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR', jobCorrelator)
'src', core.exportVariable('GITHUB_DEPENDENCY_GRAPH_JOB_ID', github.context.runId)
'resources', core.exportVariable(
'init-scripts', 'GITHUB_DEPENDENCY_GRAPH_REPORT_DIR',
'github-dependency-graph.init.gradle' path.resolve(layout.workspaceDirectory(), 'dependency-graph-reports')
) )
const args = [ }
`-Dorg.gradle.github.env.GRADLE_BUILD_PATH=${buildPath}`,
'--init-script',
initScript,
':GitHubDependencyGraphPlugin_generateDependencyGraph'
]
await execution.executeGradleBuild(executable, buildRootDirectory, args) export async function complete(option: DependencyGraphOption): Promise<void> {
const dependencyGraphJson = copyDependencyGraphToBuildRoot(buildRootDirectory) switch (option) {
case DependencyGraphOption.Disabled:
return
case DependencyGraphOption.Generate:
await uploadDependencyGraphs()
return
case DependencyGraphOption.GenerateAndSubmit:
await submitDependencyGraphs(await uploadDependencyGraphs())
return
case DependencyGraphOption.DownloadAndSubmit:
await downloadAndSubmitDependencyGraphs()
}
}
async function uploadDependencyGraphs(): Promise<string[]> {
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)
return graphFiles
} }
function copyDependencyGraphToBuildRoot(buildRootDirectory: string): string { 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 +74,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 +130,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 +150,29 @@ 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, 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.debug(`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_-\s]/g, '')
.replace(/\s+/g, '_')
.toLowerCase()
}

View file

@ -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,27 @@ 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)
}
export function getDependencyGraphOption(): DependencyGraphOption {
const val = core.getInput('dependency-graph')
switch (val.toLowerCase().trim()) {
case 'disabled':
return DependencyGraphOption.Disabled
case 'generate':
return DependencyGraphOption.Generate
case 'generate-and-submit':
return DependencyGraphOption.GenerateAndSubmit
case 'download-and-submit':
return DependencyGraphOption.DownloadAndSubmit
}
throw TypeError(
`The value '${val} is not valid for 'dependency-graph. Valid values are: [disabled, generate-and-upload, generate-and-submit, download-and-submit]. The default value is 'disabled'.`
)
}
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()) {
@ -75,3 +96,10 @@ function getBooleanInput(paramName: string, paramDefault = false): boolean {
} }
throw TypeError(`The value '${paramValue} is not valid for '${paramName}. Valid values are: [true, false]`) throw TypeError(`The value '${paramValue} is not valid for '${paramName}. Valid values are: [true, false]`)
} }
export enum DependencyGraphOption {
Disabled,
Generate,
GenerateAndSubmit,
DownloadAndSubmit
}

View file

@ -0,0 +1,9 @@
buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath "org.gradle:github-dependency-graph-gradle-plugin:0.0.3"
}
}
apply plugin: org.gradle.github.GitHubDependencyGraphPlugin

View file

@ -1,12 +1,24 @@
import org.gradle.github.GitHubDependencyGraphPlugin import org.gradle.util.GradleVersion
initscript {
repositories { if (System.env.GITHUB_DEPENDENCY_GRAPH_ENABLED != "true") {
maven { return
url = uri("https://plugins.gradle.org/m2/")
} }
if (GradleVersion.current().baseVersion < GradleVersion.version("5.0")) {
println "::warning::Dependency Graph is not supported for Gradle versions < 5.0. No dependency snapshot will be generated."
return
} }
dependencies {
classpath("org.gradle:github-dependency-graph-gradle-plugin:+") 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 snapshot 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
} }
}
apply plugin: GitHubDependencyGraphPlugin 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'

View file

@ -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,8 @@ 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())
dependencyGraph.setup(params.getDependencyGraphOption())
} }
export async function complete(): Promise<void> { export async function complete(): Promise<void> {
@ -58,6 +61,8 @@ export async function complete(): Promise<void> {
} else { } else {
logJobSummary(buildResults, cacheListener) logJobSummary(buildResults, cacheListener)
} }
dependencyGraph.complete(params.getDependencyGraphOption())
} }
async function determineGradleUserHome(): Promise<string> { async function determineGradleUserHome(): Promise<string> {

View file

@ -0,0 +1,87 @@
package com.gradle.gradlebuildaction
import static org.junit.Assume.assumeTrue
class TestDependencyGraph extends BaseInitScriptTest {
def initScript = 'github-dependency-graph.init.gradle'
static final List<TestGradleVersion> NO_DEPENDENCY_GRAPH_VERSIONS = [GRADLE_3_X, GRADLE_4_X]
static final List<TestGradleVersion> DEPENDENCY_GRAPH_VERSIONS = ALL_VERSIONS - NO_DEPENDENCY_GRAPH_VERSIONS
def "does not produce dependency graph when not enabled"() {
assumeTrue testGradleVersion.compatibleWithCurrentJvm
when:
run(['help'], initScript, testGradleVersion.gradleVersion)
then:
assert !reportsDir.exists()
where:
testGradleVersion << ALL_VERSIONS
}
def "produces dependency graph when enabled"() {
assumeTrue testGradleVersion.compatibleWithCurrentJvm
when:
run(['help'], initScript, testGradleVersion.gradleVersion, [], envVars)
then:
assert reportFile.exists()
where:
testGradleVersion << DEPENDENCY_GRAPH_VERSIONS
}
def "warns and produces no dependency graph when enabled for older Gradle versions"() {
assumeTrue testGradleVersion.compatibleWithCurrentJvm
when:
def result = run(['help'], initScript, testGradleVersion.gradleVersion, [], envVars)
then:
assert !reportsDir.exists()
assert result.output.contains("::warning::Dependency Graph is not supported")
where:
testGradleVersion << NO_DEPENDENCY_GRAPH_VERSIONS
}
def "warns and does not overwrite existing report file"() {
assumeTrue testGradleVersion.compatibleWithCurrentJvm
when:
reportsDir.mkdirs()
reportFile << "DUMMY CONTENT"
def result = run(['help'], initScript, testGradleVersion.gradleVersion, [], envVars)
then:
assert reportFile.text == "DUMMY CONTENT"
assert result.output.contains("::warning::No dependency snapshot generated for step")
where:
testGradleVersion << DEPENDENCY_GRAPH_VERSIONS
}
def getEnvVars() {
return [
GITHUB_DEPENDENCY_GRAPH_ENABLED: "true",
GITHUB_DEPENDENCY_GRAPH_JOB_CORRELATOR: "CORRELATOR",
GITHUB_DEPENDENCY_GRAPH_JOB_ID: "1",
GITHUB_DEPENDENCY_GRAPH_REPORT_DIR: reportsDir.absolutePath,
GITHUB_REF: "main",
GITHUB_SHA: "123456",
GITHUB_WORKSPACE: testProjectDir.absolutePath
]
}
def getReportsDir() {
return new File(testProjectDir, 'build/reports/github-dependency-graph-snapshots')
}
def getReportFile() {
return new File(reportsDir, "CORRELATOR.json")
}
}

View file

@ -0,0 +1,34 @@
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('workflow_withcommas-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('replaces spaces', () => {
const id = dependencyGraph.constructJobCorrelator('Workflow !_ with () characters, and spaces', 'job-*id', '{"foo": "bar!@#$%^&*("}')
expect(id).toBe('workflow___with_characters_and_spaces-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-value_with_comma')
})
})
})