1
0
mirror of synced 2026-07-09 12:50:19 +00:00

Compare commits

...

83 Commits

Author SHA1 Message Date
Josh Cummings 89c815032c Fix Index Out of Bounds 2022-10-17 14:52:03 -06:00
Josh Cummings ff055cf07a Remote antoraUpdateVersion task from CI 2022-10-17 14:24:13 -06:00
github-actions[bot] b08a06aa76 Release 5.6.8 2022-10-17 18:55:50 +00:00
Marcus Da Coregio 465d80c162 Remove antoreUpdateVersion task from release automation 2022-10-17 15:50:44 -03:00
Marcus Da Coregio 4992e8ce62 Update org.springframework.data to 2021.1.8
Closes gh-12007
2022-10-13 09:24:21 -03:00
Marcus Da Coregio c772daab92 Update org.springframework to 5.3.23
Closes gh-12006
2022-10-13 09:24:20 -03:00
Marcus Da Coregio 45a4a89960 Update hibernate-entitymanager to 5.6.12.Final
Closes gh-12005
2022-10-13 09:24:20 -03:00
Marcus Da Coregio b43c7e927f Update org.eclipse.jetty to 9.4.49.v20220914
Closes gh-12004
2022-10-13 09:24:20 -03:00
Marcus Da Coregio 50d23622d0 Update io.rsocket to 1.1.3
Closes gh-12003
2022-10-13 09:24:20 -03:00
Marcus Da Coregio 2c2603ba0f Update io.projectreactor to 2020.0.24
Closes gh-12001
2022-10-13 09:24:20 -03:00
Marcus Da Coregio f7f53ea2b7 Update jackson-bom to 2.13.4.20221012
Closes gh-11997
2022-10-13 09:22:28 -03:00
Marcus Da Coregio e0f8c711d8 Add default value for cloneOutputDirectory property
Closes gh-11969
2022-10-10 11:00:47 -03:00
Dan Allen e0843aabb1 automatically manage docs version (with collector) 2022-10-05 13:56:22 -05:00
Daniel Garnier-Moiroux 26bb60c567 Add rncToXsd task description to CONTRIBUTING.adoc 2022-10-03 10:09:27 -03:00
Dan Allen c44230ba24 switch to offical Antora plugin for Gradle
- lock version to latest release of Antora 3.1
- rename properties on extension block
- use Node.js version provided by plugin
- remove package.json file
- assign environment variables using environments property on extension block
- use single quotes where possible in build script
- use default setting for log format
2022-09-29 14:05:09 -05:00
Jerome Prinet 8d2fb6858f Update Gradle Enterprise plugin to 3.11.1 2022-09-16 13:14:53 -03:00
Marcus Da Coregio 7756247c3a Simplify checkSamples task
Closes gh-11814
2022-09-16 09:36:12 -03:00
Dan Allen 3387149a0f repurpose 5.6.x branch to provide local docs build
* remove unused workflows, scripts, and configuration (now handled by docs-build branch)
* upgrade Antora to 3.1 (and Node.js to 16)
* tune playbook settings
* reconfigure docs build for local build only
* add patch to support using linked worktree as Antora content source
* remove Antora extensions not needed for local builds
2022-09-12 15:41:12 -05:00
Underground Hill 8b74bf9742 Updated reference to architecture page
In the context of Servlet Authentication page, "Architecture" should probably link to "Servlet Authentication Architecture" page
2022-09-01 09:38:10 -03:00
Marcus Da Coregio 40abf87ae6 Add buildScan to checkRemote
Closes gh-11766
2022-08-30 09:11:08 -03:00
Steve Riesenberg 1c014eb512 Use 6.0.x instead of 3.0.x as default branch 2022-08-24 16:38:27 -05:00
Rob Winch 8c69699458 Remove backportbot.yml
Issue gh-11736
2022-08-23 13:46:32 -05:00
Rob Winch fc10d5fc29 repository=spring-projects/spring-security
Previously the repository used spring-project (missing the s)
2022-08-23 13:30:20 -05:00
Rob Winch c79ebf4edf Setup Forward Merge 2022-08-22 16:19:44 -05:00
Marcus Da Coregio a8d6c1d21f Consistently set AuthenticationEventPublisher in AuthenticationManagerBuilder
Prior to this, the HttpSecurity bean was not consistent with WebSecurityConfigurerAdapter's HttpSecurity because it did not setup a default AuthenticationEventPublisher. This also fixes a problem where the AuthenticationEventPublisher bean would only be considered if there was a UserDetailsService

Closes gh-11449
Closes gh-11726
2022-08-19 09:58:22 -03:00
Steve Riesenberg 7c7f9380c7 Refresh remote JWK when unknown KID error occurs
Closes gh-11621
2022-08-18 16:54:45 -05:00
tinolazreg 888715bbb2 Add tests for unknown KID error
Issue gh-11621
2022-08-18 16:54:45 -05:00
jujunChen 13feb87171 Modify words
- <dependencyManagement> to dependencyManagement
- pom.xml to build.gradle
2022-08-16 14:51:36 -06:00
Rob Winch faf9fb7337 NamespaceLdapAuthenticationProviderTests use Dynamic Port
Closes gh-11710
2022-08-15 15:26:46 -05:00
Rob Winch f33d7253b6 GitHubMilestoneApiTests due_on Uses LocalDate
`GitHubMilestoneApiTests` uses `Instant.now()` for `due_on`. Since
`Instant.now()` is UTC time based,
`isMilestoneDueTodayWhenDueTodayThenTrue` fails when the computer that runs
the test is not the same day as it is in UTC time.

To fix it, `due_on` should be set to an `Instant` based upon the timezone
of the current computer.

Closes gh-11706
2022-08-15 13:04:29 -05:00
github-actions[bot] db74e9d128 Next development version 2022-08-15 16:07:33 +00:00
github-actions[bot] 4559d269e0 Release 5.6.7 2022-08-15 15:25:05 +00:00
Marcus Da Coregio 627809d2dc Update org.springframework.data to 2021.1.6
Closes gh-11686
2022-08-10 14:52:51 -03:00
Marcus Da Coregio 4b1d7e9479 Update org.springframework to 5.3.22
Closes gh-11685
2022-08-10 14:52:51 -03:00
Marcus Da Coregio d9980a4dfe Update jsonassert to 1.5.1
Closes gh-11684
2022-08-10 14:52:51 -03:00
Marcus Da Coregio 8eb7e589eb Update hibernate-entitymanager to 5.6.10.Final
Closes gh-11683
2022-08-10 14:52:51 -03:00
Marcus Da Coregio 0d7dce9d71 Update org.eclipse.jetty to 9.4.48.v20220622
Closes gh-11682
2022-08-10 14:52:51 -03:00
Marcus Da Coregio da09788be9 Update io.projectreactor to 2020.0.22
Closes gh-11680
2022-08-10 14:52:51 -03:00
Marcus Da Coregio ead587c597 Consistently handle RequestRejectedException if it is wrapped
Closes gh-11645
2022-08-09 08:32:42 -03:00
Steve Riesenberg 02459919cc Skip workflows on forks of spring-security 2022-07-28 15:13:56 -05:00
Steve Riesenberg 57d212ddca Use cache and user.name system property on Windows 2022-07-28 15:13:55 -05:00
Steve Riesenberg 539b17f6da Only run prerequisites job if on upstream repo 2022-07-28 15:13:54 -05:00
Steve Riesenberg 37e1ad27fe Simplify dependency graph 2022-07-28 15:13:53 -05:00
Steve Riesenberg 043fdd6f03 Use Spring Gradle Build Action
Closes gh-11630
2022-07-28 15:13:52 -05:00
Steve Riesenberg 3234e05085 Polish gh-11367 2022-07-28 15:13:51 -05:00
naveen f957e3c051 Set permissions for GitHub actions
Restrict the GitHub token permissions only to the required ones; this
way, even if the attackers will succeed in compromising your workflow,
they won’t be able to do much.

- Included permissions for the action.

https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions

https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions

https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs

Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests
https://securitylab.github.com/research/github-actions-preventing-pwn-requests/

Signed-off-by: naveen <172697+naveensrinivasan@users.noreply.github.com>

Closes gh-11367
2022-07-28 15:13:51 -05:00
Marcus Da Coregio d66ad22652 Add Deprecated annotation to WebSecurity#securityInterceptor
Closes gh-11634
2022-07-27 14:32:44 -03:00
Rob Winch 7a860e1568 Fix Snapshot Sources/Javadoc
This commit merges a workaround to an issue in JFrog's Gradle plugin
which causes SNAPSHOT javadoc and sources to become out of sync and thus
prevents users from being able to download either.

Closes gh-10602
2022-07-26 16:26:31 -05:00
Desmond Silveira 0d3c3c676d "Well-Know" should be "Well-Known" 2022-07-26 15:45:27 -05:00
Yuriy Savchenko 0f64d4c091 Add Kotlin example for WebTestClient setup docs
Closes gh-9998
2022-07-22 14:04:16 -03:00
Josh Cummings 56a6133b20 Merge Same-named Attribute Elements
Closes gh-11042
2022-07-20 18:43:25 -06:00
Steve Riesenberg aaf20e7b61 Build only on branches
Issue gh-11480
2022-07-18 11:47:25 -05:00
Steve Riesenberg 148756076c Backport release automation and github actions
Closes gh-11501
2022-07-13 15:51:25 -05:00
Steve Riesenberg 6f321a27c4 Fix inconsistency in hasProperty check 2022-07-13 15:51:25 -05:00
Steve Riesenberg 539443b4be Add GitHubReleasePlugin with createGitHubRelease task
Issue gh-10456
Issue gh-10457
2022-07-13 15:51:24 -05:00
Josh Cummings 28424f8ae5 Correct input validation for 31 rounds
Closes gh-11470
2022-07-11 14:51:51 -06:00
Rob Winch a7d21f1b34 Document sagan Release tasks require read:org scope
Closes gh-11423
2022-06-21 14:49:38 -05:00
Joe Grandja d3a024786b Next Development Version 2022-06-20 15:05:30 -04:00
Joe Grandja fa4c5449e7 Release 5.6.6 2022-06-20 14:50:24 -04:00
Joe Grandja 2a3845a7ed Update org.opensaml:opensaml-core4 to 4.1.1
Closes gh-11420
2022-06-20 14:50:24 -04:00
Joe Grandja ff96a7b5ad Update spring-ldap-core to 2.3.8.RELEASE
Closes gh-11419
2022-06-20 14:41:13 -04:00
Joe Grandja c37ff42234 Update org.springframework.data to 2021.1.5
Closes gh-11418
2022-06-20 14:41:10 -04:00
Joe Grandja 0a00d84838 Update org.springframework to 5.3.21
Closes gh-11417
2022-06-20 14:41:08 -04:00
Joe Grandja 96c6751a1d Update hibernate-entitymanager to 5.6.9.Final
Closes gh-11416
2022-06-20 14:41:05 -04:00
Joe Grandja 8ee9c32788 Update io.projectreactor to 2020.0.20
Closes gh-11414
2022-06-20 14:41:00 -04:00
Joe Grandja 7a5fb9eaf7 Update jackson-bom to 2.13.3
Closes gh-11411
2022-06-20 14:40:53 -04:00
Joe Grandja 8cbb972cef Add dependency update exclusion for spring-javaformat-checkstyle 2022-06-20 14:27:40 -04:00
Josh Cummings 539a11d0a4 Encode postLogoutRedirectUri query params
Closes gh-11379
2022-06-16 16:13:42 -06:00
Zhivko Delchev e97c5a533b Reverse content type check
When MultipartFormData is enabled currently the CsrfWebFilter compares
the content-type header against MULTIPART_FORM_DATA MediaType which
leads to NullPointerExecption when there is no content-type header.
This commit reverse the check to compare the MULTIPART_FORM_DATA
MediaType against the content-type which contains null check and avoids
the exception.

closes gh-11204
Closes gh-11205
2022-06-06 15:47:35 -05:00
shirohoo e0fa644b08 Fix typo in BasicLookupStrategy Javadoc
Closes gh-11336
2022-06-06 14:16:28 -05:00
Rob Winch 592db9180d Enable BackportBot on 5.6.x 2022-06-06 14:16:28 -05:00
André Luis Gomes aca3fc2412 Update opaque-token.adoc
Fixing yaml sample in Servlet and Reactive pages
2022-06-01 08:51:44 -03:00
Claudio Consolmagno 07f9afe057 Use 'md:' prefix in EntityDescriptor XML
Create the EntityDescriptor object with
EntityDescriptor.DEFAULT_ELEMENT_NAME instead of
EntityDescriptor.ELEMENT_QNAME. That ensures the EntityDescriptor tag
is marshalled to xml with the 'md:' prefix, consistent with all other
metadata tags.

Closes #11283
2022-05-31 17:11:02 -06:00
Josh Cummings d7077b441a Correct access(String) reference
Closes gh-11280
2022-05-27 15:00:15 -06:00
Josh Cummings 101f11ba94 Improve ContextConfiguration Docs
Point to updated Spring Reference

Issue gh-10934
2022-05-27 13:12:56 -06:00
Josh Cummings 18b903f6e3 Polish ExtendWith Docs
Use spring-framework-reference-url placeholder

Issue gh-10934
2022-05-27 13:12:56 -06:00
nor-ek 038266a94f Update JUnit 5 annotations in documentation
- replace Before with BeforeEach
- replace RunWith with ExtendWith

Closes gh-10934
2022-05-27 13:12:55 -06:00
Evgeniy Cheban cf559ab224 Some Security Expressions cause NPE when used within Query annotation
Added trustResolver, roleHierarchy, permissionEvaluator, defaultRolePrefix
fields to SecurityEvaluationContextExtension.

Closes gh-11196
Closes gh-11290
2022-05-26 17:44:28 -05:00
Juny Tse 649428b49a Use Base64 encoder with no CRLF in output for SAML 2.0 messages
Closes gh-11262
2022-05-25 12:06:27 -06:00
Steve Riesenberg 0355e960d7 Next development version 2022-05-18 11:52:05 -05:00
Steve Riesenberg fdad14af63 Release 5.6.5 2022-05-18 11:03:25 -05:00
Rob Winch 077c9e0b3e StrictHttpFirewall allows CJKV characters
Closes gh-11264
2022-05-18 08:56:57 -05:00
Rob Winch dc648cf79f Next Developement Version 2022-05-16 11:51:06 -05:00
114 changed files with 4990 additions and 1455 deletions
-20
View File
@@ -1,20 +0,0 @@
{
"index_name": "security-docs",
"start_urls": [
"https://docs.spring.io/spring-security/reference/"
],
"selectors": {
"lvl0": {
"selector": "//nav[@class='crumbs']//li[@class='crumb'][last()-1]",
"type": "xpath",
"global": true,
"default_value": "Home"
},
"lvl1": ".doc h1",
"lvl2": ".doc h2",
"lvl3": ".doc h3",
"lvl4": ".doc h4",
"text": ".doc p, .doc td.content, .doc th.tableblock"
}
}
-20
View File
@@ -1,20 +0,0 @@
#!/bin/bash
HOST="$1"
HOST_PATH="$2"
SSH_PRIVATE_KEY="$3"
SSH_KNOWN_HOST="$4"
if [ "$#" -ne 4 ]; then
echo -e "not enough arguments USAGE:\n\n$0 \$HOST \$HOST_PATH \$SSH_PRIVATE_KEY \$SSH_KNOWN_HOSTS \n\n" >&2
exit 1
fi
# Use a non-default path to avoid overriding when testing locally
SSH_PRIVATE_KEY_PATH=~/.ssh/github-actions-docs
install -m 600 -D /dev/null "$SSH_PRIVATE_KEY_PATH"
echo "$SSH_PRIVATE_KEY" > "$SSH_PRIVATE_KEY_PATH"
echo "$SSH_KNOWN_HOST" > ~/.ssh/known_hosts
rsync --delete -avze "ssh -i $SSH_PRIVATE_KEY_PATH" docs/build/site/ "$HOST:$HOST_PATH"
rm -f "$SSH_PRIVATE_KEY_PATH"
@@ -1,21 +0,0 @@
#!/bin/bash
###
# Docs
# config.json https://docsearch.algolia.com/docs/config-file
# Run the crawler https://docsearch.algolia.com/docs/run-your-own/#run-the-crawl-from-the-docker-image
### USAGE
if [ "$#" -ne 3 ]; then
echo -e "not enough arguments USAGE:\n\n$0 \$ALGOLIA_APPLICATION_ID \$ALGOLIA_API_KEY \$CONFIG_FILE\n\n" >&2
exit 1
fi
# Script Parameters
APPLICATION_ID=$1
API_KEY=$2
CONFIG_FILE=$3
#### Script
script_dir=$(dirname $0)
docker run -e "APPLICATION_ID=$APPLICATION_ID" -e "API_KEY=$API_KEY" -e "CONFIG=$(cat $CONFIG_FILE | jq -r tostring)" algolia/docsearch-scraper
-16
View File
@@ -1,16 +0,0 @@
name: Update Algolia Index
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
jobs:
update:
name: Update Algolia Index
runs-on: ubuntu-latest
steps:
- name: Checkout Source
uses: actions/checkout@v2
- name: Update Index
run: ${GITHUB_WORKSPACE}/.github/actions/algolia-docsearch-scraper.sh "${{ secrets.ALGOLIA_APPLICATION_ID }}" "${{ secrets.ALGOLIA_WRITE_API_KEY }}" "${GITHUB_WORKSPACE}/.github/actions/algolia-config.json"
+9
View File
@@ -10,12 +10,21 @@ on:
env:
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- name: Checkout Source
uses: actions/checkout@v2
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
distribution: 'adopt'
- name: Generate antora.yml
run: ./gradlew :spring-security-docs:generateAntora
- name: Extract Branch Name
@@ -3,9 +3,15 @@ on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
permissions:
contents: read
jobs:
main:
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Delete artifacts in cron job
env:
@@ -2,6 +2,8 @@ name: CI
on:
push:
branches:
- '**'
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
@@ -16,19 +18,28 @@ env:
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: read
jobs:
prerequisites:
name: Pre-requisites for building
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
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: |
# Run jobs if in upstream repository
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]
@@ -40,18 +51,14 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v2
with:
java-version: '11'
- name: Setup gradle user name
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') }}
distribution: 'adopt'
- name: Set up Gradle
uses: gradle/gradle-build-action@v2
- name: Set up gradle user name
run: echo 'systemProp.user.name=spring-builds+github' >> gradle.properties
- name: Build with Gradle
env:
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
@@ -65,14 +72,11 @@ jobs:
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
distribution: 'adopt'
- name: Snapshot Tests
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
@@ -86,24 +90,23 @@ jobs:
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
distribution: 'adopt'
- name: Check samples project
env:
LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos
SAMPLES_INIT_SCRIPT: ${{ github.workspace }}/build/includeRepo/spring-security-ci.gradle
SAMPLES_DIR: ../spring-security-samples
VERSION: ${{ needs.prerequisites.outputs.project_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"
./gradlew publishMavenJavaPublicationToLocalRepository
./gradlew checkSamples -PsamplesInitScript="$SAMPLES_INIT_SCRIPT" -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" --stacktrace
./gradlew cloneSamples -PcloneOutputDirectory="$SAMPLES_DIR"
./gradlew --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$VERSION" :runAllTests
check_tangles:
name: Check for Package Tangles
needs: [ prerequisites ]
@@ -111,14 +114,11 @@ jobs:
if: needs.prerequisites.outputs.runjobs
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
distribution: 'adopt'
- name: Check for package tangles
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
@@ -131,14 +131,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
distribution: 'adopt'
- name: Deploy artifacts
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
@@ -158,14 +155,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
distribution: 'adopt'
- name: Deploy Docs
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
@@ -182,14 +176,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK
uses: actions/setup-java@v1
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
- name: Setup gradle user name
run: |
mkdir -p ~/.gradle
echo 'systemProp.user.name=spring-builds+github' >> ~/.gradle/gradle.properties
distribution: 'adopt'
- name: Deploy Schema
run: |
export GRADLE_ENTERPRISE_CACHE_USERNAME="$GRADLE_ENTERPRISE_CACHE_USER"
@@ -200,14 +191,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
permissions:
contents: write
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 gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
distribution: 'adopt'
- 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
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
permissions:
contents: read
issues: write
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
- name: Set up gradle
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: [perform_release, perform_post_release]
if: failure()
runs-on: ubuntu-latest
permissions:
actions: read
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 }}
-33
View File
@@ -1,33 +0,0 @@
name: Build & Deploy Reference
on:
repository_dispatch:
types: request-build-reference
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
jobs:
deploy:
name: deploy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
cache: gradle
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b
- 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 }}"
@@ -5,10 +5,15 @@ on:
env:
DUE_ON: ${{ github.event.milestone.due_on }}
TITLE: ${{ github.event.milestone.title }}
permissions:
contents: read
jobs:
spring-releasetrain-checks:
name: Check DueOn is on a Release Date
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: none
steps:
- name: Print Milestone Being Checked
run: echo "Validating DueOn '$DUE_ON' for milestone '$TITLE'"
@@ -23,6 +28,8 @@ jobs:
needs: [spring-releasetrain-checks]
if: failure()
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Send Slack message
uses: Gamesight/slack-workflow-status@v1.0.1
+7 -14
View File
@@ -2,27 +2,20 @@ name: PR Build
on: pull_request
env:
RUN_JOBS: ${{ github.repository == 'spring-projects/spring-security' }}
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
if: ${{ github.repository == 'spring-projects/spring-security' }}
steps:
- if: env.RUN_JOBS == 'true'
uses: actions/checkout@v2
- name: Set up JDK
if: env.RUN_JOBS == 'true'
uses: actions/setup-java@v1
- uses: actions/checkout@v2
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
- name: Cache Gradle packages
if: env.RUN_JOBS == 'true'
uses: actions/cache@v2
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
distribution: 'adopt'
- name: Build with Gradle
if: env.RUN_JOBS == 'true'
run: ./gradlew clean build --continue --scan
@@ -0,0 +1,80 @@
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 }}
permissions:
contents: read
jobs:
update_scheduled_release_version:
name: Initiate Release If Scheduled
if: ${{ github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- id: checkout-source
name: Checkout Source Code
uses: actions/checkout@v2
with:
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Set up gradle
uses: spring-io/spring-gradle-build-action@v1
with:
java-version: '11'
distribution: 'adopt'
- 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
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'
+9 -2
View File
@@ -43,9 +43,12 @@ If you're considering anything more than correcting a typo or fixing a minor bug
If you have not previously done so, please fill out and submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement].
= Create your branch from main
= Create your branch from oldest maintenance branch
Create your topic branch to be submitted as a pull request from main. The Spring team will consider your pull request for backporting on a case-by-case basis; you don't need to worry about submitting anything for backporting.
Create your topic branch to be submitted as a pull request from the oldest impacted and supported maintenance branch.
You can find the supported versions by looking at the https://github.com/spring-projects/spring-security/milestones[milestones page].
Switch to a branch named `<major>.<minor>.x` from the smallest milestone in the format of `<major>.<minor>.<patch>(-<prerelease>)`.
The spring team will ensure the code gets merged forward into additional branches.
= Use short branch names
@@ -133,6 +136,10 @@ Search the codebase to find related unit tests and add additional `@Test` method
Update the https://www.relaxng.org[RELAX NG] schema `spring-security-x.y.rnc` instead of `spring-security-x.y.xsd` if you contribute changes to supported XML configuration. The XML schema file can be generated the following Gradle task:
----
./gradlew :spring-security-config:rncToXsd
----
Changes to the XML schema will be overwritten by the Gradle build task.
= Squash commits
+1 -1
View File
@@ -135,7 +135,7 @@ git push origin 5.4.0-RC1
The following command will update https://spring.io/projects/spring-security#learn with the new release version using the following parameters
<github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`
<github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `read:org` as https://spring.io/restdocs/index.html#authentication[documented for spring.io api]
<next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
<previous-version> - Replace with the previous release which will be removed from the listed versions (i.e. 5.5.0-M3)
@@ -73,8 +73,8 @@ import org.springframework.util.Assert;
* one in <tt>lookupObjectIdentities</tt>. These are built from the same select and "order
* by" clause, using a different where clause in each case. In order to use custom schema
* or column names, each of these SQL clauses can be customized, but they must be
* consistent with each other and with the expected result set generated by the the
* default values.
* consistent with each other and with the expected result set generated by the default
* values.
*
* @author Ben Alex
*/
+57 -11
View File
@@ -1,3 +1,5 @@
import io.spring.gradle.IncludeRepoTask
buildscript {
dependencies {
classpath "io.spring.javaformat:spring-javaformat-gradle-plugin:$springJavaformatVersion"
@@ -16,12 +18,13 @@ apply plugin: 'io.spring.nohttp'
apply plugin: 'locks'
apply plugin: 's101'
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'
apply plugin: 'org.springframework.github.release'
group = 'org.springframework.security'
description = 'Spring Security'
@@ -46,6 +49,43 @@ 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"
name = "spring-security"
}
}
tasks.named("dispatchGitHubWorkflow") {
repository {
owner = "spring-projects"
name = "spring-security"
}
}
tasks.named("updateDependencies") {
// we aren't Gradle 7 compatible yet
checkForGradleUpdate = false
@@ -95,6 +135,18 @@ updateDependenciesSettings {
selection.reject("org.opensaml maintains two different versions, so it must be updated manually");
}
}
components.withModule("io.spring.javaformat:spring-javaformat-gradle-plugin") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("spring-javaformat-gradle-plugin updates break checkstyle");
}
}
components.withModule("io.spring.javaformat:spring-javaformat-checkstyle") { selection ->
ModuleComponentIdentifier candidate = selection.getCandidate();
if (!candidate.getVersion().equals(selection.getCurrentVersion())) {
selection.reject("spring-javaformat-checkstyle updates break checkstyle");
}
}
}
}
}
@@ -150,16 +202,10 @@ nohttp {
}
tasks.register('checkSamples') {
includeCheckRemote {
repository = 'spring-projects/spring-security-samples'
ref = samplesBranch
if (project.hasProperty("samplesInitScript")) {
initScripts = [samplesInitScript]
projectProperties = ["localRepositoryPath": localRepositoryPath, "springSecurityVersion": project.version]
}
}
dependsOn checkRemote
tasks.register('cloneSamples', IncludeRepoTask) {
repository = 'spring-projects/spring-security-samples'
ref = samplesBranch
outputDirectory = project.hasProperty("cloneOutputDirectory") ? project.file("$cloneOutputDirectory") : project.file("build/samples")
}
s101 {
+11 -4
View File
@@ -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"
@@ -59,6 +62,10 @@ gradlePlugin {
id = "org.springframework.github.changelog"
implementationClass = "org.springframework.gradle.github.changelog.GitHubChangelogPlugin"
}
githubRelease {
id = "org.springframework.github.release"
implementationClass = "org.springframework.gradle.github.release.GitHubReleasePlugin"
}
s101 {
id = "s101"
implementationClass = "s101.S101Plugin"
@@ -80,7 +87,7 @@ dependencies {
implementation localGroovy()
implementation 'io.github.gradle-nexus:publish-plugin:1.1.0'
implementation 'io.projectreactor:reactor-core:3.4.18'
implementation 'io.projectreactor:reactor-core:3.4.24'
implementation 'gradle.plugin.org.gretty:gretty:3.0.1'
implementation 'com.apollographql.apollo:apollo-runtime:2.4.5'
implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
@@ -89,7 +96,7 @@ dependencies {
implementation 'io.spring.nohttp:nohttp-gradle:0.0.10'
implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0'
implementation 'org.hidetake:gradle-ssh-plugin:2.10.1'
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.24.20'
implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.29.0'
implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1'
testImplementation platform('org.junit:junit-bom:5.8.2')
@@ -54,7 +54,7 @@ abstract class IncludeRepoTask extends DefaultTask {
* Directory where the project template should be copied.
*/
@OutputDirectory
final File outputDirectory = project.file("$project.buildDir/$name")
File outputDirectory = project.file("$project.buildDir/$name")
@TaskAction
void checkoutAndCopy() {
@@ -17,6 +17,7 @@ package io.spring.gradle.convention
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.publish.maven.plugins.MavenPublishPlugin
class ArtifactoryPlugin implements Plugin<Project> {
@@ -36,8 +37,14 @@ class ArtifactoryPlugin implements Plugin<Project> {
password = artifactoryPassword
}
}
defaults {
publications('mavenJava')
}
}
project.plugins.withType(MavenPublishPlugin) {
project.artifactory {
publish {
defaults {
publications('mavenJava')
}
}
}
}
@@ -1,80 +0,0 @@
/*
* 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 io.spring.gradle.convention
import io.spring.gradle.IncludeRepoTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.GradleBuild
import org.gradle.api.tasks.TaskProvider
/**
* Adds a set of tasks that make easy to clone a remote repository and perform some task
*
* @author Marcus Da Coregio
*/
class IncludeCheckRemotePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
IncludeCheckRemoteExtension extension = project.extensions.create('includeCheckRemote', IncludeCheckRemoteExtension)
TaskProvider<IncludeRepoTask> includeRepoTask = project.tasks.register('includeRepo', IncludeRepoTask) { IncludeRepoTask it ->
it.repository = extension.repository
it.ref = extension.ref
}
project.tasks.register('checkRemote', GradleBuild) {
it.dependsOn 'includeRepo'
it.dir = includeRepoTask.get().outputDirectory
it.tasks = extension.getTasks()
extension.getInitScripts().forEach {script ->
it.startParameter.addInitScript(new File(script))
}
extension.getProjectProperties().entrySet().forEach { entry ->
it.startParameter.projectProperties.put(entry.getKey(), entry.getValue())
}
}
}
abstract static class IncludeCheckRemoteExtension {
/**
* Git repository to clone
*/
String repository;
/**
* Git ref to checkout
*/
String ref
/**
* Task to run in the repository
*/
List<String> tasks = ['check']
/**
* Init scripts for the build
*/
List<String> initScripts = []
/**
* Map of properties for the build
*/
Map<String, String> projectProperties = [:]
}
}
@@ -34,6 +34,7 @@ class RootProjectPlugin implements Plugin<Project> {
pluginManager.apply(NoHttpPlugin)
pluginManager.apply(SpringNexusPublishPlugin)
pluginManager.apply(CheckProhibitedDependenciesLifecyclePlugin)
pluginManager.apply(ArtifactoryPlugin)
pluginManager.apply("org.sonarqube")
project.repositories.mavenCentral()
@@ -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<Project> {
public class AntoraVersionPlugin implements Plugin<Project> {
public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion";
@Override
@@ -35,32 +35,29 @@ public class CheckAntoraVersionPlugin implements Plugin<Project> {
});
}
});
project.getTasks().register("antoraUpdateVersion", UpdateAntoraVersionTask.class, new Action<UpdateAntoraVersionTask>() {
@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<Project> {
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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -1,10 +1,16 @@
package org.springframework.gradle.github.milestones;
public class RepositoryRef {
package org.springframework.gradle.github;
import java.io.Serializable;
public class RepositoryRef implements Serializable {
private static final long serialVersionUID = 7151218536746822797L;
private String owner;
private String name;
RepositoryRef() {
public RepositoryRef() {
}
public RepositoryRef(String owner, String name) {
@@ -62,4 +68,3 @@ public class RepositoryRef {
}
}
}
@@ -16,6 +16,9 @@
package org.springframework.gradle.github.changelog;
import java.io.File;
import java.nio.file.Paths;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
@@ -28,12 +31,10 @@ import org.gradle.api.artifacts.repositories.IvyArtifactRepository;
import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout;
import org.gradle.api.tasks.JavaExec;
import java.io.File;
import java.nio.file.Paths;
public class GitHubChangelogPlugin implements Plugin<Project> {
public static final String CHANGELOG_GENERATOR_CONFIGURATION_NAME = "changelogGenerator";
public static final String RELEASE_NOTES_PATH = "changelog/release-notes.md";
@Override
public void apply(Project project) {
@@ -42,7 +43,7 @@ public class GitHubChangelogPlugin implements Plugin<Project> {
project.getTasks().register("generateChangelog", JavaExec.class, new Action<JavaExec>() {
@Override
public void execute(JavaExec generateChangelog) {
File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), "changelog/release-notes.md"));
File outputFile = project.file(Paths.get(project.getBuildDir().getPath(), RELEASE_NOTES_PATH));
outputFile.getParentFile().mkdirs();
generateChangelog.setGroup("Release");
generateChangelog.setDescription("Generates the changelog");
@@ -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,22 +16,35 @@
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 java.io.IOException;
import java.util.List;
import org.springframework.gradle.github.RepositoryRef;
public class GitHubMilestoneApi {
private String baseUrl = "https://api.github.com";
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();
@@ -48,26 +61,30 @@ public class GitHubMilestoneApi {
}
public long findMilestoneNumberByTitle(RepositoryRef repositoryRef, String milestoneTitle) {
List<Milestone> 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<Milestone> 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<Milestone> milestones = this.gson.fromJson(response.body().charStream(), new TypeToken<List<Milestone>>(){}.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<List<Milestone>>(){}.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);
}
}
@@ -87,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<Milestone> milestones = this.gson.fromJson(response.body().charStream(),
new TypeToken<List<Milestone>>() {
}.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<Milestone> milestones = this.gson.fromJson(response.body().charStream(),
new TypeToken<List<Milestone>>() {
}.getType());
Optional<String> 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 {
@@ -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,30 +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;
public class GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask {
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 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() {
@@ -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<RepositoryRef> 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);
}
}
@@ -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<RepositoryRef> 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);
}
}
@@ -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<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("gitHubCheckMilestoneHasNoOpenIssues", GitHubMilestoneHasNoOpenIssuesTask.class, new Action<GitHubMilestoneHasNoOpenIssuesTask>() {
@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<GitHubMilestoneNextReleaseTask> 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"));
});
}
}
@@ -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<LocalDate> {
@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());
}
}
@@ -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<LocalDateTime> {
@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);
}
}
@@ -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 + '\'' +
'}';
}
}
@@ -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;
}
}
@@ -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<RepositoryRef> 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;
}
}
@@ -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.
* <p>
* The rules are:
* <ol>
* <li>Train 1 (January-May) or 2 (July-November)</li>
* <li>Version number (e.g. 0.1.2, 1.0.0, etc.)</li>
* <li>Week of month (1st, 2nd, 3rd, 4th)</li>
* <li>Day of week (Monday-Friday)</li>
* <li>Year (e.g. 2020, 2021, etc.)</li>
* </ol>
*
* 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<String, LocalDate> getTrainDates() {
Map<String, LocalDate> 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.
* <p>
* 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<String, LocalDate> 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);
}
}
@@ -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);
}
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2002-2021 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.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.Project;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.springframework.gradle.github.RepositoryRef;
import org.springframework.gradle.github.changelog.GitHubChangelogPlugin;
/**
* @author Steve Riesenberg
*/
public class CreateGitHubReleaseTask extends DefaultTask {
@Input
private RepositoryRef repository = new RepositoryRef();
@Input @Optional
private String gitHubAccessToken;
@Input
private String version;
@Input @Optional
private String branch = "main";
@Input
private boolean createRelease = false;
@TaskAction
public void createGitHubRelease() {
String body = readReleaseNotes();
Release release = Release.tag(this.version)
.commit(this.branch)
.name(this.version)
.body(body)
.preRelease(this.version.contains("-"))
.build();
System.out.printf("%sCreating GitHub release for %s/%s@%s\n",
this.createRelease ? "" : "[DRY RUN] ",
this.repository.getOwner(),
this.repository.getName(),
this.version
);
System.out.printf(" Release Notes:\n\n----\n%s\n----\n\n", body.trim());
if (this.createRelease) {
GitHubReleaseApi github = new GitHubReleaseApi(this.gitHubAccessToken);
github.publishRelease(this.repository, release);
}
}
private String readReleaseNotes() {
Project project = getProject();
File inputFile = project.file(Paths.get(project.getBuildDir().getPath(), GitHubChangelogPlugin.RELEASE_NOTES_PATH));
try {
return Files.readString(inputFile.toPath());
} catch (IOException ex) {
throw new RuntimeException("Unable to read release notes from " + inputFile, ex);
}
}
public RepositoryRef getRepository() {
return repository;
}
public void repository(Action<RepositoryRef> 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 getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getBranch() {
return branch;
}
public void setBranch(String branch) {
this.branch = branch;
}
public boolean isCreateRelease() {
return createRelease;
}
public void setCreateRelease(boolean createRelease) {
this.createRelease = createRelease;
}
}
@@ -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<RepositoryRef> 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;
}
}
@@ -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);
}
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2002-2021 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 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 releases.
*
* @author Steve Riesenberg
*/
public class GitHubReleaseApi {
private String baseUrl = "https://api.github.com";
private final OkHttpClient httpClient;
private Gson gson = new Gson();
public GitHubReleaseApi(String gitHubAccessToken) {
this.httpClient = new OkHttpClient.Builder()
.addInterceptor(new AuthorizationInterceptor(gitHubAccessToken))
.build();
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
/**
* Publish a release with no binary attachments.
*
* @param repository The repository owner/name
* @param release The contents of the release
*/
public void publishRelease(RepositoryRef repository, Release release) {
String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/releases";
String json = this.gson.toJson(release);
RequestBody body = RequestBody.create(MediaType.parse("application/json"), json);
Request request = new Request.Builder().url(url).post(body).build();
try {
Response response = this.httpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new RuntimeException(String.format("Could not create release %s for repository %s/%s. Got response %s",
release.getName(), repository.getOwner(), repository.getName(), response));
}
} catch (IOException ex) {
throw new RuntimeException(String.format("Could not create release %s for repository %s/%s",
release.getName(), 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);
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2002-2021 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 groovy.lang.MissingPropertyException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
/**
* @author Steve Riesenberg
*/
public class GitHubReleasePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
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=...");
}
});
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"));
});
}
}
@@ -0,0 +1,156 @@
/*
* Copyright 2002-2021 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 com.google.gson.annotations.SerializedName;
/**
* @author Steve Riesenberg
*/
public class Release {
@SerializedName("tag_name")
private final String tag;
@SerializedName("target_commitish")
private final String commit;
@SerializedName("name")
private final String name;
@SerializedName("body")
private final String body;
@SerializedName("draft")
private final boolean draft;
@SerializedName("prerelease")
private final boolean preRelease;
@SerializedName("generate_release_notes")
private final boolean generateReleaseNotes;
private Release(String tag, String commit, String name, String body, boolean draft, boolean preRelease, boolean generateReleaseNotes) {
this.tag = tag;
this.commit = commit;
this.name = name;
this.body = body;
this.draft = draft;
this.preRelease = preRelease;
this.generateReleaseNotes = generateReleaseNotes;
}
public String getTag() {
return tag;
}
public String getCommit() {
return commit;
}
public String getName() {
return name;
}
public String getBody() {
return body;
}
public boolean isDraft() {
return draft;
}
public boolean isPreRelease() {
return preRelease;
}
public boolean isGenerateReleaseNotes() {
return generateReleaseNotes;
}
@Override
public String toString() {
return "Release{" +
"tag='" + tag + '\'' +
", commit='" + commit + '\'' +
", name='" + name + '\'' +
", body='" + body + '\'' +
", draft=" + draft +
", preRelease=" + preRelease +
", generateReleaseNotes=" + generateReleaseNotes +
'}';
}
public static Builder tag(String tag) {
return new Builder().tag(tag);
}
public static Builder commit(String commit) {
return new Builder().commit(commit);
}
public static final class Builder {
private String tag;
private String commit;
private String name;
private String body;
private boolean draft;
private boolean preRelease;
private boolean generateReleaseNotes;
private Builder() {
}
public Builder tag(String tag) {
this.tag = tag;
return this;
}
public Builder commit(String commit) {
this.commit = commit;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder draft(boolean draft) {
this.draft = draft;
return this;
}
public Builder preRelease(boolean preRelease) {
this.preRelease = preRelease;
return this;
}
public Builder generateReleaseNotes(boolean generateReleaseNotes) {
this.generateReleaseNotes = generateReleaseNotes;
return this;
}
public Release build() {
return new Release(tag, commit, name, body, draft, preRelease, generateReleaseNotes);
}
}
}
@@ -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<String, Object> inputs;
public WorkflowDispatch() {
}
public WorkflowDispatch(String ref, Map<String, Object> inputs) {
this.ref = ref;
this.inputs = inputs;
}
public String getRef() {
return ref;
}
public void setRef(String ref) {
this.ref = ref;
}
public Map<String, Object> getInputs() {
return inputs;
}
public void setInputs(Map<String, Object> inputs) {
this.inputs = inputs;
}
}
@@ -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());
}
}
}
@@ -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<String, String> 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);
}
}
}
@@ -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<Project> {
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<GitHubApi.FindCreateIssueResult> createIssueResultMono(UpdateDependenciesExtension updateDependenciesExtension) {
@@ -187,7 +182,7 @@ public class UpdateDependenciesPlugin implements Plugin<Project> {
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<Project> {
};
}
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<ComponentSelectionWithCurrent> excludeWithRegex(String regex, String reason) {
Pattern pattern = Pattern.compile(regex);
return (selection) -> {
@@ -242,40 +213,17 @@ public class UpdateDependenciesPlugin implements Plugin<Project> {
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<String, String> 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);
@@ -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<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("updateProjectVersion", UpdateProjectVersionTask.class, new Action<UpdateProjectVersionTask>() {
@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<UpdateToSnapshotVersionTask>() {
@Override
public void execute(UpdateToSnapshotVersionTask updateToSnapshotVersionTask) {
updateToSnapshotVersionTask.setGroup("Release");
updateToSnapshotVersionTask.setDescription(
"Updates the project version to the next snapshot in gradle.properties");
}
});
}
}
@@ -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;
});
}
}
@@ -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");
}
}
}
@@ -1 +0,0 @@
implementation-class=io.spring.gradle.convention.IncludeCheckRemotePlugin
@@ -1,148 +0,0 @@
/*
* 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 io.spring.gradle.convention;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import io.spring.gradle.IncludeRepoTask;
import org.apache.commons.io.FileUtils;
import org.gradle.api.Project;
import org.gradle.api.tasks.GradleBuild;
import org.gradle.testfixtures.ProjectBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class IncludeCheckRemotePluginTest {
Project rootProject;
@AfterEach
public void cleanup() throws Exception {
if (rootProject != null) {
FileUtils.deleteDirectory(rootProject.getProjectDir());
}
}
@Test
void applyWhenExtensionPropertiesNoTasksThenCreateCheckRemoteTaskWithDefaultTask() {
this.rootProject = ProjectBuilder.builder().build();
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
(includeCheckRemoteExtension) -> {
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
includeCheckRemoteExtension.setProperty("ref", "main");
});
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
assertThat(checkRemote.getTasks()).containsExactly("check");
}
@Test
void applyWhenExtensionPropertiesTasksThenCreateCheckRemoteWithProvidedTasks() {
this.rootProject = ProjectBuilder.builder().build();
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
(includeCheckRemoteExtension) -> {
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
includeCheckRemoteExtension.setProperty("ref", "main");
includeCheckRemoteExtension.setProperty("tasks", Arrays.asList("clean", "build", "test"));
});
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
assertThat(checkRemote.getTasks()).containsExactly("clean", "build", "test");
}
@Test
void applyWhenExtensionPropertiesInitScriptsThenCreateCheckRemoteWithProvidedTasks() {
this.rootProject = ProjectBuilder.builder().build();
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
(includeCheckRemoteExtension) -> {
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
includeCheckRemoteExtension.setProperty("ref", "main");
includeCheckRemoteExtension.setProperty("initScripts", Arrays.asList("spring-security-ci.gradle"));
});
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
assertThat(checkRemote.getStartParameter().getAllInitScripts()).extracting(File::getName).containsExactly("spring-security-ci.gradle");
}
@Test
void applyWhenExtensionPropertiesBuildPropertiesThenCreateCheckRemoteWithProvidedTasks() {
Map<String, String> projectProperties = new HashMap<>();
projectProperties.put("localRepositoryPath", "~/local/repository");
projectProperties.put("anotherProperty", "some_value");
this.rootProject = ProjectBuilder.builder().build();
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
(includeCheckRemoteExtension) -> {
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
includeCheckRemoteExtension.setProperty("ref", "main");
includeCheckRemoteExtension.setProperty("projectProperties", projectProperties);
});
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
assertThat(checkRemote.getStartParameter().getProjectProperties()).containsEntry("localRepositoryPath", "~/local/repository")
.containsEntry("anotherProperty", "some_value");
}
@Test
void applyWhenExtensionPropertiesThenRegisterIncludeRepoTaskWithExtensionProperties() {
this.rootProject = ProjectBuilder.builder().build();
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
(includeCheckRemoteExtension) -> {
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
includeCheckRemoteExtension.setProperty("ref", "main");
});
IncludeRepoTask includeRepo = (IncludeRepoTask) this.rootProject.getTasks().named("includeRepo").get();
assertThat(includeRepo).isNotNull();
assertThat(includeRepo.getRepository().get()).isEqualTo("my-project/my-repository");
assertThat(includeRepo.getRef().get()).isEqualTo("main");
}
@Test
void applyWhenRegisterTasksThenCheckRemoteDirSameAsIncludeRepoOutputDir() {
this.rootProject = ProjectBuilder.builder().build();
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
this.rootProject.getExtensions().configure(IncludeCheckRemotePlugin.IncludeCheckRemoteExtension.class,
(includeCheckRemoteExtension) -> {
includeCheckRemoteExtension.setProperty("repository", "my-project/my-repository");
includeCheckRemoteExtension.setProperty("ref", "main");
});
IncludeRepoTask includeRepo = (IncludeRepoTask) this.rootProject.getTasks().named("includeRepo").get();
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
assertThat(checkRemote.getDir()).isEqualTo(includeRepo.getOutputDirectory());
}
@Test
void applyWhenNoExtensionPropertiesThenRegisterTasks() {
this.rootProject = ProjectBuilder.builder().build();
this.rootProject.getPluginManager().apply(IncludeCheckRemotePlugin.class);
IncludeRepoTask includeRepo = (IncludeRepoTask) this.rootProject.getTasks().named("includeRepo").get();
GradleBuild checkRemote = (GradleBuild) this.rootProject.getTasks().named("checkRemote").get();
assertThat(includeRepo).isNotNull();
assertThat(checkRemote).isNotNull();
}
}
@@ -1,388 +0,0 @@
package io.spring.gradle.github.milestones;
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.milestones.GitHubMilestoneApi;
import org.springframework.gradle.github.milestones.RepositoryRef;
import java.util.concurrent.TimeUnit;
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\\nFor Security Vulnerabilities, please use https://pivotal.io/security#reporting\\r\\n-->\\r\\n\\r\\n<!--\\r\\nBefore creating new features, we recommend creating an issue to discuss the feature. This ensures that everyone is on the same page before extensive work is done.\\r\\n\\r\\nThanks for contributing to Spring Security. Please provide a brief description of your pull-request and reference any related issue numbers (prefix references with gh-).\\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);
}
}
@@ -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;
@@ -1,5 +1,12 @@
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.time.ZoneId;
import java.util.concurrent.TimeUnit;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
@@ -7,7 +14,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
import org.springframework.gradle.github.RepositoryRef;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -383,4 +390,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\":\"" + LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant().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()));
}
}
@@ -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<String, LocalDate> 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<String, LocalDate> 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<String, LocalDate> 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<String, LocalDate> 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<String, LocalDate> 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<String, LocalDate> 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);
}
}
@@ -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<String, Object> 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));
}
}
@@ -0,0 +1,155 @@
/*
* 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.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 GitHubReleaseApiTests {
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.baseUrl = this.server.url("/api").toString();
this.gitHubReleaseApi = new GitHubReleaseApi("mock-oauth-token");
this.gitHubReleaseApi.setBaseUrl(this.baseUrl);
this.repository = new RepositoryRef("spring-projects", "spring-security");
}
@AfterEach
public void cleanup() throws Exception {
this.server.shutdown();
}
@Test
public void publishReleaseWhenValidParametersThenSuccess() throws Exception {
String responseJson = "{\n" +
" \"url\": \"https://api.github.com/spring-projects/spring-security/releases/1\",\n" +
" \"html_url\": \"https://github.com/spring-projects/spring-security/releases/tags/v1.0.0\",\n" +
" \"assets_url\": \"https://api.github.com/spring-projects/spring-security/releases/1/assets\",\n" +
" \"upload_url\": \"https://uploads.github.com/spring-projects/spring-security/releases/1/assets{?name,label}\",\n" +
" \"tarball_url\": \"https://api.github.com/spring-projects/spring-security/tarball/v1.0.0\",\n" +
" \"zipball_url\": \"https://api.github.com/spring-projects/spring-security/zipball/v1.0.0\",\n" +
" \"discussion_url\": \"https://github.com/spring-projects/spring-security/discussions/90\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDc6UmVsZWFzZTE=\",\n" +
" \"tag_name\": \"v1.0.0\",\n" +
" \"target_commitish\": \"main\",\n" +
" \"name\": \"v1.0.0\",\n" +
" \"body\": \"Description of the release\",\n" +
" \"draft\": false,\n" +
" \"prerelease\": false,\n" +
" \"created_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"published_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"author\": {\n" +
" \"login\": \"sjohnr\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDQ6VXNlcjE=\",\n" +
" \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" +
" \"gravatar_id\": \"\",\n" +
" \"url\": \"https://api.github.com/users/sjohnr\",\n" +
" \"html_url\": \"https://github.com/sjohnr\",\n" +
" \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" +
" \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" +
" \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" +
" \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" +
" \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" +
" \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" +
" \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" +
" \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" +
" \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" +
" \"type\": \"User\",\n" +
" \"site_admin\": false\n" +
" },\n" +
" \"assets\": [\n" +
" {\n" +
" \"url\": \"https://api.github.com/spring-projects/spring-security/releases/assets/1\",\n" +
" \"browser_download_url\": \"https://github.com/spring-projects/spring-security/releases/download/v1.0.0/example.zip\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDEyOlJlbGVhc2VBc3NldDE=\",\n" +
" \"name\": \"example.zip\",\n" +
" \"label\": \"short description\",\n" +
" \"state\": \"uploaded\",\n" +
" \"content_type\": \"application/zip\",\n" +
" \"size\": 1024,\n" +
" \"download_count\": 42,\n" +
" \"created_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"updated_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"uploader\": {\n" +
" \"login\": \"sjohnr\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDQ6VXNlcjE=\",\n" +
" \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" +
" \"gravatar_id\": \"\",\n" +
" \"url\": \"https://api.github.com/users/sjohnr\",\n" +
" \"html_url\": \"https://github.com/sjohnr\",\n" +
" \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" +
" \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" +
" \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" +
" \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" +
" \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" +
" \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" +
" \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" +
" \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" +
" \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" +
" \"type\": \"User\",\n" +
" \"site_admin\": false\n" +
" }\n" +
" }\n" +
" ]\n" +
"}";
this.server.enqueue(new MockResponse().setBody(responseJson));
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().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.gitHubReleaseApi.publishRelease(this.repository, Release.tag("1.0.0").build()));
}
}
@@ -65,7 +65,7 @@ public class NamespaceLdapAuthenticationProviderTestsConfigs {
.ldif("classpath:users.xldif") // ldap-server@ldif
.managerDn("uid=admin,ou=system") // ldap-server@manager-dn
.managerPassword("secret") // ldap-server@manager-password
.port(33399) // ldap-server@port
.port(0) // ldap-server@port
.root("dc=springframework,dc=org"); // ldap-server@root
// .url("ldap://localhost:33389/dc-springframework,dc=org") this overrides root and port and is used for external
// @formatter:on
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -39,6 +39,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer;
@@ -79,8 +80,7 @@ public class AuthenticationConfiguration {
public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor,
ApplicationContext context) {
LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context);
AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context,
AuthenticationEventPublisher.class);
AuthenticationEventPublisher authenticationEventPublisher = getAuthenticationEventPublisher(context);
DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder(
objectPostProcessor, defaultPasswordEncoder);
if (authenticationEventPublisher != null) {
@@ -142,6 +142,13 @@ public class AuthenticationConfiguration {
this.objectPostProcessor = objectPostProcessor;
}
private AuthenticationEventPublisher getAuthenticationEventPublisher(ApplicationContext context) {
if (context.getBeanNamesForType(AuthenticationEventPublisher.class).length > 0) {
return context.getBean(AuthenticationEventPublisher.class);
}
return this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher());
}
@SuppressWarnings("unchecked")
private <T> T lazyBean(Class<T> interfaceName) {
LazyInitTargetSource lazyTargetSource = new LazyInitTargetSource();
@@ -264,6 +264,7 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
* @deprecated Use {@link #privilegeEvaluator(WebInvocationPrivilegeEvaluator)}
* instead
*/
@Deprecated
public WebSecurity securityInterceptor(FilterSecurityInterceptor securityInterceptor) {
this.filterSecurityInterceptor = securityInterceptor;
return this;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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.
@@ -24,7 +24,9 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
@@ -82,6 +84,7 @@ class HttpSecurityConfiguration {
AuthenticationManagerBuilder authenticationBuilder = new WebSecurityConfigurerAdapter.DefaultPasswordEncoderAuthenticationManagerBuilder(
this.objectPostProcessor, passwordEncoder);
authenticationBuilder.parentAuthenticationManager(authenticationManager());
authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
// @formatter:off
http
@@ -105,6 +108,13 @@ class HttpSecurityConfiguration {
: this.authenticationConfiguration.getAuthenticationManager();
}
private AuthenticationEventPublisher getAuthenticationEventPublisher() {
if (this.context.getBeanNamesForType(AuthenticationEventPublisher.class).length > 0) {
return this.context.getBean(AuthenticationEventPublisher.class);
}
return this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher());
}
private Map<Class<?>, Object> createSharedObjects() {
Map<Class<?>, Object> sharedObjects = new HashMap<>();
sharedObjects.put(ApplicationContext.class, this.context);
@@ -34,8 +34,10 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -51,6 +53,7 @@ import org.springframework.security.config.annotation.web.servlet.configuration.
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.config.users.AuthenticationTestConfiguration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -62,6 +65,7 @@ import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -294,6 +298,28 @@ public class AuthenticationConfigurationTests {
assertThatExceptionOfType(AlreadyBuiltException.class).isThrownBy(ap::build);
}
@Test
public void configureWhenDefaultsThenDefaultAuthenticationEventPublisher() {
this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class).autowire();
AuthenticationManagerBuilder authenticationManagerBuilder = this.spring.getContext()
.getBean(AuthenticationManagerBuilder.class);
AuthenticationEventPublisher eventPublisher = (AuthenticationEventPublisher) ReflectionTestUtils
.getField(authenticationManagerBuilder, "eventPublisher");
assertThat(eventPublisher).isInstanceOf(DefaultAuthenticationEventPublisher.class);
}
@Test
public void configureWhenCustomAuthenticationEventPublisherThenCustomAuthenticationEventPublisher() {
this.spring.register(AuthenticationConfiguration.class, ObjectPostProcessorConfiguration.class,
CustomAuthenticationEventPublisherConfig.class).autowire();
AuthenticationManagerBuilder authenticationManagerBuilder = this.spring.getContext()
.getBean(AuthenticationManagerBuilder.class);
AuthenticationEventPublisher eventPublisher = (AuthenticationEventPublisher) ReflectionTestUtils
.getField(authenticationManagerBuilder, "eventPublisher");
assertThat(eventPublisher)
.isInstanceOf(CustomAuthenticationEventPublisherConfig.MyAuthenticationEventPublisher.class);
}
@EnableGlobalMethodSecurity(securedEnabled = true)
static class GlobalMethodSecurityAutowiredConfig {
@@ -346,6 +372,30 @@ public class AuthenticationConfigurationTests {
}
@Configuration
static class CustomAuthenticationEventPublisherConfig {
@Bean
AuthenticationEventPublisher eventPublisher() {
return new MyAuthenticationEventPublisher();
}
static class MyAuthenticationEventPublisher implements AuthenticationEventPublisher {
@Override
public void publishAuthenticationSuccess(Authentication authentication) {
}
@Override
public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
}
}
}
interface Service {
void run();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 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,6 +16,8 @@
package org.springframework.security.config.annotation.web.configuration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import javax.servlet.http.HttpServletRequest;
@@ -27,12 +29,19 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
@@ -48,6 +57,7 @@ import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.config.Customizer.withDefaults;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@@ -200,6 +210,48 @@ public class HttpSecurityConfigurationTests {
this.mockMvc.perform(get("/login?logout")).andExpect(status().isOk());
}
@Test
public void loginWhenUsingDefaultThenAuthenticationEventPublished() throws Exception {
this.spring
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, AuthenticationEventListenerConfig.class)
.autowire();
AuthenticationEventListenerConfig.clearEvents();
this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection());
assertThat(AuthenticationEventListenerConfig.EVENTS).isNotEmpty();
assertThat(AuthenticationEventListenerConfig.EVENTS).hasSize(1);
}
@Test
public void loginWhenUsingDefaultAndNoUserDetailsServiceThenAuthenticationEventPublished() throws Exception {
this.spring
.register(SecurityEnabledConfig.class, UserDetailsConfig.class, AuthenticationEventListenerConfig.class)
.autowire();
AuthenticationEventListenerConfig.clearEvents();
this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection());
assertThat(AuthenticationEventListenerConfig.EVENTS).isNotEmpty();
assertThat(AuthenticationEventListenerConfig.EVENTS).hasSize(1);
}
@Test
public void loginWhenUsingCustomAuthenticationEventPublisherThenAuthenticationEventPublished() throws Exception {
this.spring.register(SecurityEnabledConfig.class, UserDetailsConfig.class,
CustomAuthenticationEventPublisherConfig.class).autowire();
CustomAuthenticationEventPublisherConfig.clearEvents();
this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection());
assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).isNotEmpty();
assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).hasSize(1);
}
@Test
public void loginWhenUsingCustomAuthenticationEventPublisherAndNoUserDetailsServiceThenAuthenticationEventPublished()
throws Exception {
this.spring.register(SecurityEnabledConfig.class, CustomAuthenticationEventPublisherConfig.class).autowire();
CustomAuthenticationEventPublisherConfig.clearEvents();
this.mockMvc.perform(formLogin()).andExpect(status().is3xxRedirection());
assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).isNotEmpty();
assertThat(CustomAuthenticationEventPublisherConfig.EVENTS).hasSize(1);
}
@RestController
static class NameController {
@@ -270,6 +322,55 @@ public class HttpSecurityConfigurationTests {
}
@Configuration
static class CustomAuthenticationEventPublisherConfig {
static List<Authentication> EVENTS = new ArrayList<>();
static void clearEvents() {
EVENTS.clear();
}
@Bean
AuthenticationEventPublisher publisher() {
return new AuthenticationEventPublisher() {
@Override
public void publishAuthenticationSuccess(Authentication authentication) {
EVENTS.add(authentication);
}
@Override
public void publishAuthenticationFailure(AuthenticationException exception,
Authentication authentication) {
EVENTS.add(authentication);
}
};
}
}
@Configuration
static class AuthenticationEventListenerConfig {
static List<AbstractAuthenticationEvent> EVENTS = new ArrayList<>();
static void clearEvents() {
EVENTS.clear();
}
@EventListener
void onAuthenticationSuccessEvent(AuthenticationSuccessEvent event) {
EVENTS.add(event);
}
@EventListener
void onAuthenticationFailureEvent(AbstractAuthenticationFailureEvent event) {
EVENTS.add(event);
}
}
@RestController
static class BaseController {
@@ -253,7 +253,7 @@ public class Saml2LoginConfigurerTests {
public void authenticateWithInvalidDeflatedSAMLResponseThenFailureHandlerUses() throws Exception {
this.spring.register(CustomAuthenticationFailureHandler.class).autowire();
byte[] invalidDeflated = "invalid".getBytes();
String encoded = Saml2Utils.samlEncodeNotRfc2045(invalidDeflated);
String encoded = Saml2Utils.samlEncode(invalidDeflated);
MockHttpServletRequestBuilder request = get("/login/saml2/sso/registration-id").queryParam("SAMLResponse",
encoded);
this.mvc.perform(request);
@@ -543,7 +543,7 @@ public class BCrypt {
}
else {
rounds = roundsForLogRounds(log_rounds);
if (rounds < 16 || rounds > Integer.MAX_VALUE) {
if (rounds < 16 || rounds > 2147483648L) {
throw new IllegalArgumentException("Bad number of rounds");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 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.
@@ -17,7 +17,13 @@
package org.springframework.security.data.repository.query;
import org.springframework.data.spel.spi.EvaluationContextExtension;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.DenyAllPermissionEvaluator;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -77,12 +83,21 @@ import org.springframework.security.core.context.SecurityContextHolder;
* it.
*
* @author Rob Winch
* @author Evgeniy Cheban
* @since 4.0
*/
public class SecurityEvaluationContextExtension implements EvaluationContextExtension {
private Authentication authentication;
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
private PermissionEvaluator permissionEvaluator = new DenyAllPermissionEvaluator();
private String defaultRolePrefix = "ROLE_";
/**
* Creates a new instance that uses the current {@link Authentication} found on the
* {@link org.springframework.security.core.context.SecurityContextHolder}.
@@ -106,8 +121,13 @@ public class SecurityEvaluationContextExtension implements EvaluationContextExte
@Override
public SecurityExpressionRoot getRootObject() {
Authentication authentication = getAuthentication();
return new SecurityExpressionRoot(authentication) {
SecurityExpressionRoot root = new SecurityExpressionRoot(authentication) {
};
root.setTrustResolver(this.trustResolver);
root.setRoleHierarchy(this.roleHierarchy);
root.setPermissionEvaluator(this.permissionEvaluator);
root.setDefaultRolePrefix(this.defaultRolePrefix);
return root;
}
private Authentication getAuthentication() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 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.
@@ -20,7 +20,10 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.expression.DenyAllPermissionEvaluator;
import org.springframework.security.access.expression.SecurityExpressionRoot;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -69,6 +72,17 @@ public class SecurityEvaluationContextExtensionTests {
assertThat(getRoot().getAuthentication()).isSameAs(explicit);
}
@Test
public void getRootObjectWhenAdditionalFieldsNotSetThenVerifyDefaults() {
TestingAuthenticationToken explicit = new TestingAuthenticationToken("explicit", "password", "ROLE_EXPLICIT");
this.securityExtension = new SecurityEvaluationContextExtension(explicit);
SecurityExpressionRoot root = getRoot();
assertThat(root).extracting("trustResolver").isInstanceOf(AuthenticationTrustResolverImpl.class);
assertThat(root).extracting("roleHierarchy").isInstanceOf(NullRoleHierarchy.class);
assertThat(root).extracting("permissionEvaluator").isInstanceOf(DenyAllPermissionEvaluator.class);
assertThat(root).extracting("defaultRolePrefix").isEqualTo("ROLE_");
}
private SecurityExpressionRoot getRoot() {
return this.securityExtension.getRootObject();
}
+9 -9
View File
@@ -8,13 +8,13 @@ javaPlatform {
dependencies {
api platform("org.springframework:spring-framework-bom:$springFrameworkVersion")
api platform("io.projectreactor:reactor-bom:2020.0.19")
api platform("io.rsocket:rsocket-bom:1.1.2")
api platform("io.projectreactor:reactor-bom:2020.0.24")
api platform("io.rsocket:rsocket-bom:1.1.3")
api platform("org.junit:junit-bom:5.8.2")
api platform("org.springframework.data:spring-data-bom:2021.1.4")
api platform("org.springframework.data:spring-data-bom:2021.1.8")
api platform("org.jetbrains.kotlin:kotlin-bom:$kotlinVersion")
api platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2")
api platform("com.fasterxml.jackson:jackson-bom:2.13.2.20220328")
api platform("com.fasterxml.jackson:jackson-bom:2.13.4.20221012")
constraints {
api "ch.qos.logback:logback-classic:1.2.11"
api "com.google.inject:guice:3.0"
@@ -50,11 +50,11 @@ dependencies {
api "org.assertj:assertj-core:3.21.0"
api "org.bouncycastle:bcpkix-jdk15on:1.69"
api "org.bouncycastle:bcprov-jdk15on:1.69"
api "org.eclipse.jetty:jetty-server:9.4.46.v20220331"
api "org.eclipse.jetty:jetty-servlet:9.4.46.v20220331"
api "org.eclipse.jetty:jetty-server:9.4.49.v20220914"
api "org.eclipse.jetty:jetty-servlet:9.4.49.v20220914"
api "org.eclipse.persistence:javax.persistence:2.2.1"
api "org.hamcrest:hamcrest:2.2"
api "org.hibernate:hibernate-entitymanager:5.6.8.Final"
api "org.hibernate:hibernate-entitymanager:5.6.12.Final"
api "org.hsqldb:hsqldb:2.6.1"
api "org.jasig.cas.client:cas-client-core:3.6.4"
api "org.mockito:mockito-core:3.12.4"
@@ -68,10 +68,10 @@ dependencies {
api "org.seleniumhq.selenium:htmlunit-driver:2.54.0"
api "org.seleniumhq.selenium:selenium-java:3.141.59"
api "org.seleniumhq.selenium:selenium-support:3.141.59"
api "org.skyscreamer:jsonassert:1.5.0"
api "org.skyscreamer:jsonassert:1.5.1"
api "org.slf4j:log4j-over-slf4j:1.7.36"
api "org.slf4j:slf4j-api:1.7.36"
api "org.springframework.ldap:spring-ldap-core:2.3.7.RELEASE"
api "org.springframework.ldap:spring-ldap-core:2.3.8.RELEASE"
api "org.synchronoss.cloud:nio-multipart-parser:1.1.0"
}
}
+2
View File
@@ -0,0 +1,2 @@
/package-lock.json
/node_modules/
+53
View File
@@ -0,0 +1,53 @@
'use strict'
/* Copyright (c) 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.
*/
const { promises: fsp } = require('fs')
const ospath = require('path')
/**
* Rewrites local content sources to support the use of linked worktrees.
*
* @author Dan Allen <dan@opendevise.com>
*/
module.exports.register = function () {
this.once('playbookBuilt', async ({ playbook }) => {
const expandPath = this.require('@antora/expand-path-helper')
for (const contentSource of playbook.content.sources) {
const { url, branches } = contentSource
if (url.charAt() !== '.') continue
const absdir = expandPath(url, { dot: playbook.dir })
const gitfile = ospath.join(absdir, '.git')
if (await fsp.stat(gitfile).then((stat) => !stat.isDirectory(), () => false)) {
const worktreeGitdir = await fsp.readFile(gitfile, 'utf8')
.then((contents) => contents.trimRight().substr(8))
const worktreeBranch = await fsp.readFile(ospath.join(worktreeGitdir, 'HEAD'), 'utf8')
.then((contents) => contents.trimRight().replace(/^ref: (?:refs\/heads\/)?/, ''))
const reldir = ospath.relative(
playbook.dir,
await fsp.readFile(ospath.join(worktreeGitdir, 'commondir'), 'utf8')
.then((contents) => {
const gitdir = ospath.join(worktreeGitdir, contents.trimRight())
return ospath.basename(gitdir) === '.git' ? ospath.dirname(gitdir) : gitdir
})
)
contentSource.url = reldir ? `.${ospath.sep}${reldir}` : '.'
if (!branches) continue
contentSource.branches = (branches.constructor === Array ? branches : [branches])
.map((pattern) => pattern.replaceAll('HEAD', worktreeBranch))
}
}
})
}
-26
View File
@@ -1,26 +0,0 @@
site:
title: Spring Security
url: https://docs.spring.io/spring-security/reference/
asciidoc:
attributes:
page-pagination: true
content:
sources:
- url: https://github.com/spring-io/spring-generated-docs
branches: [spring-projects/spring-security/*]
- url: https://github.com/spring-projects/spring-security
branches: [main,5.6.x]
start_path: docs
urls:
latest_version_segment_strategy: redirect:to
latest_version_segment: ''
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true
pipeline:
extensions:
- require: ./antora/extensions/major-minor-segment.js
- require: ./antora/extensions/root-component-name.js
+11 -1
View File
@@ -1,2 +1,12 @@
name: ROOT
version: '5.6.4'
version: true
title: Documentation
nav:
- modules/ROOT/nav.adoc
ext:
collector:
run:
command: gradlew -q -PbuildSrc.skipTests=true "-Dorg.gradle.jvmargs=-Xmx3g -XX:+HeapDumpOnOutOfMemoryError" :spring-security-docs:generateAntora
local: true
scan:
dir: ./build/generateAntora
@@ -1,200 +0,0 @@
// https://gitlab.com/antora/antora/-/issues/132#note_712132072
'use strict'
const { posix: path } = require('path')
module.exports.register = (pipeline, { config }) => {
pipeline.on('contentClassified', ({ contentCatalog }) => {
contentCatalog.getComponents().forEach(component => {
const componentName = component.name;
const generationToVersion = new Map();
component.versions.forEach(version => {
const generation = getGeneration(version.version);
const original = generationToVersion.get(generation);
if (original === undefined || (original.prerelease && !version.prerelease)) {
generationToVersion.set(generation, version);
}
});
const versionToGeneration = Array.from(generationToVersion.entries()).reduce((acc, entry) => {
const [ generation, version ] = entry;
acc.set(version.version, generation);
return acc;
}, new Map());
contentCatalog.findBy({ component: componentName }).forEach((file) => {
const candidateVersion = file.src.version;
if (versionToGeneration.has(candidateVersion)) {
const generation = versionToGeneration.get(candidateVersion);
if (file.out) {
if (file.out) {
file.out.dirname = file.out.dirname.replace(candidateVersion, generation)
file.out.path = file.out.path.replace(candidateVersion, generation);
}
}
if (file.pub) {
file.pub.url = file.pub.url.replace(candidateVersion, generation)
}
}
});
versionToGeneration.forEach((generation, mappedVersion) => {
contentCatalog.getComponent(componentName).versions.filter(version => version.version === mappedVersion).forEach((version) => {
version.url = version.url.replace(mappedVersion, generation);
})
const symbolicVersionAlias = createSymbolicVersionAlias(
componentName,
mappedVersion,
generation,
'redirect:to'
)
symbolicVersionAlias.src.version = generation;
contentCatalog.addFile(symbolicVersionAlias);
});
})
})
}
function createSymbolicVersionAlias (component, version, symbolicVersionSegment, strategy) {
if (symbolicVersionSegment == null || symbolicVersionSegment === version) return
const family = 'alias'
const baseVersionAliasSrc = { component, module: 'ROOT', family, relative: '', basename: '', stem: '', extname: '' }
const symbolicVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version: symbolicVersionSegment })
const symbolicVersionAlias = {
src: symbolicVersionAliasSrc,
pub: computePub(
symbolicVersionAliasSrc,
computeOut(symbolicVersionAliasSrc, family, symbolicVersionSegment),
family
),
}
const originalVersionAliasSrc = Object.assign({}, baseVersionAliasSrc, { version })
const originalVersionSegment = computeVersionSegment(component, version, 'original')
const originalVersionAlias = {
src: originalVersionAliasSrc,
pub: computePub(
originalVersionAliasSrc,
computeOut(originalVersionAliasSrc, family, originalVersionSegment),
family
),
}
if (strategy === 'redirect:to') {
originalVersionAlias.out = undefined
originalVersionAlias.rel = symbolicVersionAlias
return originalVersionAlias
} else {
symbolicVersionAlias.out = undefined
symbolicVersionAlias.rel = originalVersionAlias
return symbolicVersionAlias
}
}
function computeOut (src, family, version, htmlUrlExtensionStyle) {
let { component, module: module_, basename, extname, relative, stem } = src
if (module_ === 'ROOT') module_ = ''
let indexifyPathSegment = ''
let familyPathSegment = ''
if (family === 'page') {
if (stem !== 'index' && htmlUrlExtensionStyle === 'indexify') {
basename = 'index.html'
indexifyPathSegment = stem
} else if (extname === '.adoc') {
basename = stem + '.html'
}
} else if (family === 'image') {
familyPathSegment = '_images'
} else if (family === 'attachment') {
familyPathSegment = '_attachments'
}
const modulePath = path.join(component, version, module_)
const dirname = path.join(modulePath, familyPathSegment, path.dirname(relative), indexifyPathSegment)
const path_ = path.join(dirname, basename)
const moduleRootPath = path.relative(dirname, modulePath) || '.'
const rootPath = path.relative(dirname, '') || '.'
return { dirname, basename, path: path_, moduleRootPath, rootPath }
}
function computePub (src, out, family, version, htmlUrlExtensionStyle) {
const pub = {}
let url
if (family === 'nav') {
const urlSegments = version ? [src.component, version] : [src.component]
if (src.module && src.module !== 'ROOT') urlSegments.push(src.module)
// an artificial URL used for resolving page references in navigation model
url = '/' + urlSegments.join('/') + '/'
pub.moduleRootPath = '.'
} else if (family === 'page') {
const urlSegments = out.path.split('/')
const lastUrlSegmentIdx = urlSegments.length - 1
if (htmlUrlExtensionStyle === 'drop') {
// drop just the .html extension or, if the filename is index.html, the whole segment
const lastUrlSegment = urlSegments[lastUrlSegmentIdx]
urlSegments[lastUrlSegmentIdx] =
lastUrlSegment === 'index.html' ? '' : lastUrlSegment.substr(0, lastUrlSegment.length - 5)
} else if (htmlUrlExtensionStyle === 'indexify') {
urlSegments[lastUrlSegmentIdx] = ''
}
url = '/' + urlSegments.join('/')
} else {
url = '/' + out.path
if (family === 'alias' && !src.relative.length) pub.splat = true
}
pub.url = ~url.indexOf(' ') ? url.replace(SPACE_RX, '%20') : url
if (out) {
pub.moduleRootPath = out.moduleRootPath
pub.rootPath = out.rootPath
}
return pub
}
function computeVersionSegment (name, version, mode) {
if (mode === 'original') return !version || version === 'master' ? '' : version
const strategy = this.latestVersionUrlSegmentStrategy
// NOTE: special exception; revisit in Antora 3
if (!version || version === 'master') {
if (mode !== 'alias') return ''
if (strategy === 'redirect:to') return
}
if (strategy === 'redirect:to' || strategy === (mode === 'alias' ? 'redirect:from' : 'replace')) {
const component = this.getComponent(name)
const componentVersion = component && this.getComponentVersion(component, version)
if (componentVersion) {
const segment =
componentVersion === component.latest
? this.latestVersionUrlSegment
: componentVersion === component.latestPrerelease
? this.latestPrereleaseVersionUrlSegment
: undefined
return segment == null ? version : segment
}
}
return version
}
function getGeneration(version) {
if (!version) return version;
const firstIndex = version.indexOf('.')
if (firstIndex < 0) {
return version;
}
const secondIndex = version.indexOf('.', firstIndex + 1);
const result = version.substr(0, secondIndex);
return result;
}
function out(args) {
console.log(JSON.stringify(args, no_data, 2));
}
function no_data(key, value) {
if (key == "data" || key == "files") {
return value ? "__data__" : value;
}
return value;
}
@@ -1,40 +0,0 @@
// https://gitlab.com/antora/antora/-/issues/132#note_712132072
'use strict'
const { posix: path } = require('path')
module.exports.register = (pipeline, { config }) => {
pipeline.on('contentClassified', ({ contentCatalog }) => {
const rootComponentName = config.rootComponentName || 'ROOT'
const rootComponentNameLength = rootComponentName.length
contentCatalog.findBy({ component: rootComponentName }).forEach((file) => {
if (file.out) {
file.out.dirname = file.out.dirname.substr(rootComponentNameLength)
file.out.path = file.out.path.substr(rootComponentNameLength + 1)
file.out.rootPath = fixPath(file.out.rootPath)
}
if (file.pub) {
file.pub.url = file.pub.url.substr(rootComponentNameLength + 1)
if (file.pub.rootPath) {
file.pub.rootPath = fixPath(file.pub.rootPath)
}
}
if (file.rel) {
if (file.rel.pub) {
file.rel.pub.url = file.rel.pub.url.substr(rootComponentNameLength + 1)
file.rel.pub.rootPath = fixPath(file.rel.pub.rootPath);
}
}
})
const rootComponent = contentCatalog.getComponent(rootComponentName)
rootComponent?.versions?.forEach((version) => {
version.url = version.url.substr(rootComponentName.length + 1)
})
// const siteStartPage = contentCatalog.getById({ component: '', version: '', module: '', family: 'alias', relative: 'index.adoc' })
// if (siteStartPage) delete siteStartPage.out
})
function fixPath(path) {
return path.split('/').slice(1).join('/') || '.'
}
}
+14 -16
View File
@@ -1,26 +1,24 @@
# The purpose of this Antora playbook is to generate a preview of the docs in the current branch.
antora:
extensions:
- ./antora-linked-worktree-patch.js
- '@antora/collector-extension'
site:
title: Spring Security
url: https://docs.spring.io/spring-security/reference/
asciidoc:
attributes:
page-pagination: true
url: https://docs.spring.io/spring-security/reference
content:
sources:
- url: ../../spring-io/spring-generated-docs
branches: [spring-projects/spring-security/*]
- url: ../../spring-projects/spring-security
branches: [main,5.6.x]
start_path: docs
- url: ./..
branches: HEAD
start_path: docs
worktrees: true
asciidoc:
attributes:
page-pagination: ''
hide-uri-scheme: '@'
urls:
latest_version_segment_strategy: redirect:to
latest_version_segment: ''
redirect_facility: httpd
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true
pipeline:
extensions:
- require: ./antora/extensions/major-minor-segment.js
- require: ./antora/extensions/root-component-name.js
@@ -495,7 +495,7 @@ XML Configuration requires the `NoOpPasswordEncoder` bean name to be `passwordEn
Most applications that allow a user to specify a password also require a feature for updating that password.
https://w3c.github.io/webappsec-change-password-url/[A Well-Know URL for Changing Passwords] indicates a mechanism by which password managers can discover the password update endpoint for a given application.
https://w3c.github.io/webappsec-change-password-url/[A Well-Known URL for Changing Passwords] indicates a mechanism by which password managers can discover the password update endpoint for a given application.
You can configure Spring Security to provide this discovery endpoint.
For example, if the change password endpoint in your application is `/change-password`, then you can configure Spring Security like so:
@@ -278,7 +278,7 @@ If you use additional features (such as LDAP, OpenID, and others), you need to a
Spring Security builds against Spring Framework {spring-core-version} but should generally work with any newer version of Spring Framework 5.x.
Many users are likely to run afoul of the fact that Spring Security's transitive dependencies resolve Spring Framework {spring-core-version}, which can cause strange classpath problems.
The easiest way to resolve this is to use the `spring-framework-bom` within your `<dependencyManagement>` section of your `pom.xml`.
The easiest way to resolve this is to use the `spring-framework-bom` within your `dependencyManagement` section of your `build.gradle`.
You can do so by using the https://github.com/spring-gradle-plugins/dependency-management-plugin[Dependency Management Plugin], as the following example shows:
.build.gradle
@@ -23,13 +23,14 @@ To specify where the introspection endpoint is, simply do:
[source,yaml]
----
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
spring:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
----
Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint.
@@ -8,7 +8,7 @@ Here is a minimal sample of what we can do:
.Java
[source,java,role="primary"]
----
@RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
public class HelloWorldMessageServiceTests {
@Autowired
@@ -42,7 +42,7 @@ public class HelloWorldMessageServiceTests {
.Kotlin
[source,kotlin,role="secondary"]
----
@RunWith(SpringRunner::class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class])
class HelloWorldMessageServiceTests {
@Autowired
@@ -2,9 +2,11 @@
The basic setup looks like this:
[source,java]
====
.Java
[source,java,role="primary"]
----
@RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
public class HelloWebfluxMethodApplicationTests {
@Autowired
@@ -12,16 +14,42 @@ public class HelloWebfluxMethodApplicationTests {
WebTestClient rest;
@Before
@BeforeEach
public void setup() {
this.rest = WebTestClient
.bindToApplicationContext(this.context)
// add Spring Security test Support
.apply(springSecurity())
.configureClient()
.filter(basicAuthentication())
.filter(basicAuthentication("user", "password"))
.build();
}
// ...
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class])
class HelloWebfluxMethodApplicationTests {
@Autowired
lateinit var context: ApplicationContext
lateinit var rest: WebTestClient
@BeforeEach
fun setup() {
this.rest = WebTestClient
.bindToApplicationContext(this.context)
// add Spring Security test Support
.apply(springSecurity())
.configureClient()
.filter(basicAuthentication("user", "password"))
.build()
}
// ...
}
----
====
@@ -2,7 +2,7 @@
= Authentication
Spring Security provides comprehensive support for xref:features/authentication/index.adoc#authentication[Authentication].
We start by discussing the overall xref:servlet/architecture.adoc#servlet-architecture[Servlet Authentication Architecture].
We start by discussing the overall xref:servlet/authentication/architecture.adoc[Servlet Authentication Architecture].
As you might expect, this section is more abstract describing the architecture without much discussion on how it applies to concrete flows.
If you prefer, you can refer to <<servlet-authentication-mechanisms,Authentication Mechanisms>> for concrete ways in which users can authenticate.
@@ -69,7 +69,11 @@ SecurityFilterChain web(HttpSecurity http) throws Exception {
.authorizeHttpRequests(authorize -> authorize // <1>
.mvcMatchers("/resources/**", "/signup", "/about").permitAll() // <2>
.mvcMatchers("/admin/**").hasRole("ADMIN") // <3>
.mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") // <4>
.mvcMatchers("/db/**").access((authentication, request) ->
Optional.of(hasRole("ADMIN").check(authentication, request))
.filter((decision) -> !decision.isGranted())
.orElseGet(() -> hasRole("DBA").check(authentication, request));
) // <4>
.anyRequest().denyAll() // <5>
);
@@ -24,13 +24,14 @@ To specify where the introspection endpoint is, simply do:
[source,yaml]
----
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
spring:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
----
Where `https://idp.example.com/introspect` is the introspection endpoint hosted by your authorization server and `client-id` and `client-secret` are the credentials needed to hit that endpoint.
@@ -49,7 +49,7 @@ Before we can use Spring Security Test support, we must perform some setup. An e
.Java
[source,java,role="primary"]
----
@RunWith(SpringJUnit4ClassRunner.class) // <1>
@ExtendWith(SpringExtension.class) // <1>
@ContextConfiguration // <2>
public class WithMockUserTests {
----
@@ -57,16 +57,15 @@ public class WithMockUserTests {
.Kotlin
[source,kotlin,role="secondary"]
----
@RunWith(SpringJUnit4ClassRunner::class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration
class WithMockUserTests {
----
====
This is a basic example of how to setup Spring Security Test. The highlights are:
<1> `@RunWith` instructs the spring-test module that it should create an `ApplicationContext`. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#integration-testing-annotations-standard[Spring Reference]
<2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the https://docs.spring.io/spring-framework/docs/4.0.x/spring-framework-reference/htmlsingle/#testcontext-ctx-management[Spring Reference]
<1> `@ExtendWith` instructs the spring-test module that it should create an `ApplicationContext`. For additional information, refer to the {spring-framework-reference-url}testing.html#testcontext-junit-jupiter-extension[Spring reference].
<2> `@ContextConfiguration` instructs the spring-test the configuration to use to create the `ApplicationContext`. Since no configuration is specified, the default configuration locations will be tried. This is no different than using the existing Spring Test support. For additional information, refer to the {spring-framework-reference-url}testing.html#spring-testing-annotation-contextconfiguration[Spring Reference]
NOTE: Spring Security hooks into Spring Test support using the `WithSecurityContextTestExecutionListener` which will ensure our tests are ran with the correct user.
It does this by populating the `SecurityContextHolder` prior to running our tests.
@@ -225,7 +224,7 @@ For example, the following would run every test with a user with the username "a
.Java
[source,java,role="primary"]
----
@RunWith(SpringJUnit4ClassRunner.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {
@@ -234,7 +233,7 @@ public class WithMockUserTests {
.Kotlin
[source,kotlin,role="secondary"]
----
@RunWith(SpringJUnit4ClassRunner::class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WithMockUser(username="admin",roles=["USER","ADMIN"])
class WithMockUserTests {
@@ -304,7 +303,7 @@ For example, the following will run withMockUser1 and withMockUser2 using <<test
.Java
[source,java,role="primary"]
----
@RunWith(SpringJUnit4ClassRunner.class)
@ExtendWith(SpringExtension.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {
@@ -327,7 +326,7 @@ public class WithUserClassLevelAuthenticationTests {
.Kotlin
[source,kotlin,role="secondary"]
----
@RunWith(SpringJUnit4ClassRunner::class)
@ExtendWith(SpringExtension.class)
@WithMockUser
class WithUserClassLevelAuthenticationTests {
@Test
@@ -15,7 +15,7 @@ NOTE: Spring Security's testing support requires spring-test-4.1.3.RELEASE or gr
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SecurityConfig.class)
@WebAppConfiguration
public class CsrfShowcaseTests {
@@ -25,7 +25,7 @@ public class CsrfShowcaseTests {
private MockMvc mvc;
@Before
@BeforeEach
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
@@ -39,7 +39,7 @@ public class CsrfShowcaseTests {
.Kotlin
[source,kotlin,role="secondary"]
----
@RunWith(SpringJUnit4ClassRunner::class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = [SecurityConfig::class])
@WebAppConfiguration
class CsrfShowcaseTests {
@@ -49,7 +49,7 @@ class CsrfShowcaseTests {
private var mvc: MockMvc? = null
@Before
@BeforeEach
fun setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
+41 -40
View File
@@ -1,53 +1,56 @@
plugins {
id "io.github.rwinch.antora" version "0.0.2"
id "org.springframework.antora.check-version"
id 'org.antora' version '1.0.0-alpha.3'
}
apply plugin: 'io.spring.convention.docs'
apply plugin: 'java'
antora {
antoraVersion = "3.0.0-alpha.8"
arguments = ["--fetch"]
}
tasks.antora {
version = '~3.1'
playbook = file('local-antora-playbook.yml')
options = ['--clean', '--stacktrace']
environment = [
"ALGOLIA_API_KEY" : "82c7ead946afbac3cf98c32446154691",
"ALGOLIA_APP_ID" : "244V8V9FGG",
"ALGOLIA_INDEX_NAME" : "security-docs"
'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691',
'ALGOLIA_APP_ID': '244V8V9FGG',
'ALGOLIA_INDEX_NAME': 'security-docs'
]
dependencies = [
'@antora/collector-extension': '1.0.0-alpha.2'
]
}
tasks.register("generateAntora") {
group = "Documentation"
description = "Generates the antora.yml for dynamic properties"
tasks.register('generateAntora') {
group = 'Documentation'
description = 'Generates the antora.yml for dynamic properties'
doLast {
def docsTag = snapshotBuild ? 'current' : project.version
def ghTag = snapshotBuild ? 'main' : project.version
def ghUrl = "https://github.com/spring-projects/spring-security/tree/$ghTag"
def ghOldSamplesUrl = "https://github.com/spring-projects/spring-security/tree/5.4.x/samples"
def ghOldSamplesUrl = 'https://github.com/spring-projects/spring-security/tree/5.4.x/samples'
def ghSamplesUrl = "https://github.com/spring-projects/spring-security-samples/tree/$samplesBranch"
def securityDocsUrl = "https://docs.spring.io/spring-security/site/docs/$docsTag"
def securityApiUrl = "$securityDocsUrl/api/"
def securityReferenceUrl = "$securityDocsUrl/reference/html5/"
def springFrameworkApiUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/javadoc-api/"
def springFrameworkReferenceUrl = "https://docs.spring.io/spring-framework/docs/$springFrameworkVersion/reference/html/"
def versions = resolvedVersions(project.configurations.testRuntimeClasspath)
def ymlVersions = ""
versions.call().each { name, version ->
ymlVersions += """
${name}: ${version}"""
}
def outputFile = new File("$buildDir/generateAntora/antora.yml")
outputFile.getParentFile().mkdirs()
outputFile.createNewFile()
def antoraYmlText = file("antora.yml").getText()
outputFile.setText("""$antoraYmlText
title: Spring Security
start_page: ROOT:index.adoc
nav:
- modules/ROOT/nav.adoc
def ymlVersions = resolvedVersions(project.configurations.testRuntimeClasspath).call()
.collect(v -> " ${v.getKey()}: ${v.getValue()}")
.join('\n')
def outputFile = layout.buildDirectory.file('generateAntora/antora.yml').get().asFile
mkdir(outputFile.getParentFile())
def components = project.version.split(/(?=-)/)
def mainVersion = components[0];
def prerelease = components.length > 1 ? components[1] : null
def antoraYmlText = file('antora.yml').text
layout.buildDirectory.file('.antora.yml').get().asFile.text = antoraYmlText
antoraYmlText = antoraYmlText.lines().collect { l ->
if (l.startsWith('version: ')) {
return prerelease == null ? "version: '${mainVersion}'" : "version: '${mainVersion}'\nprerelease: '${prerelease}'"
}
if (l.startsWith('title: ')) return "title: ${project.parent.description}"
return l == 'ext:' || l.getAt(0) == ' ' ? null : l
}.findAll(Objects::nonNull).join('\n')
outputFile.text = """$antoraYmlText
asciidoc:
attributes:
icondir: icons
@@ -60,30 +63,28 @@ asciidoc:
spring-framework-reference-url: $springFrameworkReferenceUrl
spring-security-version: ${project.version}
${ymlVersions}
""")
"""
}
}
dependencies {
testImplementation platform(project(":spring-security-dependencies"))
testImplementation "com.unboundid:unboundid-ldapsdk"
testImplementation "org.apache.directory.server:apacheds-core"
testImplementation "org.springframework:spring-core"
testImplementation platform(project(':spring-security-dependencies'))
testImplementation 'com.unboundid:unboundid-ldapsdk'
testImplementation 'org.apache.directory.server:apacheds-core'
testImplementation 'org.springframework:spring-core'
}
def resolvedVersions(Configuration configuration) {
return {
configuration.resolvedConfiguration
.resolvedArtifacts
.collectEntries { [(it.name + "-version"): it.moduleVersion.id.version] }
.collectEntries { [(it.name + '-version'): it.moduleVersion.id.version] }
}
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/release" }
maven { url "https://repo.spring.io/milestone" }
maven { url "https://repo.spring.io/snapshot" }
maven { url 'https://repo.spring.io/release' }
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/ruby
require 'json'
require 'net/http'
require 'yaml'
require 'logger'
$log = Logger.new(STDOUT)
$log.level = Logger::WARN
class ForwardMerge
attr_reader :issue, :milestone, :message, :line
def initialize(issue, milestone, message, line)
@issue = issue
@milestone = milestone
@message = message
@line = line
end
end
def find_forward_merges(message_file)
$log.debug "Searching for forward merge"
rev=`git rev-parse -q --verify MERGE_HEAD`.strip
$log.debug "Found #{rev} from git rev-parse"
return nil unless rev
message = File.read(message_file)
forward_merges = []
message.each_line do |line|
$log.debug "Checking #{line} for message"
match = /^(?:Fixes|Closes) gh-(\d+) in (\d\.\d\.[\dx](?:[\.\-](?:M|RC)\d)?)$/.match(line)
if match then
issue = match[1]
milestone = match[2]
$log.debug "Matched reference to issue #{issue} in milestone #{milestone}"
forward_merges << ForwardMerge.new(issue, milestone, message, line)
end
end
$log.debug "No match in merge message" unless forward_merges
return forward_merges
end
def get_issue(username, password, repository, number)
$log.debug "Getting issue #{number} from GitHub repository #{repository}"
uri = URI("https://api.github.com/repos/#{repository}/issues/#{number}")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl=true
request = Net::HTTP::Get.new(uri.path)
request.basic_auth(username, password)
response = http.request(request)
$log.debug "Get HTTP response #{response.code}"
return JSON.parse(response.body) unless response.code != '200'
puts "Failed to retrieve issue #{number}: #{response.message}"
exit 1
end
def find_milestone(username, password, repository, title)
$log.debug "Finding milestone #{title} from GitHub repository #{repository}"
uri = URI("https://api.github.com/repos/#{repository}/milestones")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl=true
request = Net::HTTP::Get.new(uri.path)
request.basic_auth(username, password)
response = http.request(request)
milestones = JSON.parse(response.body)
if title.end_with?(".x")
prefix = title.delete_suffix('.x')
$log.debug "Finding nearest milestone from candidates starting with #{prefix}"
titles = milestones.map { |milestone| milestone['title'] }
titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x')}
titles = titles.sort_by { |v| Gem::Version.new(v) }
$log.debug "Considering candidates #{titles}"
if(titles.empty?)
puts "Cannot find nearest milestone for prefix #{title}"
exit 1
end
title = titles.first
$log.debug "Found nearest milestone #{title}"
end
milestones.each do |milestone|
$log.debug "Considering #{milestone['title']}"
return milestone['number'] if milestone['title'] == title
end
puts "Milestone #{title} not found in #{repository}"
exit 1
end
def create_issue(username, password, repository, original, title, labels, milestone, milestone_name, dry_run)
$log.debug "Finding forward-merge issue in GitHub repository #{repository} for '#{title}'"
uri = URI("https://api.github.com/repos/#{repository}/issues")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl=true
request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
request.basic_auth(username, password)
request.body = {
title: title,
labels: labels,
milestone: milestone.to_i,
body: "Forward port of issue ##{original} to #{milestone_name}."
}.to_json
if dry_run then
puts "Dry run"
puts "POSTing to #{uri} with body #{request.body}"
return "dry-run"
end
response = JSON.parse(http.request(request).body)
$log.debug "Created new issue #{response['number']}"
return response['number']
end
$log.debug "Running forward-merge hook script"
message_file=ARGV[0]
forward_merges = find_forward_merges(message_file)
exit 0 unless forward_merges
$log.debug "Loading config from ~/.spring-boot/forward_merge.yml"
config = YAML.load_file(File.join(Dir.home, '.spring-boot', 'forward-merge.yml'))
username = config['github']['credentials']['username']
password = config['github']['credentials']['password']
dry_run = config['dry_run']
repository = 'spring-projects/spring-security'
forward_merges.each do |forward_merge|
existing_issue = get_issue(username, password, repository, forward_merge.issue)
title = existing_issue['title']
labels = existing_issue['labels'].map { |label| label['name'] }
labels << "status: forward-port"
$log.debug "Processing issue '#{title}'"
milestone = find_milestone(username, password, repository, forward_merge.milestone)
new_issue_number = create_issue(username, password, repository, forward_merge.issue, title, labels, milestone, forward_merge.milestone, dry_run)
puts "Created gh-#{new_issue_number} for forward port of gh-#{forward_merge.issue} into #{forward_merge.milestone}"
rewritten_message = forward_merge.message.sub(forward_merge.line, "Closes gh-#{new_issue_number}\n")
File.write(message_file, rewritten_message)
end
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/ruby
require 'json'
require 'net/http'
require 'yaml'
require 'logger'
$main_branch = "6.0.x"
$log = Logger.new(STDOUT)
$log.level = Logger::WARN
def get_fixed_issues()
$log.debug "Searching for for forward merge"
rev=`git rev-parse -q --verify MERGE_HEAD`.strip
$log.debug "Found #{rev} from git rev-parse"
return nil unless rev
fixed = []
message = `git log -1 --pretty=%B #{rev}`
message.each_line do |line|
$log.debug "Checking #{line} for message"
fixed << line.strip if /^(?:Fixes|Closes) gh-(\d+)/.match(line)
end
$log.debug "Found fixed issues #{fixed}"
return fixed;
end
def rewrite_message(message_file, fixed)
current_branch = `git rev-parse --abbrev-ref HEAD`.strip
if current_branch == "main"
current_branch = $main_branch
end
rewritten_message = ""
message = File.read(message_file)
message.each_line do |line|
match = /^Merge.*branch\ '(.*)'(?:\ into\ (.*))?$/.match(line)
if match
from_branch = match[1]
if from_branch.include? "/"
from_branch = from_branch.partition("/").last
end
to_brach = match[2]
$log.debug "Rewriting merge message"
line = "Merge branch '#{from_branch}'" + (to_brach ? " into #{to_brach}\n" : "\n")
end
if fixed and line.start_with?("#")
$log.debug "Adding fixed"
rewritten_message << "\n"
fixed.each do |fixes|
rewritten_message << "#{fixes} in #{current_branch}\n"
end
fixed = nil
end
rewritten_message << line
end
return rewritten_message
end
$log.debug "Running prepare-forward-merge hook script"
message_file=ARGV[0]
message_type=ARGV[1]
if message_type != "merge"
$log.debug "Not a merge commit"
exit 0;
end
$log.debug "Searching for for forward merge"
fixed = get_fixed_issues()
rewritten_message = rewrite_message(message_file, fixed)
File.write(message_file, rewritten_message)
+2 -2
View File
@@ -1,9 +1,9 @@
aspectjVersion=1.9.9.1
springJavaformatVersion=0.0.31
springBootVersion=2.4.2
springFrameworkVersion=5.3.20
springFrameworkVersion=5.3.23
openSamlVersion=3.4.6
version=5.6.4
version=5.6.8
kotlinVersion=1.5.32
samplesBranch=5.6.x
org.gradle.jvmargs=-Xmx3g -XX:MaxPermSize=2048m -XX:+HeapDumpOnOutOfMemoryError
-17
View File
@@ -1,17 +0,0 @@
site:
title: Spring Security
start_page: security::index.adoc
asciidoc:
attributes:
page-pagination: true
content:
sources:
- url: ./
branches: [HEAD]
start_path: docs
- url: ../../rwinch/spring-security-docs-generated
branches: [HEAD]
ui:
bundle:
url: https://github.com/rwinch/antora-ui-spring/releases/download/latest/ui-bundle.zip
snapshot: true
@@ -85,13 +85,13 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo
return Mono.empty();
}
String idToken = idToken(authentication);
URI postLogoutRedirectUri = postLogoutRedirectUri(exchange.getExchange().getRequest());
String postLogoutRedirectUri = postLogoutRedirectUri(exchange.getExchange().getRequest());
return Mono.just(endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri));
})
.switchIfEmpty(
this.serverLogoutSuccessHandler.onLogoutSuccess(exchange, authentication).then(Mono.empty())
)
.flatMap((endpointUri) -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri));
.flatMap((endpointUri) -> this.redirectStrategy.sendRedirect(exchange.getExchange(), URI.create(endpointUri)));
// @formatter:on
}
@@ -106,20 +106,20 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo
return null;
}
private URI endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) {
private String endpointUri(URI endSessionEndpoint, String idToken, String postLogoutRedirectUri) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
builder.queryParam("id_token_hint", idToken);
if (postLogoutRedirectUri != null) {
builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
}
return builder.encode(StandardCharsets.UTF_8).build().toUri();
return builder.encode(StandardCharsets.UTF_8).build().toUriString();
}
private String idToken(Authentication authentication) {
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
}
private URI postLogoutRedirectUri(ServerHttpRequest request) {
private String postLogoutRedirectUri(ServerHttpRequest request) {
if (this.postLogoutRedirectUri == null) {
return null;
}
@@ -131,7 +131,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler implements ServerLogo
.build();
return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri)
.buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString()))
.toUri();
.toUriString();
// @formatter:on
}
@@ -150,6 +150,19 @@ public class OidcClientInitiatedServerLogoutSuccessHandlerTests {
"https://endpoint?" + "id_token_hint=id-token&" + "post_logout_redirect_uri=https://rp.example.org");
}
// gh-11379
@Test
public void logoutWhenUsingPostLogoutRedirectUriWithQueryParametersThenBuildsItForRedirect() {
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken(TestOidcUsers.create(),
AuthorityUtils.NO_AUTHORITIES, this.registration.getRegistrationId());
given(this.exchange.getPrincipal()).willReturn(Mono.just(token));
this.handler.setPostLogoutRedirectUri("https://rp.example.org/context?forwardUrl=secured%3Fparam%3Dtrue");
WebFilterExchange f = new WebFilterExchange(this.exchange, this.chain);
this.handler.onLogoutSuccess(f, token).block();
assertThat(redirectedUrl(this.exchange)).isEqualTo("https://endpoint?id_token_hint=id-token&"
+ "post_logout_redirect_uri=https://rp.example.org/context?forwardUrl%3Dsecured%253Fparam%253Dtrue");
}
@Test
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null));
@@ -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.
@@ -338,8 +338,8 @@ public final class NimbusJwtDecoder implements JwtDecoder {
if (this.cache == null) {
return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever);
}
ResourceRetriever cachingJwkSetRetriever = new CachingResourceRetriever(this.cache, jwkSetRetriever);
return new RemoteJWKSet<>(toURL(this.jwkSetUri), cachingJwkSetRetriever, new NoOpJwkSetCache());
JWKSetCache jwkSetCache = new SpringJWKSetCache(this.jwkSetUri, this.cache);
return new RemoteJWKSet<>(toURL(this.jwkSetUri), jwkSetRetriever, jwkSetCache);
}
JWTProcessor<SecurityContext> processor() {
@@ -371,52 +371,48 @@ public final class NimbusJwtDecoder implements JwtDecoder {
}
}
private static class NoOpJwkSetCache implements JWKSetCache {
private static final class SpringJWKSetCache implements JWKSetCache {
private final String jwkSetUri;
private final Cache cache;
private JWKSet jwkSet;
SpringJWKSetCache(String jwkSetUri, Cache cache) {
this.jwkSetUri = jwkSetUri;
this.cache = cache;
this.updateJwkSetFromCache();
}
private void updateJwkSetFromCache() {
String cachedJwkSet = this.cache.get(this.jwkSetUri, String.class);
if (cachedJwkSet != null) {
try {
this.jwkSet = JWKSet.parse(cachedJwkSet);
}
catch (ParseException ignored) {
// Ignore invalid cache value
}
}
}
// Note: Only called from inside a synchronized block in RemoteJWKSet.
@Override
public void put(JWKSet jwkSet) {
this.jwkSet = jwkSet;
this.cache.put(this.jwkSetUri, jwkSet.toString(false));
}
@Override
public JWKSet get() {
return null;
return (!requiresRefresh()) ? this.jwkSet : null;
}
@Override
public boolean requiresRefresh() {
return true;
}
}
private static class CachingResourceRetriever implements ResourceRetriever {
private final Cache cache;
private final ResourceRetriever resourceRetriever;
CachingResourceRetriever(Cache cache, ResourceRetriever resourceRetriever) {
this.cache = cache;
this.resourceRetriever = resourceRetriever;
}
@Override
public Resource retrieveResource(URL url) throws IOException {
try {
String jwkSet = this.cache.get(url.toString(),
() -> this.resourceRetriever.retrieveResource(url).getContent());
return new Resource(jwkSet, "UTF-8");
}
catch (Cache.ValueRetrievalException ex) {
Throwable thrownByValueLoader = ex.getCause();
if (thrownByValueLoader instanceof IOException) {
throw (IOException) thrownByValueLoader;
}
throw new IOException(thrownByValueLoader);
}
catch (Exception ex) {
throw new IOException(ex);
}
return this.cache.get(this.jwkSetUri) == null;
}
}
@@ -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.
@@ -32,10 +32,10 @@ import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.crypto.SecretKey;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
@@ -82,6 +82,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -97,10 +98,14 @@ public class NimbusJwtDecoderTests {
private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
private static final String NEW_KID_JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"kid\":\"two\",\"n\":\"ra9UJw4I0fCHuOqr1xWJsh-qcVeZWtKEU3uoqq1sAg5fG67dujNCm_Q16yuO0ZdDiU0vlJkbc_MXFAvm4ZxdJ_qR7PAneV-BOGNtLpSaiPclscCy3m7zjRWkaqwt9ZZEsdK5UqXyPlBpcYhNKsmnQGjnX4sYb7d8b2jSCM_qto48-6451rbyEhXXywtFy_JqtTpbsw_IIdQHMr1O-MdSjsQxX9kkvZwPU8LsC-CcqlcsZ7mnpOhmIXaf4tbRwAaluXwYft0yykFsp8e5C4t9mMs9Vu8AB5gT8o-D_ovXd2qh4k3ejzVpYLtzD4nbfvPJA_TXmjhn-9GOPAqkzfON2Q\"}]}";
private static final String MALFORMED_JWK_SET = "malformed";
private static final String SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA";
private static final String NEW_KID_SIGNED_JWT = "eyJraWQiOiJ0d28iLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjIxMzMyNzg4MjV9.DQJn_qg0HfZ_sjlx9MJkdCjkp9t-0zOj3FzVp_UPzx6RCcBb8Jk373dNgcyfOP5CS29wv5gKX6geWEDj5cgqcJdTS53zqOaLETdNnKACd056SkPqgTLJv12gdJx7tr5WbBqRB9Y0ce96vbH6wwQGfqU_1Lz1RhZ7ZZuvIuWLp75ujld7dOshScg728Z9BQsiFOH_yFp09XraO15spwTXp9RO5TJRUSLih-5V3sdxHa5rPTm6by7me8I_l4iMJN81Z95_O7sbLeYH-4zZ-3T49uPyAC5suEOd-P5aFP89zPKh9Y3Uviu2OyvpUuXmpUjTtdAKf3p96dOEeLJvT3hkSg";
private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A";
private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9.";
@@ -644,10 +649,38 @@ public class NimbusJwtDecoderTests {
}
@Test
public void decodeWhenCacheThenRetrieveFromCache() {
public void decodeWhenCacheStoredThenAbleToRetrieveJwkSetFromCache() {
Cache cache = new ConcurrentMapCache("test-jwk-set-cache");
RestOperations restOperations = mock(RestOperations.class);
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(JWK_SET, HttpStatus.OK));
// @formatter:off
NimbusJwtDecoder jwtDecoder1 = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI)
.restOperations(restOperations)
.cache(cache)
.build();
// @formatter:on
jwtDecoder1.decode(SIGNED_JWT);
assertThat(cache.get(JWK_SET_URI, String.class)).isEqualTo(JWK_SET);
verify(restOperations).exchange(any(RequestEntity.class), eq(String.class));
// @formatter:off
NimbusJwtDecoder jwtDecoder2 = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI)
.restOperations(restOperations)
.cache(cache)
.build();
// @formatter:on
jwtDecoder2.decode(SIGNED_JWT);
verifyNoMoreInteractions(restOperations);
}
// gh-11621
@Test
public void decodeWhenCacheThenRetrieveFromCache() throws Exception {
RestOperations restOperations = mock(RestOperations.class);
Cache cache = mock(Cache.class);
given(cache.get(eq(JWK_SET_URI), any(Callable.class))).willReturn(JWK_SET);
given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET);
given(cache.get(eq(JWK_SET_URI))).willReturn(mock(Cache.ValueWrapper.class));
// @formatter:off
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI)
.cache(cache)
@@ -655,11 +688,65 @@ public class NimbusJwtDecoderTests {
.build();
// @formatter:on
jwtDecoder.decode(SIGNED_JWT);
verify(cache).get(eq(JWK_SET_URI), any(Callable.class));
verify(cache).get(eq(JWK_SET_URI), eq(String.class));
verify(cache, times(2)).get(eq(JWK_SET_URI));
verifyNoMoreInteractions(cache);
verifyNoInteractions(restOperations);
}
// gh-11621
@Test
public void decodeWhenCacheAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException {
RestOperations restOperations = mock(RestOperations.class);
Cache cache = mock(Cache.class);
given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET);
given(restOperations.exchange(any(RequestEntity.class), eq(String.class)))
.willReturn(new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK));
// @formatter:off
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI)
.cache(cache)
.restOperations(restOperations)
.build();
// @formatter:on
// Decode JWT with new KID
jwtDecoder.decode(NEW_KID_SIGNED_JWT);
ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class);
verify(restOperations).exchange(requestEntityCaptor.capture(), eq(String.class));
verifyNoMoreInteractions(restOperations);
assertThat(requestEntityCaptor.getValue().getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON,
APPLICATION_JWK_SET_JSON);
}
// gh-11621
@Test
public void decodeWithoutCacheSpecifiedAndUnknownKidShouldTriggerFetchOfJwkSet() throws JOSEException {
RestOperations restOperations = mock(RestOperations.class);
given(restOperations.exchange(any(RequestEntity.class), eq(String.class))).willReturn(
new ResponseEntity<>(JWK_SET, HttpStatus.OK), new ResponseEntity<>(NEW_KID_JWK_SET, HttpStatus.OK));
// @formatter:off
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI)
.restOperations(restOperations)
.build();
// @formatter:on
jwtDecoder.decode(SIGNED_JWT);
// Decode JWT with new KID
jwtDecoder.decode(NEW_KID_SIGNED_JWT);
ArgumentCaptor<RequestEntity> requestEntityCaptor = ArgumentCaptor.forClass(RequestEntity.class);
verify(restOperations, times(2)).exchange(requestEntityCaptor.capture(), eq(String.class));
verifyNoMoreInteractions(restOperations);
List<RequestEntity> requestEntities = requestEntityCaptor.getAllValues();
assertThat(requestEntities.get(0).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON,
APPLICATION_JWK_SET_JSON);
assertThat(requestEntities.get(1).getHeaders().getAccept()).contains(MediaType.APPLICATION_JSON,
APPLICATION_JWK_SET_JSON);
}
@Test
public void decodeWhenCacheIsConfiguredAndValueLoaderErrorsThenThrowsJwtException() {
Cache cache = new ConcurrentMapCache("test-jwk-set-cache");
@@ -678,6 +765,27 @@ public class NimbusJwtDecoderTests {
// @formatter:on
}
// gh-11621
@Test
public void decodeWhenCacheIsConfiguredAndParseFailsOnCachedValueThenExceptionIgnored() {
RestOperations restOperations = mock(RestOperations.class);
Cache cache = mock(Cache.class);
given(cache.get(eq(JWK_SET_URI), eq(String.class))).willReturn(JWK_SET);
given(cache.get(eq(JWK_SET_URI))).willReturn(mock(Cache.ValueWrapper.class));
// @formatter:off
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI)
.cache(cache)
.restOperations(restOperations)
.build();
// @formatter:on
jwtDecoder.decode(SIGNED_JWT);
verify(cache).get(eq(JWK_SET_URI), eq(String.class));
verify(cache, times(2)).get(eq(JWK_SET_URI));
verifyNoMoreInteractions(cache);
verifyNoInteractions(restOperations);
}
// gh-8730
@Test
public void withJwkSetUriWhenUsingCustomTypeHeaderThenRefuseOmittedType() throws Exception {
@@ -46,9 +46,9 @@ dependencies {
api "org.opensaml:opensaml-core"
api "org.opensaml:opensaml-saml-api"
api "org.opensaml:opensaml-saml-impl"
opensaml4MainImplementation "org.opensaml:opensaml-core:4.1.0"
opensaml4MainImplementation "org.opensaml:opensaml-saml-api:4.1.0"
opensaml4MainImplementation "org.opensaml:opensaml-saml-impl:4.1.0"
opensaml4MainImplementation "org.opensaml:opensaml-core:4.1.1"
opensaml4MainImplementation "org.opensaml:opensaml-saml-api:4.1.1"
opensaml4MainImplementation "org.opensaml:opensaml-saml-impl:4.1.1"
provided 'jakarta.servlet:jakarta.servlet-api'
@@ -36,7 +36,7 @@ final class Saml2Utils {
}
static String samlEncode(byte[] b) {
return Base64.getMimeEncoder().encodeToString(b);
return Base64.getEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
@@ -40,7 +40,7 @@ final class Saml2Utils {
}
static String samlEncode(byte[] b) {
return Base64.getMimeEncoder().encodeToString(b);
return Base64.getEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {
@@ -70,7 +70,7 @@ public final class OpenSamlMetadataResolver implements Saml2MetadataResolver {
@Override
public String resolve(RelyingPartyRegistration relyingPartyRegistration) {
EntityDescriptor entityDescriptor = build(EntityDescriptor.ELEMENT_QNAME);
EntityDescriptor entityDescriptor = build(EntityDescriptor.DEFAULT_ELEMENT_NAME);
entityDescriptor.setEntityID(relyingPartyRegistration.getEntityId());
SPSSODescriptor spSsoDescriptor = buildSpSsoDescriptor(relyingPartyRegistration);
entityDescriptor.getRoleDescriptors(SPSSODescriptor.DEFAULT_ELEMENT_NAME).add(spSsoDescriptor);
@@ -40,7 +40,7 @@ final class Saml2Utils {
}
static String samlEncode(byte[] b) {
return Base64.getMimeEncoder().encodeToString(b);
return Base64.getEncoder().encodeToString(b);
}
static byte[] samlDecode(String s) {

Some files were not shown because too many files have changed in this diff Show More