diff --git a/.devops/update-versions.yml b/.devops/update-versions.yml new file mode 100644 index 0000000..ae26b13 --- /dev/null +++ b/.devops/update-versions.yml @@ -0,0 +1,199 @@ +# 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: "$(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: + # create-pull-request opens/updates a PR; dry-run only prints the detected + # changes. Defaults to create-pull-request so scheduled runs (which always use + # the default) open the PR; pick dry-run manually for testing. + - name: mode + displayName: "Run mode" + type: string + default: create-pull-request + values: + - create-pull-request + - dry-run + +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/.github/workflows/update-jdk-versions.yml b/.github/workflows/update-jdk-versions.yml deleted file mode 100644 index 896eacc..0000000 --- a/.github/workflows/update-jdk-versions.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: Update JDK Versions - -on: - schedule: - # Tuesdays and Thursdays at 12:00 AM PT, so version-bump PRs land before the - # automated Monday/Wednesday/Friday image builds. GitHub Actions cron is UTC - # only and does not observe DST, so this is pinned to 08:00 UTC (midnight PST). - - cron: "0 8 * * 2,4" - workflow_dispatch: - inputs: - mode: - description: "dry-run only prints the detected changes; create-pull-request opens/updates a PR" - type: choice - default: dry-run - options: - - dry-run - - create-pull-request - -permissions: - contents: write - pull-requests: write - -jobs: - update-versions: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - # Rewrites versions.json in place with the latest patch versions pulled from - # the upstream sources (Microsoft download page for msopenjdk, Adoptium API - # for temurin). No-op if everything is already current. - - name: Update versions.json from upstream release sources - run: ./scripts/update-versions.sh versions.json - - # Determine whether the script actually changed anything, and surface the - # diff on the run's summary page so it is visible without opening logs. - - name: Detect changes - id: diff - run: | - if git diff --quiet -- versions.json; then - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No JDK version changes detected." >> "$GITHUB_STEP_SUMMARY" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - { - echo "### Detected JDK version changes" - echo '```diff' - git --no-pager diff -- versions.json - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - git --no-pager diff -- versions.json - fi - - # Manual dry-run: changes were found but the user asked not to open a PR, so - # just report what would have happened. Only reachable from workflow_dispatch. - - name: Dry run (no pull request created) - if: steps.diff.outputs.changed == 'true' && github.event_name == 'workflow_dispatch' && inputs.mode == 'dry-run' - run: echo "::notice title=Dry run::Changes detected but skipping pull request creation (mode=dry-run)." - - # Open (or refresh) the version-bump PR. Runs on every scheduled execution, - # and on manual runs unless dry-run mode was selected. - - name: Create or update pull request - if: steps.diff.outputs.changed == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.mode == 'dry-run') - env: - GH_TOKEN: ${{ github.token }} - run: | - branch="auto/update-jdk-versions" - - # Commit the updated versions.json as the bot on a dedicated branch. - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - # 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 "$branch" - git add versions.json - git commit -m "Update JDK versions from upstream release sources" - git push -f origin "$branch" - - # Check if there is already an open PR for this branch. If none is open, - # create one; if one already exists, the force-push above updated it, so - # there is nothing more to do. - if [ "$(gh pr list --head "$branch" --state open --json number --jq 'length')" = "0" ]; then - gh pr create \ - --head "$branch" \ - --base "${{ github.event.repository.default_branch }}" \ - --title "Update JDK versions" \ - --body "Automated update of \`versions.json\` from upstream release sources: [Microsoft Build of OpenJDK](https://learn.microsoft.com/en-us/java/openjdk/download) (msopenjdk) and the [Adoptium API](https://api.adoptium.net) (temurin). Generated by the \`Update JDK Versions\` workflow." - fi diff --git a/scripts/open-version-pr.sh b/scripts/open-version-pr.sh new file mode 100755 index 0000000..1f9c13e --- /dev/null +++ b/scripts/open-version-pr.sh @@ -0,0 +1,74 @@ +#!/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. +# Rebuild the bump branch cleanly on top of the base branch so the PR contains +# ONLY the versions.json change - not whatever commits happen to be on the branch +# the pipeline ran from. Capture the regenerated file, reset onto origin/, +# then re-apply it. +tmp="$(mktemp)" +cp versions.json "${tmp}" +git fetch origin "${BASE_BRANCH}" +git checkout -f -B "${BUMP_BRANCH}" "origin/${BASE_BRANCH}" +cp "${tmp}" versions.json +rm -f "${tmp}" + +git add versions.json +if git diff --cached --quiet; then + echo "versions.json already up to date on ${BASE_BRANCH}; nothing to do." + exit 0 +fi + +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 OpenJDK Docker - 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)" + '