From 148756076c22f0993fad4ab612232ec7b0fc5a36 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Mon, 11 Jul 2022 17:10:19 -0500 Subject: [PATCH] Backport release automation and github actions Closes gh-11501 --- .github/workflows/antora-generate.yml | 8 + .../continuous-integration-workflow.yml | 152 +++- .github/workflows/deploy-reference.yml | 17 +- .github/workflows/pr-build-workflow.yml | 9 +- .../update-scheduled-release-version.yml | 83 ++ build.gradle | 31 + buildSrc/build.gradle | 7 +- ...onPlugin.java => AntoraVersionPlugin.java} | 38 +- .../gradle/antora/AntoraVersionUtils.java | 55 ++ .../antora/UpdateAntoraVersionTask.java | 138 +++ .../gradle/github/RepositoryRef.java | 7 +- .../github/milestones/GitHubMilestoneApi.java | 189 +++- .../GitHubMilestoneHasNoOpenIssuesTask.java | 46 +- .../GitHubMilestoneNextReleaseTask.java | 93 ++ ...itHubMilestoneNextVersionDueTodayTask.java | 102 +++ .../milestones/GitHubMilestonePlugin.java | 54 +- .../github/milestones/LocalDateAdapter.java | 23 + .../milestones/LocalDateTimeAdapter.java | 25 + .../gradle/github/milestones/Milestone.java | 43 +- .../github/milestones/NextVersionYml.java | 29 + .../milestones/ScheduleNextReleaseTask.java | 147 +++ .../github/milestones/SpringReleaseTrain.java | 136 +++ .../milestones/SpringReleaseTrainSpec.java | 205 +++++ .../release/DispatchGitHubWorkflowTask.java | 84 ++ .../github/release/GitHubActionsApi.java | 98 ++ .../github/release/GitHubReleasePlugin.java | 37 +- .../github/release/WorkflowDispatch.java | 51 ++ .../convention/versions/CommandLineUtils.java | 49 + .../convention/versions/FileUtils.java | 49 + .../versions/UpdateDependenciesPlugin.java | 62 +- .../versions/UpdateProjectVersionPlugin.java | 44 + .../versions/UpdateProjectVersionTask.java | 63 ++ .../versions/UpdateToSnapshotVersionTask.java | 68 ++ .../milestones/GitHubMilestoneApiTests.java | 389 -------- ...sts.java => AntoraVersionPluginTests.java} | 62 +- .../milestones/GitHubMilestoneApiTests.java | 836 ++++++++++++++++++ .../milestones/SpringReleaseTrainTests.java | 245 +++++ .../github/release/GitHubActionsApiTests.java | 89 ++ .../github/release/GitHubReleaseApiTests.java | 26 +- 39 files changed, 3299 insertions(+), 590 deletions(-) create mode 100644 .github/workflows/update-scheduled-release-version.yml rename buildSrc/src/main/java/org/springframework/gradle/antora/{CheckAntoraVersionPlugin.java => AntoraVersionPlugin.java} (72%) create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java create mode 100644 buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java create mode 100644 buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java delete mode 100644 buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java rename buildSrc/src/test/java/org/springframework/gradle/antora/{CheckAntoraVersionPluginTests.java => AntoraVersionPluginTests.java} (78%) create mode 100644 buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java create mode 100644 buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java diff --git a/.github/workflows/antora-generate.yml b/.github/workflows/antora-generate.yml index 089f0ac041..80f1a79a6a 100644 --- a/.github/workflows/antora-generate.yml +++ b/.github/workflows/antora-generate.yml @@ -16,6 +16,14 @@ jobs: steps: - name: Checkout Source uses: actions/checkout@v2 + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Generate antora.yml run: ./gradlew :spring-security-docs:generateAntora - name: Extract Branch Name diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 422b58c4f3..ca79130b4a 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -24,11 +24,17 @@ jobs: runs-on: ubuntu-latest outputs: runjobs: ${{ steps.continue.outputs.runjobs }} + project_version: ${{ steps.continue.outputs.project_version }} steps: + - uses: actions/checkout@v2 - id: continue name: Determine if should continue if: env.RUN_JOBS == 'true' - run: echo "::set-output name=runjobs::true" + run: | + echo "::set-output name=runjobs::true" + # Extract version from gradle.properties + version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') + echo "::set-output name=project_version::$version" build_jdk_11: name: Build JDK 11 needs: [prerequisites] @@ -47,11 +53,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties - - name: Cache Gradle packages - uses: actions/cache@v2 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Build with Gradle env: GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} @@ -73,6 +78,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Snapshot Tests run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -94,6 +103,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Check samples project env: LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos @@ -119,6 +132,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Check for package tangles run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -139,6 +156,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Deploy artifacts run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -166,6 +187,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Deploy Docs run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -190,6 +215,10 @@ jobs: run: | mkdir -p ~/.gradle echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle - name: Deploy Schema run: | export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" @@ -200,14 +229,121 @@ jobs: DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }} DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }} DOCS_HOST: ${{ secrets.DOCS_HOST }} + perform_release: + name: Perform release + needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema] + runs-on: ubuntu-latest + timeout-minutes: 90 + if: ${{ !endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }} + env: + REPO: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + TOKEN: ${{ github.token }} + VERSION: ${{ needs.prerequisites.outputs.project_version }} + steps: + - uses: actions/checkout@v2 + with: + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Setup gradle user name + run: | + mkdir -p ~/.gradle + echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle + - name: Wait for Artifactory Artifacts + if: ${{ contains(needs.prerequisites.outputs.project_version, '-RC') || contains(needs.prerequisites.outputs.project_version, '-M') }} + run: | + echo "Wait for artifacts of $REPO@$VERSION to appear on Artifactory." + until curl -f -s https://repo.spring.io/artifactory/milestone/org/springframework/security/spring-security-core/$VERSION/ > /dev/null + do + sleep 30 + echo "." + done + echo "Artifacts for $REPO@$VERSION have been released to Artifactory." + - name: Wait for Maven Central Artifacts + if: ${{ !contains(needs.prerequisites.outputs.project_version, '-RC') && !contains(needs.prerequisites.outputs.project_version, '-M') }} + run: | + echo "Wait for artifacts of $REPO@$VERSION to appear on Maven Central." + until curl -f -s https://repo1.maven.org/maven2/org/springframework/security/spring-security-core/$VERSION/ > /dev/null + do + sleep 30 + echo "." + done + echo "Artifacts for $REPO@$VERSION have been released to Maven Central." + - name: Create GitHub Release + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + echo "Tagging and publishing $REPO@$VERSION release on GitHub." + ./gradlew createGitHubRelease -PnextVersion=$VERSION -Pbranch=$BRANCH -PcreateRelease=true -PgitHubAccessToken=$TOKEN + - name: Announce Release on Slack + id: spring-security-announcing + uses: slackapi/slack-github-action@v1.19.0 + with: + payload: | + { + "text": "spring-security-announcing `${{ env.VERSION }}` is available now", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "spring-security-announcing `${{ env.VERSION }}` is available now" + } + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SPRING_RELEASE_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + - name: Setup git config + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + - name: Update to next Snapshot Version + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + echo "Updating $REPO@$VERSION to next snapshot version." + ./gradlew :updateToSnapshotVersion + ./gradlew :spring-security-docs:antoraUpdateVersion + git commit -am "Next development version" + git push + perform_post_release: + name: Perform post-release + needs: [prerequisites, deploy_artifacts, deploy_docs, deploy_schema] + runs-on: ubuntu-latest + timeout-minutes: 90 + if: ${{ endsWith(needs.prerequisites.outputs.project_version, '-SNAPSHOT') }} + env: + TOKEN: ${{ github.token }} + VERSION: ${{ needs.prerequisites.outputs.project_version }} + steps: + - uses: actions/checkout@v2 + - uses: spring-io/spring-gradle-build-action@v1 + with: + java-version: '11' + distribution: 'adopt' + - name: Schedule next release (if not already scheduled) + run: ./gradlew scheduleNextRelease -PnextVersion=$VERSION -PgitHubAccessToken=$TOKEN notify_result: name: Check for failures - needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema] + needs: [build_jdk_11, snapshot_tests, check_samples, check_tangles, deploy_artifacts, deploy_docs, deploy_schema, perform_release, perform_post_release] if: failure() runs-on: ubuntu-latest steps: - name: Send Slack message - uses: Gamesight/slack-workflow-status@v1.0.1 + # Workaround while waiting for Gamesight/slack-workflow-status#38 to be fixed + # See https://github.com/Gamesight/slack-workflow-status/issues/38 + uses: sjohnr/slack-workflow-status@v1-beta with: repo_token: ${{ secrets.GITHUB_TOKEN }} slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/deploy-reference.yml b/.github/workflows/deploy-reference.yml index a0033b926b..2b493ebd36 100644 --- a/.github/workflows/deploy-reference.yml +++ b/.github/workflows/deploy-reference.yml @@ -18,16 +18,19 @@ jobs: with: java-version: '11' distribution: 'adopt' - cache: gradle - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle + with: + # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. + # Restoring these files from a GitHub Actions cache might cause problems for future builds. + gradle-home-cache-excludes: | + caches/modules-2/modules-2.lock + caches/modules-2/gc.properties - name: Build with Gradle run: ./gradlew :spring-security-docs:antora --stacktrace - - name: Cleanup Gradle Cache - # Remove some files from the Gradle cache, so they aren't cached by GitHub Actions. - # Restoring these files from a GitHub Actions cache might cause problems for future builds. - run: | - rm -f ~/.gradle/caches/modules-2/modules-2.lock - rm -f ~/.gradle/caches/modules-2/gc.properties - name: Deploy run: ${GITHUB_WORKSPACE}/.github/actions/algolia-deploy.sh "${{ secrets.DOCS_USERNAME }}@${{ secrets.DOCS_HOST }}" "/opt/www/domains/spring.io/docs/htdocs/spring-security/reference/" "${{ secrets.DOCS_SSH_KEY }}" "${{ secrets.DOCS_SSH_HOST_KEY }}" diff --git a/.github/workflows/pr-build-workflow.yml b/.github/workflows/pr-build-workflow.yml index 0e7d5e7fdf..ac62acb676 100644 --- a/.github/workflows/pr-build-workflow.yml +++ b/.github/workflows/pr-build-workflow.yml @@ -17,12 +17,13 @@ jobs: uses: actions/setup-java@v1 with: java-version: '11' - - name: Cache Gradle packages + - name: Setup Gradle if: env.RUN_JOBS == 'true' - uses: actions/cache@v2 + uses: gradle/gradle-build-action@v2 with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + cache-read-only: true + env: + GRADLE_USER_HOME: ~/.gradle - name: Build with Gradle if: env.RUN_JOBS == 'true' run: ./gradlew clean build --continue --scan diff --git a/.github/workflows/update-scheduled-release-version.yml b/.github/workflows/update-scheduled-release-version.yml new file mode 100644 index 0000000000..d9ae79c77f --- /dev/null +++ b/.github/workflows/update-scheduled-release-version.yml @@ -0,0 +1,83 @@ +name: Update Scheduled Release Version + +on: + workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main. + +env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + GRADLE_ENTERPRISE_CACHE_USER: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }} + GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }} + GRADLE_ENTERPRISE_SECRET_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }} + +jobs: + update_scheduled_release_version: + name: Initiate Release If Scheduled + if: ${{ github.repository == 'spring-projects/spring-security' }} + runs-on: ubuntu-latest + steps: + - id: checkout-source + name: Checkout Source Code + uses: actions/checkout@v2 + with: + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + - id: setup-jdk + name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: '11' + - name: Setup gradle user name + run: | + mkdir -p ~/.gradle + echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + env: + GRADLE_USER_HOME: ~/.gradle + - id: check-release-due + name: Check Release Due + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + ./gradlew gitHubCheckNextVersionDueToday + echo "::set-output name=is_due_today::$(cat build/github/milestones/is-due-today)" + - id: check-open-issues + name: Check for open issues + if: steps.check-release-due.outputs.is_due_today == 'true' + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + ./gradlew gitHubCheckMilestoneHasNoOpenIssues + echo "::set-output name=is_open_issues::$(cat build/github/milestones/is-open-issues)" + - id: validate-release-state + name: Validate State of Release + if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'true' + run: | + echo "The release is due today but there are open issues" + exit 1 + - id: update-version-and-push + name: Update version and push + if: steps.check-release-due.outputs.is_due_today == 'true' && steps.check-open-issues.outputs.is_open_issues == 'false' + run: | + export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER" + export GRADLE_ENTERPRISE_CACHE_PASSWORD="$GRADLE_ENTERPRISE_CACHE_PASSWORD" + export GRADLE_ENTERPRISE_ACCESS_KEY="$GRADLE_ENTERPRISE_SECRET_ACCESS_KEY" + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + ./gradlew :updateProjectVersion + ./gradlew :spring-security-docs:antoraUpdateVersion + updatedVersion=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}') + git commit -am "Release $updatedVersion" + git tag $updatedVersion + git push + git push origin $updatedVersion + - id: send-slack-notification + name: Send Slack message + if: failure() + uses: Gamesight/slack-workflow-status@v1.0.1 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + channel: '#spring-security-ci' + name: 'CI Notifier' diff --git a/build.gradle b/build.gradle index e52918acd1..33a5c58c6c 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ apply plugin: 'io.spring.convention.root' apply plugin: 'io.spring.convention.include-check-remote' apply plugin: 'org.jetbrains.kotlin.jvm' apply plugin: 'org.springframework.security.update-dependencies' +apply plugin: 'org.springframework.security.update-version' apply plugin: 'org.springframework.security.sagan' apply plugin: 'org.springframework.github.milestone' apply plugin: 'org.springframework.github.changelog' @@ -47,6 +48,29 @@ tasks.named("gitHubCheckMilestoneHasNoOpenIssues") { } } +tasks.named("gitHubNextReleaseMilestone") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + +tasks.named("gitHubCheckNextVersionDueToday") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + +tasks.named("scheduleNextRelease") { + repository { + owner = "spring-projects" + name = "spring-security" + } + weekOfMonth = 3 + dayOfWeek = 1 +} + tasks.named("createGitHubRelease") { repository { owner = "spring-projects" @@ -54,6 +78,13 @@ tasks.named("createGitHubRelease") { } } +tasks.named("dispatchGitHubWorkflow") { + repository { + owner = "spring-projects" + name = "spring-security" + } +} + tasks.named("updateDependencies") { // we aren't Gradle 7 compatible yet checkForGradleUpdate = false diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 37651ef076..e64aef29d2 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -5,7 +5,6 @@ plugins { id 'com.apollographql.apollo' version '2.4.5' } - sourceCompatibility = 1.8 repositories { @@ -29,7 +28,7 @@ gradlePlugin { plugins { checkAntoraVersion { id = "org.springframework.antora.check-version" - implementationClass = "org.springframework.gradle.antora.CheckAntoraVersionPlugin" + implementationClass = "org.springframework.gradle.antora.AntoraVersionPlugin" } trang { id = "trang" @@ -47,6 +46,10 @@ gradlePlugin { id = "org.springframework.security.update-dependencies" implementationClass = "org.springframework.security.convention.versions.UpdateDependenciesPlugin" } + updateProjectVersion { + id = "org.springframework.security.update-version" + implementationClass = "org.springframework.security.convention.versions.UpdateProjectVersionPlugin" + } sagan { id = "org.springframework.security.sagan" implementationClass = "org.springframework.gradle.sagan.SaganPlugin" diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java similarity index 72% rename from buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java rename to buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java index 464b7ce677..9a1f95d5b4 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/antora/CheckAntoraVersionPlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionPlugin.java @@ -8,7 +8,7 @@ import org.gradle.api.Task; import org.gradle.api.tasks.TaskProvider; import org.gradle.language.base.plugins.LifecycleBasePlugin; -public class CheckAntoraVersionPlugin implements Plugin { +public class AntoraVersionPlugin implements Plugin { public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion"; @Override @@ -35,32 +35,29 @@ public class CheckAntoraVersionPlugin implements Plugin { }); } }); + project.getTasks().register("antoraUpdateVersion", UpdateAntoraVersionTask.class, new Action() { + @Override + public void execute(UpdateAntoraVersionTask antoraUpdateVersion) { + antoraUpdateVersion.setGroup("Release"); + antoraUpdateVersion.setDescription("Updates the antora.yml version properties to match the Gradle version"); + antoraUpdateVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml"))); + } + }); } private static String getDefaultAntoraVersion(Project project) { String projectVersion = getProjectVersion(project); - int preReleaseIndex = getSnapshotIndex(projectVersion); - return isSnapshot(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + return AntoraVersionUtils.getDefaultAntoraVersion(projectVersion); } private static String getDefaultAntoraPrerelease(Project project) { String projectVersion = getProjectVersion(project); - if (isSnapshot(projectVersion)) { - int preReleaseIndex = getSnapshotIndex(projectVersion); - return projectVersion.substring(preReleaseIndex); - } - if (isPreRelease(projectVersion)) { - return Boolean.TRUE.toString(); - } - return null; + return AntoraVersionUtils.getDefaultAntoraPrerelease(projectVersion); } private static String getDefaultAntoraDisplayVersion(Project project) { String projectVersion = getProjectVersion(project); - if (!isSnapshot(projectVersion) && isPreRelease(projectVersion)) { - return getDefaultAntoraVersion(project); - } - return null; + return AntoraVersionUtils.getDefaultAntoraDisplayVersion(projectVersion); } private static String getProjectVersion(Project project) { @@ -71,15 +68,4 @@ public class CheckAntoraVersionPlugin implements Plugin { return String.valueOf(projectVersion); } - private static boolean isSnapshot(String projectVersion) { - return getSnapshotIndex(projectVersion) >= 0; - } - - private static int getSnapshotIndex(String projectVersion) { - return projectVersion.lastIndexOf("-SNAPSHOT"); - } - - private static boolean isPreRelease(String projectVersion) { - return projectVersion.lastIndexOf("-") >= 0; - } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java new file mode 100644 index 0000000000..9bb17b553e --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/AntoraVersionUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.antora; + +public class AntoraVersionUtils { + + public static String getDefaultAntoraVersion(String projectVersion) { + int preReleaseIndex = getSnapshotIndex(projectVersion); + return isSnapshot(projectVersion) ? projectVersion.substring(0, preReleaseIndex) : projectVersion; + } + + public static String getDefaultAntoraPrerelease(String projectVersion) { + if (isSnapshot(projectVersion)) { + int preReleaseIndex = getSnapshotIndex(projectVersion); + return projectVersion.substring(preReleaseIndex); + } + if (isPreRelease(projectVersion)) { + return Boolean.TRUE.toString(); + } + return null; + } + + public static String getDefaultAntoraDisplayVersion(String projectVersion) { + if (!isSnapshot(projectVersion) && isPreRelease(projectVersion)) { + return getDefaultAntoraVersion(projectVersion); + } + return null; + } + + private static boolean isSnapshot(String projectVersion) { + return getSnapshotIndex(projectVersion) >= 0; + } + + private static int getSnapshotIndex(String projectVersion) { + return projectVersion.lastIndexOf("-SNAPSHOT"); + } + + private static boolean isPreRelease(String projectVersion) { + return projectVersion.lastIndexOf("-") >= 0; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java b/buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java new file mode 100644 index 0000000000..95c403e247 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/antora/UpdateAntoraVersionTask.java @@ -0,0 +1,138 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.antora; + +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Project; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.nodes.NodeTuple; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; + +import org.springframework.gradle.github.milestones.NextVersionYml; + +public abstract class UpdateAntoraVersionTask extends DefaultTask { + + @TaskAction + public void update() throws IOException { + String projectVersion = getProject().getVersion().toString(); + File antoraYmlFile = getAntoraYmlFile().getAsFile().get(); + String updatedAntoraVersion = AntoraVersionUtils.getDefaultAntoraVersion(projectVersion); + String updatedAntoraPrerelease = AntoraVersionUtils.getDefaultAntoraPrerelease(projectVersion); + String updatedAntoraDisplayVersion = AntoraVersionUtils.getDefaultAntoraDisplayVersion(projectVersion); + + Representer representer = new Representer(); + representer.getPropertyUtils().setSkipMissingProperties(true); + + Yaml yaml = new Yaml(new Constructor(AntoraYml.class), representer); + AntoraYml antoraYml = yaml.load(new FileInputStream(antoraYmlFile)); + + System.out.println("Updating the version parameters in " + antoraYmlFile.getName() + " to version: " + + updatedAntoraVersion + ", prerelease: " + updatedAntoraPrerelease + ", display_version: " + + updatedAntoraDisplayVersion); + antoraYml.setVersion(updatedAntoraVersion); + antoraYml.setPrerelease(updatedAntoraPrerelease); + antoraYml.setDisplay_version(updatedAntoraDisplayVersion); + + FileWriter outputWriter = new FileWriter(antoraYmlFile); + getYaml().dump(antoraYml, outputWriter); + } + + @InputFile + public abstract RegularFileProperty getAntoraYmlFile(); + + public static class AntoraYml { + + private String name; + + private String version; + + private String prerelease; + + private String display_version; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public String getPrerelease() { + return prerelease; + } + + public void setPrerelease(String prerelease) { + this.prerelease = prerelease; + } + + public String getDisplay_version() { + return display_version; + } + + public void setDisplay_version(String display_version) { + this.display_version = display_version; + } + + } + + private Yaml getYaml() { + Representer representer = new Representer() { + @Override + protected NodeTuple representJavaBeanProperty(Object javaBean, + org.yaml.snakeyaml.introspector.Property property, Object propertyValue, Tag customTag) { + // Don't write out null values + if (propertyValue == null) { + return null; + } + else { + return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag); + } + } + }; + representer.addClassTag(AntoraYml.class, Tag.MAP); + DumperOptions ymlOptions = new DumperOptions(); + ymlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + ymlOptions.setDefaultScalarStyle(DumperOptions.ScalarStyle.SINGLE_QUOTED); + return new Yaml(representer, ymlOptions); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java index e570a47e90..1791c4fc9f 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/RepositoryRef.java @@ -1,6 +1,11 @@ package org.springframework.gradle.github; -public class RepositoryRef { +import java.io.Serializable; + +public class RepositoryRef implements Serializable { + + private static final long serialVersionUID = 7151218536746822797L; + private String owner; private String name; diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java index fd3c0d817b..3e0b839bd7 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneApi.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,13 +17,21 @@ package org.springframework.gradle.github.milestones; import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.google.common.reflect.TypeToken; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import okhttp3.Interceptor; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import org.springframework.gradle.github.RepositoryRef; @@ -33,7 +41,10 @@ public class GitHubMilestoneApi { private OkHttpClient client; - private Gson gson = new Gson(); + private final Gson gson = new GsonBuilder() + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) + .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter().nullSafe()) + .create(); public GitHubMilestoneApi() { this.client = new OkHttpClient.Builder().build(); @@ -50,26 +61,30 @@ public class GitHubMilestoneApi { } public long findMilestoneNumberByTitle(RepositoryRef repositoryRef, String milestoneTitle) { + List milestones = this.getMilestones(repositoryRef); + for (Milestone milestone : milestones) { + if (milestoneTitle.equals(milestone.getTitle())) { + return milestone.getNumber(); + } + } + if (milestones.size() <= 100) { + throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + } + throw new RuntimeException("It is possible there are too many open milestones (only 100 are supported). Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + } + + public List getMilestones(RepositoryRef repositoryRef) { String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + "/milestones?per_page=100"; Request request = new Request.Builder().get().url(url) .build(); try { Response response = this.client.newCall(request).execute(); if (!response.isSuccessful()) { - throw new RuntimeException("Could not find milestone with title " + milestoneTitle + " for repository " + repositoryRef + ". Response " + response); + throw new RuntimeException("Could not retrieve milestones for repository " + repositoryRef + ". Response " + response); } - List milestones = this.gson.fromJson(response.body().charStream(), new TypeToken>(){}.getType()); - for (Milestone milestone : milestones) { - if (milestoneTitle.equals(milestone.getTitle())) { - return milestone.getNumber(); - } - } - if (milestones.size() <= 100) { - throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); - } - throw new RuntimeException("It is possible there are too many open milestones open (only 100 are supported). Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + return this.gson.fromJson(response.body().charStream(), new TypeToken>(){}.getType()); } catch (IOException e) { - throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef, e); + throw new RuntimeException("Could not retrieve milestones for repository " + repositoryRef, e); } } @@ -89,10 +104,150 @@ public class GitHubMilestoneApi { } } -// public boolean isOpenIssuesForMilestoneName(String owner, String repository, String milestoneName) { -// -// } + /** + * Check if the given milestone is due today or past due. + * + * @param repositoryRef The repository owner/name + * @param milestoneTitle The title of the milestone whose due date should be checked + * @return true if the given milestone is due today or past due, false otherwise + */ + public boolean isMilestoneDueToday(RepositoryRef repositoryRef, String milestoneTitle) { + String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + + "/milestones?per_page=100"; + Request request = new Request.Builder().get().url(url).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException("Could not find milestone with title " + milestoneTitle + " for repository " + + repositoryRef + ". Response " + response); + } + List milestones = this.gson.fromJson(response.body().charStream(), + new TypeToken>() { + }.getType()); + for (Milestone milestone : milestones) { + if (milestoneTitle.equals(milestone.getTitle())) { + LocalDate today = LocalDate.now(); + return milestone.getDueOn() != null && today.compareTo(milestone.getDueOn().toLocalDate()) >= 0; + } + } + if (milestones.size() <= 100) { + throw new RuntimeException("Could not find open milestone with title " + milestoneTitle + + " for repository " + repositoryRef + " Got " + milestones); + } + throw new RuntimeException( + "It is possible there are too many open milestones open (only 100 are supported). Could not find open milestone with title " + + milestoneTitle + " for repository " + repositoryRef + " Got " + milestones); + } + catch (IOException e) { + throw new RuntimeException( + "Could not find open milestone with title " + milestoneTitle + " for repository " + repositoryRef, + e); + } + } + /** + * Calculate the next release version based on the current version. + * + * The current version must conform to the pattern MAJOR.MINOR.PATCH-SNAPSHOT. If the + * current version is a snapshot of a patch release, then the patch release will be + * returned. For example, if the current version is 5.6.1-SNAPSHOT, then 5.6.1 will be + * returned. If the current version is a snapshot of a version that is not GA (i.e the + * PATCH segment is 0), then GitHub will be queried to find the next milestone or + * release candidate. If no pre-release versions are found, then the next version will + * be assumed to be the GA. + * @param repositoryRef The repository owner/name + * @param currentVersion The current project version + * @return the next matching milestone/release candidate or null if none exist + */ + public String getNextReleaseMilestone(RepositoryRef repositoryRef, String currentVersion) { + Pattern snapshotPattern = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)-SNAPSHOT$"); + Matcher snapshotVersion = snapshotPattern.matcher(currentVersion); + + if (snapshotVersion.find()) { + String patchSegment = snapshotVersion.group(3); + String currentVersionNoIdentifier = currentVersion.replace("-SNAPSHOT", ""); + if (patchSegment.equals("0")) { + String nextPreRelease = getNextPreRelease(repositoryRef, currentVersionNoIdentifier); + return nextPreRelease != null ? nextPreRelease : currentVersionNoIdentifier; + } + else { + return currentVersionNoIdentifier; + } + } + else { + throw new IllegalStateException( + "Cannot calculate next release version because the current project version does not conform to the expected format"); + } + } + + /** + * Calculate the next pre-release version (milestone or release candidate) based on + * the current version. + * + * The current version must conform to the pattern MAJOR.MINOR.PATCH. If no matching + * milestone or release candidate is found in GitHub then it will return null. + * @param repositoryRef The repository owner/name + * @param currentVersionNoIdentifier The current project version without any + * identifier + * @return the next matching milestone/release candidate or null if none exist + */ + private String getNextPreRelease(RepositoryRef repositoryRef, String currentVersionNoIdentifier) { + String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + + "/milestones?per_page=100"; + Request request = new Request.Builder().get().url(url).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException( + "Could not get milestones for repository " + repositoryRef + ". Response " + response); + } + List milestones = this.gson.fromJson(response.body().charStream(), + new TypeToken>() { + }.getType()); + Optional nextPreRelease = milestones.stream().map(Milestone::getTitle) + .filter(m -> m.startsWith(currentVersionNoIdentifier + "-")) + .min((m1, m2) -> { + Pattern preReleasePattern = Pattern.compile("^.*-([A-Z]+)([0-9]+)$"); + Matcher matcher1 = preReleasePattern.matcher(m1); + Matcher matcher2 = preReleasePattern.matcher(m2); + matcher1.find(); + matcher2.find(); + if (!matcher1.group(1).equals(matcher2.group(1))) { + return m1.compareTo(m2); + } + else { + return Integer.valueOf(matcher1.group(2)).compareTo(Integer.valueOf(matcher2.group(2))); + } + }); + return nextPreRelease.orElse(null); + } + catch (IOException e) { + throw new RuntimeException("Could not find open milestones with for repository " + repositoryRef, e); + } + } + + /** + * Create a milestone. + * + * @param repository The repository owner/name + * @param milestone The milestone containing a title and due date + */ + public void createMilestone(RepositoryRef repository, Milestone milestone) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/milestones"; + String json = this.gson.toJson(milestone); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create milestone %s for repository %s/%s. Got response %s", + milestone.getTitle(), repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create release %s for repository %s/%s", + milestone.getTitle(), repository.getOwner(), repository.getName()), ex); + } + } private static class AuthorizationInterceptor implements Interceptor { diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java index 40b026c804..f3fc7b4df2 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneHasNoOpenIssuesTask.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,32 +17,66 @@ package org.springframework.gradle.github.milestones; import org.gradle.api.Action; import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import org.springframework.gradle.github.RepositoryRef; -public class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask { +@DisableCachingByDefault(because = "the due date needs to be checked every time in case it changes") +public abstract class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask { @Input private RepositoryRef repository = new RepositoryRef(); - @Input + @Input @Optional private String milestoneTitle; + @InputFile @Optional + public abstract RegularFileProperty getNextVersionFile(); + @Input @Optional private String gitHubAccessToken; + @OutputFile + public abstract RegularFileProperty getIsOpenIssuesFile(); + private GitHubMilestoneApi milestones = new GitHubMilestoneApi(); @TaskAction - public void checkHasNoOpenIssues() { + public void checkHasNoOpenIssues() throws IOException { + if (this.milestoneTitle == null) { + File nextVersionFile = getNextVersionFile().getAsFile().get(); + Yaml yaml = new Yaml(new Constructor(NextVersionYml.class)); + NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile)); + String nextVersion = nextVersionYml.getVersion(); + if (nextVersion == null) { + throw new IllegalArgumentException( + "Could not find version property in provided file " + nextVersionFile.getName()); + } + this.milestoneTitle = nextVersion; + } long milestoneNumber = this.milestones.findMilestoneNumberByTitle(this.repository, this.milestoneTitle); boolean isOpenIssues = this.milestones.isOpenIssuesForMilestoneNumber(this.repository, milestoneNumber); + Path isOpenIssuesPath = getIsOpenIssuesFile().getAsFile().get().toPath(); + Files.write(isOpenIssuesPath, String.valueOf(isOpenIssues).getBytes()); if (isOpenIssues) { - throw new IllegalStateException("The repository " + this.repository + " has open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); + System.out.println("The repository " + this.repository + " has open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); + } + else { + System.out.println("The repository " + this.repository + " has no open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); } - System.out.println("The repository " + this.repository + " has no open issues for milestone with the title " + this.milestoneTitle + " and number " + milestoneNumber); } public RepositoryRef getRepository() { diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java new file mode 100644 index 0000000000..87605c0886 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextReleaseTask.java @@ -0,0 +1,93 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.milestones; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.nodes.Tag; +import org.yaml.snakeyaml.representer.Representer; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +import org.springframework.gradle.github.RepositoryRef; + +public abstract class GitHubMilestoneNextReleaseTask extends DefaultTask { + + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + @Optional + private String gitHubAccessToken; + + private GitHubMilestoneApi milestones = new GitHubMilestoneApi(); + + @TaskAction + public void calculateNextReleaseMilestone() throws IOException { + String currentVersion = getProject().getVersion().toString(); + String nextPreRelease = this.milestones.getNextReleaseMilestone(this.repository, currentVersion); + System.out.println("The next release milestone is: " + nextPreRelease); + NextVersionYml nextVersionYml = new NextVersionYml(); + nextVersionYml.setVersion(nextPreRelease); + File outputFile = getNextReleaseFile().get().getAsFile(); + FileWriter outputWriter = new FileWriter(outputFile); + Yaml yaml = getYaml(); + yaml.dump(nextVersionYml, outputWriter); + } + + @OutputFile + public abstract RegularFileProperty getNextReleaseFile(); + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + this.milestones = new GitHubMilestoneApi(gitHubAccessToken); + } + + private Yaml getYaml() { + Representer representer = new Representer(); + representer.addClassTag(NextVersionYml.class, Tag.MAP); + DumperOptions ymlOptions = new DumperOptions(); + ymlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + return new Yaml(representer, ymlOptions); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java new file mode 100644 index 0000000000..1ff7ec51f0 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestoneNextVersionDueTodayTask.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.milestones; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.work.DisableCachingByDefault; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.springframework.gradle.github.RepositoryRef; + +@DisableCachingByDefault(because = "the due date needs to be checked every time in case it changes") +public abstract class GitHubMilestoneNextVersionDueTodayTask extends DefaultTask { + + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + @Optional + private String gitHubAccessToken; + + @InputFile + public abstract RegularFileProperty getNextVersionFile(); + + @OutputFile + public abstract RegularFileProperty getIsDueTodayFile(); + + private GitHubMilestoneApi milestones = new GitHubMilestoneApi(); + + @TaskAction + public void checkReleaseDueToday() throws IOException { + File nextVersionFile = getNextVersionFile().getAsFile().get(); + Yaml yaml = new Yaml(new Constructor(NextVersionYml.class)); + NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile)); + String nextVersion = nextVersionYml.getVersion(); + if (nextVersion == null) { + throw new IllegalArgumentException( + "Could not find version property in provided file " + nextVersionFile.getName()); + } + boolean milestoneDueToday = this.milestones.isMilestoneDueToday(this.repository, nextVersion); + Path isDueTodayPath = getIsDueTodayFile().getAsFile().get().toPath(); + Files.writeString(isDueTodayPath, String.valueOf(milestoneDueToday)); + if (milestoneDueToday) { + System.out.println("The milestone with the title " + nextVersion + " in the repository " + this.repository + + " is due today"); + } + else { + System.out.println("The milestone with the title " + nextVersion + " in the repository " + + this.repository + " is not due yet"); + } + + } + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + this.milestones = new GitHubMilestoneApi(gitHubAccessToken); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java index 81663f2561..ef5568424c 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/GitHubMilestonePlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2020 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,55 @@ package org.springframework.gradle.github.milestones; -import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; +import org.gradle.api.tasks.TaskProvider; public class GitHubMilestonePlugin implements Plugin { @Override public void apply(Project project) { - project.getTasks().register("gitHubCheckMilestoneHasNoOpenIssues", GitHubMilestoneHasNoOpenIssuesTask.class, new Action() { - @Override - public void execute(GitHubMilestoneHasNoOpenIssuesTask githubCheckMilestoneHasNoOpenIssues) { - githubCheckMilestoneHasNoOpenIssues.setGroup("Release"); - githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone"); - githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion")); - if (project.hasProperty("gitHubAccessToken")) { - githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); - } + TaskProvider nextReleaseMilestoneTask = project.getTasks().register("gitHubNextReleaseMilestone", GitHubMilestoneNextReleaseTask.class, (gitHubMilestoneNextReleaseTask) -> { + gitHubMilestoneNextReleaseTask.doNotTrackState("API call to GitHub needs to check for new milestones every time"); + gitHubMilestoneNextReleaseTask.setGroup("Release"); + gitHubMilestoneNextReleaseTask.setDescription("Calculates the next release version based on the current version and outputs it to a yaml file"); + gitHubMilestoneNextReleaseTask.getNextReleaseFile() + .fileProvider(project.provider(() -> project.file("next-release.yml"))); + if (project.hasProperty("gitHubAccessToken")) { + gitHubMilestoneNextReleaseTask + .setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); } }); + project.getTasks().register("gitHubCheckMilestoneHasNoOpenIssues", GitHubMilestoneHasNoOpenIssuesTask.class, (githubCheckMilestoneHasNoOpenIssues) -> { + githubCheckMilestoneHasNoOpenIssues.setGroup("Release"); + githubCheckMilestoneHasNoOpenIssues.setDescription("Checks if there are any open issues for the specified repository and milestone"); + githubCheckMilestoneHasNoOpenIssues.getIsOpenIssuesFile().value(project.getLayout().getBuildDirectory().file("github/milestones/is-open-issues")); + githubCheckMilestoneHasNoOpenIssues.setMilestoneTitle((String) project.findProperty("nextVersion")); + if (!project.hasProperty("nextVersion")) { + githubCheckMilestoneHasNoOpenIssues.getNextVersionFile().convention( + nextReleaseMilestoneTask.flatMap(GitHubMilestoneNextReleaseTask::getNextReleaseFile)); + } + if (project.hasProperty("gitHubAccessToken")) { + githubCheckMilestoneHasNoOpenIssues.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + } + }); + project.getTasks().register("gitHubCheckNextVersionDueToday", GitHubMilestoneNextVersionDueTodayTask.class, (gitHubMilestoneNextVersionDueTodayTask) -> { + gitHubMilestoneNextVersionDueTodayTask.setGroup("Release"); + gitHubMilestoneNextVersionDueTodayTask.setDescription("Checks if the next release version is due today or past due, will fail if the next version is not due yet"); + gitHubMilestoneNextVersionDueTodayTask.getIsDueTodayFile().value(project.getLayout().getBuildDirectory().file("github/milestones/is-due-today")); + gitHubMilestoneNextVersionDueTodayTask.getNextVersionFile().convention( + nextReleaseMilestoneTask.flatMap(GitHubMilestoneNextReleaseTask::getNextReleaseFile)); + if (project.hasProperty("gitHubAccessToken")) { + gitHubMilestoneNextVersionDueTodayTask + .setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + } + }); + project.getTasks().register("scheduleNextRelease", ScheduleNextReleaseTask.class, (scheduleNextRelease) -> { + scheduleNextRelease.doNotTrackState("API call to GitHub needs to check for new milestones every time"); + scheduleNextRelease.setGroup("Release"); + scheduleNextRelease.setDescription("Schedule the next release (even months only) or release train (series of milestones starting in January or July) based on the current version"); + + scheduleNextRelease.setVersion((String) project.findProperty("nextVersion")); + scheduleNextRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + }); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java new file mode 100644 index 0000000000..b98e21afb7 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateAdapter.java @@ -0,0 +1,23 @@ +package org.springframework.gradle.github.milestones; + +import java.io.IOException; +import java.time.LocalDate; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * @author Steve Riesenberg + */ +class LocalDateAdapter extends TypeAdapter { + @Override + public void write(JsonWriter jsonWriter, LocalDate localDate) throws IOException { + jsonWriter.value(localDate.toString()); + } + + @Override + public LocalDate read(JsonReader jsonReader) throws IOException { + return LocalDate.parse(jsonReader.nextString()); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java new file mode 100644 index 0000000000..875658748f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/LocalDateTimeAdapter.java @@ -0,0 +1,25 @@ +package org.springframework.gradle.github.milestones; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +/** + * @author Steve Riesenberg + */ +class LocalDateTimeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException { + jsonWriter.value(localDateTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + } + + @Override + public LocalDateTime read(JsonReader jsonReader) throws IOException { + return LocalDateTime.parse(jsonReader.nextString(), DateTimeFormatter.ISO_ZONED_DATE_TIME); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java index 5d0ff23489..83aab12159 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/Milestone.java @@ -1,9 +1,35 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.springframework.gradle.github.milestones; +import com.google.gson.annotations.SerializedName; + +import java.time.LocalDateTime; + +/** + * @author Steve Riesenberg + */ public class Milestone { private String title; - private long number; + private Long number; + + @SerializedName("due_on") + private LocalDateTime dueOn; public String getTitle() { return title; @@ -13,19 +39,28 @@ public class Milestone { this.title = title; } - public long getNumber() { + public Long getNumber() { return number; } - public void setNumber(long number) { + public void setNumber(Long number) { this.number = number; } + public LocalDateTime getDueOn() { + return dueOn; + } + + public void setDueOn(LocalDateTime dueOn) { + this.dueOn = dueOn; + } + @Override public String toString() { return "Milestone{" + "title='" + title + '\'' + - ", number=" + number + + ", number='" + number + '\'' + + ", dueOn='" + dueOn + '\'' + '}'; } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java new file mode 100644 index 0000000000..5dce3f06bc --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/NextVersionYml.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.milestones; + +public class NextVersionYml { + private String version; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java new file mode 100644 index 0000000000..ecaa3d2c87 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/ScheduleNextReleaseTask.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.milestones; + +import java.time.LocalDate; +import java.time.LocalTime; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * @author Steve Riesenberg + */ +public class ScheduleNextReleaseTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + private String gitHubAccessToken; + + @Input + private String version; + + @Input + private Integer weekOfMonth; + + @Input + private Integer dayOfWeek; + + @TaskAction + public void scheduleNextRelease() { + GitHubMilestoneApi gitHubMilestoneApi = new GitHubMilestoneApi(this.gitHubAccessToken); + String nextReleaseMilestone = gitHubMilestoneApi.getNextReleaseMilestone(this.repository, this.version); + + // If the next release contains a dash (e.g. 5.6.0-RC1), it is already scheduled + if (nextReleaseMilestone.contains("-")) { + return; + } + + // Check to see if a scheduled GA version already exists + boolean hasExistingMilestone = gitHubMilestoneApi.getMilestones(this.repository).stream() + .anyMatch(milestone -> nextReleaseMilestone.equals(milestone.getTitle())); + if (hasExistingMilestone) { + return; + } + + // Next milestone is either a patch version or minor version + // Note: Major versions will be handled like minor and get a release + // train which can be manually updated to match the desired schedule. + if (nextReleaseMilestone.endsWith(".0")) { + // Create M1, M2, M3, RC1 and GA milestones for release train + getReleaseTrain(nextReleaseMilestone).getTrainDates().forEach((milestoneTitle, dueOn) -> { + Milestone milestone = new Milestone(); + milestone.setTitle(milestoneTitle); + // Note: GitHub seems to store full date/time as UTC then displays + // as a date (no time) in your timezone, which means the date will + // not always be the same date as we intend. + // Using 12pm/noon UTC allows GitHub to schedule and display the + // correct date. + milestone.setDueOn(dueOn.atTime(LocalTime.NOON)); + gitHubMilestoneApi.createMilestone(this.repository, milestone); + }); + } else { + // Create GA milestone for patch release on the next even month + LocalDate startDate = LocalDate.now(); + LocalDate dueOn = getReleaseTrain(nextReleaseMilestone).getNextReleaseDate(startDate); + Milestone milestone = new Milestone(); + milestone.setTitle(nextReleaseMilestone); + milestone.setDueOn(dueOn.atTime(LocalTime.NOON)); + gitHubMilestoneApi.createMilestone(this.repository, milestone); + } + } + + private SpringReleaseTrain getReleaseTrain(String nextReleaseMilestone) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .nextTrain() + .version(nextReleaseMilestone) + .weekOfMonth(this.weekOfMonth) + .dayOfWeek(this.dayOfWeek) + .build(); + + return new SpringReleaseTrain(releaseTrainSpec); + } + + public RepositoryRef getRepository() { + return this.repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return this.gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getVersion() { + return this.version; + } + + public void setVersion(String version) { + this.version = version; + } + + public Integer getWeekOfMonth() { + return weekOfMonth; + } + + public void setWeekOfMonth(Integer weekOfMonth) { + this.weekOfMonth = weekOfMonth; + } + + public Integer getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(Integer dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java new file mode 100644 index 0000000000..e0ed561eb6 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrain.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.milestones; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import java.time.temporal.TemporalAdjuster; +import java.time.temporal.TemporalAdjusters; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Spring release train generator based on rules contained in a specification. + *

+ * The rules are: + *

    + *
  1. Train 1 (January-May) or 2 (July-November)
  2. + *
  3. Version number (e.g. 0.1.2, 1.0.0, etc.)
  4. + *
  5. Week of month (1st, 2nd, 3rd, 4th)
  6. + *
  7. Day of week (Monday-Friday)
  8. + *
  9. Year (e.g. 2020, 2021, etc.)
  10. + *
+ * + * The release train generated will contain M1, M2, M3, RC1 and GA versions + * mapped to their respective dates in the train. + * + * @author Steve Riesenberg + */ +public final class SpringReleaseTrain { + private final SpringReleaseTrainSpec releaseTrainSpec; + + public SpringReleaseTrain(SpringReleaseTrainSpec releaseTrainSpec) { + this.releaseTrainSpec = releaseTrainSpec; + } + + /** + * Calculate release train dates based on the release train specification. + * + * @return A mapping of release milestones to scheduled release dates + */ + public Map getTrainDates() { + Map releaseDates = new LinkedHashMap<>(); + switch (this.releaseTrainSpec.getTrain()) { + case ONE: + addTrainDate(releaseDates, "M1", Month.JANUARY); + addTrainDate(releaseDates, "M2", Month.FEBRUARY); + addTrainDate(releaseDates, "M3", Month.MARCH); + addTrainDate(releaseDates, "RC1", Month.APRIL); + addTrainDate(releaseDates, null, Month.MAY); + break; + case TWO: + addTrainDate(releaseDates, "M1", Month.JULY); + addTrainDate(releaseDates, "M2", Month.AUGUST); + addTrainDate(releaseDates, "M3", Month.SEPTEMBER); + addTrainDate(releaseDates, "RC1", Month.OCTOBER); + addTrainDate(releaseDates, null, Month.NOVEMBER); + break; + } + + return releaseDates; + } + + /** + * Determine if a given date matches the due date of given version. + * + * @param version The version number (e.g. 5.6.0-M1, 5.6.0, etc.) + * @param expectedDate The expected date + * @return true if the given date matches the due date of the given version, false otherwise + */ + public boolean isTrainDate(String version, LocalDate expectedDate) { + return expectedDate.isEqual(getTrainDates().get(version)); + } + + /** + * Calculate the next release date following the given date. + *

+ * The next release date is always on an even month so that a patch release + * is the month after the GA version of a release train. This method does + * not consider the year of the release train, only the given start date. + * + * @param startDate The start date + * @return The next release date following the given date + */ + public LocalDate getNextReleaseDate(LocalDate startDate) { + LocalDate trainDate; + LocalDate currentDate = startDate; + do { + trainDate = calculateReleaseDate( + Year.of(currentDate.getYear()), + currentDate.getMonth(), + this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(), + this.releaseTrainSpec.getWeekOfMonth().getDayOffset() + ); + currentDate = currentDate.plusMonths(1); + } while (!trainDate.isAfter(startDate) || trainDate.getMonthValue() % 2 != 0); + + return trainDate; + } + + private void addTrainDate(Map releaseDates, String milestone, Month month) { + LocalDate releaseDate = calculateReleaseDate( + this.releaseTrainSpec.getYear(), + month, + this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(), + this.releaseTrainSpec.getWeekOfMonth().getDayOffset() + ); + String suffix = (milestone == null) ? "" : "-" + milestone; + releaseDates.put(this.releaseTrainSpec.getVersion() + suffix, releaseDate); + } + + private static LocalDate calculateReleaseDate(Year year, Month month, DayOfWeek dayOfWeek, int dayOffset) { + TemporalAdjuster nextMonday = TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY); + TemporalAdjuster nextDayOfWeek = TemporalAdjusters.nextOrSame(dayOfWeek); + + LocalDate firstDayOfMonth = year.atMonth(month).atDay(1); + LocalDate firstMondayOfMonth = firstDayOfMonth.with(nextMonday); + + return firstMondayOfMonth.with(nextDayOfWeek).plusDays(dayOffset); + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java new file mode 100644 index 0000000000..792e390c00 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/milestones/SpringReleaseTrainSpec.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.milestones; + +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; + +import org.springframework.util.Assert; + +/** + * A specification for a release train. + * + * @author Steve Riesenberg + * @see SpringReleaseTrain + */ +public final class SpringReleaseTrainSpec { + private final Train train; + private final String version; + private final WeekOfMonth weekOfMonth; + private final DayOfWeek dayOfWeek; + private final Year year; + + public SpringReleaseTrainSpec(Train train, String version, WeekOfMonth weekOfMonth, DayOfWeek dayOfWeek, Year year) { + this.train = train; + this.version = version; + this.weekOfMonth = weekOfMonth; + this.dayOfWeek = dayOfWeek; + this.year = year; + } + + public Train getTrain() { + return train; + } + + public String getVersion() { + return version; + } + + public WeekOfMonth getWeekOfMonth() { + return weekOfMonth; + } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public Year getYear() { + return year; + } + + public static Builder builder() { + return new Builder(); + } + + public enum WeekOfMonth { + FIRST(0), SECOND(7), THIRD(14), FOURTH(21); + + private final int dayOffset; + + WeekOfMonth(int dayOffset) { + this.dayOffset = dayOffset; + } + + public int getDayOffset() { + return dayOffset; + } + } + + public enum DayOfWeek { + MONDAY(java.time.DayOfWeek.MONDAY), + TUESDAY(java.time.DayOfWeek.TUESDAY), + WEDNESDAY(java.time.DayOfWeek.WEDNESDAY), + THURSDAY(java.time.DayOfWeek.THURSDAY), + FRIDAY(java.time.DayOfWeek.FRIDAY); + + private final java.time.DayOfWeek dayOfWeek; + + DayOfWeek(java.time.DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + + public java.time.DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + } + + public enum Train { + ONE, TWO + } + + public static class Builder { + private Train train; + private String version; + private WeekOfMonth weekOfMonth; + private DayOfWeek dayOfWeek; + private Year year; + + private Builder() { + } + + public Builder train(int train) { + switch (train) { + case 1: this.train = Train.ONE; break; + case 2: this.train = Train.TWO; break; + default: throw new IllegalArgumentException("Invalid train: " + train); + } + return this; + } + + public Builder train(Train train) { + this.train = train; + return this; + } + + public Builder nextTrain() { + // Search for next train starting with this month + return nextTrain(LocalDate.now().withDayOfMonth(1)); + } + + public Builder nextTrain(LocalDate startDate) { + Train nextTrain = null; + + // Search for next train from a given start date + LocalDate currentDate = startDate; + while (nextTrain == null) { + if (currentDate.getMonth() == Month.JANUARY) { + nextTrain = Train.ONE; + } else if (currentDate.getMonth() == Month.JULY) { + nextTrain = Train.TWO; + } + + currentDate = currentDate.plusMonths(1); + } + + return train(nextTrain).year(currentDate.getYear()); + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public Builder weekOfMonth(int weekOfMonth) { + switch (weekOfMonth) { + case 1: this.weekOfMonth = WeekOfMonth.FIRST; break; + case 2: this.weekOfMonth = WeekOfMonth.SECOND; break; + case 3: this.weekOfMonth = WeekOfMonth.THIRD; break; + case 4: this.weekOfMonth = WeekOfMonth.FOURTH; break; + default: throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth); + } + return this; + } + + public Builder weekOfMonth(WeekOfMonth weekOfMonth) { + this.weekOfMonth = weekOfMonth; + return this; + } + + public Builder dayOfWeek(int dayOfWeek) { + switch (dayOfWeek) { + case 1: this.dayOfWeek = DayOfWeek.MONDAY; break; + case 2: this.dayOfWeek = DayOfWeek.TUESDAY; break; + case 3: this.dayOfWeek = DayOfWeek.WEDNESDAY; break; + case 4: this.dayOfWeek = DayOfWeek.THURSDAY; break; + case 5: this.dayOfWeek = DayOfWeek.FRIDAY; break; + default: throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek); + } + return this; + } + + public Builder dayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + return this; + } + + public Builder year(int year) { + this.year = Year.of(year); + return this; + } + + public SpringReleaseTrainSpec build() { + Assert.notNull(train, "train cannot be null"); + Assert.notNull(version, "version cannot be null"); + Assert.notNull(weekOfMonth, "weekOfMonth cannot be null"); + Assert.notNull(dayOfWeek, "dayOfWeek cannot be null"); + Assert.notNull(year, "year cannot be null"); + return new SpringReleaseTrainSpec(train, version, weekOfMonth, dayOfWeek, year); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java new file mode 100644 index 0000000000..3afc056517 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/DispatchGitHubWorkflowTask.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * @author Steve Riesenberg + */ +public class DispatchGitHubWorkflowTask extends DefaultTask { + @Input + private RepositoryRef repository = new RepositoryRef(); + + @Input + private String gitHubAccessToken; + + @Input + private String branch; + + @Input + private String workflowId; + + @TaskAction + public void dispatchGitHubWorkflow() { + GitHubActionsApi gitHubActionsApi = new GitHubActionsApi(this.gitHubAccessToken); + WorkflowDispatch workflowDispatch = new WorkflowDispatch(this.branch, null); + gitHubActionsApi.dispatchWorkflow(this.repository, this.workflowId, workflowDispatch); + } + + public RepositoryRef getRepository() { + return repository; + } + + public void repository(Action repository) { + repository.execute(this.repository); + } + + public void setRepository(RepositoryRef repository) { + this.repository = repository; + } + + public String getGitHubAccessToken() { + return gitHubAccessToken; + } + + public void setGitHubAccessToken(String gitHubAccessToken) { + this.gitHubAccessToken = gitHubAccessToken; + } + + public String getBranch() { + return branch; + } + + public void setBranch(String branch) { + this.branch = branch; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java new file mode 100644 index 0000000000..3fb2034c1d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubActionsApi.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.io.IOException; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +import org.springframework.gradle.github.RepositoryRef; + +/** + * Manage GitHub Actions. + * + * @author Steve Riesenberg + */ +public class GitHubActionsApi { + private String baseUrl = "https://api.github.com"; + + private final OkHttpClient client; + + private final Gson gson = new GsonBuilder().create(); + + public GitHubActionsApi() { + this.client = new OkHttpClient.Builder().build(); + } + + public GitHubActionsApi(String gitHubToken) { + this.client = new OkHttpClient.Builder() + .addInterceptor(new AuthorizationInterceptor(gitHubToken)) + .build(); + } + + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * Create a workflow dispatch event. + * + * @param repository The repository owner/name + * @param workflowId The ID of the workflow or the name of the workflow file name + * @param workflowDispatch The workflow dispatch containing a ref (branch) and optional inputs + */ + public void dispatchWorkflow(RepositoryRef repository, String workflowId, WorkflowDispatch workflowDispatch) { + String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/actions/workflows/" + workflowId + "/dispatches"; + String json = this.gson.toJson(workflowDispatch); + RequestBody body = RequestBody.create(MediaType.parse("application/json"), json); + Request request = new Request.Builder().url(url).post(body).build(); + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + throw new RuntimeException(String.format("Could not create workflow dispatch %s for repository %s/%s. Got response %s", + workflowId, repository.getOwner(), repository.getName(), response)); + } + } catch (IOException ex) { + throw new RuntimeException(String.format("Could not create workflow dispatch %s for repository %s/%s", + workflowId, repository.getOwner(), repository.getName()), ex); + } + } + + private static class AuthorizationInterceptor implements Interceptor { + private final String token; + + public AuthorizationInterceptor(String token) { + this.token = token; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer " + this.token) + .build(); + + return chain.proceed(request); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java index ae2c44a769..7bb5e4e7b8 100644 --- a/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/GitHubReleasePlugin.java @@ -17,7 +17,6 @@ package org.springframework.gradle.github.release; import groovy.lang.MissingPropertyException; -import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -27,23 +26,29 @@ import org.gradle.api.Project; public class GitHubReleasePlugin implements Plugin { @Override public void apply(Project project) { - project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, new Action() { - @Override - public void execute(CreateGitHubReleaseTask createGitHubRelease) { - createGitHubRelease.setGroup("Release"); - createGitHubRelease.setDescription("Create a github release"); - createGitHubRelease.dependsOn("generateChangelog"); + project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, (createGitHubRelease) -> { + createGitHubRelease.setGroup("Release"); + createGitHubRelease.setDescription("Create a github release"); + createGitHubRelease.dependsOn("generateChangelog"); - createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease"))); - createGitHubRelease.setVersion((String) project.findProperty("nextVersion")); - if (project.hasProperty("branch")) { - createGitHubRelease.setBranch((String) project.findProperty("branch")); - } - createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); - if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) { - throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=..."); - } + createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease"))); + createGitHubRelease.setVersion((String) project.findProperty("nextVersion")); + if (project.hasProperty("branch")) { + createGitHubRelease.setBranch((String) project.findProperty("branch")); } + createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); + if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) { + throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=..."); + } + }); + + project.getTasks().register("dispatchGitHubWorkflow", DispatchGitHubWorkflowTask.class, (dispatchGitHubWorkflow) -> { + dispatchGitHubWorkflow.setGroup("Release"); + dispatchGitHubWorkflow.setDescription("Create a workflow_dispatch event on a given branch"); + + dispatchGitHubWorkflow.setBranch((String) project.findProperty("branch")); + dispatchGitHubWorkflow.setWorkflowId((String) project.findProperty("workflowId")); + dispatchGitHubWorkflow.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken")); }); } } diff --git a/buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java b/buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java new file mode 100644 index 0000000000..531531bebf --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/gradle/github/release/WorkflowDispatch.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.util.Map; + +/** + * @author Steve Riesenberg + */ +public class WorkflowDispatch { + private String ref; + private Map inputs; + + public WorkflowDispatch() { + } + + public WorkflowDispatch(String ref, Map inputs) { + this.ref = ref; + this.inputs = inputs; + } + + public String getRef() { + return ref; + } + + public void setRef(String ref) { + this.ref = ref; + } + + public Map getInputs() { + return inputs; + } + + public void setInputs(Map inputs) { + this.inputs = inputs; + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java new file mode 100644 index 0000000000..ae073aff8b --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/CommandLineUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.convention.versions; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Scanner; + +class CommandLineUtils { + static void runCommand(File dir, String... args) { + try { + Process process = new ProcessBuilder() + .directory(dir) + .command(args) + .start(); + writeLinesTo(process.getInputStream(), System.out); + writeLinesTo(process.getErrorStream(), System.out); + if (process.waitFor() != 0) { + new RuntimeException("Failed to run " + Arrays.toString(args)); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Failed to run " + Arrays.toString(args), e); + } + } + + private static void writeLinesTo(InputStream input, PrintStream out) { + Scanner scanner = new Scanner(input); + while(scanner.hasNextLine()) { + out.println(scanner.nextLine()); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java new file mode 100644 index 0000000000..0be520f451 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/FileUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.convention.versions; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.function.Function; + +class FileUtils { + static void replaceFileText(File file, Function replaceText) { + String buildFileText = readString(file); + String updatedBuildFileText = replaceText.apply(buildFileText); + writeString(file, updatedBuildFileText); + } + + static String readString(File file) { + try { + byte[] bytes = Files.readAllBytes(file.toPath()); + return new String(bytes); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static void writeString(File file, String text) { + try { + Files.write(file.toPath(), text.getBytes()); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java index 4d9af9efe6..4f68fc656e 100644 --- a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateDependenciesPlugin.java @@ -33,13 +33,8 @@ import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import reactor.core.publisher.Mono; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.nio.file.Files; import java.time.Duration; import java.util.*; -import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -168,7 +163,7 @@ public class UpdateDependenciesPlugin implements Plugin { Integer issueNumber = gitHubApi.createIssue(createIssueResult.getRepositoryId(), title, createIssueResult.getLabelIds(), createIssueResult.getMilestoneId(), createIssueResult.getAssigneeId()).delayElement(Duration.ofSeconds(1)).block(); commitMessage += "\n\nCloses gh-" + issueNumber; } - runCommand(rootDir, "git", "commit", "-am", commitMessage); + CommandLineUtils.runCommand(rootDir, "git", "commit", "-am", commitMessage); } private Mono createIssueResultMono(UpdateDependenciesExtension updateDependenciesExtension) { @@ -187,7 +182,7 @@ public class UpdateDependenciesPlugin implements Plugin { if (current.compareTo(running) > 0) { String title = "Update Gradle to " + current.getVersion(); System.out.println(title); - runCommand(project.getRootDir(), "./gradlew", "wrapper", "--gradle-version", current.getVersion(), "--no-daemon"); + CommandLineUtils.runCommand(project.getRootDir(), "./gradlew", "wrapper", "--gradle-version", current.getVersion(), "--no-daemon"); afterGroup(updateDependenciesSettings, project.getRootDir(), title, createIssueResultMono(updateDependenciesSettings)); } } @@ -204,30 +199,6 @@ public class UpdateDependenciesPlugin implements Plugin { }; } - static void runCommand(File dir, String... args) { - try { - Process process = new ProcessBuilder() - .directory(dir) - .command(args) - .start(); - writeLinesTo(process.getInputStream(), System.out); - writeLinesTo(process.getErrorStream(), System.out); - if (process.waitFor() != 0) { - new RuntimeException("Failed to run " + Arrays.toString(args)); - } - } catch (IOException | InterruptedException e) { - throw new RuntimeException("Failed to run " + Arrays.toString(args), e); - } - } - - static void writeLinesTo(InputStream input, PrintStream out) { - Scanner scanner = new Scanner(input); - while(scanner.hasNextLine()) { - out.println(scanner.nextLine()); - } - } - - static Action excludeWithRegex(String regex, String reason) { Pattern pattern = Pattern.compile(regex); return (selection) -> { @@ -242,40 +213,17 @@ public class UpdateDependenciesPlugin implements Plugin { String ga = dependency.getGroup() + ":" + dependency.getName() + ":"; String originalDependency = ga + dependency.getVersion(); String replacementDependency = ga + updatedVersion(dependency); - replaceFileText(buildFile, buildFileText -> buildFileText.replace(originalDependency, replacementDependency)); - } - - static void replaceFileText(File file, Function replaceText) { - String buildFileText = readString(file); - String updatedBuildFileText = replaceText.apply(buildFileText); - writeString(file, updatedBuildFileText); - } - - private static String readString(File file) { - try { - byte[] bytes = Files.readAllBytes(file.toPath()); - return new String(bytes); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static void writeString(File file, String text) { - try { - Files.write(file.toPath(), text.getBytes()); - } catch (IOException e) { - throw new RuntimeException(e); - } + FileUtils.replaceFileText(buildFile, buildFileText -> buildFileText.replace(originalDependency, replacementDependency)); } static void updateDependencyWithVersionVariable(File scanFile, File gradlePropertiesFile, DependencyOutdated dependency) { if (!gradlePropertiesFile.exists()) { return; } - replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { + FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { String ga = dependency.getGroup() + ":" + dependency.getName() + ":"; Pattern pattern = Pattern.compile("\"" + ga + "\\$\\{?([^'\"]+?)\\}?\""); - String buildFileText = readString(scanFile); + String buildFileText = FileUtils.readString(scanFile); Matcher matcher = pattern.matcher(buildFileText); while (matcher.find()) { String versionVariable = matcher.group(1); diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java new file mode 100644 index 0000000000..f9041108b5 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionPlugin.java @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.convention.versions; + +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +public class UpdateProjectVersionPlugin implements Plugin { + @Override + public void apply(Project project) { + project.getTasks().register("updateProjectVersion", UpdateProjectVersionTask.class, new Action() { + @Override + public void execute(UpdateProjectVersionTask updateProjectVersionTask) { + updateProjectVersionTask.setGroup("Release"); + updateProjectVersionTask.setDescription("Updates the project version to the next release in gradle.properties"); + updateProjectVersionTask.dependsOn("gitHubNextReleaseMilestone"); + updateProjectVersionTask.getNextVersionFile().fileProvider(project.provider(() -> project.file("next-release.yml"))); + } + }); + project.getTasks().register("updateToSnapshotVersion", UpdateToSnapshotVersionTask.class, new Action() { + @Override + public void execute(UpdateToSnapshotVersionTask updateToSnapshotVersionTask) { + updateToSnapshotVersionTask.setGroup("Release"); + updateToSnapshotVersionTask.setDescription( + "Updates the project version to the next snapshot in gradle.properties"); + } + }); + } +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java new file mode 100644 index 0000000000..63aef230a6 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateProjectVersionTask.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.convention.versions; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.TaskAction; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.Constructor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +import org.springframework.gradle.github.milestones.NextVersionYml; + +public abstract class UpdateProjectVersionTask extends DefaultTask { + + @InputFile + public abstract RegularFileProperty getNextVersionFile(); + + @TaskAction + public void checkReleaseDueToday() throws FileNotFoundException { + File nextVersionFile = getNextVersionFile().getAsFile().get(); + Yaml yaml = new Yaml(new Constructor(NextVersionYml.class)); + NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile)); + String nextVersion = nextVersionYml.getVersion(); + if (nextVersion == null) { + throw new IllegalArgumentException( + "Could not find version property in provided file " + nextVersionFile.getName()); + } + String currentVersion = getProject().getVersion().toString(); + File gradlePropertiesFile = getProject().getRootProject().file(Project.GRADLE_PROPERTIES); + if (!gradlePropertiesFile.exists()) { + return; + } + System.out.println("Updating the project version in " + Project.GRADLE_PROPERTIES + " from " + currentVersion + + " to " + nextVersion); + FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { + gradlePropertiesText = gradlePropertiesText.replace("version=" + currentVersion, "version=" + nextVersion); + return gradlePropertiesText; + }); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java new file mode 100644 index 0000000000..42caf5f971 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/security/convention/versions/UpdateToSnapshotVersionTask.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.convention.versions; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.tasks.TaskAction; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public abstract class UpdateToSnapshotVersionTask extends DefaultTask { + + private static final String RELEASE_VERSION_PATTERN = "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-M\\d+|-RC\\d+)?$"; + + @TaskAction + public void updateToSnapshotVersion() { + String currentVersion = getProject().getVersion().toString(); + File gradlePropertiesFile = getProject().getRootProject().file(Project.GRADLE_PROPERTIES); + if (!gradlePropertiesFile.exists()) { + return; + } + String nextVersion = calculateNextSnapshotVersion(currentVersion); + System.out.println("Updating the project version in " + Project.GRADLE_PROPERTIES + " from " + currentVersion + + " to " + nextVersion); + FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> { + gradlePropertiesText = gradlePropertiesText.replace("version=" + currentVersion, "version=" + nextVersion); + return gradlePropertiesText; + }); + } + + private String calculateNextSnapshotVersion(String currentVersion) { + Pattern releaseVersionPattern = Pattern.compile(RELEASE_VERSION_PATTERN); + Matcher releaseVersion = releaseVersionPattern.matcher(currentVersion); + + if (releaseVersion.find()) { + String majorSegment = releaseVersion.group(1); + String minorSegment = releaseVersion.group(2); + String patchSegment = releaseVersion.group(3); + String modifier = releaseVersion.group(4); + if (modifier == null) { + patchSegment = String.valueOf(Integer.parseInt(patchSegment) + 1); + } + System.out.println("modifier = " + modifier); + return String.format("%s.%s.%s-SNAPSHOT", majorSegment, minorSegment, patchSegment); + } + else { + throw new IllegalStateException( + "Cannot calculate next snapshot version because the current project version does not conform to the expected format"); + } + } + +} diff --git a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java deleted file mode 100644 index b9b0764ee5..0000000000 --- a/buildSrc/src/test/java/io/spring/gradle/github/milestones/GitHubMilestoneApiTests.java +++ /dev/null @@ -1,389 +0,0 @@ -package io.spring.gradle.github.milestones; - -import java.util.concurrent.TimeUnit; - -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.gradle.github.RepositoryRef; -import org.springframework.gradle.github.milestones.GitHubMilestoneApi; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - - -public class GitHubMilestoneApiTests { - private GitHubMilestoneApi github; - - private RepositoryRef repositoryRef = RepositoryRef.owner("spring-projects").repository("spring-security").build(); - - private MockWebServer server; - - private String baseUrl; - - @BeforeEach - public void setup() throws Exception { - this.server = new MockWebServer(); - this.server.start(); - this.github = new GitHubMilestoneApi("mock-oauth-token"); - this.baseUrl = this.server.url("/api").toString(); - this.github.setBaseUrl(this.baseUrl); - } - - @AfterEach - public void cleanup() throws Exception { - this.server.shutdown(); - } - - @Test - public void findMilestoneNumberByTitleWhenFoundThenSuccess() throws Exception { - String responseJson = "[\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + - " \"id\":6611880,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + - " \"number\":207,\n" + - " \"title\":\"5.6.x\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jgrandja\",\n" + - " \"id\":10884212,\n" + - " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jgrandja\",\n" + - " \"html_url\":\"https://github.com/jgrandja\",\n" + - " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":1,\n" + - " \"closed_issues\":0,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + - " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + - " \"due_on\":null,\n" + - " \"closed_at\":null\n" + - " },\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + - " \"id\":5884208,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + - " \"number\":191,\n" + - " \"title\":\"5.5.0-RC1\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":21,\n" + - " \"closed_issues\":23,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + - " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + - " \"closed_at\":null\n" + - " }\n" + - "]"; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - long milestoneNumberByTitle = this.github.findMilestoneNumberByTitle(this.repositoryRef, "5.5.0-RC1"); - - RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); - - assertThat(milestoneNumberByTitle).isEqualTo(191); - } - - @Test - public void findMilestoneNumberByTitleWhenNotFoundThenException() throws Exception { - String responseJson = "[\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + - " \"id\":6611880,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + - " \"number\":207,\n" + - " \"title\":\"5.6.x\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jgrandja\",\n" + - " \"id\":10884212,\n" + - " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jgrandja\",\n" + - " \"html_url\":\"https://github.com/jgrandja\",\n" + - " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":1,\n" + - " \"closed_issues\":0,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + - " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + - " \"due_on\":null,\n" + - " \"closed_at\":null\n" + - " },\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + - " \"id\":5884208,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + - " \"number\":191,\n" + - " \"title\":\"5.5.0-RC1\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":21,\n" + - " \"closed_issues\":23,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + - " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + - " \"closed_at\":null\n" + - " }\n" + - "]"; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> this.github.findMilestoneNumberByTitle(this.repositoryRef, "missing")); - } - - @Test - public void isOpenIssuesForMilestoneNumberWhenAllClosedThenFalse() throws Exception { - String responseJson = "[]"; - long milestoneNumber = 202; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThat(this.github.isOpenIssuesForMilestoneNumber(this.repositoryRef, milestoneNumber)).isFalse(); - - RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/issues?per_page=1&milestone=" + milestoneNumber); - } - - @Test - public void isOpenIssuesForMilestoneNumberWhenOpenIssuesThenTrue() throws Exception { - String responseJson = "[\n" + - " {\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562\",\n" + - " \"repository_url\":\"https://api.github.com/repos/spring-projects/spring-security\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/labels{/name}\",\n" + - " \"comments_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/comments\",\n" + - " \"events_url\":\"https://api.github.com/repos/spring-projects/spring-security/issues/9562/events\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/pull/9562\",\n" + - " \"id\":851886504,\n" + - " \"node_id\":\"MDExOlB1bGxSZXF1ZXN0NjEwMjMzMDcw\",\n" + - " \"number\":9562,\n" + - " \"title\":\"Add package-list\",\n" + - " \"user\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"labels\":[\n" + - " {\n" + - " \"id\":322225043,\n" + - " \"node_id\":\"MDU6TGFiZWwzMjIyMjUwNDM=\",\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/labels/in:%20build\",\n" + - " \"name\":\"in: build\",\n" + - " \"color\":\"e8f9de\",\n" + - " \"default\":false,\n" + - " \"description\":\"An issue in the build\"\n" + - " },\n" + - " {\n" + - " \"id\":322225079,\n" + - " \"node_id\":\"MDU6TGFiZWwzMjIyMjUwNzk=\",\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/labels/type:%20bug\",\n" + - " \"name\":\"type: bug\",\n" + - " \"color\":\"e3d9fc\",\n" + - " \"default\":false,\n" + - " \"description\":\"A general bug\"\n" + - " }\n" + - " ],\n" + - " \"state\":\"open\",\n" + - " \"locked\":false,\n" + - " \"assignee\":{\n" + - " \"login\":\"rwinch\",\n" + - " \"id\":362503,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjUwMw==\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/362503?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/rwinch\",\n" + - " \"html_url\":\"https://github.com/rwinch\",\n" + - " \"followers_url\":\"https://api.github.com/users/rwinch/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/rwinch/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/rwinch/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/rwinch/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/rwinch/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/rwinch/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/rwinch/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/rwinch/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/rwinch/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"assignees\":[\n" + - " {\n" + - " \"login\":\"rwinch\",\n" + - " \"id\":362503,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjUwMw==\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/362503?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/rwinch\",\n" + - " \"html_url\":\"https://github.com/rwinch\",\n" + - " \"followers_url\":\"https://api.github.com/users/rwinch/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/rwinch/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/rwinch/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/rwinch/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/rwinch/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/rwinch/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/rwinch/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/rwinch/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/rwinch/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " }\n" + - " ],\n" + - " \"milestone\":{\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + - " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + - " \"id\":5884208,\n" + - " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + - " \"number\":191,\n" + - " \"title\":\"5.5.0-RC1\",\n" + - " \"description\":\"\",\n" + - " \"creator\":{\n" + - " \"login\":\"jzheaux\",\n" + - " \"id\":3627351,\n" + - " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + - " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + - " \"gravatar_id\":\"\",\n" + - " \"url\":\"https://api.github.com/users/jzheaux\",\n" + - " \"html_url\":\"https://github.com/jzheaux\",\n" + - " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + - " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + - " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + - " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + - " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + - " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + - " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + - " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + - " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + - " \"type\":\"User\",\n" + - " \"site_admin\":false\n" + - " },\n" + - " \"open_issues\":21,\n" + - " \"closed_issues\":23,\n" + - " \"state\":\"open\",\n" + - " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + - " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + - " \"closed_at\":null\n" + - " },\n" + - " \"comments\":0,\n" + - " \"created_at\":\"2021-04-06T23:47:10Z\",\n" + - " \"updated_at\":\"2021-04-07T17:00:00Z\",\n" + - " \"closed_at\":null,\n" + - " \"author_association\":\"MEMBER\",\n" + - " \"active_lock_reason\":null,\n" + - " \"pull_request\":{\n" + - " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/pulls/9562\",\n" + - " \"html_url\":\"https://github.com/spring-projects/spring-security/pull/9562\",\n" + - " \"diff_url\":\"https://github.com/spring-projects/spring-security/pull/9562.diff\",\n" + - " \"patch_url\":\"https://github.com/spring-projects/spring-security/pull/9562.patch\"\n" + - " },\n" + - " \"body\":\"Closes gh-9528\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\",\n" + - " \"performed_via_github_app\":null\n" + - " }\n" + - "]"; - long milestoneNumber = 191; - this.server.enqueue(new MockResponse().setBody(responseJson)); - - assertThat(this.github.isOpenIssuesForMilestoneNumber(this.repositoryRef, milestoneNumber)).isTrue(); - - RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); - assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/issues?per_page=1&milestone=" + milestoneNumber); - } - -} diff --git a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java b/buildSrc/src/test/java/org/springframework/gradle/antora/AntoraVersionPluginTests.java similarity index 78% rename from buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java rename to buildSrc/src/test/java/org/springframework/gradle/antora/AntoraVersionPluginTests.java index 98eedad65e..6b1a424bfd 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/antora/CheckAntoraVersionPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/antora/AntoraVersionPluginTests.java @@ -15,16 +15,16 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIOException; -class CheckAntoraVersionPluginTests { +class AntoraVersionPluginTests { @Test void defaultsPropertiesWhenSnapshot() { String expectedVersion = "1.0.0-SNAPSHOT"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -40,9 +40,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-M1"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -58,9 +58,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-RC1"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -76,9 +76,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -92,9 +92,9 @@ class CheckAntoraVersionPluginTests { @Test void explicitProperties() { Project project = ProjectBuilder.builder().build(); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; checkAntoraVersionTask.getAntoraVersion().set("1.0.0"); @@ -110,9 +110,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -125,9 +125,9 @@ class CheckAntoraVersionPluginTests { String expectedVersion = "1.0.0-SNAPSHOT"; Project project = ProjectBuilder.builder().build(); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -142,9 +142,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -160,9 +160,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -177,9 +177,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0-M1'\nprerelease: 'true'\ndisplay_version: '1.0.0-M1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -194,9 +194,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0-RC1'\nprerelease: 'true'\ndisplay_version: '1.0.0-RC1'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -211,9 +211,9 @@ class CheckAntoraVersionPluginTests { File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); project.setVersion(expectedVersion); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); @@ -226,9 +226,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; @@ -241,9 +241,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("version: '1.0.0'\nprerelease: '-SNAPSHOT'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; @@ -257,9 +257,9 @@ class CheckAntoraVersionPluginTests { Project project = ProjectBuilder.builder().build(); File rootDir = project.getRootDir(); IOUtils.write("name: 'ROOT'\nversion: '1.0.0'", new FileOutputStream(new File(rootDir, "antora.yml")), StandardCharsets.UTF_8); - project.getPluginManager().apply(CheckAntoraVersionPlugin.class); + project.getPluginManager().apply(AntoraVersionPlugin.class); - Task task = project.getTasks().findByName(CheckAntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); + Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME); assertThat(task).isInstanceOf(CheckAntoraVersionTask.class); CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task; diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java index 0a1a293ab0..c74b148d71 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/GitHubMilestoneApiTests.java @@ -1,5 +1,9 @@ package org.springframework.gradle.github.milestones; +import java.nio.charset.Charset; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; @@ -385,4 +389,836 @@ public class GitHubMilestoneApiTests { assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/issues?per_page=1&milestone=" + milestoneNumber); } + @Test + public void isMilestoneDueTodayWhenNotFoundThenException() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.isMilestoneDueToday(this.repositoryRef, "missing")); + } + + @Test + public void isMilestoneDueTodayWhenPastDueThenTrue() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isTrue(); + } + + @Test + public void isMilestoneDueTodayWhenDueTodayThenTrue() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"" + Instant.now().toString() + "\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isTrue(); + } + + @Test + public void isMilestoneDueTodayWhenNoDueDateThenFalse() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isFalse(); + } + + @Test + public void isMilestoneDueTodayWhenDueDateInFutureThenFalse() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + boolean dueToday = this.github.isMilestoneDueToday(this.repositoryRef, "5.5.0-RC1"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(dueToday).isFalse(); + } + + @Test + public void calculateNextReleaseMilestoneWhenCurrentVersionIsNotSnapshotThenException() { + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-RC1")); + } + + @Test + public void calculateNextReleaseMilestoneWhenPatchSegmentGreaterThan0ThenReturnsVersionWithoutSnapshot() { + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.1-SNAPSHOT"); + + assertThat(nextVersion).isEqualTo("5.5.1"); + } + + @Test + public void calculateNextReleaseMilestoneWhenMilestoneAndRcExistThenReturnsMilestone() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.5.0-M1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC1\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0-M1"); + } + + @Test + public void calculateNextReleaseMilestoneWhenTwoMilestonesExistThenReturnsSmallerMilestone() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.5.0-M9\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-M10\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0-M9"); + } + + @Test + public void calculateNextReleaseMilestoneWhenTwoRcsExistThenReturnsSmallerRc() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.5.0-RC9\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.5.0-RC10\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"3000-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0-RC9"); + } + + @Test + public void calculateNextReleaseMilestoneWhenNoPreReleaseThenReturnsVersionWithoutSnapshot() throws Exception { + String responseJson = "[\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/207\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/207/labels\",\n" + + " \"id\":6611880,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNjYxMTg4MA==\",\n" + + " \"number\":207,\n" + + " \"title\":\"5.6.x\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jgrandja\",\n" + + " \"id\":10884212,\n" + + " \"node_id\":\"MDQ6VXNlcjEwODg0MjEy\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/10884212?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jgrandja\",\n" + + " \"html_url\":\"https://github.com/jgrandja\",\n" + + " \"followers_url\":\"https://api.github.com/users/jgrandja/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jgrandja/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jgrandja/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jgrandja/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jgrandja/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jgrandja/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jgrandja/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jgrandja/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jgrandja/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":1,\n" + + " \"closed_issues\":0,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2021-03-31T11:29:17Z\",\n" + + " \"updated_at\":\"2021-03-31T11:30:47Z\",\n" + + " \"due_on\":null,\n" + + " \"closed_at\":null\n" + + " },\n" + + " {\n" + + " \"url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191\",\n" + + " \"html_url\":\"https://github.com/spring-projects/spring-security/milestone/191\",\n" + + " \"labels_url\":\"https://api.github.com/repos/spring-projects/spring-security/milestones/191/labels\",\n" + + " \"id\":5884208,\n" + + " \"node_id\":\"MDk6TWlsZXN0b25lNTg4NDIwOA==\",\n" + + " \"number\":191,\n" + + " \"title\":\"5.4.3\",\n" + + " \"description\":\"\",\n" + + " \"creator\":{\n" + + " \"login\":\"jzheaux\",\n" + + " \"id\":3627351,\n" + + " \"node_id\":\"MDQ6VXNlcjM2MjczNTE=\",\n" + + " \"avatar_url\":\"https://avatars.githubusercontent.com/u/3627351?v=4\",\n" + + " \"gravatar_id\":\"\",\n" + + " \"url\":\"https://api.github.com/users/jzheaux\",\n" + + " \"html_url\":\"https://github.com/jzheaux\",\n" + + " \"followers_url\":\"https://api.github.com/users/jzheaux/followers\",\n" + + " \"following_url\":\"https://api.github.com/users/jzheaux/following{/other_user}\",\n" + + " \"gists_url\":\"https://api.github.com/users/jzheaux/gists{/gist_id}\",\n" + + " \"starred_url\":\"https://api.github.com/users/jzheaux/starred{/owner}{/repo}\",\n" + + " \"subscriptions_url\":\"https://api.github.com/users/jzheaux/subscriptions\",\n" + + " \"organizations_url\":\"https://api.github.com/users/jzheaux/orgs\",\n" + + " \"repos_url\":\"https://api.github.com/users/jzheaux/repos\",\n" + + " \"events_url\":\"https://api.github.com/users/jzheaux/events{/privacy}\",\n" + + " \"received_events_url\":\"https://api.github.com/users/jzheaux/received_events\",\n" + + " \"type\":\"User\",\n" + + " \"site_admin\":false\n" + + " },\n" + + " \"open_issues\":21,\n" + + " \"closed_issues\":23,\n" + + " \"state\":\"open\",\n" + + " \"created_at\":\"2020-09-16T13:28:03Z\",\n" + + " \"updated_at\":\"2021-04-06T23:47:10Z\",\n" + + " \"due_on\":\"2021-04-12T07:00:00Z\",\n" + + " \"closed_at\":null\n" + + " }\n" + + "]"; + this.server.enqueue(new MockResponse().setBody(responseJson)); + + String nextVersion = this.github.getNextReleaseMilestone(this.repositoryRef, "5.5.0-SNAPSHOT"); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("get"); + assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones?per_page=100"); + + assertThat(nextVersion).isEqualTo("5.5.0"); + } + + @Test + public void createMilestoneWhenValidParametersThenSuccess() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(204)); + Milestone milestone = new Milestone(); + milestone.setTitle("1.0.0"); + milestone.setDueOn(LocalDate.of(2022, 5, 4).atTime(LocalTime.NOON)); + this.github.createMilestone(this.repositoryRef, milestone); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()) + .isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/milestones"); + assertThat(recordedRequest.getBody().readString(Charset.defaultCharset())) + .isEqualTo("{\"title\":\"1.0.0\",\"due_on\":\"2022-05-04T12:00:00Z\"}"); + } + + @Test + public void createMilestoneWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.github.createMilestone(this.repositoryRef, new Milestone())); + } + } diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java new file mode 100644 index 0000000000..69bce2df80 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/milestones/SpringReleaseTrainTests.java @@ -0,0 +1,245 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.milestones; + +import java.time.LocalDate; +import java.time.Year; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import org.springframework.gradle.github.milestones.SpringReleaseTrainSpec.Train; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Steve Riesenberg + */ +public class SpringReleaseTrainTests { + @ParameterizedTest + @CsvSource({ + "2019-12-31, ONE, 2020", + "2020-01-01, ONE, 2020", + "2020-01-31, ONE, 2020", + "2020-02-01, TWO, 2020", + "2020-07-31, TWO, 2020", + "2020-08-01, ONE, 2021" + }) + public void nextTrainWhenBoundaryConditionsThenSuccess(LocalDate startDate, Train expectedTrain, Year expectedYear) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .nextTrain(startDate) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .build(); + assertThat(releaseTrainSpec.getTrain()).isEqualTo(expectedTrain); + assertThat(releaseTrainSpec.getYear()).isEqualTo(expectedYear); + } + + @Test + public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2020ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2020) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 1, 14)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 2, 11)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 3, 10)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 4, 14)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 5, 12)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2020ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2020) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 7, 14)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 8, 11)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 9, 15)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 10, 13)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 11, 10)); + } + + @Test + public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 11)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 15)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 15)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 12)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 10)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(2) + .dayOfWeek(2) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 12)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 9)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 13)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 11)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 15)); + } + + @Test + public void getTrainDatesWhenTrainOneIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 17)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 21)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 21)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 18)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 16)); + } + + @Test + public void getTrainDatesWhenTrainTwoIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(2) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + Map trainDates = releaseTrain.getTrainDates(); + assertThat(trainDates).hasSize(5); + assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 18)); + assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 15)); + assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 19)); + assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 17)); + assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 21)); + } + + @Test + public void isTrainDateWhenTrainOneIsThirdMondayOf2022ThenSuccess() { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M1", LocalDate.of(2022, 1, dayOfMonth))).isEqualTo(dayOfMonth == 17); + } + for (int dayOfMonth = 1; dayOfMonth <= 28; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M2", LocalDate.of(2022, 2, dayOfMonth))).isEqualTo(dayOfMonth == 21); + } + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-M3", LocalDate.of(2022, 3, dayOfMonth))).isEqualTo(dayOfMonth == 21); + } + for (int dayOfMonth = 1; dayOfMonth <= 30; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0-RC1", LocalDate.of(2022, 4, dayOfMonth))).isEqualTo(dayOfMonth == 18); + } + for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) { + assertThat(releaseTrain.isTrainDate("1.0.0", LocalDate.of(2022, 5, dayOfMonth))).isEqualTo(dayOfMonth == 16); + } + } + + @ParameterizedTest + @CsvSource({ + "2022-01-01, 2022-02-21", + "2022-02-01, 2022-02-21", + "2022-02-21, 2022-04-18", + "2022-03-01, 2022-04-18", + "2022-04-01, 2022-04-18", + "2022-04-18, 2022-06-20", + "2022-05-01, 2022-06-20", + "2022-06-01, 2022-06-20", + "2022-06-20, 2022-08-15", + "2022-07-01, 2022-08-15", + "2022-08-01, 2022-08-15", + "2022-08-15, 2022-10-17", + "2022-09-01, 2022-10-17", + "2022-10-01, 2022-10-17", + "2022-10-17, 2022-12-19", + "2022-11-01, 2022-12-19", + "2022-12-01, 2022-12-19", + "2022-12-19, 2023-02-20" + }) + public void getNextReleaseDateWhenBoundaryConditionsThenSuccess(LocalDate startDate, LocalDate expectedDate) { + SpringReleaseTrainSpec releaseTrainSpec = + SpringReleaseTrainSpec.builder() + .train(1) + .version("1.0.0") + .weekOfMonth(3) + .dayOfWeek(1) + .year(2022) + .build(); + + SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec); + assertThat(releaseTrain.getNextReleaseDate(startDate)).isEqualTo(expectedDate); + } +} diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java new file mode 100644 index 0000000000..51372480c0 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubActionsApiTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.gradle.github.release; + +import java.nio.charset.Charset; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.gradle.github.RepositoryRef; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Steve Riesenberg + */ +public class GitHubActionsApiTests { + private GitHubActionsApi gitHubActionsApi; + + private MockWebServer server; + + private String baseUrl; + + private RepositoryRef repository; + + @BeforeEach + public void setup() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + this.baseUrl = this.server.url("/api").toString(); + this.gitHubActionsApi = new GitHubActionsApi("mock-oauth-token"); + this.gitHubActionsApi.setBaseUrl(this.baseUrl); + this.repository = new RepositoryRef("spring-projects", "spring-security"); + } + + @AfterEach + public void cleanup() throws Exception { + this.server.shutdown(); + } + + @Test + public void dispatchWorkflowWhenValidParametersThenSuccess() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(204)); + + Map inputs = new LinkedHashMap<>(); + inputs.put("input-1", "value"); + inputs.put("input-2", false); + WorkflowDispatch workflowDispatch = new WorkflowDispatch("main", inputs); + this.gitHubActionsApi.dispatchWorkflow(this.repository, "test-workflow.yml", workflowDispatch); + + RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); + assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); + assertThat(recordedRequest.getRequestUrl().toString()) + .isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/actions/workflows/test-workflow.yml/dispatches"); + assertThat(recordedRequest.getBody().readString(Charset.defaultCharset())) + .isEqualTo("{\"ref\":\"main\",\"inputs\":{\"input-1\":\"value\",\"input-2\":false}}"); + } + + @Test + public void dispatchWorkflowWhenErrorResponseThenException() throws Exception { + this.server.enqueue(new MockResponse().setResponseCode(400)); + + WorkflowDispatch workflowDispatch = new WorkflowDispatch("main", null); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> this.gitHubActionsApi.dispatchWorkflow(this.repository, "test-workflow.yml", workflowDispatch)); + } +} diff --git a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java index 6ac7955722..3d91574d5b 100644 --- a/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java +++ b/buildSrc/src/test/java/org/springframework/gradle/github/release/GitHubReleaseApiTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,15 @@ package org.springframework.gradle.github.release; +import java.nio.charset.Charset; import java.util.concurrent.TimeUnit; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; -import org.junit.Test; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.gradle.github.RepositoryRef; @@ -34,21 +35,22 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; * @author Steve Riesenberg */ public class GitHubReleaseApiTests { - private GitHubReleaseApi github; - - private RepositoryRef repository = new RepositoryRef("spring-projects", "spring-security"); + private GitHubReleaseApi gitHubReleaseApi; private MockWebServer server; private String baseUrl; + private RepositoryRef repository; + @BeforeEach public void setup() throws Exception { this.server = new MockWebServer(); this.server.start(); - this.github = new GitHubReleaseApi("mock-oauth-token"); this.baseUrl = this.server.url("/api").toString(); - this.github.setBaseUrl(this.baseUrl); + this.gitHubReleaseApi = new GitHubReleaseApi("mock-oauth-token"); + this.gitHubReleaseApi.setBaseUrl(this.baseUrl); + this.repository = new RepositoryRef("spring-projects", "spring-security"); } @AfterEach @@ -134,18 +136,20 @@ public class GitHubReleaseApiTests { " ]\n" + "}"; this.server.enqueue(new MockResponse().setBody(responseJson)); - this.github.publishRelease(this.repository, Release.tag("1.0.0").build()); + this.gitHubReleaseApi.publishRelease(this.repository, Release.tag("1.0.0").build()); RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS); assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post"); - assertThat(recordedRequest.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases"); - assertThat(recordedRequest.getBody().toString()).isEqualTo("{\"tag_name\":\"1.0.0\"}"); + assertThat(recordedRequest.getRequestUrl().toString()) + .isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases"); + assertThat(recordedRequest.getBody().readString(Charset.defaultCharset())) + .isEqualTo("{\"tag_name\":\"1.0.0\",\"draft\":false,\"prerelease\":false,\"generate_release_notes\":false}"); } @Test public void publishReleaseWhenErrorResponseThenException() throws Exception { this.server.enqueue(new MockResponse().setResponseCode(400)); assertThatExceptionOfType(RuntimeException.class) - .isThrownBy(() -> this.github.publishRelease(this.repository, Release.tag("1.0.0").build())); + .isThrownBy(() -> this.gitHubReleaseApi.publishRelease(this.repository, Release.tag("1.0.0").build())); } }