Skip to content

Spec File Update Workflow: orchestration-feat/granite-guardian-4-1 #1761

Spec File Update Workflow: orchestration-feat/granite-guardian-4-1

Spec File Update Workflow: orchestration-feat/granite-guardian-4-1 #1761

Workflow file for this run

name: 'Spec File Update Workflow'
run-name: 'Spec File Update Workflow: ${{ github.event.inputs.service }}-${{ github.event.inputs.ref }}'
# Notes:
# To use this workflow as a service owner:
# 1. Ask an SAP Cloud SDK for AI team member to add you to the service-owner team (read access is enough).
# 2. Generate a fine-grained PAT with:
# - repo access: SAP/ai-sdk-js and SAP/ai-sdk-java (you have to select SAP as the resource owner to see these repos)
# - permissions: Actions - read and write
#. - token expiration: up to you, you are responsible for rotation
# 3. Save the PAT as a secret under `PUBLIC_GITHUB_SDKS_PAT`.
# 4. Ask an SAP Cloud SDK for AI team member to create the sdk-integration and sdk-integration-cleanup workflows.
on:
workflow_dispatch:
inputs:
service:
description: 'Which spec file to update'
type: choice
required: false
options:
- core
- document-grounding
- orchestration
- prompt-registry
- sap-rpt
- batch
default: orchestration
ref:
description: 'Branch or tag to checkout the spec file from'
required: false
default: main
type: string
create-pr:
description: 'Create a pull request after updating the spec file'
required: false
default: true
type: boolean
is-weekly:
description: 'Flag to signal this is triggered by the weekly update run'
required: false
default: false
type: boolean
env:
MVN_MULTI_THREADED_ARGS: --batch-mode --no-transfer-progress --fail-at-end --show-version --threads 1C
JAVA_VERSION: 21
# keep the following two variables in sync with our 'cache-maven-dependencies.yaml' workflow
MAVEN_CACHE_KEY: maven-dependencies
MAVEN_CACHE_DIR: ~/.m2
jobs:
generate:
name: 'Download, Generate, Compile and Push'
runs-on: [ubuntu-latest]
permissions: {}
outputs:
spec_diff: ${{ steps.spec_diff.outputs.spec_diff }}
branch: ${{ steps.push.outputs.branch }}
pr_url: ${{ steps.create-pr.outputs.pr_url }}
compilation_result: ${{ steps.compile.outputs.compilation_result }}
test_result: ${{ steps.compile.outputs.test_result }}
api_compat_result: ${{ steps.api_compat.outputs.api_compat_result }}
env:
API_BASE_URL: 'https://github.tools.sap/api/v3/repos'
SERVICE: ${{ github.event.inputs.service }}
REF: ${{ github.event.inputs.ref }}
CREATE_PR: ${{ github.event.inputs.create-pr }}
steps:
- name: 'Generate GitHub App token'
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ secrets.SAP_AI_SDK_BOT_CLIENT_ID }}
private-key: ${{ secrets.SAP_AI_SDK_BOT_PRIVATE_KEY }}
owner: SAP
repositories: ai-sdk-java
permission-contents: write
permission-pull-requests: write
- name: 'Checkout repository'
uses: actions/checkout@v7
with:
token: ${{ steps.app-token.outputs.token }}
- name: 'Determine PR base'
id: pr_base
# If updating orchestration and orchestration-staging exists on remote, use it as PR base.
run: |
BASE=main
if [ "${{ env.SERVICE }}" = "orchestration" ] && [ -n "$(git ls-remote --heads origin orchestration-staging)" ]; then
BASE=orchestration-staging
fi
echo "PR base: $BASE"
echo "BASE=$BASE" >> "$GITHUB_OUTPUT"
- name: 'Setup Java'
uses: actions/setup-java@v5.7.0
with:
distribution: 'sapmachine'
java-version: ${{ env.JAVA_VERSION }}
cache: 'maven'
- name: 'Restore Dependencies'
id: restore-dependencies
uses: actions/cache/restore@v6
with:
key: ${{ env.MAVEN_CACHE_KEY }}
path: ${{ env.MAVEN_CACHE_DIR }}
- name: 'Checkout or Create Branch'
id: branch
env:
BASE: ${{ steps.pr_base.outputs.BASE }}
SERVICE: ${{ env.SERVICE }}
REF: ${{ env.REF }}
run: |
BRANCH="spec-update/$SERVICE/$REF"
# try to fetch the base and the target branch (ignore failures if missing)
git fetch --no-tags --depth=1 origin "$BASE" || true
git fetch origin "$BRANCH" || true
# if remote target branch exists, base the local branch on it
if git ls-remote --heads origin "$BRANCH" | grep -q "refs/heads/$BRANCH"; then
git checkout -B "$BRANCH" "origin/$BRANCH"
else
# otherwise, create the branch from origin/BASE if it exists, else from current HEAD
if git ls-remote --heads origin "$BASE" | grep -q "refs/heads/$BASE"; then
git checkout -B "$BRANCH" "origin/$BASE"
else
git checkout -B "$BRANCH"
fi
fi
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
- name: 'Download specification file'
id: download
env:
GH_ENTERPRISE_TOKEN: ${{ secrets.GH_TOOLS_TOKEN }}
run: |
case $SERVICE in
core)
API_URL="$API_BASE_URL/cloudsdk/cloud-sdk-java-tests/contents/aicore.yaml?ref=$REF"
FILE_PATH='core/src/main/resources/spec/aicore.yaml'
MODULE_PATH='core'
;;
document-grounding)
API_URL="$API_BASE_URL/AI/rage-proxy-inference/contents/docs/public/business_api_hub/api_hub_merged_spec.yaml?ref=$REF"
FILE_PATH='core-services/document-grounding/src/main/resources/spec/grounding.yaml'
MODULE_PATH='core-services/document-grounding'
;;
orchestration)
API_URL="$API_BASE_URL/AI/llm-orchestration/contents/src/spec/v2.yaml?ref=$REF"
FILE_PATH='orchestration/src/main/resources/spec/orchestration.yaml'
MODULE_PATH='orchestration'
;;
prompt-registry)
API_URL="$API_BASE_URL/AI/prompt-registry/contents/components/prompt-registry-api/src/spec/generated/prompt-registry-combined.yaml?ref=$REF"
FILE_PATH='core-services/prompt-registry/src/main/resources/spec/prompt-registry.yaml'
MODULE_PATH='core-services/prompt-registry'
;;
sap-rpt) # https://github.tools.sap/DL-COE/sap-rpt-1-public-content
API_URL="$API_BASE_URL/DL-COE/sap-rpt-1-public-content/contents/sap-rpt-1_openapi.json?ref=$REF"
FILE_PATH='foundation-models/sap-rpt/src/main/resources/spec/sap-rpt-1_openapi.json'
MODULE_PATH='foundation-models/sap-rpt'
;;
batch)
API_URL="$API_BASE_URL/AI/llm-batch-service/contents/components/llm-batch-service-api/src/spec/batch-service.yaml?ref=$REF"
FILE_PATH='core-services/batch/src/main/resources/spec/batch-service.yaml'
MODULE_PATH='core-services/batch'
;;
esac
echo "module_path=$MODULE_PATH" >> "$GITHUB_OUTPUT"
echo "Downloading $SERVICE specification file from $API_URL ..."
gh api "$API_URL" -H "Accept: application/vnd.github.raw" > $FILE_PATH
- name: 'Exit if there are no spec changes'
id: spec_diff
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
# if there are no spec changes in this commit and this is not run during the weekly update,
# check if the previous run on this branch is red. If it is red, exit 1; if it is green, exit 0.
if [[ `git status --porcelain` ]]; then
echo "Spec changes detected, continuing with generation."
echo "spec_diff=true" >> "$GITHUB_OUTPUT"
elif [[ "${{ github.event.inputs.is-weekly }}" == "true" ]]; then
echo "No spec changes detected during weekly spec update. This run succeeds."
echo "spec_diff=false" >> "$GITHUB_OUTPUT"
else
echo "No spec changes detected. Checking status of previous run."
echo "spec_diff=false" >> "$GITHUB_OUTPUT"
# The current run of this workflow
CURRENT_RUN=${{ github.run_id }}
# name must match the workflow run-name pattern above
RUN_NAME="Spec File Update Workflow: $SERVICE-$REF"
echo "Run name used for search: $RUN_NAME"
# Get the most recent completed run of this workflow with respect to the same feature branch
PREVIOUS_RUN=$(gh run list \
--workflow "${{ github.workflow }}" \
--branch main \
--json databaseId,status,conclusion,displayTitle \
--jq "map(select(.databaseId != ${CURRENT_RUN} and (.displayTitle | contains(\"${RUN_NAME}\")))) | .[0]")
echo "Previous run: $PREVIOUS_RUN"
if [ -n "$PREVIOUS_RUN" ] && [ "$PREVIOUS_RUN" != "null" ]; then
CONCLUSION=$(echo "$PREVIOUS_RUN" | jq -r '.conclusion')
echo "Previous run conclusion: $CONCLUSION"
if [ "$CONCLUSION" = "failure" ]; then
echo "Previous run failed and there were no spec changes since, thus failing this run as well."
echo "prev_run_success=false" >> "$GITHUB_OUTPUT"
else
echo "Previous run succeeded and there were no spec changes since, thus the current run succeeds as well."
echo "prev_run_success=true" >> "$GITHUB_OUTPUT"
fi
else
echo "There was no previous run, thus the current run succeeds."
echo "prev_run_success=true" >> "$GITHUB_OUTPUT"
fi
fi
- name: 'Build baseline JAR'
id: baseline
if: steps.spec_diff.outputs.spec_diff == 'true'
env:
MODULE_PATH: ${{ steps.download.outputs.module_path }}
BASE: ${{ steps.pr_base.outputs.BASE }}
run: |
# Extract $BASE branch source into a temporary directory
git fetch --no-tags --depth=1 origin "$BASE"
mkdir -p /tmp/api-baseline
git archive "origin/$BASE" | tar -x -C /tmp/api-baseline
# Build inside a subshell to avoid polluting the working directory
(
cd /tmp/api-baseline
mvn package -DskipTests -pl "$MODULE_PATH" -am ${{ env.MVN_MULTI_THREADED_ARGS }}
FINAL_NAME=$(mvn -q -pl "$MODULE_PATH" help:evaluate -Dexpression=project.build.finalName -DforceStdout)
cp "$MODULE_PATH/target/${FINAL_NAME}.jar" /tmp/baseline.jar
)
echo "Baseline JAR ready: $(ls -lh /tmp/baseline.jar)"
- name: 'Generate'
id: generate
if: steps.spec_diff.outputs.spec_diff == 'true'
run: |
if mvn install -DskipTests -Dgenerate ${{ env.MVN_MULTI_THREADED_ARGS }} ; then
echo "generation_result=success" >> "$GITHUB_OUTPUT"
else
echo "generation_result=failure" >> "$GITHUB_OUTPUT"
fi
- name: 'API compatibility check'
id: api_compat
if: >
steps.spec_diff.outputs.spec_diff == 'true' &&
steps.generate.outputs.generation_result == 'success'
env:
MODULE_PATH: ${{ steps.download.outputs.module_path }}
run: |
FINAL_NAME=$(mvn -q -pl "$MODULE_PATH" help:evaluate -Dexpression=project.build.finalName -DforceStdout)
CURRENT_JAR="$MODULE_PATH/target/${FINAL_NAME}.jar"
# Run japicmp: compare baseline ($BASE) JAR vs newly generated JAR
if mvn com.github.siom79.japicmp:japicmp-maven-plugin:cmp@api-compatibility \
-pl "$MODULE_PATH" \
-Djapicmp.oldVersion.file=/tmp/baseline.jar \
-Djapicmp.newVersion.file="$CURRENT_JAR" \
${{ env.MVN_MULTI_THREADED_ARGS }} ; then
echo "api_compat_result=compatible" >> "$GITHUB_OUTPUT"
else
echo "api_compat_result=incompatible" >> "$GITHUB_OUTPUT"
fi
- name: 'Upload japicmp report'
if: always() && steps.api_compat.outcome != 'skipped'
uses: actions/upload-artifact@v7
with:
name: japicmp-report
path: ${{ steps.download.outputs.module_path }}/target/japicmp/
if-no-files-found: ignore
- name: 'Compile and Test'
id: compile
if: steps.spec_diff.outputs.spec_diff == 'true'
# Compilation can easily fail e.g. from re-namings and has to be fixed manually.
# Thus, this action raises the PR anyway and only reports success or failure of compilation and testing.
run: |
if mvn test-compile ${{ env.MVN_MULTI_THREADED_ARGS }} ; then
echo "compilation_result=success" >> "$GITHUB_OUTPUT"
if mvn test ${{ env.MVN_MULTI_THREADED_ARGS }} ; then
echo "test_result=success" >> "$GITHUB_OUTPUT"
else
echo "test_result=failure" >> "$GITHUB_OUTPUT"
fi
else
echo "compilation_result=failure" >> "$GITHUB_OUTPUT"
echo "test_result=skipped" >> "$GITHUB_OUTPUT"
fi
- name: 'Push changes'
id: push
if: steps.spec_diff.outputs.spec_diff == 'true'
run: |
git config --global user.email "cloudsdk@sap.com"
git config --global user.name "SAP Cloud SDK Bot"
git add --all
git status
git commit -m "Update $SERVICE based on $REF"
git push --set-upstream origin ${{ steps.branch.outputs.branch }}
- name: 'Create PR'
id: create-pr
if: ${{ env.CREATE_PR == 'true' && steps.spec_diff.outputs.spec_diff == 'true'}}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
BRANCH: ${{ steps.branch.outputs.branch }}
BASE: ${{ steps.pr_base.outputs.BASE }}
run: |
if gh pr list --head $BRANCH --json number -q '.[].number' | grep -q .; then
echo "An open PR already exists for this branch. Skipping PR creation."
exit 0
fi
PR_URL=$(gh pr create --base $BASE --head $BRANCH --title "feat: [DevOps] Update $SERVICE specification" --body "
## Context
Update $SERVICE specification file based on $REF.
This PR was created automatically by the [spec-update workflow](https://github.com/SAP/ai-sdk-java/actions/workflows/spec-update.yaml).
You can commit on top of this branch, but as long as this PR is open the action can't be re-run.
- Compilation outcome: ${{ steps.compile.outputs.compilation_result }}
- Test run outcome: ${{ steps.compile.outputs.test_result }}
- API compatibility: ${{ steps.api_compat.outputs.api_compat_result || 'skipped' }}
Before merging, make sure to update tests and release notes, if necessary.
## Definition of Done
- [ ] (Optional) Unit tests cover new classes
- [ ] Release notes updated
- [ ] Documentation code samples updated
") && echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
- name: 'Generate Job Summary'
if: ${{ always() }}
env:
BRANCH: ${{ steps.branch.outputs.branch }}
PR_URL: ${{ steps.create-pr.outputs.pr_url }}
BASE: ${{ steps.pr_base.outputs.BASE }}
run: |
DIFF_URL="https://github.com/SAP/ai-sdk-java/compare/$BASE...$BRANCH"
echo "## Workflow Execution Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| File Download | ${{ steps.download.outcome == 'success' && '✅' || '❌' }} ${{ steps.download.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "| Spec File Changes | ${{ steps.spec_diff.outputs.spec_diff == 'true' && '🔄 Changes Detected' || '⏹️ No Changes' }}" >> $GITHUB_STEP_SUMMARY
if ${{ steps.spec_diff.outputs.spec_diff == 'false' }}; then
if ${{ github.event.inputs.is-weekly == 'true' }}; then
echo "| Weekly Run | ✅ No spec changes detected, run succeeds." >> $GITHUB_STEP_SUMMARY
else
echo "| Outcome Previous Run | ${{ steps.spec_diff.outputs.prev_run_success == 'true' && '✅ (Current run succeeds as a result.)' || '❌ (Current run fails as a result.)' }}" >> $GITHUB_STEP_SUMMARY
fi
fi
if ${{ steps.spec_diff.outputs.spec_diff == 'true' }}; then
echo "| Client Generation | ${{ steps.generate.outputs.generation_result == 'success' && '✅' || '❌' }} ${{ steps.generate.outputs.generation_result }}" >> $GITHUB_STEP_SUMMARY
echo "| Client Compilation | ${{ steps.compile.outputs.compilation_result == 'success' && '✅' || '❌' }} ${{ steps.compile.outputs.compilation_result }}" >> $GITHUB_STEP_SUMMARY
echo "| Client Testing | ${{ steps.compile.outputs.test_result == 'success' && '✅' || steps.compile.outputs.test_result == 'skipped' && '⏩' || '❌' }} ${{ steps.compile.outputs.test_result }}" >> $GITHUB_STEP_SUMMARY
echo "| API Compatibility | ${{ steps.api_compat.outputs.api_compat_result == 'compatible' && '✅' || steps.api_compat.outputs.api_compat_result == 'incompatible' && '❌' || '⏩' }} ${{ steps.api_compat.outputs.api_compat_result || 'skipped' }}" >> $GITHUB_STEP_SUMMARY
echo "| Branch Creation | ${{ steps.push.outcome == 'success' && '✅ [Branch Link]($DIFF_URL)' || '❌ failure' }}" >> $GITHUB_STEP_SUMMARY
echo "| Pull Request Creation | ${{ env.CREATE_PR == 'false' && '⏩ skipped' || '' }}${{ env.CREATE_PR == 'true' && steps.push.outcome == 'success' && '✅ [PR Link]($PR_URL)' || '' }}" >> $GITHUB_STEP_SUMMARY
fi
- name: 'Fail if generation failed'
if: steps.generate.outputs.generation_result == 'failure'
run: |
echo "Client generation failed. Please check the Generate step logs for details."
exit 1
- name: 'Fail if API incompatible'
if: steps.api_compat.outputs.api_compat_result == 'incompatible'
run: |
echo "API compatibility check detected breaking changes. Check the japicmp-report artifact in the workflow run summary for details."
exit 1
- name: 'Fail if no spec changes and previous run failed'
if: steps.spec_diff.outputs.prev_run_success == 'false' && github.event.inputs.is-weekly != 'true'
run: |
echo "Previous run failed and there were no spec changes since, thus failing this run as well."
exit 1
- name: 'Slack Notification'
if: failure() && github.event.inputs.create-pr == 'true'
uses: slackapi/slack-github-action@v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
blocks:
- type: "section"
text:
type: "mrkdwn"
text: "⚠️ Weekly spec update failed! 😬 Please inspect & fix by clicking <https://github.com/SAP/ai-sdk-java/actions/runs/${{ github.run_id }}|here>"