mirror of
https://github.com/gradle/gradle-build-action.git
synced 2024-11-25 01:31:06 -05:00
Add basic support for GitHub Dependency Graph (#782)
This commit is contained in:
commit
7a67f395d2
25 changed files with 18145 additions and 160 deletions
|
@ -12,6 +12,7 @@
|
|||
"import/no-namespace": "off",
|
||||
"i18n-text/no-en": "off",
|
||||
"no-unused-vars": "off",
|
||||
"no-shadow": "off",
|
||||
"sort-imports": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
|
@ -30,6 +31,7 @@
|
|||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-shadow": "error",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
|
|
5
.github/workflows/ci-full-check.yml
vendored
5
.github/workflows/ci-full-check.yml
vendored
|
@ -29,6 +29,11 @@ jobs:
|
|||
with:
|
||||
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:
|
||||
uses: ./.github/workflows/integ-test-execution-with-caching.yml
|
||||
with:
|
||||
|
|
7
.github/workflows/ci-quick-check.yml
vendored
7
.github/workflows/ci-quick-check.yml
vendored
|
@ -50,6 +50,13 @@ jobs:
|
|||
runner-os: '["ubuntu-latest"]'
|
||||
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:
|
||||
needs: build-distribution
|
||||
uses: ./.github/workflows/integ-test-execution-with-caching.yml
|
||||
|
|
68
.github/workflows/integ-test-dependency-graph.yml
vendored
Normal file
68
.github/workflows/integ-test-dependency-graph.yml
vendored
Normal 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
117
README.md
|
@ -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
|
||||
- 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.
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
|
|
10
action.yml
10
action.yml
|
@ -58,6 +58,11 @@ inputs:
|
|||
required: false
|
||||
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
|
||||
# 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`.
|
||||
|
@ -75,6 +80,11 @@ inputs:
|
|||
required: 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:
|
||||
build-scan-url:
|
||||
description: Link to the build scan if any
|
||||
|
|
24
actions/clear-dependency-graph/action.yml
Normal file
24
actions/clear-dependency-graph/action.yml
Normal 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
|
|
@ -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'
|
|
@ -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
8367
dist/main/index.js
vendored
File diff suppressed because it is too large
Load diff
2
dist/main/index.js.map
vendored
2
dist/main/index.js.map
vendored
File diff suppressed because one or more lines are too long
9264
dist/post/index.js
vendored
9264
dist/post/index.js
vendored
File diff suppressed because it is too large
Load diff
2
dist/post/index.js.map
vendored
2
dist/post/index.js.map
vendored
File diff suppressed because one or more lines are too long
|
@ -11,9 +11,7 @@
|
|||
|
||||
"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-dependency-graph-generate": "ncc build src/dependency-graph-generate.ts --out dist/dependency-graph-generate --source-map --no-source-map-register",
|
||||
"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",
|
||||
"compile": "npm run compile-main && npm run compile-post",
|
||||
|
||||
"test": "jest",
|
||||
"check": "npm run format && npm run lint",
|
||||
|
|
|
@ -172,7 +172,12 @@ export class GradleStateCache {
|
|||
}
|
||||
|
||||
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) {
|
||||
const initScriptContent = this.readInitScriptAsString(initScriptFilename)
|
||||
const initScriptPath = path.resolve(initScriptsDir, 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
|
||||
|
|
|
@ -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()
|
|
@ -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()
|
|
@ -8,59 +8,64 @@ import {Octokit} from '@octokit/rest'
|
|||
import * as path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
import * as execution from './execution'
|
||||
import * as layout from './repository-layout'
|
||||
import {DependencyGraphOption, getJobMatrix} from './input-params'
|
||||
|
||||
const DEPENDENCY_GRAPH_ARTIFACT = 'dependency-graph'
|
||||
const DEPENDENCY_GRAPH_FILE = 'dependency-graph.json'
|
||||
|
||||
export async function generateDependencyGraph(executable: string | undefined): Promise<void> {
|
||||
const workspaceDirectory = layout.workspaceDirectory()
|
||||
const buildRootDirectory = layout.buildRootDirectory()
|
||||
const buildPath = getRelativePathFromWorkspace(buildRootDirectory)
|
||||
export function setup(option: DependencyGraphOption): void {
|
||||
if (option === DependencyGraphOption.Disabled || option === DependencyGraphOption.DownloadAndSubmit) {
|
||||
return
|
||||
}
|
||||
|
||||
const initScript = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'src',
|
||||
'resources',
|
||||
'init-scripts',
|
||||
'github-dependency-graph.init.gradle'
|
||||
core.info('Enabling dependency graph generation')
|
||||
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')
|
||||
)
|
||||
const args = [
|
||||
`-Dorg.gradle.github.env.GRADLE_BUILD_PATH=${buildPath}`,
|
||||
'--init-script',
|
||||
initScript,
|
||||
':GitHubDependencyGraphPlugin_generateDependencyGraph'
|
||||
]
|
||||
}
|
||||
|
||||
await execution.executeGradleBuild(executable, buildRootDirectory, args)
|
||||
const dependencyGraphJson = copyDependencyGraphToBuildRoot(buildRootDirectory)
|
||||
export async function complete(option: DependencyGraphOption): Promise<void> {
|
||||
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()
|
||||
artifactClient.uploadArtifact(DEPENDENCY_GRAPH_ARTIFACT, [dependencyGraphJson], workspaceDirectory)
|
||||
artifactClient.uploadArtifact(DEPENDENCY_GRAPH_ARTIFACT, graphFiles, workspaceDirectory)
|
||||
|
||||
return graphFiles
|
||||
}
|
||||
|
||||
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> {
|
||||
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 +74,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 +130,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 +150,29 @@ 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, 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()
|
||||
}
|
||||
|
|
|
@ -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,27 @@ export function isJobSummaryEnabled(): boolean {
|
|||
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 {
|
||||
const paramValue = core.getInput(paramName)
|
||||
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]`)
|
||||
}
|
||||
|
||||
export enum DependencyGraphOption {
|
||||
Disabled,
|
||||
Generate,
|
||||
GenerateAndSubmit,
|
||||
DownloadAndSubmit
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -1,12 +1,24 @@
|
|||
import org.gradle.github.GitHubDependencyGraphPlugin
|
||||
initscript {
|
||||
repositories {
|
||||
maven {
|
||||
url = uri("https://plugins.gradle.org/m2/")
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath("org.gradle:github-dependency-graph-gradle-plugin:+")
|
||||
}
|
||||
import org.gradle.util.GradleVersion
|
||||
|
||||
if (System.env.GITHUB_DEPENDENCY_GRAPH_ENABLED != "true") {
|
||||
return
|
||||
}
|
||||
apply plugin: GitHubDependencyGraphPlugin
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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,8 @@ export async function setup(): Promise<void> {
|
|||
await caches.restore(gradleUserHome, cacheListener)
|
||||
|
||||
core.saveState(CACHE_LISTENER, cacheListener.stringify())
|
||||
|
||||
dependencyGraph.setup(params.getDependencyGraphOption())
|
||||
}
|
||||
|
||||
export async function complete(): Promise<void> {
|
||||
|
@ -58,6 +61,8 @@ export async function complete(): Promise<void> {
|
|||
} else {
|
||||
logJobSummary(buildResults, cacheListener)
|
||||
}
|
||||
|
||||
dependencyGraph.complete(params.getDependencyGraphOption())
|
||||
}
|
||||
|
||||
async function determineGradleUserHome(): Promise<string> {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
34
test/jest/dependency-graph.test.ts
Normal file
34
test/jest/dependency-graph.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue