diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index aa49c59..f82b69e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -420,3 +420,40 @@ jobs:
# popd > /dev/null
- name: Remove components
run: gu remove espresso llvm-toolchain nodejs python ruby wasm
+ test-sbom:
+ name: test 'native-image-enable-sbom' option
+ runs-on: ${{ matrix.os }}
+ permissions:
+ contents: write
+ strategy:
+ matrix:
+ java-version: ['24-ea', 'latest-ea']
+ distribution: ['graalvm']
+ os: [macos-latest, windows-latest, ubuntu-latest]
+ set-gds-token: [false]
+ components: ['']
+ steps:
+ - uses: actions/checkout@v4
+ - name: Run setup-graalvm action
+ uses: ./
+ with:
+ java-version: ${{ matrix.java-version }}
+ distribution: ${{ matrix.distribution }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ components: ${{ matrix.components }}
+ gds-token: ${{ matrix.set-gds-token && secrets.GDS_TOKEN || '' }}
+ native-image-enable-sbom: 'true'
+ - name: Build Maven project and verify that SBOM was generated and its contents
+ run: |
+ cd __tests__/sbom/main-test-app
+ mvn --no-transfer-progress -Pnative package
+ bash verify-sbom.sh
+ shell: bash
+ if: runner.os != 'Windows'
+ - name: Build Maven project and verify that SBOM was generated and its contents (Windows)
+ run: |
+ cd __tests__\sbom\main-test-app
+ mvn --no-transfer-progress -Pnative package
+ cmd /c verify-sbom.cmd
+ shell: cmd
+ if: runner.os == 'Windows'
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 18e337d..ed52253 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,4 +96,7 @@ Thumbs.db
# Ignore built ts files
__tests__/runner/*
-lib/**/*
\ No newline at end of file
+lib/**/*
+
+# Ignore target directory in __tests__
+__tests__/**/target
diff --git a/README.md b/README.md
index 58a589a..a8590ee 100644
--- a/README.md
+++ b/README.md
@@ -205,6 +205,7 @@ This actions can be configured with the following options:
| `native-image-job-reports` *) | `'false'` | If set to `'true'`, post a job summary containing a Native Image build report. |
| `native-image-pr-reports` *) | `'false'` | If set to `'true'`, post a comment containing a Native Image build report on pull requests. Requires `write` permissions for the [`pull-requests` scope][gha-permissions]. |
| `native-image-pr-reports-update-existing` *) | `'false'` | Instead of posting another comment, update an existing PR comment with the latest Native Image build report. Requires `native-image-pr-reports` to be `true`. |
+| `native-image-enable-sbom` | `'false'` | If set to `'true'`, generate a minimal SBOM based on the Native Image static analysis and submit it to GitHub's dependency submission API. This enables the [dependency graph feature](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-the-dependency-graph) for dependency tracking and vulnerability analysis. Requires `write` permissions for the [`contents` scope][gha-permissions] and the dependency graph to be actived (on by default for public repositories - see [how to activate](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/configuring-the-dependency-graph#enabling-and-disabling-the-dependency-graph-for-a-private-repository)). Only available in Oracle GraalVM for JDK 24 or later. |
| `components` | `''` | Comma-separated list of GraalVM components (e.g., `native-image` or `ruby,nodejs`) that will be installed by the [GraalVM Updater][gu]. |
| `version` | `''` | `X.Y.Z` (e.g., `22.3.0`) for a specific [GraalVM release][releases] up to `22.3.2`
`mandrel-X.Y.Z.W` or `X.Y.Z.W-Final` (e.g., `mandrel-21.3.0.0-Final` or `21.3.0.0-Final`) for a specific [Mandrel release][mandrel-releases],
`mandrel-latest` or `latest` for the latest Mandrel stable release. |
| `gds-token` | `''` Download token for the GraalVM Download Service. If a non-empty token is provided, the action will set up Oracle GraalVM (see [Oracle GraalVM via GDS template](#template-for-oracle-graalvm-via-graalvm-download-service)) or GraalVM Enterprise Edition (see [GraalVM EE template](#template-for-graalvm-enterprise-edition)) via GDS. |
diff --git a/__tests__/cleanup.test.ts b/__tests__/cleanup.test.ts
index b43d151..9a70f2a 100644
--- a/__tests__/cleanup.test.ts
+++ b/__tests__/cleanup.test.ts
@@ -49,7 +49,7 @@ describe('cleanup', () => {
resetState()
})
- it('does not fail nor warn even when the save provess throws a ReserveCacheError', async () => {
+ it('does not fail nor warn even when the save process throws a ReserveCacheError', async () => {
spyCacheSave.mockImplementation((paths: string[], key: string) =>
Promise.reject(
new cache.ReserveCacheError(
diff --git a/__tests__/sbom.test.ts b/__tests__/sbom.test.ts
new file mode 100644
index 0000000..f0fabf6
--- /dev/null
+++ b/__tests__/sbom.test.ts
@@ -0,0 +1,306 @@
+import * as c from '../src/constants'
+import {setUpSBOMSupport, processSBOM} from '../src/features/sbom'
+import * as core from '@actions/core'
+import * as github from '@actions/github'
+import * as glob from '@actions/glob'
+import {join} from 'path'
+import {tmpdir} from 'os'
+import {mkdtempSync, writeFileSync, rmSync} from 'fs'
+
+jest.mock('@actions/glob')
+jest.mock('@actions/github', () => ({
+ getOctokit: jest.fn(() => ({
+ request: jest.fn().mockResolvedValue(undefined)
+ })),
+ context: {
+ repo: {
+ owner: 'test-owner',
+ repo: 'test-repo'
+ },
+ sha: 'test-sha',
+ ref: 'test-ref',
+ workflow: 'test-workflow',
+ job: 'test-job',
+ runId: '12345'
+ }
+}))
+
+function mockFindSBOM(files: string[]) {
+ const mockCreate = jest.fn().mockResolvedValue({
+ glob: jest.fn().mockResolvedValue(files)
+ })
+ ;(glob.create as jest.Mock).mockImplementation(mockCreate)
+}
+
+// Mocks the GitHub dependency submission API return value
+// 'undefined' is treated as a successful request
+function mockGithubAPIReturnValue(returnValue: Error | undefined = undefined) {
+ const mockOctokit = {
+ request:
+ returnValue === undefined
+ ? jest.fn().mockResolvedValue(returnValue)
+ : jest.fn().mockRejectedValue(returnValue)
+ }
+ ;(github.getOctokit as jest.Mock).mockReturnValue(mockOctokit)
+ return mockOctokit
+}
+
+describe('sbom feature', () => {
+ let spyInfo: jest.SpyInstance>
+ let spyWarning: jest.SpyInstance>
+ let spyExportVariable: jest.SpyInstance<
+ void,
+ Parameters
+ >
+ let workspace: string
+ let originalEnv: NodeJS.ProcessEnv
+ const javaVersion = '24.0.0'
+ const distribution = c.DISTRIBUTION_GRAALVM
+
+ beforeEach(() => {
+ originalEnv = process.env
+
+ process.env = {
+ ...process.env,
+ GITHUB_REPOSITORY: 'test-owner/test-repo',
+ GITHUB_TOKEN: 'fake-token'
+ }
+
+ workspace = mkdtempSync(join(tmpdir(), 'setup-graalvm-sbom-'))
+ mockGithubAPIReturnValue()
+
+ spyInfo = jest.spyOn(core, 'info').mockImplementation(() => null)
+ spyWarning = jest.spyOn(core, 'warning').mockImplementation(() => null)
+ spyExportVariable = jest
+ .spyOn(core, 'exportVariable')
+ .mockImplementation(() => null)
+ jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
+ if (name === 'native-image-enable-sbom') {
+ return 'true'
+ }
+ if (name === 'github-token') {
+ return 'fake-token'
+ }
+ return ''
+ })
+ })
+
+ afterEach(() => {
+ process.env = originalEnv
+ jest.clearAllMocks()
+ spyInfo.mockRestore()
+ spyWarning.mockRestore()
+ spyExportVariable.mockRestore()
+ rmSync(workspace, {recursive: true, force: true})
+ })
+
+ describe('setup', () => {
+ it('should throw an error when the distribution is not Oracle GraalVM', () => {
+ const not_supported_distributions = [
+ c.DISTRIBUTION_GRAALVM_COMMUNITY,
+ c.DISTRIBUTION_MANDREL,
+ c.DISTRIBUTION_LIBERICA,
+ ''
+ ]
+ for (const distribution of not_supported_distributions) {
+ expect(() => setUpSBOMSupport(javaVersion, distribution)).toThrow()
+ }
+ })
+
+ it('should throw an error when the java-version is not supported', () => {
+ const not_supported_versions = ['23', '23-ea', '21.0.3', 'dev', '17', '']
+ for (const version of not_supported_versions) {
+ expect(() => setUpSBOMSupport(version, distribution)).toThrow()
+ }
+ })
+
+ it('should not throw an error when the java-version is supported', () => {
+ const supported_versions = ['24', '24-ea', '24.0.2', 'latest-ea']
+ for (const version of supported_versions) {
+ expect(() => setUpSBOMSupport(version, distribution)).not.toThrow()
+ }
+ })
+
+ it('should set the SBOM option when activated', () => {
+ setUpSBOMSupport(javaVersion, distribution)
+
+ expect(spyExportVariable).toHaveBeenCalledWith(
+ c.NATIVE_IMAGE_OPTIONS_ENV,
+ expect.stringContaining('--enable-sbom=export')
+ )
+ expect(spyInfo).toHaveBeenCalledWith(
+ 'Enabled SBOM generation for Native Image build'
+ )
+ expect(spyWarning).not.toHaveBeenCalled()
+ })
+
+ it('should not set the SBOM option when not activated', () => {
+ jest.spyOn(core, 'getInput').mockReturnValue('false')
+ setUpSBOMSupport(javaVersion, distribution)
+
+ expect(spyExportVariable).not.toHaveBeenCalled()
+ expect(spyInfo).not.toHaveBeenCalled()
+ expect(spyWarning).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('process', () => {
+ async function setUpAndProcessSBOM(sbom: object): Promise {
+ setUpSBOMSupport(javaVersion, distribution)
+ spyInfo.mockClear()
+
+ // Mock 'native-image' invocation by creating the SBOM file
+ const sbomPath = join(workspace, 'test.sbom.json')
+ writeFileSync(sbomPath, JSON.stringify(sbom, null, 2))
+
+ mockFindSBOM([sbomPath])
+
+ await processSBOM()
+ }
+
+ const sampleSBOM = {
+ bomFormat: 'CycloneDX',
+ specVersion: '1.5',
+ version: 1,
+ serialNumber: 'urn:uuid:52c977f8-6d04-3c07-8826-597a036d61a6',
+ components: [
+ {
+ type: 'library',
+ group: 'org.json',
+ name: 'json',
+ version: '20241224',
+ purl: 'pkg:maven/org.json/json@20241224',
+ 'bom-ref': 'pkg:maven/org.json/json@20241224',
+ properties: [
+ {
+ name: 'syft:cpe23',
+ value: 'cpe:2.3:a:json:json:20241224:*:*:*:*:*:*:*'
+ }
+ ]
+ },
+ {
+ type: 'library',
+ group: 'com.oracle',
+ name: 'main-test-app',
+ version: '1.0-SNAPSHOT',
+ purl: 'pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT',
+ 'bom-ref': 'pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT'
+ }
+ ],
+ dependencies: [
+ {
+ ref: 'pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT',
+ dependsOn: ['pkg:maven/org.json/json@20241224']
+ },
+ {
+ ref: 'pkg:maven/org.json/json@20241224',
+ dependsOn: []
+ }
+ ]
+ }
+
+ it('should process SBOM and display components', async () => {
+ await setUpAndProcessSBOM(sampleSBOM)
+
+ expect(spyInfo).toHaveBeenCalledWith(
+ 'Found SBOM: ' + join(workspace, 'test.sbom.json')
+ )
+ expect(spyInfo).toHaveBeenCalledWith('=== SBOM Content ===')
+ expect(spyInfo).toHaveBeenCalledWith('- pkg:maven/org.json/json@20241224')
+ expect(spyInfo).toHaveBeenCalledWith(
+ '- pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT'
+ )
+ expect(spyInfo).toHaveBeenCalledWith(
+ ' depends on: pkg:maven/org.json/json@20241224'
+ )
+ expect(spyWarning).not.toHaveBeenCalled()
+ })
+
+ it('should handle components without purl', async () => {
+ const sbomWithoutPurl = {
+ ...sampleSBOM,
+ components: [
+ {
+ type: 'library',
+ name: 'no-purl-package',
+ version: '1.0.0',
+ 'bom-ref': 'no-purl-package@1.0.0'
+ }
+ ]
+ }
+ await setUpAndProcessSBOM(sbomWithoutPurl)
+
+ expect(spyInfo).toHaveBeenCalledWith('=== SBOM Content ===')
+ expect(spyInfo).toHaveBeenCalledWith('- no-purl-package@1.0.0')
+ expect(spyWarning).not.toHaveBeenCalled()
+ })
+
+ it('should handle missing SBOM file', async () => {
+ setUpSBOMSupport(javaVersion, distribution)
+ spyInfo.mockClear()
+
+ mockFindSBOM([])
+
+ await expect(processSBOM()).rejects.toBeInstanceOf(Error)
+ })
+
+ it('should throw when JSON contains an invalid SBOM', async () => {
+ const invalidSBOM = {
+ 'out-of-spec-field': {}
+ }
+ try {
+ await setUpAndProcessSBOM(invalidSBOM)
+ fail('Expected an error since invalid JSON was passed')
+ } catch (error) {
+ expect(error).toBeInstanceOf(Error)
+ }
+ })
+
+ it('should submit dependencies when processing valid SBOM', async () => {
+ const mockOctokit = mockGithubAPIReturnValue(undefined)
+ await setUpAndProcessSBOM(sampleSBOM)
+
+ expect(mockOctokit.request).toHaveBeenCalledWith(
+ 'POST /repos/{owner}/{repo}/dependency-graph/snapshots',
+ expect.objectContaining({
+ owner: 'test-owner',
+ repo: 'test-repo',
+ version: expect.any(Number),
+ sha: 'test-sha',
+ ref: 'test-ref',
+ job: expect.objectContaining({
+ correlator: 'test-workflow_test-job',
+ id: '12345'
+ }),
+ manifests: expect.objectContaining({
+ 'test.sbom.json': expect.objectContaining({
+ name: 'test.sbom.json',
+ resolved: expect.objectContaining({
+ json: expect.objectContaining({
+ package_url: 'pkg:maven/org.json/json@20241224',
+ dependencies: []
+ }),
+ 'main-test-app': expect.objectContaining({
+ package_url:
+ 'pkg:maven/com.oracle/main-test-app@1.0-SNAPSHOT',
+ dependencies: ['pkg:maven/org.json/json@20241224']
+ })
+ })
+ })
+ })
+ })
+ )
+ expect(spyInfo).toHaveBeenCalledWith(
+ 'Dependency snapshot submitted successfully.'
+ )
+ })
+
+ it('should handle GitHub API submission errors gracefully', async () => {
+ mockGithubAPIReturnValue(new Error('API submission failed'))
+
+ await expect(setUpAndProcessSBOM(sampleSBOM)).rejects.toBeInstanceOf(
+ Error
+ )
+ })
+ })
+})
diff --git a/__tests__/sbom/main-test-app/pom.xml b/__tests__/sbom/main-test-app/pom.xml
new file mode 100644
index 0000000..ec9ae9f
--- /dev/null
+++ b/__tests__/sbom/main-test-app/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ com.oracle
+ main-test-app
+ 1.0.0
+
+
+ 17
+ 17
+
+
+
+
+ org.json
+ json
+ 20241224
+
+
+
+
+
+ native
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+ 0.10.3
+
+
+
+ compile-no-fork
+
+ package
+
+
+
+ com.oracle.sbom.SBOMTestApplication
+
+ -Ob
+ --no-fallback
+ -H:+ReportExceptionStackTraces
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/__tests__/sbom/main-test-app/src/main/java/com/oracle/sbom/SBOMTestApplication.java b/__tests__/sbom/main-test-app/src/main/java/com/oracle/sbom/SBOMTestApplication.java
new file mode 100644
index 0000000..5164f7b
--- /dev/null
+++ b/__tests__/sbom/main-test-app/src/main/java/com/oracle/sbom/SBOMTestApplication.java
@@ -0,0 +1,12 @@
+package com.oracle.sbom;
+
+import org.json.JSONObject;
+
+public class SBOMTestApplication {
+ public static void main(String argv[]) {
+ JSONObject jo = new JSONObject();
+ jo.put("lorem", "ipsum");
+ jo.put("dolor", "sit amet");
+ System.out.println(jo);
+ }
+}
diff --git a/__tests__/sbom/main-test-app/verify-sbom.cmd b/__tests__/sbom/main-test-app/verify-sbom.cmd
new file mode 100644
index 0000000..de5cd0c
--- /dev/null
+++ b/__tests__/sbom/main-test-app/verify-sbom.cmd
@@ -0,0 +1,14 @@
+@echo off
+set "SCRIPT_DIR=%~dp0"
+
+for %%p in (
+ "\"pkg:maven/org.json/json@20241224\""
+ "\"main-test-app\""
+ "\"svm\""
+ "\"nativeimage\""
+) do (
+ echo Checking for %%p
+ findstr /c:%%p "%SCRIPT_DIR%target\main-test-app.sbom.json" || exit /b 1
+)
+
+echo SBOM was successfully generated and contained the expected components
\ No newline at end of file
diff --git a/__tests__/sbom/main-test-app/verify-sbom.sh b/__tests__/sbom/main-test-app/verify-sbom.sh
new file mode 100644
index 0000000..c9c2f7d
--- /dev/null
+++ b/__tests__/sbom/main-test-app/verify-sbom.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+required_patterns=(
+ '"pkg:maven/org.json/json@20241224"'
+ '"main-test-app"'
+ '"svm"'
+ '"nativeimage"'
+)
+
+for pattern in "${required_patterns[@]}"; do
+ echo "Checking for $pattern"
+ if ! grep -q "$pattern" "$script_dir/target/main-test-app.sbom.json"; then
+ echo "Pattern not found: $pattern"
+ exit 1
+ fi
+done
+
+echo "SBOM was successfully generated and contained the expected components"
\ No newline at end of file
diff --git a/action.yml b/action.yml
index 47fa5b6..a261d35 100644
--- a/action.yml
+++ b/action.yml
@@ -51,6 +51,10 @@ inputs:
required: false
description: 'Instead of posting another comment, update an existing PR comment with the latest Native Image build report.'
default: 'false'
+ native-image-enable-sbom:
+ required: false
+ description: 'Automatically generate an SBOM and submit it to the GitHub dependency submission API for vulnerability and dependency tracking.'
+ default: 'false'
version:
required: false
description: 'GraalVM version (release, latest, dev).'
diff --git a/dist/cleanup/index.js b/dist/cleanup/index.js
index c6bc466..76f4a5d 100644
--- a/dist/cleanup/index.js
+++ b/dist/cleanup/index.js
@@ -98011,6 +98011,7 @@ const core = __importStar(__nccwpck_require__(2186));
const constants = __importStar(__nccwpck_require__(9042));
const cache_1 = __nccwpck_require__(9179);
const reports_1 = __nccwpck_require__(2046);
+const sbom_1 = __nccwpck_require__(9181);
/**
* Check given input and run a save process for the specified package manager
* @returns Promise that will be resolved when the save process finishes
@@ -98043,6 +98044,7 @@ function ignoreErrors(promise) {
function run() {
return __awaiter(this, void 0, void 0, function* () {
yield ignoreErrors((0, reports_1.generateReports)());
+ yield ignoreErrors((0, sbom_1.processSBOM)());
yield ignoreErrors(saveCache());
});
}
@@ -98064,7 +98066,7 @@ else {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0;
+exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.NATIVE_IMAGE_OPTIONS_ENV = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0;
exports.ACTION_VERSION = '1.2.7';
exports.INPUT_VERSION = 'version';
exports.INPUT_GDS_TOKEN = 'gds-token';
@@ -98077,6 +98079,7 @@ exports.INPUT_SET_JAVA_HOME = 'set-java-home';
exports.INPUT_CACHE = 'cache';
exports.INPUT_CHECK_FOR_UPDATES = 'check-for-updates';
exports.INPUT_NI_MUSL = 'native-image-musl';
+exports.NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS';
exports.IS_LINUX = process.platform === 'linux';
exports.IS_MACOS = process.platform === 'darwin';
exports.IS_WINDOWS = process.platform === 'win32';
@@ -98420,10 +98423,8 @@ const core = __importStar(__nccwpck_require__(2186));
const fs = __importStar(__nccwpck_require__(7147));
const github = __importStar(__nccwpck_require__(5438));
const semver = __importStar(__nccwpck_require__(1383));
-const path_1 = __nccwpck_require__(1017);
-const os_1 = __nccwpck_require__(2037);
const utils_1 = __nccwpck_require__(1314);
-const BUILD_OUTPUT_JSON_PATH = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-build-output.json');
+const BUILD_OUTPUT_JSON_PATH = (0, utils_1.tmpfile)('native-image-build-output.json');
const BYTES_TO_KiB = 1024;
const BYTES_TO_MiB = 1024 * 1024;
const BYTES_TO_GiB = 1024 * 1024 * 1024;
@@ -98431,9 +98432,6 @@ const DOCS_BASE = 'https://github.com/oracle/graal/blob/master/docs/reference-ma
const INPUT_NI_JOB_REPORTS = 'native-image-job-reports';
const INPUT_NI_PR_REPORTS = 'native-image-pr-reports';
const INPUT_NI_PR_REPORTS_UPDATE = 'native-image-pr-reports-update-existing';
-const NATIVE_IMAGE_CONFIG_FILE = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-options.properties');
-const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS';
-const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE';
const PR_COMMENT_TITLE = '## GraalVM Native Image Build Report';
function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev, graalVMVersion) {
return __awaiter(this, void 0, void 0, function* () {
@@ -98450,7 +98448,7 @@ function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev
core.warning(`Build reports for PRs and job summaries are only available in GraalVM 22.2.0 or later. This build job uses GraalVM ${graalVMVersion}.`);
return;
}
- setNativeImageOption(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows
+ (0, utils_1.setNativeImageOption)(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows
});
}
exports.setUpNativeImageBuildReports = setUpNativeImageBuildReports;
@@ -98492,38 +98490,6 @@ function arePRReportsEnabled() {
function arePRReportsUpdateEnabled() {
return (0, utils_1.isPREvent)() && core.getInput(INPUT_NI_PR_REPORTS_UPDATE) === 'true';
}
-function setNativeImageOption(javaVersionOrDev, optionValue) {
- const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev);
- if ((coercedJavaVersionOrDev &&
- semver.gte(coercedJavaVersionOrDev, '22.0.0')) ||
- javaVersionOrDev === c.VERSION_DEV ||
- javaVersionOrDev.endsWith('-ea')) {
- /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */
- let newOptionValue = optionValue;
- const existingOptions = process.env[NATIVE_IMAGE_OPTIONS_ENV];
- if (existingOptions) {
- newOptionValue = `${existingOptions} ${newOptionValue}`;
- }
- core.exportVariable(NATIVE_IMAGE_OPTIONS_ENV, newOptionValue);
- }
- else {
- const optionsFile = getNativeImageOptionsFile();
- if (fs.existsSync(optionsFile)) {
- fs.appendFileSync(optionsFile, ` ${optionValue}`);
- }
- else {
- fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`);
- }
- }
-}
-function getNativeImageOptionsFile() {
- let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV];
- if (optionsFile === undefined) {
- optionsFile = NATIVE_IMAGE_CONFIG_FILE;
- core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile);
- }
- return optionsFile;
-}
function createReport(data) {
const context = github.context;
const info = data.general_info;
@@ -98751,6 +98717,239 @@ function secondsToHuman(seconds) {
}
+/***/ }),
+
+/***/ 9181:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.processSBOM = exports.setUpSBOMSupport = void 0;
+const c = __importStar(__nccwpck_require__(9042));
+const core = __importStar(__nccwpck_require__(2186));
+const fs = __importStar(__nccwpck_require__(7147));
+const github = __importStar(__nccwpck_require__(5438));
+const glob = __importStar(__nccwpck_require__(8090));
+const path_1 = __nccwpck_require__(1017);
+const semver = __importStar(__nccwpck_require__(1383));
+const utils_1 = __nccwpck_require__(1314);
+const INPUT_NI_SBOM = 'native-image-enable-sbom';
+const SBOM_FILE_SUFFIX = '.sbom.json';
+const MIN_JAVA_VERSION = '24.0.0';
+let javaVersionOrLatestEA = null;
+function setUpSBOMSupport(javaVersionOrDev, distribution) {
+ if (!isFeatureEnabled()) {
+ return;
+ }
+ validateJavaVersionAndDistribution(javaVersionOrDev, distribution);
+ javaVersionOrLatestEA = javaVersionOrDev;
+ (0, utils_1.setNativeImageOption)(javaVersionOrLatestEA, '--enable-sbom=export');
+ core.info('Enabled SBOM generation for Native Image build');
+}
+exports.setUpSBOMSupport = setUpSBOMSupport;
+function validateJavaVersionAndDistribution(javaVersionOrDev, distribution) {
+ if (distribution !== c.DISTRIBUTION_GRAALVM) {
+ throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for Oracle GraalVM (distribution '${c.DISTRIBUTION_GRAALVM}'), but found distribution '${distribution}'.`);
+ }
+ if (javaVersionOrDev === 'dev') {
+ throw new Error(`The '${INPUT_NI_SBOM}' option is not supported for java-version 'dev'.`);
+ }
+ if (javaVersionOrDev === 'latest-ea') {
+ return;
+ }
+ const coercedJavaVersion = semver.coerce(javaVersionOrDev);
+ if (!coercedJavaVersion || semver.gt(MIN_JAVA_VERSION, coercedJavaVersion)) {
+ throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for GraalVM for JDK ${MIN_JAVA_VERSION} or later, but found java-version '${javaVersionOrDev}'.`);
+ }
+}
+function processSBOM() {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (!isFeatureEnabled()) {
+ return;
+ }
+ if (javaVersionOrLatestEA === null) {
+ throw new Error('setUpSBOMSupport must be called before processSBOM');
+ }
+ const sbomPath = yield findSBOMFilePath();
+ try {
+ const sbomContent = fs.readFileSync(sbomPath, 'utf8');
+ const sbomData = parseSBOM(sbomContent);
+ const components = mapToComponentsWithDependencies(sbomData);
+ printSBOMContent(components);
+ const snapshot = convertSBOMToSnapshot(sbomPath, components);
+ yield submitDependencySnapshot(snapshot);
+ }
+ catch (error) {
+ throw new Error(`Failed to process and submit SBOM to the GitHub dependency submission API: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ });
+}
+exports.processSBOM = processSBOM;
+function isFeatureEnabled() {
+ return core.getInput(INPUT_NI_SBOM) === 'true';
+}
+function findSBOMFilePath() {
+ return __awaiter(this, void 0, void 0, function* () {
+ const globber = yield glob.create(`**/*${SBOM_FILE_SUFFIX}`);
+ const sbomFiles = yield globber.glob();
+ if (sbomFiles.length === 0) {
+ throw new Error('No SBOM found. Make sure native-image build completed successfully.');
+ }
+ if (sbomFiles.length > 1) {
+ throw new Error(`Expected one SBOM but found multiple: ${sbomFiles.join(', ')}.`);
+ }
+ core.info(`Found SBOM: ${sbomFiles[0]}`);
+ return sbomFiles[0];
+ });
+}
+function parseSBOM(jsonString) {
+ try {
+ const sbomData = JSON.parse(jsonString);
+ return sbomData;
+ }
+ catch (error) {
+ throw new Error(`Failed to parse SBOM JSON: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+// Maps the SBOM to a list of components with their dependencies
+function mapToComponentsWithDependencies(sbom) {
+ if (!sbom || sbom.components.length === 0) {
+ throw new Error('Invalid SBOM data or no components found.');
+ }
+ return sbom.components.map((component) => {
+ var _a, _b;
+ const dependencies = ((_b = (_a = sbom.dependencies) === null || _a === void 0 ? void 0 : _a.find((dep) => dep.ref === component['bom-ref'])) === null || _b === void 0 ? void 0 : _b.dependsOn) || [];
+ return {
+ name: component.name,
+ version: component.version,
+ purl: component.purl,
+ dependencies,
+ 'bom-ref': component['bom-ref']
+ };
+ });
+}
+function printSBOMContent(components) {
+ core.info('=== SBOM Content ===');
+ for (const component of components) {
+ core.info(`- ${component['bom-ref']}`);
+ if (component.dependencies && component.dependencies.length > 0) {
+ core.info(` depends on: ${component.dependencies.join(', ')}`);
+ }
+ }
+ core.info('==================');
+}
+function convertSBOMToSnapshot(sbomPath, components) {
+ const context = github.context;
+ const sbomFileName = (0, path_1.basename)(sbomPath);
+ if (!sbomFileName.endsWith(SBOM_FILE_SUFFIX)) {
+ throw new Error(`Invalid SBOM file name: ${sbomFileName}. Expected a file ending with ${SBOM_FILE_SUFFIX}.`);
+ }
+ return {
+ version: 0,
+ sha: context.sha,
+ ref: context.ref,
+ job: {
+ correlator: `${context.workflow}_${context.job}`,
+ id: context.runId.toString(),
+ html_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
+ },
+ detector: {
+ name: 'Oracle GraalVM',
+ version: javaVersionOrLatestEA !== null && javaVersionOrLatestEA !== void 0 ? javaVersionOrLatestEA : '',
+ url: 'https://www.graalvm.org/'
+ },
+ scanned: new Date().toISOString(),
+ manifests: {
+ [sbomFileName]: {
+ name: sbomFileName,
+ resolved: mapComponentsToGithubAPIFormat(components),
+ metadata: {
+ generated_by: 'SBOM generated by GraalVM Native Image',
+ action_version: c.ACTION_VERSION
+ }
+ }
+ }
+ };
+}
+function mapComponentsToGithubAPIFormat(components) {
+ return Object.fromEntries(components
+ .filter(component => {
+ if (!component.purl) {
+ core.info(`Component ${component.name} does not have a valid package URL (purl). Skipping.`);
+ }
+ return component.purl;
+ })
+ .map(component => [
+ component.name,
+ {
+ package_url: component.purl,
+ dependencies: component.dependencies || []
+ }
+ ]));
+}
+function submitDependencySnapshot(snapshotData) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const token = core.getInput(c.INPUT_GITHUB_TOKEN, { required: true });
+ const octokit = github.getOctokit(token);
+ const context = github.context;
+ try {
+ yield octokit.request('POST /repos/{owner}/{repo}/dependency-graph/snapshots', {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ version: snapshotData.version,
+ sha: snapshotData.sha,
+ ref: snapshotData.ref,
+ job: snapshotData.job,
+ detector: snapshotData.detector,
+ metadata: {},
+ scanned: snapshotData.scanned,
+ manifests: snapshotData.manifests,
+ headers: {
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+ });
+ core.info('Dependency snapshot submitted successfully.');
+ }
+ catch (error) {
+ throw new Error(`Failed to submit dependency snapshot for SBOM: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ });
+}
+
+
/***/ }),
/***/ 1314:
@@ -98791,18 +98990,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0;
+exports.setNativeImageOption = exports.tmpfile = exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0;
const c = __importStar(__nccwpck_require__(9042));
const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438));
const httpClient = __importStar(__nccwpck_require__(6255));
const semver = __importStar(__nccwpck_require__(1383));
const tc = __importStar(__nccwpck_require__(7784));
+const fs = __importStar(__nccwpck_require__(7147));
const exec_1 = __nccwpck_require__(1514);
const fs_1 = __nccwpck_require__(7147);
const core_1 = __nccwpck_require__(6762);
const crypto_1 = __nccwpck_require__(6113);
const path_1 = __nccwpck_require__(1017);
+const os_1 = __nccwpck_require__(2037);
// Set up Octokit for github.com only and in the same way as @actions/github (see https://git.io/Jy9YP)
const baseUrl = 'https://api.github.com';
const GitHubDotCom = core_1.Octokit.defaults({
@@ -98999,6 +99200,45 @@ function createPRComment(content) {
});
}
exports.createPRComment = createPRComment;
+function tmpfile(fileName) {
+ return (0, path_1.join)((0, os_1.tmpdir)(), fileName);
+}
+exports.tmpfile = tmpfile;
+function setNativeImageOption(javaVersionOrDev, optionValue) {
+ const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev);
+ if ((coercedJavaVersionOrDev &&
+ semver.gte(coercedJavaVersionOrDev, '22.0.0')) ||
+ javaVersionOrDev === c.VERSION_DEV ||
+ javaVersionOrDev.endsWith('-ea')) {
+ /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */
+ let newOptionValue = optionValue;
+ const existingOptions = process.env[c.NATIVE_IMAGE_OPTIONS_ENV];
+ if (existingOptions) {
+ newOptionValue = `${existingOptions} ${newOptionValue}`;
+ }
+ core.exportVariable(c.NATIVE_IMAGE_OPTIONS_ENV, newOptionValue);
+ }
+ else {
+ const optionsFile = getNativeImageOptionsFile();
+ if (fs.existsSync(optionsFile)) {
+ fs.appendFileSync(optionsFile, ` ${optionValue}`);
+ }
+ else {
+ fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`);
+ }
+ }
+}
+exports.setNativeImageOption = setNativeImageOption;
+const NATIVE_IMAGE_CONFIG_FILE = tmpfile('native-image-options.properties');
+const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE';
+function getNativeImageOptionsFile() {
+ let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV];
+ if (optionsFile === undefined) {
+ optionsFile = NATIVE_IMAGE_CONFIG_FILE;
+ core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile);
+ }
+ return optionsFile;
+}
/***/ }),
diff --git a/dist/main/index.js b/dist/main/index.js
index 74a4102..f0e2eae 100644
--- a/dist/main/index.js
+++ b/dist/main/index.js
@@ -97949,7 +97949,7 @@ function wrappy (fn, cb) {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0;
+exports.ERROR_HINT = exports.ERROR_REQUEST = exports.EVENT_NAME_PULL_REQUEST = exports.ENV_GITHUB_EVENT_NAME = exports.GDS_GRAALVM_PRODUCT_ID = exports.GDS_BASE = exports.MANDREL_NAMESPACE = exports.GRAALVM_RELEASES_REPO = exports.GRAALVM_PLATFORM = exports.GRAALVM_GH_USER = exports.GRAALVM_FILE_EXTENSION = exports.GRAALVM_ARCH = exports.JDK_HOME_SUFFIX = exports.JDK_PLATFORM = exports.JDK_ARCH = exports.VERSION_LATEST = exports.VERSION_DEV = exports.DISTRIBUTION_LIBERICA = exports.DISTRIBUTION_MANDREL = exports.DISTRIBUTION_GRAALVM_COMMUNITY = exports.DISTRIBUTION_GRAALVM = exports.EXECUTABLE_SUFFIX = exports.IS_WINDOWS = exports.IS_MACOS = exports.IS_LINUX = exports.NATIVE_IMAGE_OPTIONS_ENV = exports.INPUT_NI_MUSL = exports.INPUT_CHECK_FOR_UPDATES = exports.INPUT_CACHE = exports.INPUT_SET_JAVA_HOME = exports.INPUT_GITHUB_TOKEN = exports.INPUT_COMPONENTS = exports.INPUT_DISTRIBUTION = exports.INPUT_JAVA_PACKAGE = exports.INPUT_JAVA_VERSION = exports.INPUT_GDS_TOKEN = exports.INPUT_VERSION = exports.ACTION_VERSION = void 0;
exports.ACTION_VERSION = '1.2.7';
exports.INPUT_VERSION = 'version';
exports.INPUT_GDS_TOKEN = 'gds-token';
@@ -97962,6 +97962,7 @@ exports.INPUT_SET_JAVA_HOME = 'set-java-home';
exports.INPUT_CACHE = 'cache';
exports.INPUT_CHECK_FOR_UPDATES = 'check-for-updates';
exports.INPUT_NI_MUSL = 'native-image-musl';
+exports.NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS';
exports.IS_LINUX = process.platform === 'linux';
exports.IS_MACOS = process.platform === 'darwin';
exports.IS_WINDOWS = process.platform === 'win32';
@@ -98519,10 +98520,8 @@ const core = __importStar(__nccwpck_require__(42186));
const fs = __importStar(__nccwpck_require__(57147));
const github = __importStar(__nccwpck_require__(95438));
const semver = __importStar(__nccwpck_require__(11383));
-const path_1 = __nccwpck_require__(71017);
-const os_1 = __nccwpck_require__(22037);
const utils_1 = __nccwpck_require__(71314);
-const BUILD_OUTPUT_JSON_PATH = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-build-output.json');
+const BUILD_OUTPUT_JSON_PATH = (0, utils_1.tmpfile)('native-image-build-output.json');
const BYTES_TO_KiB = 1024;
const BYTES_TO_MiB = 1024 * 1024;
const BYTES_TO_GiB = 1024 * 1024 * 1024;
@@ -98530,9 +98529,6 @@ const DOCS_BASE = 'https://github.com/oracle/graal/blob/master/docs/reference-ma
const INPUT_NI_JOB_REPORTS = 'native-image-job-reports';
const INPUT_NI_PR_REPORTS = 'native-image-pr-reports';
const INPUT_NI_PR_REPORTS_UPDATE = 'native-image-pr-reports-update-existing';
-const NATIVE_IMAGE_CONFIG_FILE = (0, path_1.join)((0, os_1.tmpdir)(), 'native-image-options.properties');
-const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS';
-const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE';
const PR_COMMENT_TITLE = '## GraalVM Native Image Build Report';
function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev, graalVMVersion) {
return __awaiter(this, void 0, void 0, function* () {
@@ -98549,7 +98545,7 @@ function setUpNativeImageBuildReports(isGraalVMforJDK17OrLater, javaVersionOrDev
core.warning(`Build reports for PRs and job summaries are only available in GraalVM 22.2.0 or later. This build job uses GraalVM ${graalVMVersion}.`);
return;
}
- setNativeImageOption(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows
+ (0, utils_1.setNativeImageOption)(javaVersionOrDev, `-H:BuildOutputJSONFile=${BUILD_OUTPUT_JSON_PATH.replace(/\\/g, '\\\\')}`); // Escape backslashes for Windows
});
}
exports.setUpNativeImageBuildReports = setUpNativeImageBuildReports;
@@ -98591,38 +98587,6 @@ function arePRReportsEnabled() {
function arePRReportsUpdateEnabled() {
return (0, utils_1.isPREvent)() && core.getInput(INPUT_NI_PR_REPORTS_UPDATE) === 'true';
}
-function setNativeImageOption(javaVersionOrDev, optionValue) {
- const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev);
- if ((coercedJavaVersionOrDev &&
- semver.gte(coercedJavaVersionOrDev, '22.0.0')) ||
- javaVersionOrDev === c.VERSION_DEV ||
- javaVersionOrDev.endsWith('-ea')) {
- /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */
- let newOptionValue = optionValue;
- const existingOptions = process.env[NATIVE_IMAGE_OPTIONS_ENV];
- if (existingOptions) {
- newOptionValue = `${existingOptions} ${newOptionValue}`;
- }
- core.exportVariable(NATIVE_IMAGE_OPTIONS_ENV, newOptionValue);
- }
- else {
- const optionsFile = getNativeImageOptionsFile();
- if (fs.existsSync(optionsFile)) {
- fs.appendFileSync(optionsFile, ` ${optionValue}`);
- }
- else {
- fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`);
- }
- }
-}
-function getNativeImageOptionsFile() {
- let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV];
- if (optionsFile === undefined) {
- optionsFile = NATIVE_IMAGE_CONFIG_FILE;
- core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile);
- }
- return optionsFile;
-}
function createReport(data) {
const context = github.context;
const info = data.general_info;
@@ -98850,6 +98814,239 @@ function secondsToHuman(seconds) {
}
+/***/ }),
+
+/***/ 69181:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+ return new (P || (P = Promise))(function (resolve, reject) {
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
+ });
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+exports.processSBOM = exports.setUpSBOMSupport = void 0;
+const c = __importStar(__nccwpck_require__(69042));
+const core = __importStar(__nccwpck_require__(42186));
+const fs = __importStar(__nccwpck_require__(57147));
+const github = __importStar(__nccwpck_require__(95438));
+const glob = __importStar(__nccwpck_require__(28090));
+const path_1 = __nccwpck_require__(71017);
+const semver = __importStar(__nccwpck_require__(11383));
+const utils_1 = __nccwpck_require__(71314);
+const INPUT_NI_SBOM = 'native-image-enable-sbom';
+const SBOM_FILE_SUFFIX = '.sbom.json';
+const MIN_JAVA_VERSION = '24.0.0';
+let javaVersionOrLatestEA = null;
+function setUpSBOMSupport(javaVersionOrDev, distribution) {
+ if (!isFeatureEnabled()) {
+ return;
+ }
+ validateJavaVersionAndDistribution(javaVersionOrDev, distribution);
+ javaVersionOrLatestEA = javaVersionOrDev;
+ (0, utils_1.setNativeImageOption)(javaVersionOrLatestEA, '--enable-sbom=export');
+ core.info('Enabled SBOM generation for Native Image build');
+}
+exports.setUpSBOMSupport = setUpSBOMSupport;
+function validateJavaVersionAndDistribution(javaVersionOrDev, distribution) {
+ if (distribution !== c.DISTRIBUTION_GRAALVM) {
+ throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for Oracle GraalVM (distribution '${c.DISTRIBUTION_GRAALVM}'), but found distribution '${distribution}'.`);
+ }
+ if (javaVersionOrDev === 'dev') {
+ throw new Error(`The '${INPUT_NI_SBOM}' option is not supported for java-version 'dev'.`);
+ }
+ if (javaVersionOrDev === 'latest-ea') {
+ return;
+ }
+ const coercedJavaVersion = semver.coerce(javaVersionOrDev);
+ if (!coercedJavaVersion || semver.gt(MIN_JAVA_VERSION, coercedJavaVersion)) {
+ throw new Error(`The '${INPUT_NI_SBOM}' option is only supported for GraalVM for JDK ${MIN_JAVA_VERSION} or later, but found java-version '${javaVersionOrDev}'.`);
+ }
+}
+function processSBOM() {
+ return __awaiter(this, void 0, void 0, function* () {
+ if (!isFeatureEnabled()) {
+ return;
+ }
+ if (javaVersionOrLatestEA === null) {
+ throw new Error('setUpSBOMSupport must be called before processSBOM');
+ }
+ const sbomPath = yield findSBOMFilePath();
+ try {
+ const sbomContent = fs.readFileSync(sbomPath, 'utf8');
+ const sbomData = parseSBOM(sbomContent);
+ const components = mapToComponentsWithDependencies(sbomData);
+ printSBOMContent(components);
+ const snapshot = convertSBOMToSnapshot(sbomPath, components);
+ yield submitDependencySnapshot(snapshot);
+ }
+ catch (error) {
+ throw new Error(`Failed to process and submit SBOM to the GitHub dependency submission API: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ });
+}
+exports.processSBOM = processSBOM;
+function isFeatureEnabled() {
+ return core.getInput(INPUT_NI_SBOM) === 'true';
+}
+function findSBOMFilePath() {
+ return __awaiter(this, void 0, void 0, function* () {
+ const globber = yield glob.create(`**/*${SBOM_FILE_SUFFIX}`);
+ const sbomFiles = yield globber.glob();
+ if (sbomFiles.length === 0) {
+ throw new Error('No SBOM found. Make sure native-image build completed successfully.');
+ }
+ if (sbomFiles.length > 1) {
+ throw new Error(`Expected one SBOM but found multiple: ${sbomFiles.join(', ')}.`);
+ }
+ core.info(`Found SBOM: ${sbomFiles[0]}`);
+ return sbomFiles[0];
+ });
+}
+function parseSBOM(jsonString) {
+ try {
+ const sbomData = JSON.parse(jsonString);
+ return sbomData;
+ }
+ catch (error) {
+ throw new Error(`Failed to parse SBOM JSON: ${error instanceof Error ? error.message : String(error)}`);
+ }
+}
+// Maps the SBOM to a list of components with their dependencies
+function mapToComponentsWithDependencies(sbom) {
+ if (!sbom || sbom.components.length === 0) {
+ throw new Error('Invalid SBOM data or no components found.');
+ }
+ return sbom.components.map((component) => {
+ var _a, _b;
+ const dependencies = ((_b = (_a = sbom.dependencies) === null || _a === void 0 ? void 0 : _a.find((dep) => dep.ref === component['bom-ref'])) === null || _b === void 0 ? void 0 : _b.dependsOn) || [];
+ return {
+ name: component.name,
+ version: component.version,
+ purl: component.purl,
+ dependencies,
+ 'bom-ref': component['bom-ref']
+ };
+ });
+}
+function printSBOMContent(components) {
+ core.info('=== SBOM Content ===');
+ for (const component of components) {
+ core.info(`- ${component['bom-ref']}`);
+ if (component.dependencies && component.dependencies.length > 0) {
+ core.info(` depends on: ${component.dependencies.join(', ')}`);
+ }
+ }
+ core.info('==================');
+}
+function convertSBOMToSnapshot(sbomPath, components) {
+ const context = github.context;
+ const sbomFileName = (0, path_1.basename)(sbomPath);
+ if (!sbomFileName.endsWith(SBOM_FILE_SUFFIX)) {
+ throw new Error(`Invalid SBOM file name: ${sbomFileName}. Expected a file ending with ${SBOM_FILE_SUFFIX}.`);
+ }
+ return {
+ version: 0,
+ sha: context.sha,
+ ref: context.ref,
+ job: {
+ correlator: `${context.workflow}_${context.job}`,
+ id: context.runId.toString(),
+ html_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
+ },
+ detector: {
+ name: 'Oracle GraalVM',
+ version: javaVersionOrLatestEA !== null && javaVersionOrLatestEA !== void 0 ? javaVersionOrLatestEA : '',
+ url: 'https://www.graalvm.org/'
+ },
+ scanned: new Date().toISOString(),
+ manifests: {
+ [sbomFileName]: {
+ name: sbomFileName,
+ resolved: mapComponentsToGithubAPIFormat(components),
+ metadata: {
+ generated_by: 'SBOM generated by GraalVM Native Image',
+ action_version: c.ACTION_VERSION
+ }
+ }
+ }
+ };
+}
+function mapComponentsToGithubAPIFormat(components) {
+ return Object.fromEntries(components
+ .filter(component => {
+ if (!component.purl) {
+ core.info(`Component ${component.name} does not have a valid package URL (purl). Skipping.`);
+ }
+ return component.purl;
+ })
+ .map(component => [
+ component.name,
+ {
+ package_url: component.purl,
+ dependencies: component.dependencies || []
+ }
+ ]));
+}
+function submitDependencySnapshot(snapshotData) {
+ return __awaiter(this, void 0, void 0, function* () {
+ const token = core.getInput(c.INPUT_GITHUB_TOKEN, { required: true });
+ const octokit = github.getOctokit(token);
+ const context = github.context;
+ try {
+ yield octokit.request('POST /repos/{owner}/{repo}/dependency-graph/snapshots', {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ version: snapshotData.version,
+ sha: snapshotData.sha,
+ ref: snapshotData.ref,
+ job: snapshotData.job,
+ detector: snapshotData.detector,
+ metadata: {},
+ scanned: snapshotData.scanned,
+ manifests: snapshotData.manifests,
+ headers: {
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+ });
+ core.info('Dependency snapshot submitted successfully.');
+ }
+ catch (error) {
+ throw new Error(`Failed to submit dependency snapshot for SBOM: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ });
+}
+
+
/***/ }),
/***/ 19543:
@@ -99684,6 +99881,7 @@ const musl_1 = __nccwpck_require__(10316);
const msvc_1 = __nccwpck_require__(31165);
const reports_1 = __nccwpck_require__(92046);
const exec_1 = __nccwpck_require__(71514);
+const sbom_1 = __nccwpck_require__(69181);
function run() {
return __awaiter(this, void 0, void 0, function* () {
try {
@@ -99795,6 +99993,7 @@ function run() {
yield (0, cache_2.restore)(cache);
}
(0, reports_1.setUpNativeImageBuildReports)(isGraalVMforJDK17OrLater, javaVersion, graalVMVersion);
+ (0, sbom_1.setUpSBOMSupport)(javaVersion, distribution);
core.startGroup(`Successfully set up '${(0, path_1.basename)(graalVMHome)}'`);
yield (0, exec_1.exec)((0, path_1.join)(graalVMHome, 'bin', `java${c.EXECUTABLE_SUFFIX}`), [
javaVersion.startsWith('8') ? '-version' : '--version'
@@ -100117,18 +100316,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
-exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0;
+exports.setNativeImageOption = exports.tmpfile = exports.createPRComment = exports.updatePRComment = exports.findExistingPRCommentId = exports.isPREvent = exports.toSemVer = exports.calculateSHA256 = exports.downloadExtractAndCacheJDK = exports.downloadAndExtractJDK = exports.getMatchingTags = exports.getTaggedRelease = exports.getContents = exports.getLatestRelease = exports.exec = void 0;
const c = __importStar(__nccwpck_require__(69042));
const core = __importStar(__nccwpck_require__(42186));
const github = __importStar(__nccwpck_require__(95438));
const httpClient = __importStar(__nccwpck_require__(96255));
const semver = __importStar(__nccwpck_require__(11383));
const tc = __importStar(__nccwpck_require__(27784));
+const fs = __importStar(__nccwpck_require__(57147));
const exec_1 = __nccwpck_require__(71514);
const fs_1 = __nccwpck_require__(57147);
const core_1 = __nccwpck_require__(76762);
const crypto_1 = __nccwpck_require__(6113);
const path_1 = __nccwpck_require__(71017);
+const os_1 = __nccwpck_require__(22037);
// Set up Octokit for github.com only and in the same way as @actions/github (see https://git.io/Jy9YP)
const baseUrl = 'https://api.github.com';
const GitHubDotCom = core_1.Octokit.defaults({
@@ -100325,6 +100526,45 @@ function createPRComment(content) {
});
}
exports.createPRComment = createPRComment;
+function tmpfile(fileName) {
+ return (0, path_1.join)((0, os_1.tmpdir)(), fileName);
+}
+exports.tmpfile = tmpfile;
+function setNativeImageOption(javaVersionOrDev, optionValue) {
+ const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev);
+ if ((coercedJavaVersionOrDev &&
+ semver.gte(coercedJavaVersionOrDev, '22.0.0')) ||
+ javaVersionOrDev === c.VERSION_DEV ||
+ javaVersionOrDev.endsWith('-ea')) {
+ /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */
+ let newOptionValue = optionValue;
+ const existingOptions = process.env[c.NATIVE_IMAGE_OPTIONS_ENV];
+ if (existingOptions) {
+ newOptionValue = `${existingOptions} ${newOptionValue}`;
+ }
+ core.exportVariable(c.NATIVE_IMAGE_OPTIONS_ENV, newOptionValue);
+ }
+ else {
+ const optionsFile = getNativeImageOptionsFile();
+ if (fs.existsSync(optionsFile)) {
+ fs.appendFileSync(optionsFile, ` ${optionValue}`);
+ }
+ else {
+ fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`);
+ }
+ }
+}
+exports.setNativeImageOption = setNativeImageOption;
+const NATIVE_IMAGE_CONFIG_FILE = tmpfile('native-image-options.properties');
+const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE';
+function getNativeImageOptionsFile() {
+ let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV];
+ if (optionsFile === undefined) {
+ optionsFile = NATIVE_IMAGE_CONFIG_FILE;
+ core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile);
+ }
+ return optionsFile;
+}
/***/ }),
diff --git a/package-lock.json b/package-lock.json
index 3c98fd1..da52382 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"@actions/tool-cache": "^2.0.2",
"@octokit/core": "^5.2.0",
"@octokit/types": "^12.6.0",
+ "@github/dependency-submission-toolkit": "^2.0.4",
"semver": "^7.6.3",
"uuid": "^11.0.5"
},
@@ -1111,6 +1112,22 @@
"integrity": "sha512-gIhjdJp/c2beaIWWIlsXdqXVRUz3r2BxBCpfz/F3JXHvSAQ1paMYjLH+maEATtENg+k5eLV7gA+9yPp762ieuw==",
"dev": true
},
+ "node_modules/@github/dependency-submission-toolkit": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@github/dependency-submission-toolkit/-/dependency-submission-toolkit-2.0.4.tgz",
+ "integrity": "sha512-uQia1YSLTrVmy+f6XpAzy/MEFDvjMg/VOm9pdROxVKQA5SvLXDvXeGgxLwy9fH+sXHqtDWRnVOI1+UAcQ4pi/w==",
+ "license": "MIT",
+ "workspaces": [
+ "example"
+ ],
+ "dependencies": {
+ "@actions/core": "^1.10.1",
+ "@actions/github": "^6.0.0",
+ "@octokit/request-error": "^5.0.1",
+ "@octokit/webhooks-types": "^7.3.1",
+ "packageurl-js": "^1.2.1"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -1796,6 +1813,12 @@
"@octokit/openapi-types": "^20.0.0"
}
},
+ "node_modules/@octokit/webhooks-types": {
+ "version": "7.6.1",
+ "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz",
+ "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==",
+ "license": "MIT"
+ },
"node_modules/@opentelemetry/api": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz",
@@ -6648,6 +6671,12 @@
"node": ">=6"
}
},
+ "node_modules/packageurl-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/packageurl-js/-/packageurl-js-1.2.1.tgz",
+ "integrity": "sha512-cZ6/MzuXaoFd16/k0WnwtI298UCaDHe/XlSh85SeOKbGZ1hq0xvNbx3ILyCMyk7uFQxl6scF3Aucj6/EO9NwcA==",
+ "license": "MIT"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -7998,10 +8027,11 @@
}
},
"node_modules/typescript": {
- "version": "5.4.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
- "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
+ "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true,
+ "license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
diff --git a/package.json b/package.json
index 5ca9135..6dea35a 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"@actions/tool-cache": "^2.0.2",
"@octokit/core": "^5.2.0",
"@octokit/types": "^12.6.0",
+ "@github/dependency-submission-toolkit": "^2.0.4",
"semver": "^7.6.3",
"uuid": "^11.0.5"
},
diff --git a/src/cleanup.ts b/src/cleanup.ts
index 3ed1b0c..4ebd75a 100644
--- a/src/cleanup.ts
+++ b/src/cleanup.ts
@@ -28,6 +28,7 @@ import * as core from '@actions/core'
import * as constants from './constants'
import {save} from './features/cache'
import {generateReports} from './features/reports'
+import {processSBOM} from './features/sbom'
/**
* Check given input and run a save process for the specified package manager
@@ -58,6 +59,7 @@ async function ignoreErrors(promise: Promise): Promise {
export async function run(): Promise {
await ignoreErrors(generateReports())
+ await ignoreErrors(processSBOM())
await ignoreErrors(saveCache())
}
diff --git a/src/constants.ts b/src/constants.ts
index acb6aaf..519700f 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -14,6 +14,8 @@ export const INPUT_CACHE = 'cache'
export const INPUT_CHECK_FOR_UPDATES = 'check-for-updates'
export const INPUT_NI_MUSL = 'native-image-musl'
+export const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS'
+
export const IS_LINUX = process.platform === 'linux'
export const IS_MACOS = process.platform === 'darwin'
export const IS_WINDOWS = process.platform === 'win32'
diff --git a/src/features/reports.ts b/src/features/reports.ts
index fd21fd3..3793b31 100644
--- a/src/features/reports.ts
+++ b/src/features/reports.ts
@@ -3,17 +3,17 @@ import * as core from '@actions/core'
import * as fs from 'fs'
import * as github from '@actions/github'
import * as semver from 'semver'
-import {join} from 'path'
-import {tmpdir} from 'os'
import {
createPRComment,
findExistingPRCommentId,
isPREvent,
toSemVer,
- updatePRComment
+ updatePRComment,
+ tmpfile,
+ setNativeImageOption
} from '../utils'
-const BUILD_OUTPUT_JSON_PATH = join(tmpdir(), 'native-image-build-output.json')
+const BUILD_OUTPUT_JSON_PATH = tmpfile('native-image-build-output.json')
const BYTES_TO_KiB = 1024
const BYTES_TO_MiB = 1024 * 1024
const BYTES_TO_GiB = 1024 * 1024 * 1024
@@ -22,12 +22,6 @@ const DOCS_BASE =
const INPUT_NI_JOB_REPORTS = 'native-image-job-reports'
const INPUT_NI_PR_REPORTS = 'native-image-pr-reports'
const INPUT_NI_PR_REPORTS_UPDATE = 'native-image-pr-reports-update-existing'
-const NATIVE_IMAGE_CONFIG_FILE = join(
- tmpdir(),
- 'native-image-options.properties'
-)
-const NATIVE_IMAGE_OPTIONS_ENV = 'NATIVE_IMAGE_OPTIONS'
-const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE'
const PR_COMMENT_TITLE = '## GraalVM Native Image Build Report'
interface AnalysisResult {
@@ -169,43 +163,6 @@ function arePRReportsUpdateEnabled(): boolean {
return isPREvent() && core.getInput(INPUT_NI_PR_REPORTS_UPDATE) === 'true'
}
-function setNativeImageOption(
- javaVersionOrDev: string,
- optionValue: string
-): void {
- const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev)
- if (
- (coercedJavaVersionOrDev &&
- semver.gte(coercedJavaVersionOrDev, '22.0.0')) ||
- javaVersionOrDev === c.VERSION_DEV ||
- javaVersionOrDev.endsWith('-ea')
- ) {
- /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */
- let newOptionValue = optionValue
- const existingOptions = process.env[NATIVE_IMAGE_OPTIONS_ENV]
- if (existingOptions) {
- newOptionValue = `${existingOptions} ${newOptionValue}`
- }
- core.exportVariable(NATIVE_IMAGE_OPTIONS_ENV, newOptionValue)
- } else {
- const optionsFile = getNativeImageOptionsFile()
- if (fs.existsSync(optionsFile)) {
- fs.appendFileSync(optionsFile, ` ${optionValue}`)
- } else {
- fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`)
- }
- }
-}
-
-function getNativeImageOptionsFile(): string {
- let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV]
- if (optionsFile === undefined) {
- optionsFile = NATIVE_IMAGE_CONFIG_FILE
- core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile)
- }
- return optionsFile
-}
-
function createReport(data: BuildOutput): string {
const context = github.context
const info = data.general_info
diff --git a/src/features/sbom.ts b/src/features/sbom.ts
new file mode 100644
index 0000000..125e077
--- /dev/null
+++ b/src/features/sbom.ts
@@ -0,0 +1,300 @@
+import * as c from '../constants'
+import * as core from '@actions/core'
+import * as fs from 'fs'
+import * as github from '@actions/github'
+import * as glob from '@actions/glob'
+import {basename} from 'path'
+import * as semver from 'semver'
+import {setNativeImageOption} from '../utils'
+
+const INPUT_NI_SBOM = 'native-image-enable-sbom'
+const SBOM_FILE_SUFFIX = '.sbom.json'
+const MIN_JAVA_VERSION = '24.0.0'
+
+let javaVersionOrLatestEA: string | null = null
+
+interface SBOM {
+ components: Component[]
+ dependencies: Dependency[]
+}
+
+interface Component {
+ name: string
+ version?: string
+ purl?: string
+ dependencies?: string[]
+ 'bom-ref': string
+}
+
+interface Dependency {
+ ref: string
+ dependsOn: string[]
+}
+
+interface DependencySnapshot {
+ version: number
+ sha: string
+ ref: string
+ job: {
+ correlator: string
+ id: string
+ html_url?: string
+ }
+ detector: {
+ name: string
+ version: string
+ url: string
+ }
+ scanned: string
+ manifests: Record<
+ string,
+ {
+ name: string
+ metadata?: Record
+ // Not including the 'file' property because we cannot specify any reasonable value for 'source_location'
+ // since the SBOM will not necessarily be saved in the repository of the user.
+ // GitHub docs: https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository
+ resolved: Record<
+ string,
+ {
+ package_url: string
+ relationship?: 'direct'
+ scope?: 'runtime'
+ dependencies?: string[]
+ }
+ >
+ }
+ >
+}
+
+export function setUpSBOMSupport(
+ javaVersionOrDev: string,
+ distribution: string
+): void {
+ if (!isFeatureEnabled()) {
+ return
+ }
+
+ validateJavaVersionAndDistribution(javaVersionOrDev, distribution)
+ javaVersionOrLatestEA = javaVersionOrDev
+ setNativeImageOption(javaVersionOrLatestEA, '--enable-sbom=export')
+ core.info('Enabled SBOM generation for Native Image build')
+}
+
+function validateJavaVersionAndDistribution(
+ javaVersionOrDev: string,
+ distribution: string
+): void {
+ if (distribution !== c.DISTRIBUTION_GRAALVM) {
+ throw new Error(
+ `The '${INPUT_NI_SBOM}' option is only supported for Oracle GraalVM (distribution '${c.DISTRIBUTION_GRAALVM}'), but found distribution '${distribution}'.`
+ )
+ }
+
+ if (javaVersionOrDev === 'dev') {
+ throw new Error(
+ `The '${INPUT_NI_SBOM}' option is not supported for java-version 'dev'.`
+ )
+ }
+
+ if (javaVersionOrDev === 'latest-ea') {
+ return
+ }
+
+ const coercedJavaVersion = semver.coerce(javaVersionOrDev)
+ if (!coercedJavaVersion || semver.gt(MIN_JAVA_VERSION, coercedJavaVersion)) {
+ throw new Error(
+ `The '${INPUT_NI_SBOM}' option is only supported for GraalVM for JDK ${MIN_JAVA_VERSION} or later, but found java-version '${javaVersionOrDev}'.`
+ )
+ }
+}
+
+export async function processSBOM(): Promise {
+ if (!isFeatureEnabled()) {
+ return
+ }
+
+ if (javaVersionOrLatestEA === null) {
+ throw new Error('setUpSBOMSupport must be called before processSBOM')
+ }
+
+ const sbomPath = await findSBOMFilePath()
+ try {
+ const sbomContent = fs.readFileSync(sbomPath, 'utf8')
+ const sbomData = parseSBOM(sbomContent)
+ const components = mapToComponentsWithDependencies(sbomData)
+ printSBOMContent(components)
+ const snapshot = convertSBOMToSnapshot(sbomPath, components)
+ await submitDependencySnapshot(snapshot)
+ } catch (error) {
+ throw new Error(
+ `Failed to process and submit SBOM to the GitHub dependency submission API: ${error instanceof Error ? error.message : String(error)}`
+ )
+ }
+}
+
+function isFeatureEnabled(): boolean {
+ return core.getInput(INPUT_NI_SBOM) === 'true'
+}
+
+async function findSBOMFilePath(): Promise {
+ const globber = await glob.create(`**/*${SBOM_FILE_SUFFIX}`)
+ const sbomFiles = await globber.glob()
+
+ if (sbomFiles.length === 0) {
+ throw new Error(
+ 'No SBOM found. Make sure native-image build completed successfully.'
+ )
+ }
+
+ if (sbomFiles.length > 1) {
+ throw new Error(
+ `Expected one SBOM but found multiple: ${sbomFiles.join(', ')}.`
+ )
+ }
+
+ core.info(`Found SBOM: ${sbomFiles[0]}`)
+ return sbomFiles[0]
+}
+
+function parseSBOM(jsonString: string): SBOM {
+ try {
+ const sbomData: SBOM = JSON.parse(jsonString)
+ return sbomData
+ } catch (error) {
+ throw new Error(
+ `Failed to parse SBOM JSON: ${error instanceof Error ? error.message : String(error)}`
+ )
+ }
+}
+
+// Maps the SBOM to a list of components with their dependencies
+function mapToComponentsWithDependencies(sbom: SBOM): Component[] {
+ if (!sbom || sbom.components.length === 0) {
+ throw new Error('Invalid SBOM data or no components found.')
+ }
+
+ return sbom.components.map((component: Component) => {
+ const dependencies =
+ sbom.dependencies?.find(
+ (dep: Dependency) => dep.ref === component['bom-ref']
+ )?.dependsOn || []
+
+ return {
+ name: component.name,
+ version: component.version,
+ purl: component.purl,
+ dependencies,
+ 'bom-ref': component['bom-ref']
+ }
+ })
+}
+
+function printSBOMContent(components: Component[]): void {
+ core.info('=== SBOM Content ===')
+ for (const component of components) {
+ core.info(`- ${component['bom-ref']}`)
+ if (component.dependencies && component.dependencies.length > 0) {
+ core.info(` depends on: ${component.dependencies.join(', ')}`)
+ }
+ }
+ core.info('==================')
+}
+
+function convertSBOMToSnapshot(
+ sbomPath: string,
+ components: Component[]
+): DependencySnapshot {
+ const context = github.context
+ const sbomFileName = basename(sbomPath)
+
+ if (!sbomFileName.endsWith(SBOM_FILE_SUFFIX)) {
+ throw new Error(
+ `Invalid SBOM file name: ${sbomFileName}. Expected a file ending with ${SBOM_FILE_SUFFIX}.`
+ )
+ }
+
+ return {
+ version: 0,
+ sha: context.sha,
+ ref: context.ref,
+ job: {
+ correlator: `${context.workflow}_${context.job}`,
+ id: context.runId.toString(),
+ html_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
+ },
+ detector: {
+ name: 'Oracle GraalVM',
+ version: javaVersionOrLatestEA ?? '',
+ url: 'https://www.graalvm.org/'
+ },
+ scanned: new Date().toISOString(),
+ manifests: {
+ [sbomFileName]: {
+ name: sbomFileName,
+ resolved: mapComponentsToGithubAPIFormat(components),
+ metadata: {
+ generated_by: 'SBOM generated by GraalVM Native Image',
+ action_version: c.ACTION_VERSION
+ }
+ }
+ }
+ }
+}
+
+function mapComponentsToGithubAPIFormat(
+ components: Component[]
+): Record {
+ return Object.fromEntries(
+ components
+ .filter(component => {
+ if (!component.purl) {
+ core.info(
+ `Component ${component.name} does not have a valid package URL (purl). Skipping.`
+ )
+ }
+ return component.purl
+ })
+ .map(component => [
+ component.name,
+ {
+ package_url: component.purl as string,
+ dependencies: component.dependencies || []
+ }
+ ])
+ )
+}
+
+async function submitDependencySnapshot(
+ snapshotData: DependencySnapshot
+): Promise {
+ const token = core.getInput(c.INPUT_GITHUB_TOKEN, {required: true})
+ const octokit = github.getOctokit(token)
+ const context = github.context
+
+ try {
+ await octokit.request(
+ 'POST /repos/{owner}/{repo}/dependency-graph/snapshots',
+ {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ version: snapshotData.version,
+ sha: snapshotData.sha,
+ ref: snapshotData.ref,
+ job: snapshotData.job,
+ detector: snapshotData.detector,
+ metadata: {},
+ scanned: snapshotData.scanned,
+ manifests: snapshotData.manifests,
+ headers: {
+ 'X-GitHub-Api-Version': '2022-11-28'
+ }
+ }
+ )
+ core.info('Dependency snapshot submitted successfully.')
+ } catch (error) {
+ throw new Error(
+ `Failed to submit dependency snapshot for SBOM: ${error instanceof Error ? error.message : String(error)}`
+ )
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 7f32b82..7a27c13 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -14,6 +14,7 @@ import {setUpNativeImageMusl} from './features/musl'
import {setUpWindowsEnvironment} from './msvc'
import {setUpNativeImageBuildReports} from './features/reports'
import {exec} from '@actions/exec'
+import {setUpSBOMSupport} from './features/sbom'
async function run(): Promise {
try {
@@ -148,7 +149,6 @@ async function run(): Promise {
if (setJavaHome) {
core.exportVariable('JAVA_HOME', graalVMHome)
}
-
await setUpGUComponents(
javaVersion,
graalVMVersion,
@@ -165,6 +165,7 @@ async function run(): Promise {
javaVersion,
graalVMVersion
)
+ setUpSBOMSupport(javaVersion, distribution)
core.startGroup(`Successfully set up '${basename(graalVMHome)}'`)
await exec(join(graalVMHome, 'bin', `java${c.EXECUTABLE_SUFFIX}`), [
diff --git a/src/utils.ts b/src/utils.ts
index 655d1d9..5273b54 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -4,11 +4,13 @@ import * as github from '@actions/github'
import * as httpClient from '@actions/http-client'
import * as semver from 'semver'
import * as tc from '@actions/tool-cache'
+import * as fs from 'fs'
import {ExecOptions, exec as e} from '@actions/exec'
import {readFileSync, readdirSync} from 'fs'
import {Octokit} from '@octokit/core'
import {createHash} from 'crypto'
import {join} from 'path'
+import {tmpdir} from 'os'
// Set up Octokit for github.com only and in the same way as @actions/github (see https://git.io/Jy9YP)
const baseUrl = 'https://api.github.com'
@@ -247,3 +249,47 @@ export async function createPRComment(content: string): Promise {
)
}
}
+
+export function tmpfile(fileName: string) {
+ return join(tmpdir(), fileName)
+}
+
+export function setNativeImageOption(
+ javaVersionOrDev: string,
+ optionValue: string
+): void {
+ const coercedJavaVersionOrDev = semver.coerce(javaVersionOrDev)
+ if (
+ (coercedJavaVersionOrDev &&
+ semver.gte(coercedJavaVersionOrDev, '22.0.0')) ||
+ javaVersionOrDev === c.VERSION_DEV ||
+ javaVersionOrDev.endsWith('-ea')
+ ) {
+ /* NATIVE_IMAGE_OPTIONS was introduced in GraalVM for JDK 22 (so were EA builds). */
+ let newOptionValue = optionValue
+ const existingOptions = process.env[c.NATIVE_IMAGE_OPTIONS_ENV]
+ if (existingOptions) {
+ newOptionValue = `${existingOptions} ${newOptionValue}`
+ }
+ core.exportVariable(c.NATIVE_IMAGE_OPTIONS_ENV, newOptionValue)
+ } else {
+ const optionsFile = getNativeImageOptionsFile()
+ if (fs.existsSync(optionsFile)) {
+ fs.appendFileSync(optionsFile, ` ${optionValue}`)
+ } else {
+ fs.writeFileSync(optionsFile, `NativeImageArgs = ${optionValue}`)
+ }
+ }
+}
+
+const NATIVE_IMAGE_CONFIG_FILE = tmpfile('native-image-options.properties')
+const NATIVE_IMAGE_CONFIG_FILE_ENV = 'NATIVE_IMAGE_CONFIG_FILE'
+
+function getNativeImageOptionsFile(): string {
+ let optionsFile = process.env[NATIVE_IMAGE_CONFIG_FILE_ENV]
+ if (optionsFile === undefined) {
+ optionsFile = NATIVE_IMAGE_CONFIG_FILE
+ core.exportVariable(NATIVE_IMAGE_CONFIG_FILE_ENV, optionsFile)
+ }
+ return optionsFile
+}