From 4d13e2b27208d5dac1c7ed4b8d7b78ca798f8416 Mon Sep 17 00:00:00 2001 From: smlambert Date: Mon, 24 Aug 2026 14:53:05 -0400 Subject: [PATCH 1/4] Add groovy wrapper for generating release notes --- generate-release-notes/Jenkinsfile | 182 +++++++++++++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 generate-release-notes/Jenkinsfile diff --git a/generate-release-notes/Jenkinsfile b/generate-release-notes/Jenkinsfile new file mode 100644 index 0000000..7d83c35 --- /dev/null +++ b/generate-release-notes/Jenkinsfile @@ -0,0 +1,182 @@ +#!groovy + +// Returns true when the tag follows the pre-JEP-322 JDK 8 scheme (e.g. jdk8u502-ga, jdk8u492-b07). +def isJdk8Tag(String tag) { + return tag ==~ /^jdk8u.*/ +} + +// Parses a JEP-322 tag (JDK 9+) and returns a map of its components. +// +// Supported formats: +// jdk-21.0.1+12 – standard release (no PATCH) +// jdk-21.0.12.1+1 – patch release; jdk-21.0.12.1+1 and jdk-21.0.12.1-ga point to the same SHA +// jdk-21.0.12-ga – GA alias for a standard release (no PATCH, no BUILD) +// jdk-21.0.12.1-ga – GA alias for a patch release +// +// Returned map: +// feature – major version number (e.g. "21") +// version – full version string from the tag (e.g. "21.0.12.1" or "21.0.12") +// patch – PATCH segment, "0" when absent (e.g. "1" or "0") +// build – BUILD number after '+', "0" when absent (e.g. "1" or "0") +def parseJdkTag(String tag) { + // 1. Strip leading "jdk-" prefix + def withoutPrefix = tag.replaceFirst(/^jdk-/, '') + + // 2. Strip optional qualifier (e.g. "-ga", "-ea"). A qualifier starts with '-' + // followed by a non-digit character and runs to the end of the string. + def withoutQualifier = withoutPrefix.replaceFirst(/-[a-zA-Z].*$/, '') + + // 3. Split on '+' to separate version from optional build number + def parts = withoutQualifier.tokenize('+') + if (parts.size() < 1 || parts.size() > 2) { + error("JDK_TAG '${tag}' does not match an expected JEP-322 format (jdk-FEATURE.INTERIM.UPDATE[.PATCH][+BUILD][-QUALIFIER])") + } + def build = parts.size() == 2 ? parts[1] : '0' + + // 4. Split version on '.' → [FEATURE, INTERIM, UPDATE] or [FEATURE, INTERIM, UPDATE, PATCH] + def versionParts = parts[0].tokenize('.') + if (versionParts.size() < 3) { + error("JDK_TAG '${tag}' version segment '${parts[0]}' must contain at least FEATURE.INTERIM.UPDATE") + } + def patch = versionParts.size() >= 4 ? versionParts[3] : '0' + // Preserve the full version string as it appears in the tag (3-part or 4-part) + def version = parts[0] + + return [ + feature: versionParts[0], + version: version, + patch : patch, + build : build, + ] +} + +pipeline { + agent { label 'master' } + parameters { + string(name: 'JDK_TAG', defaultValue: '', description: 'Required. The JDK release tag, e.g. jdk-21.0.1+12 or jdk-21.0.12.1-ga (JEP-322 format: jdk-FEATURE.INTERIM.UPDATE[.PATCH][+BUILD][-QUALIFIER])') + string(name: 'BASE_JDK_TAG', defaultValue: '', description: 'Required. The base (previous) JDK tag to compare against, e.g. jdk-21.0.0+35 or jdk-21.0.12-ga') + string(name: 'GITHUB_REPOSITORY', defaultValue: '', description: 'Optional (JDK 9+, required for JDK 8). GitHub repository, e.g. adoptium/21u or adoptium/jdk8u') + string(name: 'JDK_VERSION', defaultValue: '', description: 'Optional (JDK 9+, required for JDK 8). Version string for fetchReleaseNotes.js, e.g. 21.0.12.1 or openjdk8u462,8u501') + string(name: 'FILENAME', defaultValue: '', description: 'Optional (JDK 9+, required for JDK 8). Output filename, e.g. OpenJDK21-jdk-release-notes_21.0.12.1_1.json or OpenJDK8U-jdk-release-notes_8.0.502_07.json') + } + stages { + stage('Validate and Resolve Parameters') { + steps { + script { + if (!params.JDK_TAG) { + error('JDK_TAG parameter is required') + } + if (!params.BASE_JDK_TAG) { + error('BASE_JDK_TAG parameter is required') + } + + if (isJdk8Tag(params.JDK_TAG)) { + // JDK 8 uses a pre-JEP-322 versioning scheme (e.g. jdk8u502-ga). + // The derived values for GITHUB_REPOSITORY, JDK_VERSION and FILENAME + // cannot be calculated reliably from the tag alone (the build number + // embedded in the filename requires a GitHub tag→SHA lookup), so all + // three must be provided explicitly. + if (!params.GITHUB_REPOSITORY) { + error('GITHUB_REPOSITORY is required for JDK 8 tags (e.g. adoptium/jdk8u)') + } + if (!params.JDK_VERSION) { + error('JDK_VERSION is required for JDK 8 tags (e.g. openjdk8u462,8u501)') + } + if (!params.FILENAME) { + error('FILENAME is required for JDK 8 tags (e.g. OpenJDK8U-jdk-release-notes_8.0.502_07.json)') + } + env.RESOLVED_GITHUB_REPOSITORY = params.GITHUB_REPOSITORY + env.RESOLVED_JDK_VERSION = params.JDK_VERSION + env.RESOLVED_FILENAME = params.FILENAME + + echo "JDK_TAG: ${params.JDK_TAG} (JDK 8 — all parameters required)" + echo "BASE_JDK_TAG: ${params.BASE_JDK_TAG}" + echo "GITHUB_REPOSITORY: ${env.RESOLVED_GITHUB_REPOSITORY}" + echo "JDK_VERSION: ${env.RESOLVED_JDK_VERSION}" + echo "FILENAME: ${env.RESOLVED_FILENAME}" + } else { + def parsed = parseJdkTag(params.JDK_TAG) + + // For +BUILD tags use build as the suffix. + // For -ga tags (build == '0') fall back to patch (e.g. jdk-21.0.12.1-ga → _1). + def filenameSuffix = (parsed.build != '0') ? parsed.build : parsed.patch + + // Resolve optional parameters: use the supplied value when non-empty, + // otherwise derive from JDK_TAG. The ?: operator returns the left-hand + // side when it is truthy (non-null, non-empty), so an explicit param + // always wins over the calculated default. + env.RESOLVED_GITHUB_REPOSITORY = params.GITHUB_REPOSITORY ?: "adoptium/${parsed.feature}u" + env.RESOLVED_JDK_VERSION = params.JDK_VERSION ?: parsed.version + env.RESOLVED_FILENAME = params.FILENAME ?: "OpenJDK${parsed.feature}-jdk-release-notes_${parsed.version}_${filenameSuffix}.json" + + echo "JDK_TAG: ${params.JDK_TAG}" + echo "BASE_JDK_TAG: ${params.BASE_JDK_TAG}" + echo "GITHUB_REPOSITORY: ${env.RESOLVED_GITHUB_REPOSITORY}${params.GITHUB_REPOSITORY ? ' (provided)' : ' (derived)'}" + echo "JDK_VERSION: ${env.RESOLVED_JDK_VERSION}${params.JDK_VERSION ? ' (provided)' : ' (derived)'}" + echo "FILENAME: ${env.RESOLVED_FILENAME}${params.FILENAME ? ' (provided)' : ' (derived)'}" + } + } + } + } + stage('Checkout') { + steps { + cleanWs() + checkout scm + } + } + stage('Install Dependencies') { + steps { + dir('generate-release-notes/generate-release-notes') { + sh 'node --version' + sh 'npm install' + } + } + } + stage('Fetch Commit List') { + steps { + dir('generate-release-notes/generate-release-notes') { + sh """ + echo "Generating release notes for ${params.JDK_TAG}" + node ./fetchCommitList.js \ + --repository ${env.RESOLVED_GITHUB_REPOSITORY} \ + --baseTag ${params.BASE_JDK_TAG} \ + --tag ${params.JDK_TAG} \ + --filename ${params.JDK_TAG}-commits.json + """ + sh "cat ${params.JDK_TAG}-commits.json" + } + } + } + stage('Fetch Release Notes') { + steps { + dir('generate-release-notes/generate-release-notes') { + sh """ + node ./fetchReleaseNotes.js \ + --commitList ./${params.JDK_TAG}-commits.json \ + --filename ${env.RESOLVED_FILENAME} \ + --version ${env.RESOLVED_JDK_VERSION} + """ + } + } + } + stage('Archive Artifacts') { + steps { + dir('generate-release-notes/generate-release-notes') { + archiveArtifacts artifacts: "${params.JDK_TAG}-commits.json, ${env.RESOLVED_FILENAME}", + fingerprint: true + } + } + } + } + post { + failure { + echo "Release notes generation failed for ${params.JDK_TAG}" + } + success { + echo "Release notes successfully generated: ${env.RESOLVED_FILENAME}" + } + cleanup { + cleanWs() + } + } +} From 936ed5c921f0a57e18dc59de71747005e78b1428 Mon Sep 17 00:00:00 2001 From: smlambert Date: Wed, 26 Aug 2026 13:21:46 -0400 Subject: [PATCH 2/4] Add generate-release-notes Groovy pipeline wrapper --- generate-release-notes/Jenkinsfile | 115 ++++++++++++++---- .../lib/fetchGitHubCommits.js | 5 +- 2 files changed, 95 insertions(+), 25 deletions(-) diff --git a/generate-release-notes/Jenkinsfile b/generate-release-notes/Jenkinsfile index 7d83c35..61d6429 100644 --- a/generate-release-notes/Jenkinsfile +++ b/generate-release-notes/Jenkinsfile @@ -1,5 +1,16 @@ #!groovy +// Workaround to handle different versions of the Badge/Groovy Postbuild plugin. +def appendSummaryText(summary, text) { + try { + def currentText = summary.getText() ?: '' + summary.setText(currentText + text) + } catch (Exception e) { + echo "setText failed, trying deprecated appendText: ${e.message}" + summary.appendText(text, false) + } +} + // Returns true when the tag follows the pre-JEP-322 JDK 8 scheme (e.g. jdk8u502-ga, jdk8u492-b07). def isJdk8Tag(String tag) { return tag ==~ /^jdk8u.*/ @@ -8,8 +19,9 @@ def isJdk8Tag(String tag) { // Parses a JEP-322 tag (JDK 9+) and returns a map of its components. // // Supported formats: -// jdk-21.0.1+12 – standard release (no PATCH) -// jdk-21.0.12.1+1 – patch release; jdk-21.0.12.1+1 and jdk-21.0.12.1-ga point to the same SHA +// jdk-26-ga - Feature release (first introduction of a new JDK version) +// jdk-21.0.1+12 – CPU release (no PATCH) +// jdk-21.0.12.1+1 – CSPU release; jdk-21.0.12.1+1 and jdk-21.0.12.1-ga point to the same SHA // jdk-21.0.12-ga – GA alias for a standard release (no PATCH, no BUILD) // jdk-21.0.12.1-ga – GA alias for a patch release // @@ -51,11 +63,11 @@ def parseJdkTag(String tag) { } pipeline { - agent { label 'master' } + agent { label 'worker' } parameters { - string(name: 'JDK_TAG', defaultValue: '', description: 'Required. The JDK release tag, e.g. jdk-21.0.1+12 or jdk-21.0.12.1-ga (JEP-322 format: jdk-FEATURE.INTERIM.UPDATE[.PATCH][+BUILD][-QUALIFIER])') + string(name: 'JDK_TAG', defaultValue: '', description: 'Required. The JDK release tag, e.g. jdk-21.0.1+12 or jdk-21.0.12.1-ga. Needs to match the tag used for building and publishing binaries.') string(name: 'BASE_JDK_TAG', defaultValue: '', description: 'Required. The base (previous) JDK tag to compare against, e.g. jdk-21.0.0+35 or jdk-21.0.12-ga') - string(name: 'GITHUB_REPOSITORY', defaultValue: '', description: 'Optional (JDK 9+, required for JDK 8). GitHub repository, e.g. adoptium/21u or adoptium/jdk8u') + string(name: 'GITHUB_REPOSITORY', defaultValue: '', description: 'Optional (JDK 9+, required for JDK 8). GitHub repository, e.g. adoptium/jdk21u or adoptium/jdk8u') string(name: 'JDK_VERSION', defaultValue: '', description: 'Optional (JDK 9+, required for JDK 8). Version string for fetchReleaseNotes.js, e.g. 21.0.12.1 or openjdk8u462,8u501') string(name: 'FILENAME', defaultValue: '', description: 'Optional (JDK 9+, required for JDK 8). Output filename, e.g. OpenJDK21-jdk-release-notes_21.0.12.1_1.json or OpenJDK8U-jdk-release-notes_8.0.502_07.json') } @@ -105,7 +117,7 @@ pipeline { // otherwise derive from JDK_TAG. The ?: operator returns the left-hand // side when it is truthy (non-null, non-empty), so an explicit param // always wins over the calculated default. - env.RESOLVED_GITHUB_REPOSITORY = params.GITHUB_REPOSITORY ?: "adoptium/${parsed.feature}u" + env.RESOLVED_GITHUB_REPOSITORY = params.GITHUB_REPOSITORY ?: "adoptium/jdk${parsed.feature}u" env.RESOLVED_JDK_VERSION = params.JDK_VERSION ?: parsed.version env.RESOLVED_FILENAME = params.FILENAME ?: "OpenJDK${parsed.feature}-jdk-release-notes_${parsed.version}_${filenameSuffix}.json" @@ -127,35 +139,53 @@ pipeline { stage('Install Dependencies') { steps { dir('generate-release-notes/generate-release-notes') { - sh 'node --version' - sh 'npm install' + nvm(version: 'v24.19.0', + nvmInstallURL: 'https://raw.githubusercontent.com/creationix/nvm/v0.40.7/install.sh', + nvmNodeJsOrgMirror: 'https://nodejs.org/dist', + nvmIoJsOrgMirror: 'https://iojs.org/dist', + nvmInstallDir: '$HOME/.nvm') { + sh 'node --version' + sh 'npm install' + } } } } stage('Fetch Commit List') { steps { dir('generate-release-notes/generate-release-notes') { - sh """ - echo "Generating release notes for ${params.JDK_TAG}" - node ./fetchCommitList.js \ - --repository ${env.RESOLVED_GITHUB_REPOSITORY} \ - --baseTag ${params.BASE_JDK_TAG} \ - --tag ${params.JDK_TAG} \ - --filename ${params.JDK_TAG}-commits.json - """ - sh "cat ${params.JDK_TAG}-commits.json" + nvm(version: 'v24.19.0', + nvmInstallURL: 'https://raw.githubusercontent.com/creationix/nvm/v0.40.7/install.sh', + nvmNodeJsOrgMirror: 'https://nodejs.org/dist', + nvmIoJsOrgMirror: 'https://iojs.org/dist', + nvmInstallDir: '$HOME/.nvm') { + sh """ + echo "Generating release notes for ${params.JDK_TAG}" + node ./fetchCommitList.js \ + --repository ${env.RESOLVED_GITHUB_REPOSITORY} \ + --baseTag ${params.BASE_JDK_TAG} \ + --tag ${params.JDK_TAG} \ + --filename ${params.JDK_TAG}-commits.json + """ + sh "cat ${params.JDK_TAG}-commits.json" + } } } } stage('Fetch Release Notes') { steps { dir('generate-release-notes/generate-release-notes') { - sh """ - node ./fetchReleaseNotes.js \ - --commitList ./${params.JDK_TAG}-commits.json \ - --filename ${env.RESOLVED_FILENAME} \ - --version ${env.RESOLVED_JDK_VERSION} - """ + nvm(version: 'v24.19.0', + nvmInstallURL: 'https://raw.githubusercontent.com/creationix/nvm/v0.40.7/install.sh', + nvmNodeJsOrgMirror: 'https://nodejs.org/dist', + nvmIoJsOrgMirror: 'https://iojs.org/dist', + nvmInstallDir: '$HOME/.nvm') { + sh """ + node ./fetchReleaseNotes.js \ + --commitList ./${params.JDK_TAG}-commits.json \ + --filename ${env.RESOLVED_FILENAME} \ + --version ${env.RESOLVED_JDK_VERSION} + """ + } } } } @@ -173,7 +203,44 @@ pipeline { echo "Release notes generation failed for ${params.JDK_TAG}" } success { - echo "Release notes successfully generated: ${env.RESOLVED_FILENAME}" + script { + echo "Release notes successfully generated: ${env.RESOLVED_FILENAME}" + + // Build a pre-populated link to the release-tool publish job so the operator + // can publish the generated JSON with a single click. + // Mirrors the parambuild pattern used in ci-jenkins-pipelines/build_base_file.groovy. + def publishJobPath = 'build-scripts/release/refactor_openjdk_release_tool' + def releaseToolUrl = "${env.JENKINS_URL}job/${publishJobPath.replace('/', '/job/')}/parambuild?" + + // Derive the JDK major version for the VERSION parameter (e.g. "jdk21"). + def versionParam + if (isJdk8Tag(params.JDK_TAG)) { + versionParam = 'jdk8' + } else { + def parsed = parseJdkTag(params.JDK_TAG) + versionParam = "jdk${parsed.feature}" + } + + def encodedJobName = URLEncoder.encode(env.JOB_NAME, 'UTF-8') + def encodedArtifacts = URLEncoder.encode("**/${env.RESOLVED_FILENAME}", 'UTF-8') + def encodedTag = URLEncoder.encode(params.JDK_TAG, 'UTF-8') + + releaseToolUrl += "VERSION=${versionParam}" + releaseToolUrl += "&TAG=${encodedTag}" + releaseToolUrl += "&UPSTREAM_JOB_NAME=${encodedJobName}" + releaseToolUrl += "&UPSTREAM_JOB_NUMBER=${currentBuild.number}" + releaseToolUrl += "&ARTIFACTS_TO_COPY=${encodedArtifacts}" + releaseToolUrl += "&RELEASE=true" + releaseToolUrl += "&DRY_RUN=false" + + echo "Publish release notes — click to trigger: ${releaseToolUrl}" + + // Add a clickable summary badge to the Jenkins build page, matching the + // pattern used in ci-jenkins-pipelines/build_base_file.groovy. + def summary = manager.createSummary('document.svg') + appendSummaryText(summary, "Release notes generated: ${env.RESOLVED_FILENAME}
") + appendSummaryText(summary, "Publish release notes for ${params.JDK_TAG}") + } } cleanup { cleanWs() diff --git a/generate-release-notes/generate-release-notes/lib/fetchGitHubCommits.js b/generate-release-notes/generate-release-notes/lib/fetchGitHubCommits.js index c62ee06..019ce19 100644 --- a/generate-release-notes/generate-release-notes/lib/fetchGitHubCommits.js +++ b/generate-release-notes/generate-release-notes/lib/fetchGitHubCommits.js @@ -15,7 +15,10 @@ export const fetchCommits = async ({ console.log(`Fetching commits from ${githubQuery}`); const githubResponse = await fetch(githubQuery); if (!githubResponse.ok) { - throw new Error(`Failed to fetch commits from ${githubQuery}, status: ${githubResponse.status}`); + const hint = githubResponse.status === 404 + ? ` Check that the repository exists and both tags (${baseTag} and ${tag}) are present in it.` + : ''; + throw new Error(`Failed to fetch commits from ${githubQuery}, status: ${githubResponse.status}.${hint}`); } const githubResponseJson = await githubResponse.json(); From 13073404b94e7032e378647d6933c045b93dbadc Mon Sep 17 00:00:00 2001 From: smlambert Date: Wed, 26 Aug 2026 13:21:56 -0400 Subject: [PATCH 3/4] fix #200 --- .../generate-release-notes/fetchReleaseNotes.js | 11 ++++++++++- .../generate-release-notes/lib/fetchJiraIssues.js | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/generate-release-notes/generate-release-notes/fetchReleaseNotes.js b/generate-release-notes/generate-release-notes/fetchReleaseNotes.js index 6245e24..2d4ce0c 100755 --- a/generate-release-notes/generate-release-notes/fetchReleaseNotes.js +++ b/generate-release-notes/generate-release-notes/fetchReleaseNotes.js @@ -46,10 +46,19 @@ for (const commit of commits) { priority: null, component: null, subcomponent: null, - link: `https://bugs.openjdk.java.net/browse/${commit.id}`, + link: `https://bugs.openjdk.org/browse/${commit.id}`, type: null, backportOf: null, }; + } else if (releaseNote.type === 'Backport' && releaseNote.backportOf) { + // For backport issues, use the master bug ID and link so that the release + // notes refer to the canonical bug with a description rather than the + // backport ticket which is usually empty. + releaseNote = { + ...releaseNote, + id: releaseNote.backportOf, + link: `https://bugs.openjdk.org/browse/${releaseNote.backportOf}`, + }; } output.push(releaseNote); diff --git a/generate-release-notes/generate-release-notes/lib/fetchJiraIssues.js b/generate-release-notes/generate-release-notes/lib/fetchJiraIssues.js index 7ec3124..5d8b7ac 100644 --- a/generate-release-notes/generate-release-notes/lib/fetchJiraIssues.js +++ b/generate-release-notes/generate-release-notes/lib/fetchJiraIssues.js @@ -1,7 +1,7 @@ // Description: Fetches the JIRA issues from bugs.openjdk.org export default async function fetchReleaseNotes(version) { // fetch the release notes from the bugs.openjdk.org - const baseUrl = 'https://bugs.openjdk.java.net/rest/api/2/search?jql='; + const baseUrl = 'https://bugs.openjdk.org/rest/api/2/search?jql='; const jql = `project=JDK AND (status in (Closed, Resolved)) AND (resolution not in ("Won't Fix", "Duplicate", "Cannot Reproduce", "Not an Issue", "Withdrawn")) AND (labels not in (release-note, openjdk-na) OR labels is EMPTY) @@ -38,7 +38,7 @@ export default async function fetchReleaseNotes(version) { priority: issue.fields.priority.id, component: issue.fields.components[0].name, subcomponent: `${issue.fields.components[0].name}${issue.fields.customfield_10008?.name ? `/${issue.fields.customfield_10008?.name}` : ''}`, - link: `https://bugs.openjdk.java.net/browse/${issue.key}`, + link: `https://bugs.openjdk.org/browse/${issue.key}`, type: issue.fields.issuetype.name, backportOf: parent || null, }); From 5b31dd7c49548fe7bb432d7ad14f7f8e91e5b999 Mon Sep 17 00:00:00 2001 From: smlambert Date: Wed, 26 Aug 2026 14:05:29 -0400 Subject: [PATCH 4/4] Assisted-by: IBM Bob v2.0.3 --- generate-release-notes/Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/generate-release-notes/Jenkinsfile b/generate-release-notes/Jenkinsfile index 61d6429..3445f52 100644 --- a/generate-release-notes/Jenkinsfile +++ b/generate-release-notes/Jenkinsfile @@ -1,5 +1,6 @@ #!groovy +// Created with the assistance of IBM Bob v2.0.3 // Workaround to handle different versions of the Badge/Groovy Postbuild plugin. def appendSummaryText(summary, text) { try {