diff --git a/.devops/update-versions.yml b/.devops/update-versions.yml new file mode 100644 index 0000000..a2baed5 --- /dev/null +++ b/.devops/update-versions.yml @@ -0,0 +1,197 @@ +# update-versions.yml +# +# Azure DevOps equivalent of the "Update JDK Versions" GitHub Actions workflow. +# It refreshes versions.json from the upstream release sources and opens (or +# refreshes) a pull request on github.com/microsoft/openjdk-docker when a newer +# patch version is found. +# +# Authentication: git pushes and the PR REST call are authenticated with the +# GitHub App service connection (Github-Java-Engineering), not a PAT. The +# ms_openjdk_token_src repository resource is checked out with persistCredentials +# to obtain a short-lived OAuth token, which is then scoped to github.com/microsoft/*. + +name: "UpdateJdkVersions-$(Date:yyyyMMdd)$(Rev:.r)" + +trigger: none +pr: none + +schedules: + # Tuesdays and Thursdays at 12:00 AM PT so version-bump PRs land before the + # automated Monday/Wednesday/Friday image builds. Azure DevOps cron is UTC + # only and does not observe DST, so this is pinned to 08:00 UTC (midnight PST). + # always: true runs the pipeline even though the source branch has not changed. + - cron: "0 8 * * 2,4" + displayName: "Tue/Thu JDK version check" + branches: + include: + - main + always: true + +parameters: + # dry-run only prints the detected changes; create-pull-request opens/updates a PR. + - name: mode + displayName: "Run mode" + type: string + default: dry-run + values: + - dry-run + - create-pull-request + +resources: + repositories: + - repository: 1ESPipelineTemplates + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release + + # Token source for GitHub App auth (Github-Java-Engineering service connection). + - repository: ms_openjdk_token_src + type: github + endpoint: Github-Java-Engineering + name: microsoft/openjdk-adoptium-marketplace-data + +variables: + repoSlug: microsoft/openjdk-docker + targetBranch: main + bumpBranch: auto/update-jdk-versions + +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates + parameters: + sdl: + sourceAnalysisPool: + name: JEG-windows-x64-release + os: windows + sourceRepositoriesToScan: + exclude: + - repository: ms_openjdk_token_src + + pool: + name: JEG-pipeline-support + os: linux + + stages: + - stage: update_versions + displayName: Update JDK versions + jobs: + - job: update_versions + displayName: Update versions.json and open PR + steps: + # Check out this repo (self) so the scripts and versions.json used at + # runtime come from the branch the pipeline runs on - not from a fresh + # clone of main (which may not yet contain new scripts). + - checkout: self + path: openjdk-docker + displayName: Checkout openjdk-docker (self) + + # Obtain a short-lived GitHub App OAuth token via the token-source + # repo and scope it to github.com/microsoft/* for all git operations + # (this is what authorizes the push and PR creation against this repo). + - checkout: ms_openjdk_token_src + persistCredentials: true + path: ms-openjdk-token-src + fetchDepth: 1 + fetchTags: false + displayName: Checkout microsoft repo to obtain GitHub App token + + - bash: | + set -uo pipefail + repo_dir="$(Pipeline.Workspace)/ms-openjdk-token-src" + + # persistCredentials stores the OAuth token as an http extraheader; + # the exact key name is agent-dependent, so discover it. + key=$(git -C "$repo_dir" config --local --name-only --get-regexp '^http\..*\.extraheader$' \ + | grep -i 'github.com' | head -n1) + + if [ -z "${key}" ]; then + echo "ERROR: Could not find a github.com extraheader in ${repo_dir} git config." >&2 + git -C "$repo_dir" config --local --name-only --get-regexp '^http\.' >&2 || true + exit 1 + fi + + extraheader=$(git -C "$repo_dir" config --local --get "${key}") + if [ -z "${extraheader}" ]; then + echo "ERROR: Found key '${key}' but its value was empty." >&2 + exit 1 + fi + + git config --global "http.https://github.com/microsoft/.extraheader" "${extraheader}" + echo "Configured GitHub App credentials for github.com/microsoft/*" + displayName: Configure GitHub App credentials for microsoft/* + + # Configure the git identity used for the version-bump commit, and + # remove any per-repo auth header left by the self checkout so it does + # not stack with the global microsoft/* GitHub App credential (two + # Authorization headers on one request causes "Duplicate header"). + - bash: | + set -uo pipefail + git config user.name "Java Platform Infrastructure" + git config user.email "javaplatinfra@microsoft.com" + for key in $(git config --local --name-only --get-regexp '^http\..*\.extraheader$' || true); do + git config --local --unset-all "$key" || true + done + displayName: Configure git identity + workingDirectory: $(Pipeline.Workspace)/openjdk-docker + + # Rewrites versions.json in place with the latest patch versions from the + # upstream sources (Microsoft download page for msopenjdk, Adoptium API + # for temurin). No-op if everything is already current. + - bash: | + set -euo pipefail + ./scripts/update-versions.sh versions.json + displayName: Update versions.json from upstream release sources + workingDirectory: $(Pipeline.Workspace)/openjdk-docker + + # Determine whether the script changed anything and expose the result + # as an output variable (changed=true/false) for later steps. When + # versions changed, render a clean "vendor major: old -> new" summary + # and attach it to the run summary. + - bash: | + set -euo pipefail + if git diff --quiet -- versions.json; then + echo "No JDK version changes detected." + echo "##vso[task.setvariable variable=changed;isOutput=true]false" + exit 0 + fi + + summary="$(./scripts/summarize-version-changes.sh)" + echo "JDK version changes detected:" + echo "$summary" + + # Attach the summary to the AzDO run (equivalent of a step summary). + summary_md="$(Agent.TempDirectory)/version-changes.md" + { + echo "### Detected JDK version changes" + echo "" + echo '```' + echo "$summary" + echo '```' + } > "$summary_md" + echo "##vso[task.uploadsummary]$summary_md" + + echo "##vso[task.setvariable variable=changed;isOutput=true]true" + name: diff + displayName: Detect changes + workingDirectory: $(Pipeline.Workspace)/openjdk-docker + + # Manual dry-run: changes were found but the user asked not to open a + # PR, so just report what would have happened. + - bash: echo "Changes detected but skipping pull request creation (mode=dry-run)." + displayName: Dry run (no pull request created) + condition: and(eq(variables['diff.changed'], 'true'), eq('${{ parameters.mode }}', 'dry-run')) + + # Open (or refresh) the version-bump PR. Skipped in dry-run mode. + # The GitHub App credential scoped to microsoft/* authenticates both + # the git push and the PR REST call made by the script. + - bash: | + set -euo pipefail + auth_header="$(git config --global --get 'http.https://github.com/microsoft/.extraheader' || true)" + if [ -z "$auth_header" ]; then + echo "##vso[task.logissue type=error]Missing GitHub App credentials; auth step must run first." + exit 1 + fi + GH_AUTH_HEADER="$auth_header" \ + ./scripts/open-version-pr.sh "$(repoSlug)" "$(targetBranch)" "$(bumpBranch)" + displayName: Create or update pull request + workingDirectory: $(Pipeline.Workspace)/openjdk-docker + condition: and(eq(variables['diff.changed'], 'true'), ne('${{ parameters.mode }}', 'dry-run')) diff --git a/scripts/open-version-pr.sh b/scripts/open-version-pr.sh new file mode 100755 index 0000000..072e144 --- /dev/null +++ b/scripts/open-version-pr.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# +# Commits versions.json on a dedicated branch, force-pushes it, and opens a +# pull request on GitHub (or refreshes the existing open one). CI-agnostic: +# authentication is provided via the GH_AUTH_HEADER environment variable, which +# must contain a full HTTP Authorization header for github.com, e.g.: +# +# AUTHORIZATION: basic +# +# The same credential is expected to authorize `git push` to origin (e.g. set as +# an http extraheader in git config by the caller). +# +# Usage: +# GH_AUTH_HEADER="..." scripts/open-version-pr.sh +# +# Example: +# scripts/open-version-pr.sh microsoft/openjdk-docker main auto/update-jdk-versions + +set -euo pipefail + +REPO_SLUG="${1:?repo slug required (e.g. microsoft/openjdk-docker)}" +BASE_BRANCH="${2:?base branch required (e.g. main)}" +BUMP_BRANCH="${3:?bump branch required (e.g. auto/update-jdk-versions)}" + +if [[ -z "${GH_AUTH_HEADER:-}" ]]; then + echo "error: GH_AUTH_HEADER environment variable is required" >&2 + exit 1 +fi + +owner="${REPO_SLUG%%/*}" +api="https://api.github.com/repos/${REPO_SLUG}" + +# Reuse the same branch every run: -C resets it to the current commit so an +# existing (stale) branch is force-updated with the latest versions. +git switch -C "${BUMP_BRANCH}" +git add versions.json +git commit -m "Update JDK versions from upstream release sources" +git push -f origin "HEAD:refs/heads/${BUMP_BRANCH}" + +# Check if there is already an open PR for this branch. If none is open, create +# one; if one exists, the force-push above already updated it. +open_count="$(curl -fsSL -H "${GH_AUTH_HEADER}" -H "Accept: application/vnd.github+json" \ + "${api}/pulls?state=open&head=${owner}:${BUMP_BRANCH}" | jq 'length')" + +if [[ "${open_count}" != "0" ]]; then + echo "An open PR already exists for ${BUMP_BRANCH}; updated it with a force-push." + exit 0 +fi + +body="Automated update of versions.json from upstream release sources: the Microsoft Build of OpenJDK download page (msopenjdk) and the Adoptium API (temurin). Generated by the Update JDK Versions pipeline." +payload="$(jq -n \ + --arg title "Update JDK versions" \ + --arg head "${BUMP_BRANCH}" \ + --arg base "${BASE_BRANCH}" \ + --arg body "${body}" \ + '{title: $title, head: $head, base: $base, body: $body}')" + +curl -fsSL -X POST -H "${GH_AUTH_HEADER}" -H "Accept: application/vnd.github+json" \ + "${api}/pulls" -d "${payload}" | jq -r '"Created PR: " + .html_url' diff --git a/scripts/summarize-version-changes.sh b/scripts/summarize-version-changes.sh new file mode 100755 index 0000000..3565b44 --- /dev/null +++ b/scripts/summarize-version-changes.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# +# Prints a human-readable summary of the JDK version changes between two +# versions.json states, one line per changed entry: +# +# vendor major: old -> new +# +# By default it compares the committed versions.json (git HEAD) against the +# current working-tree versions.json. Both refs can be overridden for testing. +# +# Usage: +# scripts/summarize-version-changes.sh [current_file] [old_git_ref] +# +# Prints nothing (exit 0) when there are no version changes. + +set -euo pipefail + +CURRENT_FILE="${1:-versions.json}" +OLD_REF="${2:-HEAD}" + +if ! command -v jq >/dev/null 2>&1; then + echo "error: jq is required but not installed" >&2 + exit 1 +fi + +# Load both files into jq: `old` = the previous versions.json (from git), `new` +# = the current one. --slurpfile reads each JSON file into a single-element array, +# so $old[0] / $new[0] are the actual objects. +# +# The filter walks every "vendor -> { major: version }" entry in the new file, +# pairs each with the matching version in the old file, keeps only the ones whose +# version actually changed, and prints one line per change: +# " : -> " (old shown as "(new)" for a brand-new major) +jq -rn \ + --slurpfile old <(git show "${OLD_REF}:${CURRENT_FILE}") \ + --slurpfile new "${CURRENT_FILE}" ' + ($old[0]) as $o | ($new[0]) as $n + | [ $n | to_entries[] as $vendor | $vendor.value | to_entries[] as $major + | { vendor: $vendor.key, major: $major.key, + new: $major.value, old: ($o[$vendor.key][$major.key]) } ] + | map(select(.new != .old)) + | .[] | " \(.vendor) \(.major): \(.old // "(new)") -> \(.new)" + ' diff --git a/versions.json b/versions.json index f02e21a..527e2fc 100644 --- a/versions.json +++ b/versions.json @@ -1,11 +1,11 @@ { "temurin": { - "8": "1.8.0_472" + "8": "1.8.0_492" }, "msopenjdk": { - "11": "11.0.31", - "17": "17.0.19", - "21": "21.0.11", - "25": "25.0.3" + "11": "11.0.32", + "17": "17.0.20", + "21": "21.0.12", + "25": "25.0.4" } }