diff --git a/eng/ci/java-version-check.yml b/eng/ci/java-version-check.yml index 00459309..991683bd 100644 --- a/eng/ci/java-version-check.yml +++ b/eng/ci/java-version-check.yml @@ -143,128 +143,282 @@ extends: displayName: 'Merge version updates' - bash: | - # Configure git + set -euo pipefail + git config user.email "azfunc-java-worker-bot@microsoft.com" git config user.name "Azure Functions Java Worker Bot" - - # Check if there are changes - if git diff --quiet eng/ci/templates/java-versions.yml; then - echo "No changes detected in java-versions.yml" - echo "##vso[task.setvariable variable=HasChanges]false" + + VERSIONS_FILE="eng/ci/templates/java-versions.yml" + + # One long-lived branch, refreshed in place, so at most one bot PR is ever + # open. The previous timestamped branch name made the "does a PR already + # exist?" lookup match nothing, which cut a duplicate PR every week. + BOT_BRANCH="automated/update-java-versions" + echo "##vso[task.setvariable variable=BotBranch]$BOT_BRANCH" + + HAS_CHANGES=true + BRANCH_UP_TO_DATE=false + BOT_REMOTE_SHA="" + + if git diff --quiet -- "$VERSIONS_FILE"; then + # Still hand off to the next step rather than exiting: an open PR may + # need a nudge even on a week when nothing new has shipped. + echo "No changes versus dev." + HAS_CHANGES=false else - echo "Changes detected in java-versions.yml" - echo "##vso[task.setvariable variable=HasChanges]true" - - # Show the diff - echo "Changes:" - git diff eng/ci/templates/java-versions.yml + echo "Changes versus dev:" + git --no-pager diff -- "$VERSIONS_FILE" + + # Record whether the bot branch already carries these exact versions. + # If it does and its PR is still open, the next step skips the push so we + # don't re-run CI and churn the PR every week for identical content. + if git fetch --quiet origin "+refs/heads/${BOT_BRANCH}:refs/remotes/origin/${BOT_BRANCH}"; then + BOT_REMOTE_SHA=$(git rev-parse "refs/remotes/origin/${BOT_BRANCH}" 2>/dev/null || true) + if git show "origin/${BOT_BRANCH}:${VERSIONS_FILE}" > /tmp/bot-branch-versions.yml 2>/dev/null \ + && cmp -s /tmp/bot-branch-versions.yml "$VERSIONS_FILE"; then + BRANCH_UP_TO_DATE=true + echo "origin/${BOT_BRANCH} already carries these versions." + else + echo "origin/${BOT_BRANCH} exists with different versions and will be refreshed." + fi + else + echo "origin/${BOT_BRANCH} does not exist yet and will be created." + fi fi + + echo "##vso[task.setvariable variable=HasChanges]$HAS_CHANGES" + echo "##vso[task.setvariable variable=BranchUpToDate]$BRANCH_UP_TO_DATE" + echo "##vso[task.setvariable variable=BotRemoteSha]$BOT_REMOTE_SHA" displayName: 'Check for changes' - bash: | - BRANCH_NAME="automated/update-java-versions-$(date +%Y%m%d-%H%M%S)" - echo "Creating branch: $BRANCH_NAME" - git checkout -b $BRANCH_NAME - - # Stage and commit changes - git add eng/ci/templates/java-versions.yml - git commit -m "Update Java versions - - Auto-detected new Java versions from Microsoft OpenJDK releases. - - This PR was automatically generated by the Java version check pipeline. - - Pipeline Run: $(Build.BuildNumber)" - - # Push the branch - git push origin $BRANCH_NAME - - # Store branch name for PR creation - echo "##vso[task.setvariable variable=BranchName]$BRANCH_NAME" - displayName: 'Commit and push changes' - condition: eq(variables['HasChanges'], 'true') - - - bash: | - set -e - - BRANCH_NAME="$(BranchName)" + set -euo pipefail + + BOT_BRANCH="$(BotBranch)" + HAS_CHANGES="$(HasChanges)" + BRANCH_UP_TO_DATE="$(BranchUpToDate)" + BOT_REMOTE_SHA="$(BotRemoteSha)" GITHUB_REPO="Azure/azure-functions-java-worker" - - echo "Branch name: $BRANCH_NAME" - echo "GitHub repository: $GITHUB_REPO" - + VERSIONS_FILE="eng/ci/templates/java-versions.yml" + LABEL="automation:java-versions" + BUILD_NUM="$(Build.BuildNumber)" + # Extract GitHub token from the git remote URL credentials - # Azure Pipelines injects the token into the git config - GIT_TOKEN=$(git config --get-urlmatch http.extraheader https://github.com/${GITHUB_REPO} | sed -n 's/.*AUTHORIZATION: basic \(.*\)/\1/p' | base64 -d | cut -d: -f2) - + # Azure Pipelines injects the token into the git config. + # The trailing '|| true' matters: under 'pipefail' a miss here would + # abort the step instead of falling through to System.AccessToken. + GIT_TOKEN=$(git config --get-urlmatch http.extraheader "https://github.com/${GITHUB_REPO}" | sed -n 's/.*AUTHORIZATION: basic \(.*\)/\1/p' | base64 -d | cut -d: -f2 || true) + if [ -z "$GIT_TOKEN" ]; then echo "WARNING: Could not extract GitHub token from git config" echo "Attempting to use System.AccessToken directly..." - GIT_TOKEN="$(System.AccessToken)" + GIT_TOKEN="$SYSTEM_ACCESSTOKEN" + fi + + # Writes the response body to /tmp/gh_response.json and sets HTTP_CODE. + gh_api() { + local method="$1"; shift + local path="$1"; shift + HTTP_CODE=$(curl -sS -o /tmp/gh_response.json -w '%{http_code}' \ + -X "$method" \ + -H "Authorization: token ${GIT_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "Content-Type: application/json" \ + "$@" \ + "https://api.github.com${path}") + } + + # JSON is built and read with python rather than jq, which is not + # guaranteed on the image. The merge step in this job already runs a + # python script, so an interpreter is present; accept either name. + PY=$(command -v python3 || command -v python || true) + if [ -z "$PY" ]; then + echo "ERROR: No python interpreter found on PATH" + exit 1 + fi + + json_obj() { + "$PY" -c 'import json,sys; print(json.dumps(dict(a.split("=", 1) for a in sys.argv[1:])))' "$@" + } + + # Read the top-level array element. Grepping the raw JSON for "number" + # is fragile because nested objects such as milestones carry one too. + gh_api GET "/repos/${GITHUB_REPO}/pulls?state=open&base=dev&head=Azure:${BOT_BRANCH}" + if [ "$HTTP_CODE" != "200" ]; then + echo "ERROR: Failed to query existing pull requests (HTTP $HTTP_CODE)" + cat /tmp/gh_response.json + exit 1 + fi + PR_NUMBER=$("$PY" -c 'import json,sys; d=json.load(sys.stdin); print(d[0]["number"] if d else "")' < /tmp/gh_response.json) + + # How long the PR has been sitting, so the nudge can say something useful. + # Embedded python has to stay on one line: a YAML block scalar cannot hold + # continuation lines at column zero, and python rejects them indented. + pr_age_days() { + gh_api GET "/repos/${GITHUB_REPO}/pulls/$1" > /dev/null + "$PY" -c 'import json,sys,datetime; d=json.load(open(sys.argv[1])); c=datetime.datetime.fromisoformat(d["created_at"].replace("Z","+00:00")); print((datetime.datetime.now(datetime.timezone.utc)-c).days)' /tmp/gh_response.json + } + + # Comments on the PR, tagging whoever is currently requested for review. + # The list is read from the API rather than hardcoded so it follows + # CODEOWNERS instead of drifting from it. + nudge_reviewers() { + local pr_number="$1" + local message="$2" + local mentions="" + + gh_api GET "/repos/${GITHUB_REPO}/pulls/${pr_number}/requested_reviewers" + if [ "$HTTP_CODE" = "200" ]; then + mentions=$("$PY" -c 'import json,sys; d=json.load(open(sys.argv[1])); print(" ".join(["@"+u["login"] for u in d.get("users",[])] + ["@"+t["slug"] for t in d.get("teams",[])]))' /tmp/gh_response.json) + else + echo "WARNING: Could not read requested reviewers (HTTP $HTTP_CODE), commenting without mentions" + fi + + gh_api POST "/repos/${GITHUB_REPO}/issues/${pr_number}/comments" \ + -d "$(json_obj "body=${mentions:+$mentions }$message")" + if [ "$HTTP_CODE" = "201" ]; then + echo "Nudged reviewers on PR #${pr_number}" + else + echo "WARNING: Could not comment on PR #${pr_number} (HTTP $HTTP_CODE)" + fi + } + + if [ "$HAS_CHANGES" != "true" ]; then + if [ -n "$PR_NUMBER" ]; then + # dev already carries these versions, so the open PR is stale. + nudge_reviewers "$PR_NUMBER" "This PR has been open for $(pr_age_days "$PR_NUMBER") days, and \`dev\` already carries these Java versions, so it looks redundant. Please review, merge, or close it." + else + echo "No changes and no open PR. Nothing to do." + fi + exit 0 fi - - # Check if PR already exists - echo "Checking for existing PR from branch: $BRANCH_NAME" - EXISTING_PR=$(curl -s \ - -H "Authorization: token ${GIT_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/${GITHUB_REPO}/pulls?head=Azure:${BRANCH_NAME}&state=open") - - PR_COUNT=$(echo "$EXISTING_PR" | grep -c '"number":' || true) - - if [ "$PR_COUNT" -gt 0 ]; then - PR_NUMBER=$(echo "$EXISTING_PR" | grep -o '"number":[0-9]*' | head -1 | grep -o '[0-9]*') - echo "PR already exists for branch $BRANCH_NAME" - echo "Existing PR: https://github.com/${GITHUB_REPO}/pull/${PR_NUMBER}" + + if [ -n "$PR_NUMBER" ] && [ "$BRANCH_UP_TO_DATE" = "true" ]; then + echo "PR #${PR_NUMBER} is already open with these exact versions." + nudge_reviewers "$PR_NUMBER" "This PR has been open for $(pr_age_days "$PR_NUMBER") days and still carries the latest JDK releases. Please review and merge it so the weekly check can move on." + echo "https://github.com/${GITHUB_REPO}/pull/${PR_NUMBER}" exit 0 fi - + + # Reset the branch onto the current dev tip so the PR stays a single commit + # and cannot drift or accumulate conflicts while it waits for review. + git checkout -B "$BOT_BRANCH" + git add "$VERSIONS_FILE" + git commit -m "Update Java versions + + Auto-detected new Java versions from Microsoft OpenJDK releases. + + This PR was automatically generated by the Java version check pipeline. + + Pipeline Run: ${BUILD_NUM}" + + # The bot owns this branch exclusively, but the lease still guards against + # clobbering anything pushed since the fetch above. It has to name the + # expected SHA explicitly, because the bare --force-with-lease form fails + # with "stale info" unless the remote's fetch refspec maps the bot branch. + if [ -n "$BOT_REMOTE_SHA" ]; then + git push --force-with-lease="${BOT_BRANCH}:${BOT_REMOTE_SHA}" origin "$BOT_BRANCH" + else + git push origin "$BOT_BRANCH" + fi + CURRENT_DATE=$(date -u +"%Y-%m-%d %H:%M:%S UTC") - BUILD_NUM="$(Build.BuildNumber)" - - # Create JSON payload for PR - cat > /tmp/pr_payload.json << 'JSONEOF' - { - "title": "Update Java versions to latest releases", - "body": "This PR updates the Java version variables in `eng/ci/templates/java-versions.yml` to the latest versions.\n\n## Changes\n- Auto-detected and validated new Java versions\n- **JDK 8**: From Adoptium (Eclipse Temurin)\n- **JDK 11, 17, 21, 25**: From Microsoft OpenJDK\n- Linux JDKs validated on Linux pool\n- Windows JDKs validated on Windows pool\n- All JDK installations were validated using `java -version`\n\n## Automated Process\nThis PR was automatically created by the Java Version Check Pipeline.\n\n## Review Checklist\n- [ ] Verify version numbers are correct\n- [ ] Check that both Linux and Windows versions are updated\n- [ ] Ensure CI tests pass with new versions\n\n**Pipeline Run:** BUILDNUMBER_PLACEHOLDER\n**Date:** CURRENTDATE_PLACEHOLDER", - "head": "BRANCHNAME_PLACEHOLDER", - "base": "dev" - } - JSONEOF - - # Replace placeholders - sed -i "s|BRANCHNAME_PLACEHOLDER|${BRANCH_NAME}|g" /tmp/pr_payload.json - sed -i "s|BUILDNUMBER_PLACEHOLDER|${BUILD_NUM}|g" /tmp/pr_payload.json - sed -i "s|CURRENTDATE_PLACEHOLDER|${CURRENT_DATE}|g" /tmp/pr_payload.json - - echo "Creating pull request on GitHub..." - PR_RESPONSE=$(curl -s -w "\n%{http_code}" \ - -X POST \ - -H "Authorization: token ${GIT_TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -H "Content-Type: application/json" \ - -d @/tmp/pr_payload.json \ - "https://api.github.com/repos/${GITHUB_REPO}/pulls") - - HTTP_CODE=$(echo "$PR_RESPONSE" | tail -n1) - RESPONSE_BODY=$(echo "$PR_RESPONSE" | head -n-1) - - echo "HTTP Status: $HTTP_CODE" - - if [ "$HTTP_CODE" = "201" ]; then - PR_NUMBER=$(echo "$RESPONSE_BODY" | grep -o '"number":[0-9]*' | head -1 | grep -o '[0-9]*') - echo "Pull request created successfully! PR #$PR_NUMBER" - echo "URL: https://github.com/${GITHUB_REPO}/pull/${PR_NUMBER}" + VERSIONS=$(cat "$VERSIONS_FILE") + + # Backticks are escaped so the unquoted heredoc does not run them as commands. + BODY=$(cat < + This PR updates the Java version variables in \`eng/ci/templates/java-versions.yml\` to the latest releases. + + The Java Version Check pipeline opens this PR and then refreshes it in place, so only one of them is ever open. If newer JDKs ship before this merges, the same PR is updated instead of a new one being created. + + ## Proposed versions + + \`\`\`yaml + ${VERSIONS} + \`\`\` + + ## Sources + - **JDK 8**: Adoptium (Eclipse Temurin) + - **JDK 11, 17, 21, 25**: Microsoft OpenJDK + + Linux JDKs were validated on a Linux pool and Windows JDKs on a Windows pool. Every install was checked with \`java -version\`. + + ## Review checklist + - [ ] Version numbers are correct + - [ ] Both Linux and Windows values were updated + - [ ] CI passes with the new versions + + **Pipeline run:** ${BUILD_NUM} + **Last refreshed:** ${CURRENT_DATE} + EOF + ) + + if [ -n "$PR_NUMBER" ]; then + echo "Refreshing existing PR #${PR_NUMBER}..." + gh_api PATCH "/repos/${GITHUB_REPO}/pulls/${PR_NUMBER}" -d "$(json_obj "body=$BODY")" + if [ "$HTTP_CODE" != "200" ]; then + echo "ERROR: Failed to update pull request (HTTP $HTTP_CODE)" + cat /tmp/gh_response.json + exit 1 + fi + + COMMENT="Refreshed with newer JDK releases detected on ${CURRENT_DATE} by pipeline run ${BUILD_NUM}." + nudge_reviewers "$PR_NUMBER" "$COMMENT" else - echo "ERROR: Failed to create pull request" - echo "Response: $RESPONSE_BODY" - echo "" - echo "The branch has been pushed to: $BRANCH_NAME" - echo "You can manually create a PR at:" - echo "https://github.com/${GITHUB_REPO}/compare/dev...${BRANCH_NAME}" - exit 1 + echo "Creating pull request on GitHub..." + gh_api POST "/repos/${GITHUB_REPO}/pulls" \ + -d "$(json_obj "title=Update Java versions to latest releases" "head=$BOT_BRANCH" "base=dev" "body=$BODY")" + + if [ "$HTTP_CODE" = "201" ]; then + PR_NUMBER=$("$PY" -c 'import json,sys; print(json.load(sys.stdin)["number"])' < /tmp/gh_response.json) + elif [ "$HTTP_CODE" = "422" ]; then + # GitHub returns 422 when a PR for this head already exists, which + # happens if another run opened one after our query. Adopt it rather + # than failing, otherwise the branch is left pushed with no PR. + cp /tmp/gh_response.json /tmp/gh_create_error.json + echo "PR creation returned 422; checking for a PR opened concurrently..." + gh_api GET "/repos/${GITHUB_REPO}/pulls?state=open&base=dev&head=Azure:${BOT_BRANCH}" + PR_NUMBER=$("$PY" -c 'import json,sys; d=json.load(sys.stdin); print(d[0]["number"] if d else "")' < /tmp/gh_response.json) + if [ -z "$PR_NUMBER" ]; then + echo "ERROR: Failed to create pull request (HTTP 422) and no open PR was found" + cat /tmp/gh_create_error.json + exit 1 + fi + echo "Adopting concurrently opened PR #${PR_NUMBER}" + gh_api PATCH "/repos/${GITHUB_REPO}/pulls/${PR_NUMBER}" -d "$(json_obj "body=$BODY")" + else + echo "ERROR: Failed to create pull request (HTTP $HTTP_CODE)" + cat /tmp/gh_response.json + echo "" + echo "The branch has been pushed to: $BOT_BRANCH" + echo "You can manually create a PR at:" + echo "https://github.com/${GITHUB_REPO}/compare/dev...${BOT_BRANCH}" + exit 1 + fi + fi + + # A label plus the HTML marker in the body give a second way to find this + # PR if the branch name ever changes again. Neither call is worth failing + # the run over once the PR itself is in place, but a silent miss is not + # worth having either. + gh_api POST "/repos/${GITHUB_REPO}/labels" \ + -d "$(json_obj "name=$LABEL" "color=ededed" "description=Opened automatically by the Java version check pipeline")" + # 422 is the steady state here: the label already exists. + if [ "$HTTP_CODE" != "201" ] && [ "$HTTP_CODE" != "422" ]; then + echo "WARNING: Could not ensure the '$LABEL' label exists (HTTP $HTTP_CODE)" + fi + + gh_api POST "/repos/${GITHUB_REPO}/issues/${PR_NUMBER}/labels" \ + -d "$("$PY" -c 'import json,sys; print(json.dumps({"labels": sys.argv[1:]}))' "$LABEL")" + if [ "$HTTP_CODE" != "200" ]; then + echo "WARNING: Could not add the '$LABEL' label to PR #${PR_NUMBER} (HTTP $HTTP_CODE)" fi - displayName: 'Create Pull Request on GitHub' - condition: eq(variables['HasChanges'], 'true') + + echo "Done. PR #${PR_NUMBER}" + echo "https://github.com/${GITHUB_REPO}/pull/${PR_NUMBER}" + displayName: 'Create or refresh Pull Request on GitHub' + # No HasChanges condition: the step also runs on quiet weeks so it can + # nudge reviewers on a PR that is still sitting open. env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) diff --git a/eng/ci/templates/java-versions.yml b/eng/ci/templates/java-versions.yml index d2fbb759..df842e26 100644 --- a/eng/ci/templates/java-versions.yml +++ b/eng/ci/templates/java-versions.yml @@ -1,16 +1,16 @@ variables: # Linux JDK Versions - JDK8_LINUX_VERSION: '492' - JDK8_LINUX_BUILD: '09' - JDK11_LINUX_VERSION: '11.0.31' - JDK17_LINUX_VERSION: '17.0.19' - JDK21_LINUX_VERSION: '21.0.11' - JDK25_LINUX_VERSION: '25.0.3' + JDK8_LINUX_VERSION: '504' + JDK8_LINUX_BUILD: '01' + JDK11_LINUX_VERSION: '11.0.32' + JDK17_LINUX_VERSION: '17.0.20' + JDK21_LINUX_VERSION: '21.0.12' + JDK25_LINUX_VERSION: '25.0.4' # Windows JDK Versions - JDK8_WINDOWS_VERSION: '492' - JDK8_WINDOWS_BUILD: '09' - JDK11_WINDOWS_VERSION: '11.0.31' - JDK17_WINDOWS_VERSION: '17.0.19' - JDK21_WINDOWS_VERSION: '21.0.11' - JDK25_WINDOWS_VERSION: '25.0.3' \ No newline at end of file + JDK8_WINDOWS_VERSION: '504' + JDK8_WINDOWS_BUILD: '01' + JDK11_WINDOWS_VERSION: '11.0.32' + JDK17_WINDOWS_VERSION: '17.0.20' + JDK21_WINDOWS_VERSION: '21.0.12' + JDK25_WINDOWS_VERSION: '25.0.4' \ No newline at end of file