Skip to content

ci: enable SBOMs (#1748) #1930

ci: enable SBOMs (#1748)

ci: enable SBOMs (#1748) #1930

# Jobs in this workflow deal with secrets.
# Since they may be executed from forks by untrusted users,
# we need to ensure that the user is a member of the organization
# or that there is explicit approval for their jobs to run.
name: Secured Workflow
on:
push:
branches: [ main ]
# There are two differences to "pull_request" here:
# - The workflow will receive secrets, even in PRs from forks.
# - The workflow will be executed automatically, without requiring a manual approval.
# Therefore the workflow needs to be explicitly secured; see "known_user" and "approval_required" jobs below.
pull_request_target:
branches: [ main ] # Benchmarks aren't branched, so they will only ever work against current main.
types:
- opened
- reopened
- synchronize
paths-ignore:
- 'LICENSE*'
- '.gitignore'
- '**.md'
- '*.txt'
jobs:
# Check if the user is a member of the organization; if so, allow the PR to sail through.
known_user:
runs-on: ubuntu-latest
outputs:
is_member_of_org: ${{ steps.auth_check.outputs.authorized }}
steps:
- id: auth_check
env:
GH_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Release account is a Solver Gatekeeper.
shell: bash
run: |
# -g to allow actors such as dependabot[bot]
ORG_MEMBERSHIP=`curl -g -L -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GH_TOKEN" "https://api.github.com/orgs/TimefoldAI/memberships/${{ github.actor }}" | jq -r '.state == "active"'`
echo "authorized=$ORG_MEMBERSHIP" >> "$GITHUB_OUTPUT"
- id: validation
shell: bash
run: |
echo "Authorized user: ${{ steps.auth_check.outputs.authorized }}"
# If the user is not a member, require a member to approve the PR.
approval_required:
needs: known_user
environment:
${{
github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
(needs.known_user.outputs.is_member_of_org != 'true' || github.actor == 'dependabot[bot]') &&
'external' || 'internal'
}}
runs-on: ubuntu-latest
steps:
- run: true
integration-tests:
needs: approval_required
name: Integration Tests
runs-on: ubuntu-latest
concurrency:
group: pr-${{ github.event_name }}-${{ github.head_ref }}
cancel-in-progress: true
steps:
# Clone timefold-solver
# No need to check for stale repo, as Github merges the main repo into the fork automatically.
- name: Checkout timefold-solver
uses: actions/checkout@v5
with:
path: ./timefold-solver
ref: ${{ github.event.pull_request.head.sha }} # The GHA event will pull the main branch by default, and we must specify the PR reference version
- name: Setup Temurin 17 and Maven
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: Quickly build timefold-solver
working-directory: ./timefold-solver
shell: bash
run: mvn -B -Dquickly clean install
# Clone timefold-solver-enterprise
- name: Checkout timefold-solver-enterprise (PR) # Checkout the PR branch first, if it exists
id: checkout-solver-enterprise
uses: actions/checkout@v5
continue-on-error: true
with:
repository: TimefoldAI/timefold-solver-enterprise
ref: ${{ github.head_ref }}
token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork.
path: ./timefold-solver-enterprise
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
- name: Checkout timefold-solver-enterprise (main) # Checkout the main branch if the PR branch does not exist
if: steps.checkout-solver-enterprise.outcome != 'success'
uses: actions/checkout@v5
with:
repository: TimefoldAI/timefold-solver-enterprise
ref: main
token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork.
path: ./timefold-solver-enterprise
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
- name: Quickly build timefold-solver-enterprise
working-directory: ./timefold-solver-enterprise
shell: bash
run: mvn -B -Dquickly clean install
# Clone timefold-solver-benchmarks
- name: Checkout timefold-solver-benchmarks (PR) # Checkout the PR branch first, if it exists
if: github.head_ref # Only true if this is a PR.
id: checkout-solver-benchmarks-pr
uses: actions/checkout@v5
continue-on-error: true
with:
repository: TimefoldAI/timefold-solver-benchmarks
ref: ${{ github.head_ref }}
path: ./timefold-solver-benchmarks
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
- name: Checkout timefold-solver-benchmarks (main) # Checkout the main branch if the PR branch does not exist
if: ${{ steps.checkout-solver-benchmarks-pr.outcome != 'success' }}
uses: actions/checkout@v5
with:
repository: TimefoldAI/timefold-solver-benchmarks
ref: main
path: ./timefold-solver-benchmarks
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
- name: Build and test timefold-solver-benchmarks
working-directory: ./timefold-solver-benchmarks
shell: bash
run: mvn -B -DskipJMH clean verify
enterprise-java:
needs: approval_required
name: Enterprise Edition (Java)
runs-on: ubuntu-latest
concurrency:
group: downstream-enterprise-${{ github.event_name }}-${{ github.head_ref }}
cancel-in-progress: true
timeout-minutes: 120
steps:
# Clone timefold-solver
# No need to check for stale repo, as Github merges the main repo into the fork automatically.
- name: Checkout timefold-solver
uses: actions/checkout@v5
with:
path: ./timefold-solver
ref: ${{ github.event.pull_request.head.sha }} # The GHA event will pull the main branch by default, and we must specify the PR reference version
# Clone timefold-solver-enterprise
# Need to check for stale repo, since Github is not aware of the build chain and therefore doesn't automate it.
- name: Checkout timefold-solver-enterprise (PR) # Checkout the PR branch first, if it exists
id: checkout-solver-enterprise
uses: actions/checkout@v5
continue-on-error: true
with:
repository: TimefoldAI/timefold-solver-enterprise
ref: ${{ github.head_ref }}
token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork.
path: ./timefold-solver-enterprise
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
- name: Checkout timefold-solver-enterprise (main) # Checkout the main branch if the PR branch does not exist
if: steps.checkout-solver-enterprise.outcome != 'success'
uses: actions/checkout@v5
with:
repository: TimefoldAI/timefold-solver-enterprise
ref: main
token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork.
path: ./timefold-solver-enterprise
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
# Build and test
- name: Setup Temurin 17 and Maven
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Quickly build timefold-solver
working-directory: ./timefold-solver
shell: bash
run: mvn -B -Dquickly clean install
- name: Build and test timefold-solver-enterprise
working-directory: ./timefold-solver-enterprise
shell: bash
run: mvn -B clean verify
build_documentation:
runs-on: ubuntu-latest
needs: approval_required
name: Build Documentation
environment:
name: "documentation (preview)"
url: ${{ steps.deploy.outputs.deployment-url }}
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
steps:
- name: Checkout frontend
id: checkout-frontend
uses: actions/checkout@v5
with:
repository: TimefoldAI/frontend
token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} # Safe; only used to clone the repo and not stored in the fork.
fetch-depth: 0 # Otherwise merge will fail on account of not having history.
- name: Install pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda
- name: Set up NodeJs
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- name: Checkout timefold-solver
uses: actions/checkout@v5
with:
repository: "${{ github.event.pull_request.head.repo.owner.login || 'TimefoldAI' }}/timefold-solver"
ref: ${{ github.event.pull_request.head.sha || 'main' }} # The GHA event will pull the main branch by default, and we must specify the PR reference version
path: "./timefold-solver"
fetch-depth: 0
- name: Install yq
run: |
sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq
sudo chmod +x /usr/bin/yq
- name: Updating Antora configuration
working-directory: "./timefold-solver"
run: |
echo "=== Updating antora.yml"
cp docs/src/antora-template.yml docs/src/antora.yml
sed -i "s/\${project\.version}b0/SNAPSHOT/g" docs/src/antora.yml
sed -i "s/\${project\.version}/SNAPSHOT/g" docs/src/antora.yml
sed -i "s/\${maven\.compiler\.release}/$(find build/build-parent/ -name pom.xml -exec grep '<maven.compiler.release>' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml
sed -i "s/\${maven\.min\.version}/$(find build/build-parent/ -name pom.xml -exec grep '<maven.min.version>' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml
sed -i "s/\${version\.io\.quarkus}/$(find build/build-parent/ -name pom.xml -exec grep '<version.io.quarkus>' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml
sed -i "s/\${version\.org\.springframework\.boot}/$(find build/build-parent/ -name pom.xml -exec grep '<version.org.springframework.boot>' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml
sed -i "s/\${version\.ch\.qos\.logback}/$(find build/build-parent/ -name pom.xml -exec grep '<version.ch.qos.logback>' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml
sed -i "s/\${version\.exec\.plugin}/$(find build/build-parent/ -name pom.xml -exec grep '<version.exec.plugin>' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml
sed -i "s/\${version\.rewrite\.plugin}/$(find . -name pom.xml -exec grep '<version.rewrite.plugin>' {} \;|tail -n 1|cut -d\> -f1 --complement|cut -d\< -f1)/g" docs/src/antora.yml
cat docs/src/antora.yml
- name: Build Documentation
working-directory: "./"
env:
GIT_CREDENTIALS: ${{ secrets.GIT_CREDENTIALS }}
run: |
yq -i e 'del(.content.sources)' apps/docs/antora-playbook.yml
yq -i e 'del(.site.keys)' apps/docs/antora-playbook.yml
yq -i e '.content.sources += [{"url": "../../timefold-solver", "start_path": "docs/src"}]' apps/docs/antora-playbook.yml
pnpm install --frozen-lockfile
pnpm build --filter @timefoldai/docs
- name: Deploy Documentation (Preview Mode)
if: ${{ env.BRANCH_NAME != 'main' }}
id: deploy
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
workingDirectory: ./apps/docs
command: pages deploy ./public-serve --project-name=timefold-docs --branch=${{ github.ref }}
packageManager: pnpm
sonarcloud:
needs: approval_required
name: SonarCloud
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
ref: ${{ github.event.pull_request.head.sha }} # The GHA event will pull the main branch by default, and we must specify the PR reference version
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'temurin'
cache: 'maven'
- name: Cache SonarCloud packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
restore-keys: ${{ runner.os }}-sonar
- name: Build with Maven to measure code coverage # The ENV variables are limited to the scope of the current step. Avoid adding sensitive ENV variables here as the tests could leak them.
run: mvn -B clean install -Prun-code-coverage
- name: Run analysis
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Needed to run the SonarCloud analysis
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
PR_SHA: ${{ github.event.pull_request.head.sha }}
run: mvn -B -Psonarcloud-analysis validate org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.organization=timefold -Dsonar.projectKey=ai.timefold:timefold-solver -Dsonar.host.url=https://sonarcloud.io -Dsonar.pullrequest.key="$PR_NUMBER" -Dsonar.pullrequest.branch="$PR_BRANCH" -Dsonar.scm.revision="$PR_SHA"