diff --git a/.github/actions/await-workflow/action.yaml b/.github/actions/await-workflow/action.yaml new file mode 100644 index 000000000..4297ac2c1 --- /dev/null +++ b/.github/actions/await-workflow/action.yaml @@ -0,0 +1,100 @@ +name: "Await Workflow" +description: "Waits until a workflow run completes." + +inputs: + run-id: + description: "The id of the workflow run to wait for." + required: true + poll-interval: + description: "The interval (in seconds) to poll for the workflow run status." + required: false + default: "60" + commit-status: + description: "The commit status message. Leave empty to not create a commit status." + required: false + +outputs: + succeeded: + description: "Whether the triggered run succeeded." + value: ${{ steps.wait-for-workflow.outputs.RUN_SUCCEEDED }} + conclusion: + description: "The conclusion of the triggered workflow run." + value: ${{ steps.wait-for-workflow.outputs.CONCLUSION }} + +runs: + using: composite + steps: + - name: Print Action Input + run: | + echo "[DEBUG] Starting 'Await Workflow' Action; inputs = ${{ toJson(inputs) }}" + shell: bash + + - name: View Run + if: ${{ inputs.commit-status != '' }} + id: view-run + env: + GH_TOKEN: ${{ github.token }} + run: | + JSON=$(gh run view ${{ inputs.run-id }} --json url,headSha) + echo "URL=$(echo $JSON | jq -r '.url')" >> $GITHUB_OUTPUT + echo "HEAD_SHA=$(echo $JSON | jq -r '.headSha')" >> $GITHUB_OUTPUT + shell: bash + + - name: Create Commit Status + if: ${{ inputs.commit-status != '' }} + uses: actions/github-script@v7 + with: + script: | + github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: '${{ steps.view-run.outputs.HEAD_SHA }}', + state: 'pending', + target_url: '${{ steps.view-run.outputs.URL }}', + context: '${{ inputs.commit-status }}' + }) + + - name: Wait for Workflow to Complete + id: wait-for-workflow + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "[DEBUG] Waiting for run '${{ inputs.run-id }}' to complete..." + gh run watch ${{ inputs.run-id }} --interval ${{ inputs.poll-interval }} > /dev/null + CONCLUSION=$(gh run view ${{ inputs.run-id }} --json conclusion | jq -r '.conclusion') + + echo "CONCLUSION=$CONCLUSION" >> $GITHUB_OUTPUT + echo "[DEBUG] Run '${{ inputs.run-id }}' finished with conclusion '$CONCLUSION'." + + if [[ "$CONCLUSION" != "success" ]]; then + echo "RUN_SUCCEEDED=false" >> $GITHUB_OUTPUT + exit 1 + fi + + echo "RUN_SUCCEEDED=true" >> $GITHUB_OUTPUT + shell: bash + + - name: Determine Final Commit Status + id: determine-final-commit-status + if: ${{ always() && inputs.commit-status != '' }} + run: | + if [[ "${{ steps.wait-for-workflow.outputs.CONCLUSION }}" == "success" ]]; then + echo "FINAL_COMMIT_STATUS=success" >> $GITHUB_OUTPUT + else + echo "FINAL_COMMIT_STATUS=failure" >> $GITHUB_OUTPUT + fi + shell: bash + + - name: Update Commit Status + if: ${{ always() && inputs.commit-status != '' }} + uses: actions/github-script@v7 + with: + script: | + github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: '${{ steps.view-run.outputs.HEAD_SHA }}', + state: '${{ steps.determine-final-commit-status.outputs.FINAL_COMMIT_STATUS }}', + target_url: '${{ steps.view-run.outputs.URL }}', + context: '${{ inputs.commit-status }}' + }) diff --git a/.github/actions/trigger-workflow/action.yaml b/.github/actions/trigger-workflow/action.yaml new file mode 100644 index 000000000..0ceacf2fc --- /dev/null +++ b/.github/actions/trigger-workflow/action.yaml @@ -0,0 +1,67 @@ +name: "Trigger Workflow" +description: "Triggers a workflow without waiting for it to complete." + +inputs: + workflow: + description: "The workflow file name" + required: true + workflow-ref: + description: "The ref (i.e. branch name, or tag name) where the workflow is located." + required: true + parameters: + description: "The workflow parameters" + required: false + commit-sha: + description: "The commit SHA to trigger the workflow on" + required: false + default: ${{ github.sha }} + +outputs: + run-id: + description: "The id of the workflow run that was triggered." + value: ${{ steps.trigger-workflow.outputs.RUN_ID }} + run-url: + description: "The url of the workflow run that was triggered." + value: ${{ steps.trigger-workflow.outputs.RUN_URL }} + +runs: + using: composite + steps: + - name: Print Action Input + run: | + echo "[DEBUG] Starting 'Trigger Workflow' Action; inputs = ${{ toJson(inputs) }}" + shell: bash + + - name: Trigger Workflow + id: trigger-workflow + env: + GH_TOKEN: ${{ github.token }} + run: | + PREVIOUS_RUN_ID=$(gh run list --workflow=${{ inputs.workflow}} --commit=${{ inputs.commit-sha }} --json databaseId | jq -r '.[0].databaseId') + echo "[DEBUG] Previous run id = '$PREVIOUS_RUN_ID'" + + gh workflow run "${{ inputs.workflow }}" --ref "${{ inputs.workflow-ref }}" ${{ inputs.parameters }} + # allow for some initial delay as workflows take a moment to spin up + sleep 20 + + for i in {0..6}; do + LATEST_RUN_ID=$(gh run list --workflow=${{ inputs.workflow }} --commit=${{ inputs.commit-sha }} --json databaseId | jq -r '.[0].databaseId') + + if [[ -z "$LATEST_RUN_ID" || "$LATEST_RUN_ID" == "$PREVIOUS_RUN_ID" ]]; then + echo "[DEBUG] No new run detected. Waiting for 10 seconds." + sleep 10 + else + echo "[DEBUG] New workflow run detected: '$LATEST_RUN_ID'." + + RUN_URL=$(gh run view $LATEST_RUN_ID --json url | jq -r '.url') + echo "[DEBUG] ${{ inputs.workflow }} run #$LATEST_RUN_ID successfully triggered: $RUN_URL" + echo "[${{ inputs.workflow }} run (#$LATEST_RUN_ID)]($RUN_URL)" >> $GITHUB_STEP_SUMMARY + echo "RUN_ID=$LATEST_RUN_ID" >> $GITHUB_OUTPUT + echo "RUN_URL=$RUN_URL" >> $GITHUB_OUTPUT + exit 0 + fi + done + + echo "[DEBUG] Unable to detect new run of workflow '${{ inputs.workflow }}'." + exit 1 + shell: bash diff --git a/.github/workflows/perform-release.yaml b/.github/workflows/perform-release.yaml index 25ec7d7e2..f91c6fb0d 100644 --- a/.github/workflows/perform-release.yaml +++ b/.github/workflows/perform-release.yaml @@ -15,12 +15,14 @@ on: env: MVN_CLI_ARGS: --batch-mode --no-transfer-progress --fail-at-end --show-version -DskipTests JAVA_VERSION: 17 + DOCS_REPO: SAP/ai-sdk jobs: prerequisites: name: "Prerequisites" outputs: code-branch: ${{ steps.determine-branch-names.outputs.CODE_BRANCH_NAME }} + release-notes-branch: ${{ steps.determine-branch-names.outputs.RELEASE_NOTES_BRANCH_NAME }} release-tag: ${{ steps.determine-branch-names.outputs.RELEASE_TAG }} release-commit: ${{ steps.determine-branch-names.outputs.RELEASE_COMMIT }} permissions: write-all # contents and push are needed to see the draft release @@ -33,11 +35,13 @@ jobs: RELEASE_VERSION=$(echo $CODE_BRANCH_NAME | cut -d '-' -f2) RELEASE_TAG=rel/$RELEASE_VERSION RELEASE_COMMIT=$(gh release view $RELEASE_TAG --repo ${{github.repository}} --json targetCommitish --jq '.targetCommitish') + RELEASE_NOTES_BRANCH_NAME=java/release-notes-$RELEASE_VERSION echo "CODE_BRANCH_NAME=$CODE_BRANCH_NAME" >> $GITHUB_OUTPUT echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_OUTPUT echo "RELEASE_TAG=$RELEASE_TAG" >> $GITHUB_OUTPUT echo "RELEASE_COMMIT=$RELEASE_COMMIT" >> $GITHUB_OUTPUT + echo "RELEASE_NOTES_BRANCH_NAME=$RELEASE_NOTES_BRANCH_NAME" >> $GITHUB_OUTPUT echo -e "[DEBUG] Current GITHUB_OUTPUT:\n$(cat $GITHUB_OUTPUT)" env: @@ -64,6 +68,18 @@ jobs: workflow: "Continuous Integration" sha: ${{ steps.determine-branch-names.outputs.RELEASE_COMMIT }} + - name: "Check Whether Release Notes PR Can Be Merged" + if: ${{ inputs.skip-pr-merge != 'true' }} + uses: ./.github/actions/pr-is-mergeable + with: + pr-ref: ${{ steps.determine-branch-names.outputs.RELEASE_NOTES_BRANCH_NAME }} + repo: ${{ env.DOCS_REPO }} + token: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} + excluded-check-runs: | + { + \"Build Cloud SDK Documentation\": [\"dependabot\"] + } + release: name: "Release" needs: [ prerequisites ] @@ -114,3 +130,9 @@ jobs: run: gh release edit ${{ needs.prerequisites.outputs.release-tag }} --draft=false --repo "${{ github.repository }}" env: GH_TOKEN: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} + + - name: "Merge Release Notes PR" + if: ${{ inputs.skip-pr-merge != 'true' }} + run: gh pr merge --squash "${{ needs.prerequisites.outputs.release-notes-branch }}" --delete-branch --repo "${{ env.DOCS_REPO }}" + env: + GH_TOKEN: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml index 4563564e9..9c39e6f30 100644 --- a/.github/workflows/prepare-release.yaml +++ b/.github/workflows/prepare-release.yaml @@ -12,8 +12,10 @@ on: required: false env: + CI_BUILD_WORKFLOW: "continuous-integration.yaml" # Name of the workflow that should be triggered for CI build MVN_MULTI_THREADED_ARGS: --batch-mode --no-transfer-progress --fail-at-end --show-version --threads 1C JAVA_VERSION: 17 + DOCS_REPO: SAP/ai-sdk jobs: bump-version: @@ -59,11 +61,6 @@ jobs: git add . git commit -m "Update to version ${{ steps.determine-versions.outputs.RELEASE_VERSION }}" - # Create release notes - python .pipeline/scripts/release_notes_automation.py --version ${{ steps.determine-versions.outputs.RELEASE_VERSION }} --folder "docs/release-notes" - git add . - git commit -m "Add new release notes" - # We need to get the commit id, and push the branch so the release tag will point at the right commit afterwards RELEASE_COMMIT_ID=$(git log -1 --pretty=format:"%H") echo "RELEASE_COMMIT_ID=$RELEASE_COMMIT_ID" >> $GITHUB_OUTPUT @@ -75,9 +72,38 @@ jobs: git push origin $BRANCH_NAME git push origin $TAG_NAME + run-ci: + name: "Continuous Integration" + outputs: + ci-run-id: ${{ steps.trigger-ci.outputs.run-id }} + needs: [ bump-version ] + runs-on: ubuntu-latest + permissions: + actions: write # needed to trigger the ci-build workflow + statuses: write # needed to update the commit status + steps: + - name: "Checkout repository" + uses: actions/checkout@v4 + with: + ref: ${{ needs.bump-version.outputs.release-branch }} + + - name: "Trigger CI Workflow" + id: trigger-ci + uses: ./.github/actions/trigger-workflow + with: + workflow: ${{ env.CI_BUILD_WORKFLOW }} + workflow-ref: ${{ needs.bump-version.outputs.release-branch }} + commit-sha: ${{ needs.bump-version.outputs.release-commit }} + + - name: "Await CI Workflow" + uses: ./.github/actions/await-workflow + with: + run-id: ${{ steps.trigger-ci.outputs.run-id }} + commit-status: "Continuous Integration Workflow" + create-release: name: "Create GitHub Release" - needs: [ bump-version ] + needs: [ bump-version, run-ci ] outputs: release-name: ${{ steps.create-release.outputs.RELEASE_NAME }} release-url: ${{ steps.create-release.outputs.RELEASE_URL }} @@ -122,9 +148,88 @@ jobs: env: GH_TOKEN: ${{ github.token }} + + create-release-notes-pr: + name: "Create Release Notes PR" + needs: [ bump-version, run-ci ] + outputs: + pr-url: ${{ steps.create-release-notes-pr.outputs.PR_URL }} + runs-on: ubuntu-latest + steps: + - name: "Checkout Code Repository" + uses: actions/checkout@v4 + with: + ref: ${{ needs.bump-version.outputs.release-branch }} + token: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} + - name: "Checkout Docs Repository" + uses: actions/checkout@v4 + with: + repository: ${{ env.DOCS_REPO }} + path: .ai-sdk-docs + token: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} + - name: "Prepare Git" + working-directory: ./.ai-sdk-docs + run: | + git config --global user.email "cloudsdk@sap.com" + git config --global user.name "SAP Cloud SDK Bot" + + - name: "Create Release Notes Branch" + working-directory: ./.ai-sdk-docs + run: git checkout -B java/release-notes-${{ needs.bump-version.outputs.release-version }} + + - name: "Create Release Notes" + run: python .pipeline/scripts/release_notes_automation.py --version ${{ needs.bump-version.outputs.release-version }} --folder ".ai-sdk-docs/docs-java/release-notes" + + - name: "Commit Release Notes" + working-directory: ./.ai-sdk-docs + run: | + git add . + git commit -m "Add new release notes" + + - name: "Push Release Notes" + working-directory: ./.ai-sdk-docs + run: git push origin java/release-notes-${{ needs.bump-version.outputs.release-version }} + + - name: "Create Release Notes PR" + id: create-release-notes-pr + working-directory: ./.ai-sdk-docs + run: | + PR_TITLE="Java: Add release notes for release ${{ needs.bump-version.outputs.release-version }}" + + # if the minor version is a multiple of 15, then change the PR_BODY to "# ⚠️Update the `docs-java/release-notes/index.jsx` file⚠️" + # else the PR_BODY will be "Add the SAP Cloud SDK ${{ needs.bump-version.outputs.release-version }} release notes" + + minor_version=$(echo ${{ needs.bump-version.outputs.release-version }} | cut -d '.' -f 2) + if [[ $((minor_version % 15)) -eq 0 ]]; then + PR_BODY="# ⚠️Update the \`docs-java/release-notes/index.jsx\` file⚠️" + else + PR_BODY="Add the SAP Cloud SDK ${{ needs.bump-version.outputs.release-version }} release notes" + fi + + PR_URL=$(gh pr create --title "$PR_TITLE" --body "$PR_BODY" --repo "${{ env.DOCS_REPO }}") + echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} + + - name: "Reset Release Notes for Next Version" + run: | + rm -rf .ai-sdk-docs + + cp .pipeline/scripts/release_notes_template.md docs/release_notes.md + git add docs/release_notes.md + + CHANGED_FILES="$(git status -s)" + if [[ -z "$CHANGED_FILES" ]]; then + echo "[DEBUG] No changes to release_notes.md detected, skipping reset." + exit 0 + fi + + git commit -m "Reset release notes" + git push + create-code-pr: name: "Create Code PR" - needs: [ bump-version, create-release ] + needs: [ bump-version, run-ci, create-release, create-release-notes-pr ] outputs: pr-url: ${{ steps.create-code-pr.outputs.PR_URL }} runs-on: ubuntu-latest @@ -134,18 +239,25 @@ jobs: with: ref: ${{ needs.bump-version.outputs.release-branch }} token: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} # this is needed so that the same token is used when pushing our changes later. Otherwise, our on: push workflows (i.e. our continuous integration) won't be triggered. - - - name: "Prepare git" + - name: "Prepare Git" run: | git config --global user.email "cloudsdk@sap.com" git config --global user.name "SAP Cloud SDK Bot" + - name: "Set New Version" + run: | + python .pipeline/scripts/set-release-versions.py --version ${{ needs.bump-version.outputs.new-version }} + git add . + git commit -m "Update to version ${{ needs.bump-version.outputs.new-version }}" + git push + - name: "Create Code PR" run: | COMMIT_URL=${{ github.event.repository.html_url }}/commit/${{ needs.bump-version.outputs.release-commit }} PR_URL=$(gh pr create --title "feat: Release ${{ needs.bump-version.outputs.release-version }}" --body "## TODOs - [ ] Review the changes in [the release commit]($COMMIT_URL) + - [ ] Review **and approve** the [Release Notes PR](${{ needs.create-release-notes-pr.outputs.pr-url }}) - [ ] Add release notes to the [Draft Release](${{ needs.create-release.outputs.release-url }}) and improve formatting - [ ] Review **and approve** this PR - [ ] Trigger the [Perform Release Workflow](${{ github.event.repository.html_url }}/actions/workflows/perform-release.yaml) @@ -154,27 +266,9 @@ jobs: env: GH_TOKEN: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} - - name: "Run Continuous Integration" # It should trigger on push but sometimes doesn't - run: | - gh workflow run continuous-integration.yaml --ref ${{ needs.bump-version.outputs.release-branch }} - env: - GH_TOKEN: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} - - - name: "Set New Version and Reset Release Notes" - run: | - python .pipeline/scripts/set-release-versions.py --version ${{ needs.bump-version.outputs.new-version }} - git add . - git commit -m "Update to version ${{ needs.bump-version.outputs.new-version }}" - - # Reset release notes for next version - cp .pipeline/scripts/release_notes_template.md docs/release-notes/release_notes.md - git add docs/release-notes/release_notes.md - git commit -m "Reset release notes" - git push - handle-failure: runs-on: ubuntu-latest - needs: [ bump-version, create-release, create-code-pr ] + needs: [ bump-version, create-release, create-release-notes-pr, create-code-pr ] permissions: contents: write # needed to delete the GitHub release if: ${{ failure() }} @@ -209,3 +303,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} continue-on-error: true + + - name: "Delete Release Notes PR" + if: ${{ needs.create-release-notes-pr.outputs.pr-url != '' }} + run: gh pr close --repo "${{ env.DOCS_REPO }}" ${{ needs.create-release-notes-pr.outputs.pr-url }} --delete-branch + env: + GH_TOKEN: ${{ secrets.BOT_SDK_JS_FOR_DOCS_REPO_PR }} + continue-on-error: true diff --git a/.pipeline/scripts/release_notes_automation.py b/.pipeline/scripts/release_notes_automation.py index f60a7d24b..11418e214 100644 --- a/.pipeline/scripts/release_notes_automation.py +++ b/.pipeline/scripts/release_notes_automation.py @@ -36,14 +36,15 @@ def write_file(file_name, data): """, """### 🐛 Fixed Issues -- +- """] def remove_unchanged_sections(file, unchanged_sections): for unchanged_section in unchanged_sections: # if file contains unchanged_section, remove it file = re.sub(unchanged_section, "", file) - return file + # remove the trailing whitespace when removing 🐛 Fixed Issues + return file.strip() + "\n" def set_header(file, version): date = datetime.today().strftime('%B %d, %Y') @@ -57,6 +58,9 @@ def link_github_release(file, version): file = re.sub(old_github_release_link, new_github_release_link, file) return file +def direct_links(file): + file = re.sub("https://sap.github.io/ai-sdk/docs/java/", "", file) + return file releases_pattern = re.compile(r"^## ") @@ -69,10 +73,10 @@ def count_releases(filename): return count def find_target_file(version): - # release-notes-X-to-Y.md with every 15 versions the index increases by 15 and stays the same for 15 versions + # release-notes-X-to-Y.mdx with every 15 versions the index increases by 15 and stays the same for 15 versions minor_version = int(version.split(".")[1]) index = minor_version // 15 * 15 - return "release-notes-" + str(index) + "-to-" + str(index + 14) + ".md" + return "release-notes-" + str(index) + "-to-" + str(index + 14) + ".mdx" def write_release_notes(folder, target_file): absolute_target_file = os.path.join(folder, target_file) @@ -86,25 +90,21 @@ def write_release_notes(folder, target_file): write_file(absolute_target_file, file) -file_name = "docs/release-notes/release_notes.md" +file_name = "docs/release_notes.md" if __name__ == '__main__': parser = argparse.ArgumentParser(description='SAP Cloud SDK for AI (for Java) - Release Notes formatting script.') parser.add_argument('--version', metavar='VERSION', help='The version to be released.', required=True) - parser.add_argument('--folder', metavar='FOLDER', help='The ai-sdk-java/docs/release-notes folder.', required=True) + parser.add_argument('--folder', metavar='FOLDER', help='The ai-sdk/docs-java/release-notes folder.', required=True) args = parser.parse_args() file = read_file(file_name) + # print file contents file = remove_unchanged_sections(file, unchanged_sections) file = set_header(file, args.version) file = link_github_release(file, args.version) + file = direct_links(file) target_file = find_target_file(args.version) - - - folder_path = args.folder - write_release_notes(folder_path, target_file) - - # delete (temporary) release-notes file so it does not appear in the released version - os.remove(file_name) + write_release_notes(args.folder, target_file) diff --git a/.pipeline/scripts/release_notes_template.md b/.pipeline/scripts/release_notes_template.md index 30c6ae851..5745d6681 100644 --- a/.pipeline/scripts/release_notes_template.md +++ b/.pipeline/scripts/release_notes_template.md @@ -20,4 +20,4 @@ ### 🐛 Fixed Issues -- +- diff --git a/docs/release-notes/release_notes.md b/docs/release_notes.md similarity index 98% rename from docs/release-notes/release_notes.md rename to docs/release_notes.md index 30c6ae851..5745d6681 100644 --- a/docs/release-notes/release_notes.md +++ b/docs/release_notes.md @@ -20,4 +20,4 @@ ### 🐛 Fixed Issues -- +-