diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 019738a4f..b8e82abd7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ on: # See https://github.com/hibernate/hibernate-orm/pull/4615 for a description of the behavior we're getting. concurrency: # Consider that two builds are in the same concurrency group (cannot run concurrently) - # if they use the same workflow and are about the same branch ("ref"), pull request, and branch (for scheduled job). + # if they use the same workflow and are about the same branch ("ref"), pull request, and branch name input (for scheduled job). group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}, branch=${{ inputs.branch }}" # Cancel previous builds in the same concurrency group even if they are in process # for pull requests or pushes to forks (not the upstream repository). @@ -237,64 +237,3 @@ jobs: with: name: reports-java${{ matrix.java.name }} path: './**/build/reports/' - - snapshot: - name: Release snapshot - # Release the snapshots only if there are changes - if: github.event_name == 'push' && startsWith( github.ref, 'refs/heads/wip/' ) - runs-on: ubuntu-latest - steps: - - name: Checkout ${{ inputs.branch }} - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2.2.0 - with: - distribution: 'temurin' - java-version: 11 - - name: Create artifacts - run: ./gradlew assemble - - name: Detect the version of Hibernate Reactive - id: detect-version - run: | - sed -E 's/^projectVersion( *= *| +)([^ ]+)/::set-output name=version::\2/g' gradle/version.properties - - name: Publish snapshots to OSSRH, close repository and release - env: - ORG_GRADLE_PROJECT_sonatypeOssrhUser: ${{ secrets.SONATYPE_OSSRH_USER }} - ORG_GRADLE_PROJECT_sonatypeOssrhPassword: ${{ secrets.SONATYPE_OSSRH_PASSWORD }} - run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository - - release: - name: Release - # Releases only happen on tags - if: github.event_name == 'push' && startsWith( github.ref, 'refs/tags/' ) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set git username and email - run: | - git config --global user.email "hibernate@users.noreply.github.com" - git config --global user.name "hibernate" - - name: Set up JDK 11 - uses: actions/setup-java@v2.2.0 - with: - distribution: 'temurin' - java-version: 11 - - name: Create artifacts - run: ./gradlew assemble - - name: Install SSH key - uses: shimataro/ssh-key-action@v2 - with: - key: ${{ secrets.HIBERNATE_ORG_SSH_KEY }} - name: id_rsa_hibernate.org - known_hosts: ${{ secrets.HIBERNATE_ORG_SSH_KNOWN_HOSTS }} - config: | - Host github.com - User hibernate - IdentityFile ~/.ssh/id_rsa_hibernate.org - - name: Publish documentation on Hibernate.org - run: ./gradlew publishDocumentation -PdocPublishBranch=production - - name: Publish artifacts to OSSRH, close repository and release - env: - ORG_GRADLE_PROJECT_sonatypeOssrhUser: ${{ secrets.SONATYPE_OSSRH_USER }} - ORG_GRADLE_PROJECT_sonatypeOssrhPassword: ${{ secrets.SONATYPE_OSSRH_PASSWORD }} - run: ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.gitignore b/.gitignore index 750319d47..8016117ce 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,6 @@ bin # Vim *.swp *.swo + +# Release scripts downloaded from hibernate/hibernate-release-scripts +.release diff --git a/build.gradle b/build.gradle index cab3fdee7..43cd640fd 100644 --- a/build.gradle +++ b/build.gradle @@ -4,27 +4,16 @@ plugins { id 'java-library' id 'maven-publish' id 'com.diffplug.spotless' version '6.25.0' - id 'nu.studer.credentials' version '3.0' id 'org.asciidoctor.jvm.convert' version '4.0.2' apply false id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' } ext { - // Credentials can be specified on the command-line using project properties, - // or stored locally using the gradle-credentials-plugin. - // See below for the name of project properties. - // See https://github.com/etiennestuder/gradle-credentials-plugin to store credentials locally. - if ( !project.hasProperty( 'jbossNexusUser' ) ) { - jbossNexusUser = credentials.forKey( 'jbossNexusUser' ) - } - if ( !project.hasProperty( 'jbossNexusPassword' ) ) { - jbossNexusPassword = credentials.forKey( 'jbossNexusPassword' ) - } if ( !project.hasProperty( 'sonatypeOssrhUser' ) ) { - sonatypeOssrhUser = credentials.forKey( 'sonatypeOssrhUser' ) + sonatypeOssrhUser = null } if ( !project.hasProperty( 'sonatypeOssrhPassword' ) ) { - sonatypeOssrhPassword = credentials.forKey( 'sonatypeOssrhPassword' ) + sonatypeOssrhPassword = null } } @@ -95,10 +84,6 @@ ext { } // To release, see task ciRelease in release/build.gradle - -// To publish snapshots: -// ./gradlew publishToJBossNexus closeAndReleaseJBossNexusStagingRepository -PjbossNexusUser="" -PjbossNexusPassword="" - // To publish on Sonatype (Maven Central): // ./gradlew publishToSonatype closeAndReleaseStagingRepository -PsonatypeOssrhUser="" -PsonatypeOssrhPassword="" nexusPublishing { @@ -107,11 +92,6 @@ nexusPublishing { username = project.sonatypeOssrhUser password = project.sonatypeOssrhPassword } - jBossNexus { - snapshotRepositoryUrl = uri('https://repository.jboss.org/nexus/content/repositories/snapshots') - username = project.jbossNexusUser - password = project.jbossNexusPassword - } } } diff --git a/ci/release/Jenkinsfile b/ci/release/Jenkinsfile new file mode 100644 index 000000000..91469eb77 --- /dev/null +++ b/ci/release/Jenkinsfile @@ -0,0 +1,191 @@ +#! /usr/bin/groovy +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ + +/* + * See https://github.com/hibernate/hibernate-jenkins-pipeline-helpers + */ +@Library('hibernate-jenkins-pipeline-helpers@1.17') _ + +import org.hibernate.jenkins.pipeline.helpers.version.Version + +// -------------------------------------------- +// Global build configuration +env.PROJECT = "reactive" +env.JIRA_KEY = "HREACT" +def RELEASE_ON_PUSH = false // Set to `true` *only* on branches where you want a release on each push. + +print "INFO: env.PROJECT = ${env.PROJECT}" +print "INFO: env.JIRA_KEY = ${env.JIRA_KEY}" +print "INFO: RELEASE_ON_PUSH = ${RELEASE_ON_PUSH}" + +// -------------------------------------------- +// Build conditions + +// Avoid running the pipeline on branch indexing +if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { + print "INFO: Build skipped due to trigger being Branch Indexing" + currentBuild.result = 'NOT_BUILT' + return +} + +def manualRelease = currentBuild.getBuildCauses().toString().contains( 'UserIdCause' ) + +// Only do automatic release on branches where we opted in +if ( !manualRelease && !RELEASE_ON_PUSH ) { + print "INFO: Build skipped because automated releases are disabled on this branch. See constant RELEASE_ON_PUSH in ci/release/Jenkinsfile" + currentBuild.result = 'NOT_BUILT' + return +} + +// -------------------------------------------- +// Reusable methods + +def checkoutReleaseScripts() { + dir('.release/scripts') { + checkout scmGit(branches: [[name: '*/main']], extensions: [], + userRemoteConfigs: [[credentialsId: 'ed25519.Hibernate-CI.github.com', + url: 'https://github.com/hibernate/hibernate-release-scripts.git']]) + } +} + +// -------------------------------------------- +// Pipeline + +pipeline { + agent { + label 'Worker&&Containers' + } + tools { + jdk 'OpenJDK 11 Latest' + } + options { + buildDiscarder logRotator(daysToKeepStr: '30', numToKeepStr: '10') + rateLimitBuilds(throttle: [count: 1, durationName: 'day', userBoost: true]) + disableConcurrentBuilds(abortPrevious: false) + preserveStashes() + } + parameters { + string( + name: 'RELEASE_VERSION', + defaultValue: '', + description: 'The version to be released, e.g. 2.4.0.Final. Mandatory for manual releases, to prevent mistakes.', + trim: true + ) + string( + name: 'DEVELOPMENT_VERSION', + defaultValue: '', + description: 'The next version to be used after the release, e.g. 2.4.1-SNAPSHOT. If not set, determined automatically from the release version.', + trim: true + ) + booleanParam( + name: 'RELEASE_DRY_RUN', + defaultValue: false, + description: 'If true, just simulate the release, without pushing any commits or tags, and without uploading any artifacts or documentation.' + ) + } + stages { + stage('Release check') { + steps { + script { + checkoutReleaseScripts() + + def currentVersion = Version.parseDevelopmentVersion( sh( + script: ".release/scripts/determine-current-version.sh ${env.PROJECT}", + returnStdout: true + ).trim() ) + echo "Workspace version: ${currentVersion}" + + def releaseVersion + def developmentVersion + + if ( manualRelease ) { + echo "Release was requested manually" + + if ( !params.RELEASE_VERSION ) { + throw new IllegalArgumentException( 'Missing value for parameter RELEASE_VERSION. This parameter must be set explicitly to prevent mistakes.' ) + } + releaseVersion = Version.parseReleaseVersion( params.RELEASE_VERSION ) + + if ( !releaseVersion.toString().startsWith( currentVersion.family + '.' ) ) { + throw new IllegalArgumentException( "RELEASE_VERSION = $releaseVersion, which is different from the family of CURRENT_VERSION = $currentVersion. Did you make a mistake?" ) + } + } + else { + echo "Release was triggered automatically" + + // Avoid doing an automatic release for commits from a release + def lastCommitter = sh(script: 'git show -s --format=\'%an\'', returnStdout: true) + def secondLastCommitter = sh(script: 'git show -s --format=\'%an\' HEAD~1', returnStdout: true) + if (lastCommitter == 'Hibernate-CI' && secondLastCommitter == 'Hibernate-CI') { + print "INFO: Automatic release skipped because last commits were for the previous release" + currentBuild.result = 'ABORTED' + return + } + + releaseVersion = Version.parseReleaseVersion( sh( + script: ".release/scripts/determine-release-version.sh ${currentVersion}", + returnStdout: true + ).trim() ) + } + echo "Release version: ${releaseVersion}" + + if ( !params.DEVELOPMENT_VERSION ) { + developmentVersion = Version.parseDevelopmentVersion( sh( + script: ".release/scripts/determine-development-version.sh ${releaseVersion}", + returnStdout: true + ).trim() ) + } + else { + developmentVersion = Version.parseDevelopmentVersion( params.DEVELOPMENT_VERSION ) + } + echo "Development version: ${developmentVersion}" + + env.RELEASE_VERSION = releaseVersion.toString() + env.DEVELOPMENT_VERSION = developmentVersion.toString() + // Dry run is not supported at the moment + env.SCRIPT_OPTIONS = params.RELEASE_DRY_RUN ? "-d" : "" + + // Determine version id to check if Jira version exists + // This step doesn't work for Hibernate Reactive (the project has been created with a different type on JIRA) + // sh ".release/scripts/determine-jira-version-id.sh ${env.JIRA_KEY} ${releaseVersion.withoutFinalQualifier}" + } + } + } + stage('Publish release') { + steps { + script { + checkoutReleaseScripts() + + configFileProvider([ + configFile(fileId: 'release.config.ssh', targetLocation: "${env.HOME}/.ssh/config"), + configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") + ]) { + withCredentials([ + usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USER'), + usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'PLUGIN_PORTAL_PASSWORD', usernameVariable: 'PLUGIN_PORTAL_USERNAME'), + file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), + string(credentialsId: 'release.gpg.passphrase', variable: 'RELEASE_GPG_PASSPHRASE'), + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') + ]) { + sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { + // performs documentation upload and Sonatype release + // push to github + sh ".release/scripts/publish.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH}" + } + } + } + } + } + } + } + post { + always { + notifyBuildResult notifySuccessAfterSuccess: true, maintainers: 'davide@hibernate.org' + } + } +} \ No newline at end of file diff --git a/publish.gradle b/publish.gradle index 0ae8899fa..234091745 100644 --- a/publish.gradle +++ b/publish.gradle @@ -1,5 +1,4 @@ apply plugin: 'maven-publish' -apply plugin: 'nu.studer.credentials' task sourcesJar(type: Jar) { from sourceSets.main.allJava diff --git a/release/build.gradle b/release/build.gradle index 4961b9989..81f98cf88 100644 --- a/release/build.gradle +++ b/release/build.gradle @@ -28,46 +28,50 @@ description = 'Release module' // ./gradlew ciRelease -PreleaseVersion=x.y.z.Final -PdevelopmentVersion=x.y.z-SNAPSHOT -PgitRemote=origin -PgitBranch=main // The folder containing the rendered documentation -final String documentationDir = rootProject.project( 'documentation' ).buildDir +final String documentationDir = rootProject.layout.buildDirectory.dir( "documentation" ) // Relative path on the static website where the documentation is located final String docWebsiteRelativePath = "reactive/documentation/${projectVersion.family}" // The location of the docs when the website has been cloned -final String docWebsiteReactiveFolder = "${project.buildDir}/docs-website/${docWebsiteRelativePath}" +final String docWebsiteReactiveFolder = project.layout.buildDirectory.dir( "docs-website/${docWebsiteRelativePath}" ) /** * Assembles all documentation into the {buildDir}/documentation directory. */ -task assembleDocumentation(dependsOn: [rootProject.project( 'documentation' ).tasks.assemble]) { - group = 'Documentation' - description = 'Render the documentation' +tasks.register( 'assembleDocumentation' ) { + dependsOn ':documentation:assemble' + group 'Documentation' + description 'Render the documentation' } assemble.dependsOn assembleDocumentation /** * Clone the website */ -task removeDocsWebsite( type: Delete ) { - delete "${project.buildDir}/docs-website/" +tasks.register( 'removeDocsWebsite', Delete ) { + delete project.layout.buildDirectory.dir( "docs-website" ) } // Depending on compileJava makes sure that the buildDir exists. Otherwise this task will fail. -task cloneDocsWebsite( type: Exec, dependsOn: [removeDocsWebsite, compileJava] ) { - workingDir project.buildDir +tasks.register( 'cloneDocsWebsite', Exec ) { + dependsOn removeDocsWebsite, compileJava + workingDir project.layout.buildDirectory commandLine 'git', 'clone', docPublishRepoUri, '-b', docPublishBranch, '--sparse', '--depth', '1', 'docs-website' } -task sparseCheckoutDocumentation( type: Exec, dependsOn: cloneDocsWebsite ) { - workingDir "${project.buildDir}/docs-website" +tasks.register( 'sparseCheckoutDocumentation', Exec ) { + dependsOn cloneDocsWebsite + workingDir project.layout.buildDirectory.dir( "docs-website" ) commandLine 'git', 'sparse-checkout', 'set', docWebsiteRelativePath } /** * Update the docs on the cloned website */ -task updateDocumentation( dependsOn: [assembleDocumentation, sparseCheckoutDocumentation] ) { - description = "Update the documentation o the cloned static website" +tasks.register( 'updateDocumentation' ) { + dependsOn assembleDocumentation, sparseCheckoutDocumentation + description = "Update the documentation on the cloned static website" // copy documentation outputs into target/documentation: // * this is used in building the dist bundles @@ -94,22 +98,26 @@ task updateDocumentation( dependsOn: [assembleDocumentation, sparseCheckoutDocum /** * Push documentation changes on the remote repository */ -task stageDocChanges( type: Exec, dependsOn: updateDocumentation ) { - workingDir "${project.buildDir}/docs-website" +tasks.register( 'stageDocChanges', Exec ) { + dependsOn updateDocumentation + workingDir project.layout.buildDirectory.dir( "docs-website" ) commandLine 'git', 'add', '-A', '.' } -task commitDocChanges( type: Exec, dependsOn: stageDocChanges ) { +tasks.register( 'commitDocChanges', Exec ) { + dependsOn stageDocChanges workingDir "${project.buildDir}/docs-website" commandLine 'git', 'commit', '-m', "[HR] Hibernate Reactive documentation for ${projectVersion}" } -task pushDocChanges( type: Exec, dependsOn: commitDocChanges ) { - workingDir "${project.buildDir}/docs-website" +tasks.register( 'pushDocChanges', Exec ) { + dependsOn commitDocChanges + workingDir project.layout.buildDirectory.dir( "docs-website" ) commandLine 'git', 'push', '--atomic', 'origin', docPublishBranch } -task publishDocumentation(dependsOn: pushDocChanges) { +tasks.register( 'publishDocumentation' ) { + dependsOn pushDocChanges group = "Release" description = "Upload documentation on the website" @@ -118,16 +126,50 @@ task publishDocumentation(dependsOn: pushDocChanges) { } } +tasks.register( "releasePrepare" ) { + group = "Release" + description = "Performs release preparations on local check-out, including updating changelog" + doFirst { + if ( !project.hasProperty( 'releaseVersion' ) || !project.hasProperty( 'developmentVersion' ) + || !project.hasProperty( 'gitRemote' ) || !project.hasProperty( 'gitBranch' ) ) { + throw new GradleException( + "Task 'releasePrepare' requires all of the following properties to be set:" + + "'releaseVersion', 'developmentVersion', 'gitRemote' and 'gitBranch'." + ) + } + } + + doLast { + logger.lifecycle( "Switching to branch '${project.gitBranch}'..." ) + executeGitCommand( 'switch', project.gitBranch ) + + logger.lifecycle( "Checking that all commits are pushed..." ) + String diffWithUpstream = executeGitCommand( 'diff', '@{u}' ) + if ( !diffWithUpstream.isEmpty() ) { + throw new GradleException( + "Cannot release because there are commits on the branch to release that haven't been pushed yet." + + "\nPush your commits to the branch to release first." + ) + } + + logger.lifecycle( "Adding commit to update version to '${project.releaseVersion}'..." ) + project.projectVersionFile.text = "projectVersion=${project.releaseVersion}" + executeGitCommand( 'add', '.' ) + executeGitCommand( 'commit', '-m', project.releaseVersion ) + } +} + /* * Release everything */ -task ciRelease { +tasks.register( 'ciRelease' ) { group = "Release" description = "Triggers the release on CI: creates commits to change the version (release, then development), creates a tag, pushes everything. Then CI will take over and perform the release." + dependsOn releasePrepare doFirst { - if (!project.hasProperty('releaseVersion') || !project.hasProperty('developmentVersion') - || !project.hasProperty('gitRemote') ||!project.hasProperty('gitBranch')) { + if ( !project.hasProperty( 'releaseVersion' ) || !project.hasProperty( 'developmentVersion' ) + || !project.hasProperty( 'gitRemote' ) || !project.hasProperty( 'gitBranch' ) ) { throw new GradleException( "Task 'ciRelease' requires all of the following properties to be set:" + "'releaseVersion', 'developmentVersion', 'gitRemote' and 'gitBranch'." @@ -136,8 +178,8 @@ task ciRelease { } doLast { - logger.lifecycle("Checking that the working tree is clean...") - String uncommittedFiles = executeGitCommand('status', '--porcelain') + logger.lifecycle( "Checking that the working tree is clean..." ) + String uncommittedFiles = executeGitCommand( 'status', '--porcelain' ) if ( !uncommittedFiles.isEmpty() ) { throw new GradleException( "Cannot release because there are uncommitted or untracked files in the working tree." @@ -145,42 +187,25 @@ task ciRelease { + "\nUncommitted files:\n" + uncommittedFiles ) } - - logger.lifecycle("Switching to branch '${project.gitBranch}'...") - executeGitCommand('switch', project.gitBranch) - - logger.lifecycle("Checking that all commits are pushed...") - String diffWithUpstream = executeGitCommand('diff', '@{u}') - if ( !diffWithUpstream.isEmpty() ) { - throw new GradleException( - "Cannot release because there are commits on the branch to release that haven't been pushed yet." - + "\nPush your commits to the branch to release first." - ) - } - - logger.lifecycle("Adding commit to update version to '${project.releaseVersion}'...") - project.projectVersionFile.text = "projectVersion=${project.releaseVersion}" - executeGitCommand('add', '.') - executeGitCommand('commit', '-m', project.releaseVersion) String tag = project.releaseVersion if ( tag.endsWith( ".Final" ) ) { tag = tag.replace( ".Final", "" ) } + logger.lifecycle( "Tagging '${tag}'..." ) + executeGitCommand( 'tag', '-a', '-m', "Release ${project.releaseVersion}", tag ) - logger.lifecycle("Tagging '${tag}'...") - executeGitCommand('tag', '-a', '-m', "Release ${project.releaseVersion}", tag) - - logger.lifecycle("Adding commit to update version to '${project.developmentVersion}'...") + logger.lifecycle( "Adding commit to update version to '${project.developmentVersion}'..." ) project.projectVersionFile.text = "projectVersion=${project.developmentVersion}" - executeGitCommand('add', '.') - executeGitCommand('commit', '-m', project.developmentVersion) + executeGitCommand( 'add', '.' ) + executeGitCommand( 'commit', '-m', project.developmentVersion ) - logger.lifecycle("Pushing branch and tag to remote '${project.gitRemote}'...") - executeGitCommand('push', '--atomic', project.gitRemote, project.gitBranch, tag) + logger.lifecycle( "Pushing branch and tag to remote '${project.gitRemote}'..." ) + executeGitCommand( 'push', '--atomic', project.gitRemote, project.gitBranch, tag ) - logger.lifecycle("Done!") + logger.lifecycle( "Done!" ) - logger.lifecycle("Go to https://github.com/hibernate/hibernate-reactive/actions?query=branch%3A${tag} to check the progress of the automated release.") +// logger +// .lifecycle( "Go to https://github.com/hibernate/hibernate-reactive/actions?query=branch%3A${tag} to check the progress of the automated release." ) } } diff --git a/settings.gradle b/settings.gradle index df3e10039..a1e8817b5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,13 +30,19 @@ if ( hasProperty( 'main.jdk.version' ) || hasProperty( 'test.jdk.version' ) ) { gradle.ext.javaToolchainEnabled = true gradle.ext.javaVersions = [ main: [ - compiler: JavaLanguageVersion.of( hasProperty( 'main.jdk.version' ) - ? getProperty( 'main.jdk.version' ) : gradle.ext.baselineJavaVersion.asInt() ), + compiler: JavaLanguageVersion.of( + hasProperty( 'main.jdk.version' ) + ? getProperty( 'main.jdk.version' ) as Integer + : gradle.ext.baselineJavaVersion.asInt() + ), release: gradle.ext.baselineJavaVersion ], test: [ - compiler: JavaLanguageVersion.of( hasProperty( 'test.jdk.version' ) - ? getProperty( 'test.jdk.version' ) : gradle.ext.baselineJavaVersion.asInt() ) + compiler: JavaLanguageVersion.of( + hasProperty( 'test.jdk.version' ) + ? getProperty( 'test.jdk.version' ) as Integer + : gradle.ext.baselineJavaVersion.asInt() + ) ] ] def testCompilerVersion = gradle.ext.javaVersions.test.compiler