Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions generate-release-notes/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -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, "<b>Release notes generated: ${env.RESOLVED_FILENAME}</b><br/>")
appendSummaryText(summary, "<a href='${releaseToolUrl}'>Publish release notes for ${params.JDK_TAG}</a>")
}
}
cleanup {
cleanWs()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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,
});
Expand Down