Spec File Update Workflow: orchestration-feat/granite-guardian-4-1 #1761
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>" |