diff --git a/generate-release-notes/Jenkinsfile b/generate-release-notes/Jenkinsfile new file mode 100644 index 0000000..3445f52 --- /dev/null +++ b/generate-release-notes/Jenkinsfile @@ -0,0 +1,250 @@ +#!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 { + 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.*/ +} + +// Parses a JEP-322 tag (JDK 9+) and returns a map of its components. +// +// Supported formats: +// 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 +// +// 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 '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. 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/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') + } + 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/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" + + 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') { + 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') { + 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') { + 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} + """ + } + } + } + } + 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 { + 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/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/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(); 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, });