Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aafbedb8d3 | ||
|
|
51d59d0348 | ||
|
|
ee1b2d994c | ||
|
|
6cf8f984ce | ||
|
|
61fd4fc5d8 | ||
|
|
d028da6ae3 | ||
|
|
613c01f0e8 | ||
|
|
71b1e905e2 | ||
|
|
7b74bd8bd8 | ||
|
|
c09e29bb11 | ||
|
|
04d7e4f4bb | ||
|
|
64b43dc209 | ||
|
|
4a200f28cd | ||
|
|
a8c0509ec2 | ||
|
|
557ffcf459 | ||
|
|
f7c3ab9a9a | ||
|
|
dee12811d5 | ||
|
|
9dd2b41757 |
@@ -1,4 +0,0 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
jest.config.js
|
||||
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"i18n-text/no-en": "off",
|
||||
"eslint-comments/no-use": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"@typescript-eslint/no-for-in-array": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error", "never"],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest/globals": true
|
||||
}
|
||||
}
|
||||
26
.github/dependabot.yml
vendored
Normal file
26
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
actions-minor:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: /
|
||||
schedule:
|
||||
interval: monthly
|
||||
groups:
|
||||
npm-development:
|
||||
dependency-type: development
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
npm-production:
|
||||
dependency-type: production
|
||||
update-types:
|
||||
- patch
|
||||
61
.github/workflows/check-dist.yml
vendored
61
.github/workflows/check-dist.yml
vendored
@@ -1,9 +1,13 @@
|
||||
# `dist/index.js` is a special file in Actions.
|
||||
# When you reference an action with `uses:` in a workflow,
|
||||
# `index.js` is the code that will run.
|
||||
# For our project, we generate this file through a build process from other source files.
|
||||
# We need to make sure the checked-in `index.js` actually matches what we expect it to be.
|
||||
name: Check dist/
|
||||
# In TypeScript actions, `dist/` is a special directory. When you reference
|
||||
# an action with the `uses:` property, `dist/index.js` is the code that will be
|
||||
# run. For this project, the `dist/index.js` file is transpiled from other
|
||||
# source files. This workflow ensures the `dist/` directory contains the
|
||||
# expected transpiled code.
|
||||
#
|
||||
# If this workflow is run from a feature branch, it will act as an additional CI
|
||||
# check and fail if the checked-in `dist/` directory does not match what is
|
||||
# expected from the build.
|
||||
name: Check Transpiled JavaScript
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,40 +20,57 @@ on:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-dist:
|
||||
name: Check dist/
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Checkout
|
||||
id: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Node.js 20.x
|
||||
- name: Setup Node.js
|
||||
id: setup-node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install Dependencies
|
||||
id: install
|
||||
run: npm ci
|
||||
|
||||
- name: Rebuild the dist/ directory
|
||||
run: npm run build
|
||||
- name: Build dist/ Directory
|
||||
id: build
|
||||
run: npm run bundle
|
||||
|
||||
- name: Compare the expected and actual dist/ directories
|
||||
# This will fail the workflow if the `dist/` directory is different than
|
||||
# expected.
|
||||
- name: Compare Directories
|
||||
id: diff
|
||||
run: |
|
||||
if [ "$(git diff --ignore-space-at-eol dist/ | wc -l)" -gt "0" ]; then
|
||||
echo "Detected uncommitted changes after build. See status below:"
|
||||
git diff
|
||||
if [ ! -d dist/ ]; then
|
||||
echo "Expected dist/ directory does not exist. See status below:"
|
||||
ls -la ./
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(git diff --ignore-space-at-eol --text dist/ | wc -l)" -gt "0" ]; then
|
||||
echo "Detected uncommitted changes after build. See status below:"
|
||||
git diff --ignore-space-at-eol --text dist/
|
||||
exit 1
|
||||
fi
|
||||
id: diff
|
||||
|
||||
# If index.js was different than expected, upload the expected version as an artifact
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
|
||||
# If `dist/` was different than expected, upload the expected version as a
|
||||
# workflow artifact.
|
||||
- if: ${{ failure() && steps.diff.outcome == 'failure' }}
|
||||
name: Upload Artifact
|
||||
id: upload
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'build-test'
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,38 +8,51 @@ on:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build: # make sure build/ci work properly
|
||||
test-typescript:
|
||||
name: TypeScript Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: |
|
||||
npm install
|
||||
- run: |
|
||||
npm run all
|
||||
test:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm clean-install
|
||||
- name: Check Format
|
||||
run: npm run format:check
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
test-action:
|
||||
name: GraalVM
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
java-version: ['22', '21', '17', '20', 'dev']
|
||||
java-version: ['23', '21', '17', '20', 'dev']
|
||||
distribution: ['graalvm', 'graalvm-community']
|
||||
os: [
|
||||
ubuntu-latest,
|
||||
macos-latest, # macOS on Apple silicon
|
||||
macos-12, # macOS on Intel
|
||||
windows-latest
|
||||
]
|
||||
ubuntu-latest,
|
||||
macos-latest, # macOS on Apple silicon
|
||||
macos-13, # macOS on Intel
|
||||
windows-latest
|
||||
]
|
||||
set-gds-token: [false]
|
||||
components: ['']
|
||||
include:
|
||||
- java-version: 'latest-ea'
|
||||
distribution: 'graalvm'
|
||||
os: ubuntu-latest
|
||||
- java-version: '23-ea'
|
||||
- java-version: '24-ea'
|
||||
distribution: 'graalvm'
|
||||
os: ubuntu-latest
|
||||
- java-version: '21'
|
||||
@@ -73,6 +86,8 @@ jobs:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
components: ${{ matrix.components }}
|
||||
gds-token: ${{ matrix.set-gds-token && secrets.GDS_TOKEN || '' }}
|
||||
# Skip in PR builds that require a GDS token (secrets are not available in PR runs)
|
||||
if: github.event_name != 'pull_request' || !matrix.set-gds-token
|
||||
- name: Check environment
|
||||
run: |
|
||||
echo "GRAALVM_HOME: $GRAALVM_HOME"
|
||||
@@ -85,15 +100,17 @@ jobs:
|
||||
java --version
|
||||
java --version | grep "GraalVM" || exit 34
|
||||
native-image --version
|
||||
if: runner.os != 'Windows'
|
||||
if: runner.os != 'Windows' && (github.event_name != 'pull_request' || !matrix.set-gds-token)
|
||||
- name: Check Windows environment
|
||||
run: |
|
||||
echo "GRAALVM_HOME: $env:GRAALVM_HOME"
|
||||
echo "JAVA_HOME: $env:JAVA_HOME"
|
||||
java --version
|
||||
native-image --version
|
||||
test-ce: # make sure the action works on a clean machine without building
|
||||
needs: test
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
test-action-ce: # make sure the action works on a clean machine without building
|
||||
needs: test-action
|
||||
name: CE ${{ matrix.version }} + JDK${{ matrix.java-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -119,7 +136,7 @@ jobs:
|
||||
- version: '22.3.1'
|
||||
java-version: '11' # for JDK 11 notification
|
||||
components: 'native-image'
|
||||
os: macos-12
|
||||
os: macos-13
|
||||
- version: '22.3.1'
|
||||
java-version: '17'
|
||||
components: 'native-image'
|
||||
@@ -161,8 +178,9 @@ jobs:
|
||||
native-image --version
|
||||
gu.cmd remove native-image
|
||||
if: runner.os == 'Windows'
|
||||
test-ee:
|
||||
needs: test
|
||||
|
||||
test-action-ee:
|
||||
needs: test-action
|
||||
name: EE ${{ matrix.version }} + JDK${{ matrix.java-version }} on ${{ matrix.os }}
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -209,8 +227,9 @@ jobs:
|
||||
native-image --version
|
||||
gu.cmd remove native-image
|
||||
if: runner.os == 'Windows'
|
||||
test-mandrel:
|
||||
needs: test
|
||||
|
||||
test-action-mandrel:
|
||||
needs: test-action
|
||||
name: ${{ matrix.version }} + JDK${{ matrix.java-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -253,8 +272,9 @@ jobs:
|
||||
java --version
|
||||
native-image --version
|
||||
if: runner.os == 'Windows'
|
||||
test-liberica:
|
||||
needs: test
|
||||
|
||||
test-action-liberica:
|
||||
needs: test-action
|
||||
name: Liberica (${{ matrix.java-version }}, '${{ matrix.java-package }}', ${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -295,7 +315,8 @@ jobs:
|
||||
exit 24
|
||||
}
|
||||
if: runner.os == 'Windows'
|
||||
test-native-image-windows:
|
||||
|
||||
test-action-native-image-windows:
|
||||
name: native-image on windows-latest
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
@@ -317,7 +338,8 @@ jobs:
|
||||
javac HelloWorld.java
|
||||
native-image HelloWorld
|
||||
./helloworld
|
||||
test-native-image-windows-msvc:
|
||||
|
||||
test-action-native-image-windows-msvc:
|
||||
name: native-image on windows-2022
|
||||
runs-on: windows-2022
|
||||
permissions:
|
||||
@@ -339,7 +361,8 @@ jobs:
|
||||
javac HelloWorld.java
|
||||
native-image HelloWorld
|
||||
./helloworld
|
||||
test-native-image-musl:
|
||||
|
||||
test-action-native-image-musl:
|
||||
name: native-image-musl on ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -362,7 +385,8 @@ jobs:
|
||||
javac HelloWorld.java
|
||||
native-image --static --libc=musl HelloWorld
|
||||
./helloworld
|
||||
test-extensive:
|
||||
|
||||
test-action-extensive:
|
||||
name: extensive tests on ubuntu-latest
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -417,3 +441,41 @@ jobs:
|
||||
# popd > /dev/null
|
||||
- name: Remove components
|
||||
run: gu remove espresso llvm-toolchain nodejs python ruby wasm
|
||||
|
||||
test-action-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'
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -96,4 +96,7 @@ Thumbs.db
|
||||
|
||||
# Ignore built ts files
|
||||
__tests__/runner/*
|
||||
lib/**/*
|
||||
lib/**/*
|
||||
|
||||
# Ignore target directory in __tests__
|
||||
__tests__/**/target
|
||||
|
||||
1
.node-version
Normal file
1
.node-version
Normal file
@@ -0,0 +1 @@
|
||||
20.9.0
|
||||
@@ -1,3 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
node_modules/
|
||||
README.md
|
||||
|
||||
16
.prettierrc.yml
Normal file
16
.prettierrc.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# See: https://prettier.io/docs/en/configuration
|
||||
|
||||
printWidth: 80
|
||||
tabWidth: 2
|
||||
useTabs: false
|
||||
semi: false
|
||||
singleQuote: true
|
||||
quoteProps: as-needed
|
||||
jsxSingleQuote: false
|
||||
trailingComma: none
|
||||
bracketSpacing: true
|
||||
bracketSameLine: true
|
||||
arrowParens: always
|
||||
proseWrap: always
|
||||
htmlWhitespaceSensitivity: css
|
||||
endOfLine: lf
|
||||
@@ -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`<br>`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],<br>`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. |
|
||||
|
||||
@@ -94,7 +94,7 @@ describe('dependency cache', () => {
|
||||
beforeEach(() => {
|
||||
spyCacheRestore = jest
|
||||
.spyOn(cache, 'restoreCache')
|
||||
.mockImplementation((paths: string[], primaryKey: string) =>
|
||||
.mockImplementation((_paths: string[], _primaryKey: string) =>
|
||||
Promise.resolve(undefined)
|
||||
)
|
||||
spyWarning.mockImplementation(() => null)
|
||||
@@ -184,7 +184,7 @@ describe('dependency cache', () => {
|
||||
beforeEach(() => {
|
||||
spyCacheSave = jest
|
||||
.spyOn(cache, 'saveCache')
|
||||
.mockImplementation((paths: string[], key: string) =>
|
||||
.mockImplementation((_paths: string[], _key: string) =>
|
||||
Promise.resolve(0)
|
||||
)
|
||||
spyWarning.mockImplementation(() => null)
|
||||
|
||||
@@ -35,7 +35,6 @@ describe('cleanup', () => {
|
||||
ReturnType<typeof cache.saveCache>,
|
||||
Parameters<typeof cache.saveCache>
|
||||
>
|
||||
let spyJobStatusSuccess: jest.SpyInstance
|
||||
|
||||
beforeEach(() => {
|
||||
spyWarning = jest.spyOn(core, 'warning')
|
||||
@@ -49,8 +48,8 @@ describe('cleanup', () => {
|
||||
resetState()
|
||||
})
|
||||
|
||||
it('does not fail nor warn even when the save provess throws a ReserveCacheError', async () => {
|
||||
spyCacheSave.mockImplementation((paths: string[], key: string) =>
|
||||
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(
|
||||
'Unable to reserve cache with key, another job may be creating this cache.'
|
||||
@@ -66,7 +65,7 @@ describe('cleanup', () => {
|
||||
})
|
||||
|
||||
it('does not fail even though the save process throws error', async () => {
|
||||
spyCacheSave.mockImplementation((paths: string[], key: string) =>
|
||||
spyCacheSave.mockImplementation((_paths: string[], _key: string) =>
|
||||
Promise.reject(new Error('Unexpected error'))
|
||||
)
|
||||
jest.spyOn(core, 'getInput').mockImplementation((name: string) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ process.env['RUNNER_TOOL_CACHE'] = path.join(__dirname, 'TOOL_CACHE')
|
||||
process.env['RUNNER_TEMP'] = path.join(__dirname, 'TEMP')
|
||||
|
||||
test('request invalid version/javaVersion', async () => {
|
||||
for (var combination of [
|
||||
for (const combination of [
|
||||
['22.3.0', '7'],
|
||||
['22.3', '17'],
|
||||
['22.3', '7']
|
||||
@@ -23,7 +23,7 @@ test('request invalid version/javaVersion', async () => {
|
||||
await graalvm.setUpGraalVMRelease('', combination[0], combination[1])
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
@@ -36,17 +36,17 @@ test('request invalid version/javaVersion', async () => {
|
||||
|
||||
test('find version/javaVersion', async () => {
|
||||
// Make sure the action can find the latest Java version for known major versions
|
||||
for (var majorJavaVersion of ['17', '20']) {
|
||||
for (const majorJavaVersion of ['17', '20']) {
|
||||
await graalvm.findLatestGraalVMJDKCEJavaVersion(majorJavaVersion)
|
||||
}
|
||||
|
||||
let error = new Error('unexpected')
|
||||
try {
|
||||
await graalvm.findLatestGraalVMJDKCEJavaVersion('11')
|
||||
fail('Should not find Java version for 11')
|
||||
throw new Error('Should not find Java version for 11')
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
@@ -68,7 +68,7 @@ test('find version/javaVersion', async () => {
|
||||
findGraalVMVersion(invalidRelease)
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
@@ -78,17 +78,17 @@ test('find version/javaVersion', async () => {
|
||||
findHighestJavaVersion(latestRelease, 'invalid')
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
expect(error.message).toContain('Could not find highest Java version.')
|
||||
})
|
||||
|
||||
test('find version/javaVersion', async () => {
|
||||
let url22EA = await findLatestEABuildDownloadUrl('22-ea')
|
||||
test('find EA version/javaVersion', async () => {
|
||||
const url22EA = await findLatestEABuildDownloadUrl('22-ea')
|
||||
expect(url22EA).not.toBe('')
|
||||
let urlLatestEA = await findLatestEABuildDownloadUrl('latest-ea')
|
||||
const urlLatestEA = await findLatestEABuildDownloadUrl('latest-ea')
|
||||
expect(urlLatestEA).not.toBe('')
|
||||
|
||||
let error = new Error('unexpected')
|
||||
@@ -96,7 +96,7 @@ test('find version/javaVersion', async () => {
|
||||
await findLatestEABuildDownloadUrl('8-ea')
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {expect, test} from '@jest/globals'
|
||||
process.env['RUNNER_TOOL_CACHE'] = path.join(__dirname, 'TOOL_CACHE')
|
||||
process.env['RUNNER_TEMP'] = path.join(__dirname, 'TEMP')
|
||||
|
||||
/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectLatestToBe", "expectURL"] }] */
|
||||
|
||||
test('find latest JDK version', async () => {
|
||||
// Make sure the action can find the latest Java version for known major versions
|
||||
await expectLatestToBe('11', atLeast('11.0.22+12'))
|
||||
@@ -61,8 +63,8 @@ function atLeast(expectedMinVersion: string): verifier {
|
||||
return function (
|
||||
version: string,
|
||||
major: number,
|
||||
minor: number,
|
||||
patch: number
|
||||
_minor: number,
|
||||
_patch: number
|
||||
) {
|
||||
expect(major).toBe(expectedMajor)
|
||||
if (semver.compareBuild(version, expectedMinVersion) < 0) {
|
||||
@@ -90,9 +92,9 @@ function upToBuild(expectedMinVersion: string): verifier {
|
||||
function exactly(expectedVersion: string): verifier {
|
||||
return function (
|
||||
version: string,
|
||||
major: number,
|
||||
minor: number,
|
||||
patch: number
|
||||
_major: number,
|
||||
_minor: number,
|
||||
_patch: number
|
||||
) {
|
||||
if (semver.compareBuild(version, expectedVersion) != 0) {
|
||||
throw new Error(`Expected version ${expectedVersion} but got ${version}`)
|
||||
|
||||
@@ -7,7 +7,7 @@ process.env['RUNNER_TOOL_CACHE'] = path.join(__dirname, 'TOOL_CACHE')
|
||||
process.env['RUNNER_TEMP'] = path.join(__dirname, 'TEMP')
|
||||
|
||||
test('request invalid version/javaVersion combination', async () => {
|
||||
for (var combination of [
|
||||
for (const combination of [
|
||||
['mandrel-23.1.1.0-Final', '17'],
|
||||
['mandrel-23.0.2.1-Final', '21']
|
||||
]) {
|
||||
@@ -16,7 +16,7 @@ test('request invalid version/javaVersion combination', async () => {
|
||||
await mandrel.setUpMandrel(combination[0], combination[1])
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
@@ -27,7 +27,7 @@ test('request invalid version/javaVersion combination', async () => {
|
||||
}
|
||||
})
|
||||
test('request invalid version', async () => {
|
||||
for (var combination of [
|
||||
for (const combination of [
|
||||
['mandrel-23.1.1.0', '21'],
|
||||
['mandrel-23.0.2.1', '17']
|
||||
]) {
|
||||
@@ -36,7 +36,7 @@ test('request invalid version', async () => {
|
||||
await mandrel.setUpMandrel(combination[0], combination[1])
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
@@ -56,7 +56,7 @@ test('find latest', async () => {
|
||||
|
||||
test('get known latest Mandrel for specific JDK', async () => {
|
||||
// Test deprecated versions that won't get updates anymore
|
||||
for (var combination of [
|
||||
for (const combination of [
|
||||
['11', '22.2.0.0-Final'],
|
||||
['20', '23.0.1.2-Final']
|
||||
]) {
|
||||
@@ -68,7 +68,7 @@ test('get known latest Mandrel for specific JDK', async () => {
|
||||
|
||||
test('get latest Mandrel for specific JDK', async () => {
|
||||
// Test supported versions
|
||||
for (var javaVersion of ['17', '21']) {
|
||||
for (const javaVersion of ['17', '21']) {
|
||||
const latest = await mandrel.getLatestMandrelReleaseUrl(javaVersion)
|
||||
expect(latest).toContain(`mandrel-java${javaVersion}`)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ process.env['RUNNER_TOOL_CACHE'] = path.join(__dirname, 'TOOL_CACHE')
|
||||
process.env['RUNNER_TEMP'] = path.join(__dirname, 'TEMP')
|
||||
|
||||
test('decide whether Window env must be set up for GraalVM for JDK', async () => {
|
||||
for (var javaVersion of [
|
||||
for (const javaVersion of [
|
||||
'17',
|
||||
'17.0.8',
|
||||
'17.0',
|
||||
@@ -22,7 +22,7 @@ test('decide whether Window env must be set up for GraalVM for JDK', async () =>
|
||||
})
|
||||
|
||||
test('decide whether Window env must be set up for legacy GraalVM', async () => {
|
||||
for (var combination of [
|
||||
for (const combination of [
|
||||
['7', '22.3.0'],
|
||||
['17', '22.3'],
|
||||
['7', '22.3'],
|
||||
|
||||
309
__tests__/sbom.test.ts
Normal file
309
__tests__/sbom.test.ts
Normal file
@@ -0,0 +1,309 @@
|
||||
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<void, Parameters<typeof core.info>>
|
||||
let spyWarning: jest.SpyInstance<void, Parameters<typeof core.warning>>
|
||||
let spyExportVariable: jest.SpyInstance<
|
||||
void,
|
||||
Parameters<typeof core.exportVariable>
|
||||
>
|
||||
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<void> {
|
||||
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': {}
|
||||
}
|
||||
let error
|
||||
try {
|
||||
await setUpAndProcessSBOM(invalidSBOM)
|
||||
throw new Error('Expected an error since invalid JSON was passed')
|
||||
} catch (e) {
|
||||
error = e
|
||||
} finally {
|
||||
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
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
54
__tests__/sbom/main-test-app/pom.xml
Normal file
54
__tests__/sbom/main-test-app/pom.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.oracle</groupId>
|
||||
<artifactId>main-test-app</artifactId>
|
||||
<version>1.0.0</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20241224</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
<version>0.10.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>compile-no-fork</goal>
|
||||
</goals>
|
||||
<phase>package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<mainClass>com.oracle.sbom.SBOMTestApplication</mainClass>
|
||||
<buildArgs>
|
||||
<buildArg>-Ob</buildArg>
|
||||
<buildArg>--no-fallback</buildArg>
|
||||
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
|
||||
</buildArgs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
14
__tests__/sbom/main-test-app/verify-sbom.cmd
Normal file
14
__tests__/sbom/main-test-app/verify-sbom.cmd
Normal file
@@ -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
|
||||
19
__tests__/sbom/main-test-app/verify-sbom.sh
Normal file
19
__tests__/sbom/main-test-app/verify-sbom.sh
Normal file
@@ -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"
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as path from 'path'
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {toSemVer} from '../src/utils'
|
||||
|
||||
test('convert version', async () => {
|
||||
for (var inputAndExpectedOutput of [
|
||||
for (const inputAndExpectedOutput of [
|
||||
['22', '22.0.0'],
|
||||
['22.0', '22.0.0'],
|
||||
['22.0.0', '22.0.0'],
|
||||
@@ -17,13 +16,13 @@ test('convert version', async () => {
|
||||
})
|
||||
|
||||
test('convert invalid version', async () => {
|
||||
for (var input of ['dev', 'abc', 'a.b.c']) {
|
||||
for (const input of ['dev', 'abc', 'a.b.c']) {
|
||||
let error = new Error('unexpected')
|
||||
try {
|
||||
toSemVer(input)
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
fail(`Unexpected non-Error: ${err}`)
|
||||
throw new Error(`Unexpected non-Error: ${err}`)
|
||||
}
|
||||
error = err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ name: 'GitHub Action for GraalVM'
|
||||
description: 'Set up a specific version of the GraalVM JDK and add the command-line tools to the PATH'
|
||||
author: 'GraalVM Community'
|
||||
branding:
|
||||
icon: 'terminal'
|
||||
icon: 'terminal'
|
||||
color: 'blue'
|
||||
inputs:
|
||||
java-version:
|
||||
@@ -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).'
|
||||
|
||||
78240
dist/cleanup/index.js
generated
vendored
78240
dist/cleanup/index.js
generated
vendored
File diff suppressed because one or more lines are too long
81174
dist/main/index.js
generated
vendored
81174
dist/main/index.js
generated
vendored
File diff suppressed because one or more lines are too long
82
eslint.config.mjs
Normal file
82
eslint.config.mjs
Normal file
@@ -0,0 +1,82 @@
|
||||
// See: https://eslint.org/docs/latest/use/configure/configuration-files
|
||||
|
||||
import {fixupPluginRules} from '@eslint/compat'
|
||||
import {FlatCompat} from '@eslint/eslintrc'
|
||||
import js from '@eslint/js'
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin'
|
||||
import tsParser from '@typescript-eslint/parser'
|
||||
import _import from 'eslint-plugin-import'
|
||||
import jest from 'eslint-plugin-jest'
|
||||
import prettier from 'eslint-plugin-prettier'
|
||||
import globals from 'globals'
|
||||
import path from 'node:path'
|
||||
import {fileURLToPath} from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
})
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ['**/coverage', '**/dist', '**/linter', '**/node_modules']
|
||||
},
|
||||
...compat.extends(
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:jest/recommended',
|
||||
'plugin:prettier/recommended'
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
import: fixupPluginRules(_import),
|
||||
jest,
|
||||
prettier,
|
||||
'@typescript-eslint': typescriptEslint
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly'
|
||||
},
|
||||
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2023,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: ['tsconfig.eslint.json'],
|
||||
tsconfigRootDir: '.'
|
||||
}
|
||||
},
|
||||
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
project: 'tsconfig.eslint.json'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
rules: {
|
||||
camelcase: 'off',
|
||||
'eslint-comments/no-use': 'off',
|
||||
'eslint-comments/no-unused-disable': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}],
|
||||
'i18n-text/no-en': 'off',
|
||||
'import/no-namespace': 'off',
|
||||
'no-console': 'off',
|
||||
'no-shadow': 'off',
|
||||
'no-unused-vars': 'off',
|
||||
'prettier/prettier': 'error'
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -6,4 +6,4 @@ module.exports = {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
}
|
||||
|
||||
4260
package-lock.json
generated
4260
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -1,18 +1,17 @@
|
||||
{
|
||||
"name": "setup-graalvm",
|
||||
"version": "1.2.4",
|
||||
"version": "1.2.8",
|
||||
"private": true,
|
||||
"description": "GitHub Action for GraalVM",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"bundle": "npm run format:write && npm run package",
|
||||
"format:write": "npx prettier --write .",
|
||||
"format:check": "npx prettier --check .",
|
||||
"lint": "npx eslint .",
|
||||
"package": "ncc build -o dist/main src/main.ts && ncc build -o dist/cleanup src/cleanup.ts",
|
||||
"test": "jest",
|
||||
"all-but-test": "npm run build && npm run format && npm run lint && npm run package",
|
||||
"all": "npm run all-but-test && npm test"
|
||||
"test": "npx jest",
|
||||
"all": "npm run format:write && npm run lint && npm run test && npm run package"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,37 +26,42 @@
|
||||
"author": "GraalVM Community",
|
||||
"license": "UPL",
|
||||
"dependencies": {
|
||||
"@actions/cache": "^3.2.4",
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/cache": "^4.0.0",
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@actions/glob": "^0.4.0",
|
||||
"@actions/http-client": "^2.2.1",
|
||||
"@actions/glob": "^0.5.0",
|
||||
"@actions/http-client": "^2.2.3",
|
||||
"@actions/io": "^1.1.3",
|
||||
"@actions/tool-cache": "^2.0.1",
|
||||
"@octokit/core": "^5.1.0",
|
||||
"@actions/tool-cache": "^2.0.2",
|
||||
"@octokit/core": "^5.2.0",
|
||||
"@octokit/types": "^12.6.0",
|
||||
"semver": "^7.6.0",
|
||||
"uuid": "^9.0.1"
|
||||
"@github/dependency-submission-toolkit": "^2.0.4",
|
||||
"semver": "^7.6.3",
|
||||
"uuid": "^11.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.11.28",
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.17.12",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-github": "^4.10.2",
|
||||
"eslint-plugin-jest": "^27.9.0",
|
||||
"eslint-plugin-jsonc": "^2.14.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||
"@typescript-eslint/parser": "^8.19.1",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^28.10.0",
|
||||
"eslint-plugin-jsonc": "^2.18.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"jest": "^29.7.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-eslint": "^16.3.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.3.3"
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -45,7 +46,6 @@ async function saveCache(): Promise<void> {
|
||||
* @returns Promise that will ignore error reported by the given promise
|
||||
*/
|
||||
async function ignoreErrors(promise: Promise<void>): Promise<unknown> {
|
||||
/* eslint-disable github/no-then */
|
||||
return new Promise(resolve => {
|
||||
promise
|
||||
.catch(error => {
|
||||
@@ -58,6 +58,7 @@ async function ignoreErrors(promise: Promise<void>): Promise<unknown> {
|
||||
|
||||
export async function run(): Promise<void> {
|
||||
await ignoreErrors(generateReports())
|
||||
await ignoreErrors(processSBOM())
|
||||
await ignoreErrors(saveCache())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as otypes from '@octokit/types'
|
||||
|
||||
export const ACTION_VERSION = '1.2.4'
|
||||
export const ACTION_VERSION = '1.2.8'
|
||||
|
||||
export const INPUT_VERSION = 'version'
|
||||
export const INPUT_GDS_TOKEN = 'gds-token'
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
300
src/features/sbom.ts
Normal file
300
src/features/sbom.ts
Normal file
@@ -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<string, string>
|
||||
// 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<void> {
|
||||
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<string> {
|
||||
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<string, {package_url: string; dependencies?: string[]}> {
|
||||
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<void> {
|
||||
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)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ export async function findLatestEABuildDownloadUrl(
|
||||
response = await getContents(ORACLE_GRAALVM_REPO_EA_BUILDS, filePath)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Unable to resolve download URL for '${javaEaVersion}'. Please make sure the java-version is set correctly. ${c.ERROR_HINT}`
|
||||
`Unable to resolve download URL for '${javaEaVersion}' (reason: ${error}). Please make sure the java-version is set correctly. ${c.ERROR_HINT}`
|
||||
)
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -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<void> {
|
||||
try {
|
||||
@@ -148,7 +149,6 @@ async function run(): Promise<void> {
|
||||
if (setJavaHome) {
|
||||
core.exportVariable('JAVA_HOME', graalVMHome)
|
||||
}
|
||||
|
||||
await setUpGUComponents(
|
||||
javaVersion,
|
||||
graalVMVersion,
|
||||
@@ -165,6 +165,7 @@ async function run(): Promise<void> {
|
||||
javaVersion,
|
||||
graalVMVersion
|
||||
)
|
||||
setUpSBOMSupport(javaVersion, distribution)
|
||||
|
||||
core.startGroup(`Successfully set up '${basename(graalVMHome)}'`)
|
||||
await exec(join(graalVMHome, 'bin', `java${c.EXECUTABLE_SUFFIX}`), [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as c from './constants'
|
||||
import * as httpClient from '@actions/http-client'
|
||||
import {downloadExtractAndCacheJDK, getLatestRelease} from './utils'
|
||||
import {downloadExtractAndCacheJDK} from './utils'
|
||||
import {downloadTool} from '@actions/tool-cache'
|
||||
import {basename} from 'path'
|
||||
|
||||
@@ -11,7 +11,9 @@ const DISCO_API_BASE = 'https://api.foojay.io/disco/v3.0/packages/jdks'
|
||||
|
||||
interface JdkData {
|
||||
message: string
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
result: any
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
}
|
||||
|
||||
export async function setUpMandrel(
|
||||
@@ -22,7 +24,9 @@ export async function setUpMandrel(
|
||||
let mandrelHome
|
||||
switch (version) {
|
||||
case '':
|
||||
// fetch latest if no version is specified
|
||||
// fetch latest if no version is specified
|
||||
mandrelHome = await setUpMandrelLatest(javaVersion)
|
||||
break
|
||||
case 'latest':
|
||||
mandrelHome = await setUpMandrelLatest(javaVersion)
|
||||
break
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as semver from 'semver'
|
||||
import {execSync} from 'child_process'
|
||||
import {existsSync} from 'fs'
|
||||
import {VERSION_DEV} from './constants'
|
||||
|
||||
46
src/utils.ts
46
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<void> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
23
tsconfig.base.json
Normal file
23
tsconfig.base.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["ES2022"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"newLine": "lf",
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": false,
|
||||
"pretty": true,
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
16
tsconfig.eslint.json
Normal file
16
tsconfig.eslint.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"include": [
|
||||
"__fixtures__",
|
||||
"__tests__",
|
||||
"src",
|
||||
"eslint.config.mjs",
|
||||
"jest.config.js"
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
"exclude": ["__fixtures__", "__tests__", "coverage", "dist", "node_modules"],
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user