Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af2d210ccb | |||
| 69aeeeade7 | |||
| 4d3152894d | |||
| 4eae9f45e5 | |||
| c7047add5d | |||
| 6dd29520b0 | |||
| 0bd3b9e420 | |||
| a162875f41 | |||
| 6f0ce5c2b0 | |||
| dce87beb07 | |||
| 6bede4cf7a | |||
| 3c514bd747 | |||
| 83585125ff | |||
| 42432cc3de | |||
| c9a7f2487c | |||
| 78aaaa7bce | |||
| 837189995d | |||
| b03680ca58 | |||
| d464c00315 | |||
| 17fbf396db | |||
| 27c4ee3048 | |||
| 986414fd4f | |||
| 4886951379 | |||
| d130a18d3c | |||
| 966c01fa90 | |||
| bdec2d3044 | |||
| 6e636e6abb | |||
| 981bc1e751 | |||
| 32cf79d1a9 | |||
| 247a9fdabf | |||
| 4f6a4d6471 | |||
| 956438362d | |||
| 674ce38bd3 | |||
| 15febfc65f | |||
| b5c43290e6 | |||
| d912914ea7 | |||
| c540f885d3 | |||
| 3ab235cd56 | |||
| b9b2df845f | |||
| 4d07b9d632 | |||
| 1723d88bdf | |||
| 6f45be28a0 | |||
| 8cdd53f531 | |||
| 54b8f3874f | |||
| a73c5d754a | |||
| 67424adef3 | |||
| e9e198b1e3 | |||
| 7c1e6f36c9 | |||
| 57f7eff568 | |||
| 6e1605314a | |||
| 1d769b5afc | |||
| 0beda02a14 | |||
| abe2029cb4 | |||
| c2d88eca5a | |||
| a4c107bd3c | |||
| d09ce0baff | |||
| 515e8216b1 | |||
| d6ac0357fe | |||
| c6265c5a7c | |||
| 5ef31c17da | |||
| 533ad7107f | |||
| 8e93e4715f | |||
| d99e98515c | |||
| ba851266c5 | |||
| 3f6b6aa523 | |||
| 9749c23c74 | |||
| fd1b92b7d0 | |||
| 6511cadd2f | |||
| b90ec16fbb | |||
| df3acdad75 | |||
| 7edb123c05 | |||
| 15d9b7824c | |||
| 4131a38f9e | |||
| 5958828113 | |||
| 4491b955b3 | |||
| d2374e3e49 | |||
| aa37574118 | |||
| 602b962af4 | |||
| 0c884005e6 | |||
| 94e0adbea0 | |||
| 65adad8d1e | |||
| 598943e37f | |||
| 1bd6e853ab | |||
| 9be1095deb | |||
| 97516727a4 | |||
| cad6689659 | |||
| fbad708347 | |||
| d8f77e9649 | |||
| eb0460df03 | |||
| e920bfb3a2 | |||
| a1cab162f8 | |||
| d3977d8571 | |||
| 3d53ba62a9 | |||
| 040b879119 | |||
| bd48fdf744 | |||
| 67d3e4c9b8 | |||
| 621ab3e7cb | |||
| 22000b42e9 | |||
| 24abf45128 | |||
| 672d86cbf1 | |||
| b6de302c1b | |||
| 6ab9cc6ff7 | |||
| 9eee9887b9 | |||
| 77acaaa3b7 | |||
| 6e0fb2fc96 | |||
| 51ac8077db | |||
| 85d60580e7 | |||
| f26a7bdb93 | |||
| 09de66f3a9 | |||
| dcd2037dae | |||
| 10cf18db0b | |||
| 9d5194ac52 | |||
| d0a5ada2da | |||
| f501c8547e | |||
| 1589d19c8b | |||
| 318dec845a | |||
| d3ada48190 | |||
| 330c6a4fd5 | |||
| 459dbbc54c | |||
| 84210686d1 | |||
| c84fe6987d | |||
| 1b9553f958 | |||
| 681a85f18b |
@@ -0,0 +1,72 @@
|
||||
version: 2
|
||||
|
||||
registries:
|
||||
spring-milestones:
|
||||
type: maven-repository
|
||||
url: https://repo.spring.io/milestone
|
||||
|
||||
updates:
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "main"
|
||||
milestone: 319 # 6.2.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
registries:
|
||||
- "spring-milestones"
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "6.1.x"
|
||||
milestone: 318 # 6.1.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
|
||||
- package-ecosystem: "gradle"
|
||||
target-branch: "5.8.x"
|
||||
milestone: 246 # 5.8.x
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
time: "03:00"
|
||||
timezone: "Etc/UTC"
|
||||
labels: [ "type: dependency-upgrade" ]
|
||||
ignore:
|
||||
- dependency-name: "com.nimbusds:nimbus-jose-jwt" # nimbus-jose-jwt gets updated when oauth2-oidc-sdk is updated to ensure consistency
|
||||
- dependency-name: "org.python:jython" # jython updates break integration tests
|
||||
- dependency-name: "org.apache.directory.server:*" # ApacheDS version > 1.5.5 contains break changes
|
||||
- dependency-name: "io.mockk:mockk" # mockk updates break tests
|
||||
- dependency-name: "org.opensaml:*" # org.opensaml maintains two different versions, so it must be updated manually
|
||||
- dependency-name: "org.junit:junit-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "org.mockito:mockito-bom"
|
||||
update-types: [ "version-update:semver-major" ]
|
||||
- dependency-name: "*"
|
||||
update-types: [ "version-update:semver-major", "version-update:semver-minor" ]
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Clean build artifacts
|
||||
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:
|
||||
GH_ACTIONS_REPO_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: |
|
||||
echo "Running clean build artifacts logic"
|
||||
output=$(curl -X GET -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts | grep '"id"' | cut -d : -f2 | sed 's/,*$//g')
|
||||
echo Output is $output
|
||||
for id in $output; do curl -X DELETE -H "Authorization: token $GH_ACTIONS_REPO_TOKEN" https://api.github.com/repos/spring-projects/spring-security/actions/artifacts/$id; done;
|
||||
@@ -0,0 +1,325 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "dependabot/**"
|
||||
schedule:
|
||||
- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch: # Manual trigger
|
||||
|
||||
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 }}
|
||||
COMMIT_OWNER: ${{ github.event.pusher.name }}
|
||||
COMMIT_SHA: ${{ github.sha }}
|
||||
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
|
||||
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
|
||||
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 }}
|
||||
samples_branch: ${{ steps.continue.outputs.samples_branch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: continue
|
||||
name: Determine if should continue
|
||||
run: |
|
||||
# Run jobs if in upstream repository
|
||||
echo "runjobs=true" >>$GITHUB_OUTPUT
|
||||
# Extract version from gradle.properties
|
||||
version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
|
||||
echo "project_version=$version" >>$GITHUB_OUTPUT
|
||||
samples_branch=$(cat gradle.properties | grep "samplesBranch=" | awk -F'=' '{print $2}')
|
||||
echo "samples_branch=$samples_branch" >>$GITHUB_OUTPUT
|
||||
build_jdk_17:
|
||||
name: Build JDK 17
|
||||
needs: [prerequisites]
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: 'gradle'
|
||||
- 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 }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
run: ./gradlew clean build --continue -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD"
|
||||
snapshot_tests:
|
||||
name: Test against snapshots
|
||||
needs: [prerequisites]
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- java-version: '21-ea'
|
||||
toolchain: '21'
|
||||
- java-version: '17'
|
||||
toolchain: '17'
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: ${{ matrix.java-version }}
|
||||
distribution: 'temurin'
|
||||
- name: Snapshot Tests
|
||||
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 test --refresh-dependencies -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion='6.1.+' -PreactorVersion='2023.0.+' -PspringDataVersion='2023.1.+' -PlocksDisabled --stacktrace
|
||||
check_samples:
|
||||
name: Check Samples project
|
||||
needs: [prerequisites]
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Check samples project
|
||||
env:
|
||||
LOCAL_REPOSITORY_PATH: ${{ github.workspace }}/build/publications/repos
|
||||
SAMPLES_DIR: ../spring-security-samples
|
||||
VERSION: ${{ needs.prerequisites.outputs.project_version }}
|
||||
SAMPLES_BRANCH: ${{ needs.prerequisites.outputs.samples_branch }}
|
||||
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 cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$SAMPLES_BRANCH" -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 ]
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.prerequisites.outputs.runjobs
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Check for package tangles
|
||||
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 check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
|
||||
deploy_artifacts:
|
||||
name: Deploy Artifacts
|
||||
needs: [build_jdk_17, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Deploy artifacts
|
||||
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 publishArtifacts finalizeDeployArtifacts -PossrhUsername="$OSSRH_TOKEN_USERNAME" -PossrhPassword="$OSSRH_TOKEN_PASSWORD" -PartifactoryUsername="$ARTIFACTORY_USERNAME" -PartifactoryPassword="$ARTIFACTORY_PASSWORD" --stacktrace
|
||||
env:
|
||||
ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GPG_PASSPHRASE }}
|
||||
OSSRH_TOKEN_USERNAME: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
|
||||
OSSRH_TOKEN_PASSWORD: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
|
||||
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
deploy_docs:
|
||||
name: Deploy Docs
|
||||
needs: [build_jdk_17, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Deploy Docs
|
||||
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 deployDocs -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace
|
||||
env:
|
||||
DOCS_USERNAME: ${{ secrets.DOCS_USERNAME }}
|
||||
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
|
||||
DOCS_HOST: ${{ secrets.DOCS_HOST }}
|
||||
deploy_schema:
|
||||
name: Deploy Schema
|
||||
needs: [build_jdk_17, snapshot_tests, check_samples, check_tangles]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Deploy Schema
|
||||
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 deploySchema -PdeployDocsSshKey="$DOCS_SSH_KEY" -PdeployDocsSshUsername="$DOCS_USERNAME" -PdeployDocsHost="$DOCS_HOST" --stacktrace --info
|
||||
env:
|
||||
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@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- 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.24.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@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Schedule next release (if not already scheduled)
|
||||
run: ./gradlew scheduleNextRelease -PnextVersion=$VERSION -PgitHubAccessToken=$TOKEN
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [perform_release, perform_post_release]
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
# 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 }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- "gh-pages"
|
||||
- "dependabot/**"
|
||||
tags: '**'
|
||||
repository_dispatch:
|
||||
types: request-build-reference # legacy
|
||||
#schedule:
|
||||
#- cron: '0 10 * * *' # Once per day at 10am UTC
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: docs-build
|
||||
fetch-depth: 1
|
||||
- name: Dispatch (partial build)
|
||||
if: github.ref_type == 'branch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
|
||||
- name: Dispatch (full build)
|
||||
if: github.ref_type == 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
|
||||
@@ -0,0 +1,32 @@
|
||||
name: Execute Gradle Wrapper Upgrade
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *' # 2am UTC
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
upgrade_wrapper:
|
||||
name: Execution
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Git configuration
|
||||
env:
|
||||
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config --global url."https://unused-username:${TOKEN}@github.com/".insteadOf "https://github.com/"
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Set up Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
- name: Upgrade Wrappers
|
||||
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
|
||||
env:
|
||||
WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Check Milestone
|
||||
on:
|
||||
milestone:
|
||||
types: [created, opened, edited]
|
||||
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'"
|
||||
- name: Validate DueOn
|
||||
if: env.DUE_ON != ''
|
||||
run: |
|
||||
export TOOL_VERSION=0.1.1
|
||||
wget "https://repo.maven.apache.org/maven2/io/spring/releasetrain/spring-release-train-tools/$TOOL_VERSION/spring-release-train-tools-$TOOL_VERSION.jar"
|
||||
java -cp "spring-release-train-tools-$TOOL_VERSION.jar" io.spring.releasetrain.CheckMilestoneDueOnMain --dueOn "$DUE_ON" --expectedDayOfWeek MONDAY --expectedMondayCount 3
|
||||
notify_result:
|
||||
name: Check for failures
|
||||
needs: [spring-releasetrain-checks]
|
||||
if: failure()
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
steps:
|
||||
- name: Send Slack message
|
||||
uses: Gamesight/slack-workflow-status@v1.2.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
@@ -0,0 +1,26 @@
|
||||
name: PR Build
|
||||
|
||||
on: pull_request
|
||||
|
||||
env:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-security' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew clean build --continue --scan
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Release Scheduler
|
||||
on:
|
||||
schedule:
|
||||
- cron: '15 15 * * MON' # Every Monday at 3:15pm UTC
|
||||
workflow_dispatch:
|
||||
permissions: read-all
|
||||
jobs:
|
||||
dispatch_scheduled_releases:
|
||||
name: Dispatch scheduled releases
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
strategy:
|
||||
matrix:
|
||||
# List of active maintenance branches.
|
||||
branch: [ main, 6.1.x, 5.8.x ]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Dispatch
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
run: gh workflow run update-scheduled-release-version.yml -r ${{ matrix.branch }}
|
||||
@@ -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@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
- name: Set up gradle
|
||||
uses: spring-io/spring-gradle-build-action@v2
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- 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 "is_due_today=$(cat build/github/milestones/is-due-today)" >>$GITHUB_OUTPUT
|
||||
- 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 "is_open_issues=$(cat build/github/milestones/is-open-issues)" >>$GITHUB_OUTPUT
|
||||
- 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.2.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
channel: '#spring-security-ci'
|
||||
name: 'CI Notifier'
|
||||
@@ -28,5 +28,3 @@ s101plugin.state
|
||||
|
||||
!.idea/checkstyle-idea.xml
|
||||
!.idea/externalDependencies.xml
|
||||
|
||||
node_modules
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ See https://github.com/spring-projects/spring-security/tree/main#building-from-s
|
||||
|
||||
The wiki pages https://github.com/spring-projects/spring-framework/wiki/Code-Style[Code Style] and https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings[IntelliJ IDEA Editor Settings] define the source file coding standards we use along with some IDEA editor settings we customize.
|
||||
|
||||
To format the code as well as check the style, run `./gradlew format check`.
|
||||
To format the code as well as check the style, run `./gradle format check`.
|
||||
|
||||
[[submit-a-pull-request]]
|
||||
=== Submit a Pull Request
|
||||
|
||||
+218
-205
@@ -1,236 +1,54 @@
|
||||
= Release Process
|
||||
|
||||
The release process for Spring Security is entirely automated via the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc[Spring Security Release Plugin] and https://github.com/spring-io/spring-security-release-tools/tree/main/.github/workflows[reusable workflows].
|
||||
The following table outlines the steps that are taken by the automation.
|
||||
The release process for Spring Security is partially automated. The following table outlines which steps are automated and which are manual. Follow the links to read about each step.
|
||||
|
||||
WARNING: The `5.8.x` branch does not have all of the improvements from the `6.x.x` branches. See "Status (5.8.x)" for which steps are still manual.
|
||||
|
||||
In case of a failure, you can follow the links below to read about each step, which includes instructions for performing the step manually if applicable.
|
||||
See <<frequently-asked-questions,FAQ>> for troubleshooting tips.
|
||||
|
||||
[cols="1,1,1"]
|
||||
[cols="1,1"]
|
||||
|===
|
||||
| Step | Status (5.8.x) | Status (6.0.x+)
|
||||
| Step | Status
|
||||
|
||||
| <<update-dependencies>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
| :x: manual
|
||||
|
||||
| <<check-all-issues-are-closed>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated (scheduled release will abort if any issues are open)
|
||||
|
||||
| <<update-release-version>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<tag-release>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<push-release-commit>>
|
||||
| :white_check_mark: automated
|
||||
| <<update-antora-version>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<build-locally>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
| :x: manual (when updating dependencies)
|
||||
|
||||
| <<update-release-notes-on-github>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-version-on-project-page>>
|
||||
| :x: manual
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<close-create-milestone,Close milestone>>
|
||||
| :x: manual
|
||||
| <<push-release-commit>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<announce-release-on-slack>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<tag-release>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-to-next-development-version>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<update-version-on-project-page>>
|
||||
| :x: manual
|
||||
|
||||
| <<update-release-notes-on-github>>
|
||||
| :white_check_mark: automated
|
||||
|
||||
| <<close-create-milestone,Close milestone>>
|
||||
| :x: manual (move issues to new milestone before release)
|
||||
|
||||
| <<close-create-milestone,Create milestone>>
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated
|
||||
| :white_check_mark: automated (if not already created)
|
||||
|
||||
| <<announce-release-on-other-channels>>
|
||||
| :x: manual
|
||||
| :x: manual
|
||||
|===
|
||||
|
||||
[#update-dependencies]
|
||||
== Update dependencies
|
||||
|
||||
Dependency versions are managed in the file xref:./gradle/libs.versions.toml[libs.versions.toml] and are automatically updated by xref:./.github/dependabot.yml[dependabot].
|
||||
|
||||
[#check-all-issues-are-closed]
|
||||
== Check all issues are closed
|
||||
|
||||
The first step of a release is to check if there are any open issues remaining in a milestone.
|
||||
|
||||
NOTE: A scheduled release will not proceed if there are any open issues.
|
||||
|
||||
TIP: If you need to prevent a release from occurring automatically, the easiest way to block a release is to add an unresolved issue to the milestone.
|
||||
|
||||
The https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#checkMilestoneHasNoOpenIssues[`checkMilestoneHasOpenIssues`] command will check if there are any open issues for the release.
|
||||
Before running the command manually, replace the following values:
|
||||
|
||||
* `<next-version>` - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`. This is optional since you are unlikely to reach the rate limit for such a simple check.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew checkMilestoneHasOpenIssues -PnextVersion=<next-version> -PgitHubAccessToken=<github-personal-access-token>
|
||||
----
|
||||
|
||||
Alternatively, you can manually check using the https://github.com/spring-projects/spring-security/milestones[milestones] page.
|
||||
|
||||
[#update-release-version]
|
||||
== Update release version
|
||||
|
||||
If all issues for the release are <<check-all-issues-are-closed,closed>>, the version number is automatically updated using the milestone title.
|
||||
When performing this step manually, update the version number in `gradle.properties` for the release (for example `5.5.0`) and commit the change using the message "Release x.y.z".
|
||||
|
||||
[#tag-release]
|
||||
== Tag release
|
||||
|
||||
The release will automatically be tagged using the milestone title.
|
||||
It is not required to tag manually.
|
||||
However, you can perform this step manually by running the following command:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
git tag 5.5.0
|
||||
----
|
||||
|
||||
[#push-release-commit]
|
||||
== Push release commit
|
||||
|
||||
During a scheduled release, the release commit will automatically be pushed to trigger a build.
|
||||
If performing this step manually, you can push the commit and tag and GitHub actions will build and deploy the artifacts with the following command:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
git push --atomic origin main 5.5.0
|
||||
----
|
||||
|
||||
The build will automatically wait for artifacts to be released to Maven Central.
|
||||
You can get notified manually when uploading is complete by running the following:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./scripts/release/wait-for-done.sh 5.5.0
|
||||
----
|
||||
|
||||
[#build-locally]
|
||||
== Build
|
||||
|
||||
All checks will automatically be performed by the build prior to uploading the artifacts to Maven Central.
|
||||
If something goes wrong, you can run the build locally using:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew check
|
||||
----
|
||||
|
||||
[#update-release-notes-on-github]
|
||||
== Update release notes on GitHub
|
||||
|
||||
Once the release has been uploaded to Maven Central, release notes will automatically be generated and a GitHub release will be created.
|
||||
To do this manually, you can use the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#generateChangelog[`generateChangelog`] command to generate the release notes by replacing:
|
||||
|
||||
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew generateChangelog -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
Then copy the release notes to your clipboard (your mileage may vary with the following command):
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
cat build/changelog/release-notes.md | xclip -selection clipboard
|
||||
----
|
||||
|
||||
Finally, create the
|
||||
https://github.com/spring-projects/spring-security/releases[release on
|
||||
GitHub], associate it with the tag, and paste the generated notes.
|
||||
|
||||
Alternatively, you can run the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#createGitHubRelease[`createGitHubRelease`] command to perform these steps automatically, replacing:
|
||||
|
||||
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
|
||||
* `<branch>` - The name of the branch to be tagged (if the release commit has not already been tagged)
|
||||
* `<github-personal-access-token>` - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `write:org`
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew createGitHubRelease -PnextVersion=<next-version> -Pbranch=<branch> -PcreateRelease=true -PgitHubAccessToken=<github-personal-access-token>
|
||||
----
|
||||
|
||||
[#update-version-on-project-page]
|
||||
== Update version on project page
|
||||
|
||||
The build will automatically update the project versions on https://spring.io/projects/spring-security#learn.
|
||||
To do this manually, you can use the https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#createSaganRelease[`createSaganRelease`] and https://github.com/spring-io/spring-security-release-tools/blob/main/release-plugin/README.adoc#deleteSaganRelease[`deleteSaganRelease`] commands using the following parameters:
|
||||
|
||||
* `<next-version>` - Replace with the milestone you are releasing now (i.e. 5.5.0)
|
||||
* `<previous-version>` - Replace with the previous release which will be removed from the listed versions (i.e. 5.5.0-RC1)
|
||||
* `<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]
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew createSaganRelease deleteSaganRelease -PnextVersion=<next-version> -PpreviousVersion=<previous-version> -PgitHubAccessToken=<github-personal-access-token>
|
||||
----
|
||||
|
||||
Alternatively, you can log into Contentful and update the versions manually on the Spring Security project page.
|
||||
|
||||
[#close-create-milestone]
|
||||
== Close / Create milestone
|
||||
|
||||
The release milestone will be automatically closed once the release is complete.
|
||||
To proceed manually, perform the following steps:
|
||||
|
||||
1. Visit https://github.com/spring-projects/spring-security/milestones[GitHub
|
||||
Milestones] and create a new milestone for the next release version
|
||||
2. Move any open issues from the existing milestone you just released to the new milestone
|
||||
3. Close the milestone for the release
|
||||
|
||||
NOTE: Remember that scheduled releases <<check-all-issues-are-closed,will not proceed>> if there are still open issues in the milestone.
|
||||
|
||||
[#announce-release-on-slack]
|
||||
== Announce release on Slack
|
||||
|
||||
The release will automatically be announced on Slack.
|
||||
If proceeding manually, announce the release on Slack in the channel https://pivotal.slack.com/messages/spring-release[#spring-release], including the keyword `+spring-security-announcing+` in the message.
|
||||
Something like:
|
||||
|
||||
....
|
||||
spring-security-announcing `5.5.0` is available now
|
||||
....
|
||||
|
||||
[#update-to-next-development-version]
|
||||
== Update to next development version
|
||||
|
||||
After the release is complete and artifacts have been uploaded to Maven Central, the build will automatically update to the next development version, commit and push.
|
||||
If proceeding manually, update the version in `gradle.properties` to the next `+SNAPSHOT+` version with the commit message "Next development version" and then push.
|
||||
|
||||
[#announce-release-on-other-channels]
|
||||
== Announce release on other channels
|
||||
|
||||
* Create a blog post on Contentful
|
||||
* Tweet from https://twitter.com/springsecurity[@SpringSecurity]
|
||||
|
||||
[[frequently-asked-questions]]
|
||||
== Frequently Asked Questions
|
||||
|
||||
*When should I update dependencies manually?* Dependencies should be updated at the latest the end of the week prior to the release. This is usually the Friday following the 2nd Monday of the month (counting from the first week with a Monday). When in doubt, check the https://github.com/spring-projects/spring-security/milestones[milestones] page for release due dates.
|
||||
|
||||
*When do scheduled releases occur?* Automated releases are scheduled to occur at *3:15 PM UTC* on the *3rd Monday of the month* (counting from the first week with a Monday).
|
||||
@@ -241,10 +59,9 @@ The scheduled release process currently runs every Monday but only releases when
|
||||
The automated release process occurs on the following branches:
|
||||
|
||||
* `main`
|
||||
* `6.2.x`
|
||||
* `6.1.x`
|
||||
* `6.0.x` (commercial only)
|
||||
* `6.0.x`
|
||||
* `5.8.x`
|
||||
* `5.7.x`
|
||||
|
||||
For each of the above branches, the automated process performs the following checks before proceeding with the release:
|
||||
|
||||
@@ -254,7 +71,7 @@ For each of the above branches, the automated process performs the following che
|
||||
[IMPORTANT]
|
||||
You should ensure all issues are closed or moved to another milestone prior to a scheduled release.
|
||||
|
||||
If the above checks pass, the version number is updated (in `gradle.properties`) and a commit is pushed to trigger the CI process.
|
||||
If the above checks pass, the version number is updated (in `gradle.properties` and `antora.yml`) and a commit is pushed to trigger the CI process.
|
||||
|
||||
*How do I trigger a release manually?* You can trigger a release manually in two ways:
|
||||
|
||||
@@ -264,3 +81,199 @@ If the above checks pass, the version number is updated (in `gradle.properties`)
|
||||
*When should additional manual steps be performed?* All other automated steps listed above occur during the normal CI process. Additional manual steps can be performed at any time once the builds pass and releases are finished.
|
||||
|
||||
*What if something goes wrong?* If the normal CI process fails, you can retry by re-running the failed jobs with the "Re-run failed jobs" option in GitHub Actions. If changes are required, you should revert the "Release x.y.z" commit, delete the tag, and proceed manually.
|
||||
|
||||
[#update-dependencies]
|
||||
== Update dependencies
|
||||
|
||||
Ensure you have no changes in your local repository.
|
||||
Change to a new branch.
|
||||
For example:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ git checkout -b 5.5.0-RC1-dependencies
|
||||
----
|
||||
|
||||
Review the rules in build.gradle to ensure the rules make sense.
|
||||
For example, we should not allow major version updates in a patch release.
|
||||
Also ensure that all of the exclusions still make sense.
|
||||
|
||||
The following Gradle command will update your dependencies creating a commit for each dependency update.
|
||||
The first invocation of the command will take quite a while (~20 minutes depending on internet speed) to run because it is indexing all the versions of all the dependencies.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew updateDependencies
|
||||
----
|
||||
|
||||
Review the commits to ensure that the updated dependency versions make sense for this release. For example, we should not perform a major version update for a patch release.
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ git log
|
||||
----
|
||||
|
||||
If any of the versions don’t make sense, update `build.gradle` to ensure that the version is excluded.
|
||||
|
||||
Run all the checks:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew check
|
||||
----
|
||||
|
||||
If they don’t work, you can run a git bisect to discover what broke the build.
|
||||
Fix any commits that broke the build.
|
||||
|
||||
Check out the original brach:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ git checkout -
|
||||
----
|
||||
|
||||
The following command will update the dependencies again but this time creating a ticket for each update and placing `Closes gh-<number>` in the commit. Replacing the following values:
|
||||
|
||||
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`
|
||||
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew updateDependencies -PupdateMode=GITHUB_ISSUE -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
Apply any fixes from your previous branch that were necessary.
|
||||
|
||||
[#check-all-issues-are-closed]
|
||||
== Check all issues are closed
|
||||
|
||||
The following command will check if there are any open issues for the ticket.
|
||||
Before running the command, replace the following values:
|
||||
|
||||
* <github-personal-access-token> - Replace with a https://github.com/settings/tokens[GitHub personal access token] that has a scope of `public_repo`. This is optional since you are unlikely to reach the rate limit for such a simple check.
|
||||
* <next-version> - Replace with the title of the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew gitHubCheckMilestoneHasNoOpenIssues -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
Alternatively, you can manually check using https://github.com/spring-projects/spring-security/milestones
|
||||
|
||||
[#update-release-version]
|
||||
== Update release version
|
||||
|
||||
Update the version number in `gradle.properties` for the release, for example `5.5.0-M1`, `5.5.0-RC1`, `5.5.0`
|
||||
|
||||
[#update-antora-version]
|
||||
== Update antora version
|
||||
|
||||
You will need to update the antora.yml version.
|
||||
If you are unsure of what the values should be, the following task will instruct you what the expected values are:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./gradlew :spring-security-docs:antoraCheckVersion
|
||||
----
|
||||
|
||||
[#build-locally]
|
||||
== Build locally
|
||||
|
||||
Run the build using
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew check
|
||||
----
|
||||
|
||||
[#push-release-commit]
|
||||
== Push release commit
|
||||
|
||||
Push the commit and GitHub actions will build and deploy the artifacts
|
||||
If you are pushing to Maven Central, then you can get notified when it’s uploaded by running the following:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./scripts/release/wait-for-done.sh 5.5.0
|
||||
----
|
||||
|
||||
[#announce-release-on-slack]
|
||||
== Announce release on Slack
|
||||
|
||||
* Announce via Slack on
|
||||
https://pivotal.slack.com/messages/spring-release[#spring-release],
|
||||
including the keyword `+spring-security-announcing+` in the message.
|
||||
Something like:
|
||||
|
||||
....
|
||||
spring-security-announcing 5.5.0 is available.
|
||||
....
|
||||
|
||||
[#tag-release]
|
||||
== Tag release
|
||||
|
||||
* Tag the release and then push the tag
|
||||
|
||||
....
|
||||
git tag 5.4.0-RC1
|
||||
git push origin 5.4.0-RC1
|
||||
....
|
||||
|
||||
[#update-to-next-development-version]
|
||||
== Update to next development version
|
||||
|
||||
* Update `gradle.properties` version to next `+SNAPSHOT+` version, update antora.yml, and then push
|
||||
|
||||
[#update-version-on-project-page]
|
||||
== Update version on project page
|
||||
|
||||
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 `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)
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
$ ./gradlew saganCreateRelease saganDeleteRelease -PgitHubAccessToken=<github-personal-access-token> -PnextVersion=<next-version> -PpreviousVersion=<previous-version>
|
||||
----
|
||||
|
||||
[#update-release-notes-on-github]
|
||||
== Update release notes on GitHub
|
||||
|
||||
Generate the Release Notes replacing:
|
||||
|
||||
* <next-version> - Replace with the milestone you are releasing now (i.e. 5.5.0-RC1)
|
||||
|
||||
----
|
||||
$ ./gradlew generateChangelog -PnextVersion=<next-version>
|
||||
----
|
||||
|
||||
* Copy the release notes to your clipboard (your mileage may vary with
|
||||
the following command)
|
||||
|
||||
....
|
||||
cat build/changelog/release-notes.md | xclip -selection clipboard
|
||||
....
|
||||
|
||||
* Create the
|
||||
https://github.com/spring-projects/spring-security/releases[release on
|
||||
GitHub], associate it with the tag, and paste the generated notes
|
||||
|
||||
[#close-create-milestone]
|
||||
== Close / Create milestone
|
||||
|
||||
* In
|
||||
https://github.com/spring-projects/spring-security/milestones[GitHub
|
||||
Milestones], create a new milestone for the next release version
|
||||
* Move any open issues from the existing milestone you just released to
|
||||
the new milestone
|
||||
* Close the milestone for the release.
|
||||
|
||||
[#announce-release-on-other-channels]
|
||||
== Announce release on other channels
|
||||
|
||||
* Create a https://spring.io/admin/blog[Blog]
|
||||
* Tweet from [@SpringSecurity](https://twitter.com/springsecurity)
|
||||
|
||||
|
||||
|
||||
@@ -118,8 +118,7 @@ public class AclPermissionEvaluator implements PermissionEvaluator {
|
||||
if (permission instanceof Permission[]) {
|
||||
return Arrays.asList((Permission[]) permission);
|
||||
}
|
||||
if (permission instanceof String) {
|
||||
String permString = (String) permission;
|
||||
if (permission instanceof String permString) {
|
||||
Permission p = buildPermission(permString);
|
||||
if (p != null) {
|
||||
return Arrays.asList(p);
|
||||
|
||||
@@ -56,10 +56,9 @@ public abstract class AbstractPermission implements Permission {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(obj instanceof Permission)) {
|
||||
if (!(obj instanceof Permission other)) {
|
||||
return false;
|
||||
}
|
||||
Permission other = (Permission) obj;
|
||||
return (this.mask == other.getMask());
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ public class JdbcAclServiceTests {
|
||||
given(this.jdbcOperations.query(anyString(), eq(args), any(RowMapper.class))).willReturn(result);
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 1L);
|
||||
List<ObjectIdentity> objectIdentities = this.aclService.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("5577");
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ public class JdbcAclServiceTests {
|
||||
public void findChildrenWithoutIdType() {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockLongIdDomainObject.class, 4711L);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockUntypedIdDomainObject.class.getName());
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(5000L);
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public class JdbcAclServiceTests {
|
||||
public void findChildrenOfIdTypeLong() {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US-PAL");
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(2);
|
||||
assertThat(objectIdentities).hasSize(2);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo(4711L);
|
||||
assertThat(objectIdentities.get(1).getType()).isEqualTo(MockLongIdDomainObject.class.getName());
|
||||
@@ -155,7 +155,7 @@ public class JdbcAclServiceTests {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
|
||||
this.aclServiceIntegration.setAclClassIdSupported(true);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("US-PAL");
|
||||
}
|
||||
@@ -165,7 +165,7 @@ public class JdbcAclServiceTests {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl(MockUntypedIdDomainObject.class, 5000L);
|
||||
this.aclServiceIntegration.setAclClassIdSupported(true);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo("costcenter");
|
||||
assertThat(objectIdentities.get(0).getIdentifier())
|
||||
.isEqualTo(UUID.fromString("25d93b3f-c3aa-4814-9d5e-c7c96ced7762"));
|
||||
@@ -186,7 +186,7 @@ public class JdbcAclServiceTests {
|
||||
ObjectIdentity objectIdentity = new ObjectIdentityImpl("location", "US");
|
||||
this.aclServiceIntegration.setAclClassIdSupported(true);
|
||||
List<ObjectIdentity> objectIdentities = this.aclServiceIntegration.findChildren(objectIdentity);
|
||||
assertThat(objectIdentities.size()).isEqualTo(1);
|
||||
assertThat(objectIdentities).hasSize(1);
|
||||
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
|
||||
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL");
|
||||
}
|
||||
|
||||
+1
-1
@@ -454,7 +454,7 @@ public class JdbcMutableAclServiceTests {
|
||||
CustomSid customSid = new CustomSid("Custom sid");
|
||||
given(customJdbcMutableAclService.createOrRetrieveSidPrimaryKey("Custom sid", false, false)).willReturn(1L);
|
||||
Long result = customJdbcMutableAclService.createOrRetrieveSidPrimaryKey(customSid, false);
|
||||
assertThat(new Long(1L)).isEqualTo(result);
|
||||
assertThat(Long.valueOf(1L)).isEqualTo(result);
|
||||
}
|
||||
|
||||
protected Authentication getAuth() {
|
||||
|
||||
@@ -120,9 +120,9 @@ public class SidTests {
|
||||
PrincipalSid principalSid = new PrincipalSid(authentication);
|
||||
GrantedAuthority ga = new SimpleGrantedAuthority("ROLE_TEST");
|
||||
GrantedAuthoritySid gaSid = new GrantedAuthoritySid(ga);
|
||||
assertThat("johndoe".equals(principalSid.getPrincipal())).isTrue();
|
||||
assertThat("johndoe").isEqualTo(principalSid.getPrincipal());
|
||||
assertThat("scott".equals(principalSid.getPrincipal())).isFalse();
|
||||
assertThat("ROLE_TEST".equals(gaSid.getGrantedAuthority())).isTrue();
|
||||
assertThat("ROLE_TEST").isEqualTo(gaSid.getGrantedAuthority());
|
||||
assertThat("ROLE_TEST2".equals(gaSid.getGrantedAuthority())).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@@ -29,10 +29,4 @@ dependencies {
|
||||
testAspect sourceSets.main.output
|
||||
}
|
||||
|
||||
sourceSets.main.aspectj.srcDir "src/main/java"
|
||||
sourceSets.main.java.srcDirs = files()
|
||||
|
||||
sourceSets.test.aspectj.srcDir "src/test/java"
|
||||
sourceSets.test.java.srcDirs = files()
|
||||
|
||||
compileAspectj.ajcOptions.outxmlfile = "META-INF/aop.xml"
|
||||
|
||||
+2
-2
@@ -143,8 +143,8 @@ public class AnnotationSecurityAspectTests {
|
||||
SecurityContextHolder.getContext().setAuthentication(this.anne);
|
||||
List<String> objects = this.prePostSecured.postFilterMethod();
|
||||
assertThat(objects).hasSize(2);
|
||||
assertThat(objects.contains("apple")).isTrue();
|
||||
assertThat(objects.contains("aubergine")).isTrue();
|
||||
assertThat(objects).contains("apple");
|
||||
assertThat(objects).contains("aubergine");
|
||||
}
|
||||
|
||||
private void configureForElAnnotations() {
|
||||
|
||||
+84
-20
@@ -14,13 +14,21 @@ buildscript {
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.org.gradle.wrapper.upgrade)
|
||||
}
|
||||
|
||||
apply plugin: 'io.spring.nohttp'
|
||||
apply plugin: 'locks'
|
||||
apply plugin: 's101'
|
||||
apply plugin: 'io.spring.convention.root'
|
||||
apply plugin: 'org.jetbrains.kotlin.jvm'
|
||||
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'
|
||||
apply plugin: 'org.springframework.security.versions.verify-dependencies-versions'
|
||||
apply plugin: 'io.spring.security.release'
|
||||
|
||||
group = 'org.springframework.security'
|
||||
description = 'Spring Security'
|
||||
@@ -31,29 +39,83 @@ ext.milestoneBuild = !(snapshotBuild || releaseBuild)
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
}
|
||||
|
||||
springRelease {
|
||||
weekOfMonth = 3
|
||||
dayOfWeek = 1
|
||||
tasks.named("saganCreateRelease") {
|
||||
referenceDocUrl = "https://docs.spring.io/spring-security/reference/{version}/index.html"
|
||||
apiDocUrl = "https://docs.spring.io/spring-security/site/docs/{version}/api/"
|
||||
replaceSnapshotVersionInReferenceDocUrl = true
|
||||
}
|
||||
|
||||
tasks.named("gitHubCheckMilestoneHasNoOpenIssues") {
|
||||
repository {
|
||||
owner = "spring-projects"
|
||||
name = "spring-security"
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
def toolchainVersion() {
|
||||
if (project.hasProperty('testToolchain')) {
|
||||
return project.property('testToolchain').toString().toInteger()
|
||||
}
|
||||
return 17
|
||||
}
|
||||
|
||||
subprojects {
|
||||
plugins.withType(JavaPlugin) {
|
||||
java {
|
||||
sourceCompatibility=JavaVersion.VERSION_17
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(toolchainVersion())
|
||||
}
|
||||
}
|
||||
tasks.withType(JavaCompile) {
|
||||
kotlin {
|
||||
jvmToolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = "UTF-8"
|
||||
options.compilerArgs.add("-parameters")
|
||||
options.release.set(17)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allprojects {
|
||||
if (!['spring-security-bom', 'spring-security-docs'].contains(project.name)) {
|
||||
apply plugin: 'io.spring.javaformat'
|
||||
@@ -78,18 +140,12 @@ allprojects {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
javaCompiler = javaToolchains.compilerFor {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
develocity {
|
||||
if (hasProperty('buildScan')) {
|
||||
buildScan {
|
||||
termsOfUseUrl = 'https://gradle.com/help/legal-terms-of-use'
|
||||
termsOfUseAgree = 'yes'
|
||||
termsOfServiceUrl = 'https://gradle.com/terms-of-service'
|
||||
termsOfServiceAgree = 'yes'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +162,14 @@ tasks.register('cloneRepository', IncludeRepoTask) {
|
||||
}
|
||||
|
||||
s101 {
|
||||
repository = 'https://structure101.com/binaries/latest'
|
||||
configurationDirectory = project.file("etc/s101")
|
||||
}
|
||||
|
||||
wrapperUpgrade {
|
||||
gradle {
|
||||
'spring-security' {
|
||||
repo = 'spring-projects/spring-security'
|
||||
baseBranch = '6.1.x' // runs only on 6.1.x and the update is merged forward to main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-4
@@ -11,6 +11,7 @@ java {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
mavenCentral()
|
||||
maven { url 'https://repo.spring.io/milestone' }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@@ -26,6 +27,10 @@ sourceSets {
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
checkAntoraVersion {
|
||||
id = "org.springframework.antora.check-version"
|
||||
implementationClass = "org.springframework.gradle.antora.AntoraVersionPlugin"
|
||||
}
|
||||
trang {
|
||||
id = "trang"
|
||||
implementationClass = "trang.TrangPlugin"
|
||||
@@ -38,6 +43,26 @@ gradlePlugin {
|
||||
id = "io.spring.convention.management-configuration"
|
||||
implementationClass = "io.spring.gradle.convention.ManagementConfigurationPlugin"
|
||||
}
|
||||
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"
|
||||
}
|
||||
githubMilestone {
|
||||
id = "org.springframework.github.milestone"
|
||||
implementationClass = "org.springframework.gradle.github.milestones.GitHubMilestonePlugin"
|
||||
}
|
||||
githubChangelog {
|
||||
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"
|
||||
@@ -71,14 +96,11 @@ dependencies {
|
||||
implementation libs.com.github.spullara.mustache.java.compiler
|
||||
implementation libs.io.spring.javaformat.spring.javaformat.gradle.plugin
|
||||
implementation libs.io.spring.nohttp.nohttp.gradle
|
||||
implementation (libs.net.sourceforge.htmlunit) {
|
||||
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
|
||||
}
|
||||
implementation libs.net.sourceforge.htmlunit
|
||||
implementation libs.org.hidetake.gradle.ssh.plugin
|
||||
implementation libs.org.jfrog.buildinfo.build.info.extractor.gradle
|
||||
implementation libs.org.sonarsource.scanner.gradle.sonarqube.gradle.plugin
|
||||
implementation libs.com.squareup.okhttp3.okhttp
|
||||
implementation libs.io.spring.security.release.plugin
|
||||
|
||||
testImplementation platform(libs.org.junit.junit.bom)
|
||||
testImplementation platform(libs.org.mockito.mockito.bom)
|
||||
|
||||
@@ -29,24 +29,6 @@ class ArtifactoryPlugin implements Plugin<Project> {
|
||||
|
||||
private static final String ARTIFACTORY_RELEASE_REPOSITORY = "ARTIFACTORY_RELEASE_REPOSITORY"
|
||||
|
||||
private static final String ARTIFACTORY_PROJECT_KEY = "ARTIFACTORY_PROJECT_KEY"
|
||||
|
||||
private static final String ARTIFACTORY_BUILD_NAME = "ARTIFACTORY_BUILD_NAME"
|
||||
|
||||
private static final String ARTIFACTORY_BUILD_NUMBER = "ARTIFACTORY_BUILD_NUMBER"
|
||||
|
||||
private static final String ARTIFACTORY_BUILD_URL = "ARTIFACTORY_BUILD_URL"
|
||||
|
||||
private static final String ARTIFACTORY_BUILD_AGENT_NAME = "ARTIFACTORY_BUILD_AGENT_NAME"
|
||||
|
||||
private static final String ARTIFACTORY_BUILD_AGENT_VERSION = "ARTIFACTORY_BUILD_AGENT_VERSION"
|
||||
|
||||
private static final String ARTIFACTORY_USER_AGENT_NAME = "ARTIFACTORY_USER_AGENT_NAME"
|
||||
|
||||
private static final String ARTIFACTORY_USER_AGENT_VERSION = "ARTIFACTORY_USER_AGENT_VERSION"
|
||||
|
||||
private static final String ARTIFACTORY_VCS_REVISION = "ARTIFACTORY_VCS_REVISION"
|
||||
|
||||
private static final String DEFAULT_ARTIFACTORY_URL = "https://repo.spring.io"
|
||||
|
||||
private static final String DEFAULT_ARTIFACTORY_SNAPSHOT_REPOSITORY = "libs-snapshot-local"
|
||||
@@ -66,15 +48,6 @@ class ArtifactoryPlugin implements Plugin<Project> {
|
||||
String snapshotRepository = env.getOrDefault(ARTIFACTORY_SNAPSHOT_REPOSITORY, DEFAULT_ARTIFACTORY_SNAPSHOT_REPOSITORY)
|
||||
String milestoneRepository = env.getOrDefault(ARTIFACTORY_MILESTONE_REPOSITORY, DEFAULT_ARTIFACTORY_MILESTONE_REPOSITORY)
|
||||
String releaseRepository = env.getOrDefault(ARTIFACTORY_RELEASE_REPOSITORY, DEFAULT_ARTIFACTORY_RELEASE_REPOSITORY)
|
||||
String projectKey = env.get(ARTIFACTORY_PROJECT_KEY)
|
||||
String buildName = env.get(ARTIFACTORY_BUILD_NAME)
|
||||
String buildNumber = env.get(ARTIFACTORY_BUILD_NUMBER)
|
||||
String buildUrl = env.get(ARTIFACTORY_BUILD_URL)
|
||||
String buildAgentName = env.get(ARTIFACTORY_BUILD_AGENT_NAME)
|
||||
String buildAgentVersion = env.get(ARTIFACTORY_BUILD_AGENT_VERSION)
|
||||
String userAgentName = env.get(ARTIFACTORY_USER_AGENT_NAME)
|
||||
String userAgentVersion = env.get(ARTIFACTORY_USER_AGENT_VERSION)
|
||||
String vcsRevision = env.get(ARTIFACTORY_VCS_REVISION)
|
||||
project.artifactory {
|
||||
contextUrl = artifactoryUrl
|
||||
publish {
|
||||
@@ -86,35 +59,6 @@ class ArtifactoryPlugin implements Plugin<Project> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def buildInfo = clientConfig.info
|
||||
if (projectKey != null) {
|
||||
buildInfo.setProject(projectKey)
|
||||
}
|
||||
if (buildName != null) {
|
||||
buildInfo.setBuildName(buildName)
|
||||
}
|
||||
if (buildNumber != null) {
|
||||
buildInfo.setBuildNumber(buildNumber)
|
||||
}
|
||||
if (buildUrl != null) {
|
||||
buildInfo.setBuildUrl(buildUrl)
|
||||
}
|
||||
if (buildAgentName != null) {
|
||||
buildInfo.setBuildAgentName(buildAgentName)
|
||||
}
|
||||
if (buildAgentVersion != null) {
|
||||
buildInfo.setBuildAgentVersion(buildAgentVersion)
|
||||
}
|
||||
if (userAgentName != null) {
|
||||
buildInfo.setAgentName(userAgentName)
|
||||
}
|
||||
if (userAgentVersion != null) {
|
||||
buildInfo.setAgentVersion(userAgentVersion)
|
||||
}
|
||||
if (vcsRevision != null) {
|
||||
buildInfo.setVcsRevision(vcsRevision)
|
||||
}
|
||||
}
|
||||
project.plugins.withType(MavenPublishPlugin) {
|
||||
project.artifactory {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2023 the original author or authors.
|
||||
* Copyright 2016-2019 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
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.springframework.gradle.antora;
|
||||
|
||||
import org.gradle.api.Action;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
|
||||
public class AntoraVersionPlugin implements Plugin<Project> {
|
||||
public static final String ANTORA_CHECK_VERSION_TASK_NAME = "antoraCheckVersion";
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
TaskProvider<CheckAntoraVersionTask> antoraCheckVersion = project.getTasks().register(ANTORA_CHECK_VERSION_TASK_NAME, CheckAntoraVersionTask.class, new Action<CheckAntoraVersionTask>() {
|
||||
@Override
|
||||
public void execute(CheckAntoraVersionTask antoraCheckVersion) {
|
||||
antoraCheckVersion.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
antoraCheckVersion.setDescription("Checks the antora.yml version properties match the Gradle version");
|
||||
antoraCheckVersion.getAntoraVersion().convention(project.provider(() -> getDefaultAntoraVersion(project)));
|
||||
antoraCheckVersion.getAntoraPrerelease().convention(project.provider(() -> getDefaultAntoraPrerelease(project)));
|
||||
antoraCheckVersion.getAntoraDisplayVersion().convention(project.provider(() -> getDefaultAntoraDisplayVersion(project)));
|
||||
antoraCheckVersion.getAntoraYmlFile().fileProvider(project.provider(() -> project.file("antora.yml")));
|
||||
}
|
||||
});
|
||||
project.getPlugins().withType(LifecycleBasePlugin.class, new Action<LifecycleBasePlugin>() {
|
||||
@Override
|
||||
public void execute(LifecycleBasePlugin lifecycleBasePlugin) {
|
||||
project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME)
|
||||
.configure(check -> check.dependsOn(antoraCheckVersion));
|
||||
}
|
||||
});
|
||||
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);
|
||||
return AntoraVersionUtils.getDefaultAntoraVersion(projectVersion);
|
||||
}
|
||||
|
||||
private static String getDefaultAntoraPrerelease(Project project) {
|
||||
String projectVersion = getProjectVersion(project);
|
||||
return AntoraVersionUtils.getDefaultAntoraPrerelease(projectVersion);
|
||||
}
|
||||
|
||||
private static String getDefaultAntoraDisplayVersion(Project project) {
|
||||
String projectVersion = getProjectVersion(project);
|
||||
return AntoraVersionUtils.getDefaultAntoraDisplayVersion(projectVersion);
|
||||
}
|
||||
|
||||
private static String getProjectVersion(Project project) {
|
||||
Object projectVersion = project.getVersion();
|
||||
if (projectVersion == null) {
|
||||
throw new GradleException("Please define antoraVersion and antoraPrerelease on " + ANTORA_CHECK_VERSION_TASK_NAME + " or provide a Project version so they can be defaulted");
|
||||
}
|
||||
return String.valueOf(projectVersion);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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,96 @@
|
||||
package org.springframework.gradle.antora;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
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.TaskAction;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.constructor.Constructor;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
|
||||
public abstract class CheckAntoraVersionTask extends DefaultTask {
|
||||
|
||||
@TaskAction
|
||||
public void check() throws FileNotFoundException {
|
||||
File antoraYmlFile = getAntoraYmlFile().getAsFile().get();
|
||||
String expectedAntoraVersion = getAntoraVersion().get();
|
||||
String expectedAntoraPrerelease = getAntoraPrerelease().getOrElse(null);
|
||||
String expectedAntoraDisplayVersion = getAntoraDisplayVersion().getOrElse(null);
|
||||
|
||||
Representer representer = new Representer();
|
||||
representer.getPropertyUtils().setSkipMissingProperties(true);
|
||||
|
||||
Yaml yaml = new Yaml(new Constructor(AntoraYml.class), representer);
|
||||
AntoraYml antoraYml = yaml.load(new FileInputStream(antoraYmlFile));
|
||||
|
||||
String actualAntoraPrerelease = antoraYml.getPrerelease();
|
||||
boolean preReleaseMatches = antoraYml.getPrerelease() == null && expectedAntoraPrerelease == null ||
|
||||
(actualAntoraPrerelease != null && actualAntoraPrerelease.equals(expectedAntoraPrerelease));
|
||||
String actualAntoraDisplayVersion = antoraYml.getDisplay_version();
|
||||
boolean displayVersionMatches = antoraYml.getDisplay_version() == null && expectedAntoraDisplayVersion == null ||
|
||||
(actualAntoraDisplayVersion != null && actualAntoraDisplayVersion.equals(expectedAntoraDisplayVersion));
|
||||
String actualAntoraVersion = antoraYml.getVersion();
|
||||
if (!preReleaseMatches ||
|
||||
!displayVersionMatches ||
|
||||
!expectedAntoraVersion.equals(actualAntoraVersion)) {
|
||||
throw new GradleException("The Gradle version of '" + getProject().getVersion() + "' should have version: '"
|
||||
+ expectedAntoraVersion + "' prerelease: '" + expectedAntoraPrerelease + "' display_version: '"
|
||||
+ expectedAntoraDisplayVersion + "' defined in " + antoraYmlFile + " but got version: '"
|
||||
+ actualAntoraVersion + "' prerelease: '" + actualAntoraPrerelease + "' display_version: '" + actualAntoraDisplayVersion + "'");
|
||||
}
|
||||
}
|
||||
|
||||
@InputFile
|
||||
public abstract RegularFileProperty getAntoraYmlFile();
|
||||
|
||||
@Input
|
||||
public abstract Property<String> getAntoraVersion();
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
public abstract Property<String> getAntoraPrerelease();
|
||||
|
||||
@Input
|
||||
@Optional
|
||||
public abstract Property<String> getAntoraDisplayVersion();
|
||||
|
||||
public static class AntoraYml {
|
||||
private String version;
|
||||
|
||||
private String prerelease;
|
||||
|
||||
private String display_version;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+138
@@ -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
-3
@@ -35,9 +35,7 @@ public class CheckClasspathForProhibitedDependenciesPlugin implements Plugin<Pro
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(CheckProhibitedDependenciesLifecyclePlugin.class);
|
||||
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> {
|
||||
configureProhibitedDependencyChecks(project);
|
||||
});
|
||||
project.getPlugins().withType(JavaBasePlugin.class, javaBasePlugin -> configureProhibitedDependencyChecks(project));
|
||||
}
|
||||
|
||||
private void configureProhibitedDependencyChecks(Project project) {
|
||||
|
||||
+1
-3
@@ -34,8 +34,6 @@ public class CheckProhibitedDependenciesLifecyclePlugin implements Plugin<Projec
|
||||
task.setGroup(JavaBasePlugin.VERIFICATION_GROUP);
|
||||
task.setDescription("Checks both the compile/runtime classpath of every SourceSet for prohibited dependencies");
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> {
|
||||
checkTask.dependsOn(checkProhibitedDependencies);
|
||||
});
|
||||
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkProhibitedDependencies));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
|
||||
public RepositoryRef() {
|
||||
}
|
||||
|
||||
public RepositoryRef(String owner, String name) {
|
||||
this.owner = owner;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(String owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RepositoryRef{" +
|
||||
"owner='" + owner + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public static RepositoryRefBuilder owner(String owner) {
|
||||
return new RepositoryRefBuilder().owner(owner);
|
||||
}
|
||||
|
||||
public static final class RepositoryRefBuilder {
|
||||
private String owner;
|
||||
private String repository;
|
||||
|
||||
private RepositoryRefBuilder() {
|
||||
}
|
||||
|
||||
private RepositoryRefBuilder owner(String owner) {
|
||||
this.owner = owner;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RepositoryRefBuilder repository(String repository) {
|
||||
this.repository = repository;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RepositoryRef build() {
|
||||
return new RepositoryRef(owner, repository);
|
||||
}
|
||||
}
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.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;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.DependencySet;
|
||||
import org.gradle.api.artifacts.repositories.ExclusiveContentRepository;
|
||||
import org.gradle.api.artifacts.repositories.IvyArtifactRepository;
|
||||
import org.gradle.api.artifacts.repositories.IvyPatternRepositoryLayout;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
|
||||
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) {
|
||||
createRepository(project);
|
||||
createChangelogGeneratorConfiguration(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(), RELEASE_NOTES_PATH));
|
||||
outputFile.getParentFile().mkdirs();
|
||||
generateChangelog.setGroup("Release");
|
||||
generateChangelog.setDescription("Generates the changelog");
|
||||
generateChangelog.setWorkingDir(project.getRootDir());
|
||||
generateChangelog.classpath(project.getConfigurations().getAt(CHANGELOG_GENERATOR_CONFIGURATION_NAME));
|
||||
generateChangelog.doFirst(new Action<Task>() {
|
||||
@Override
|
||||
public void execute(Task task) {
|
||||
generateChangelog.args("--spring.config.location=scripts/release/release-notes-sections.yml", project.property("nextVersion"), outputFile.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createChangelogGeneratorConfiguration(Project project) {
|
||||
project.getConfigurations().create(CHANGELOG_GENERATOR_CONFIGURATION_NAME, new Action<Configuration>() {
|
||||
@Override
|
||||
public void execute(Configuration configuration) {
|
||||
configuration.defaultDependencies(new Action<DependencySet>() {
|
||||
@Override
|
||||
public void execute(DependencySet dependencies) {
|
||||
dependencies.add(project.getDependencies().create("spring-io:github-changelog-generator:0.0.6"));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createRepository(Project project) {
|
||||
IvyArtifactRepository repository = project.getRepositories().ivy(new Action<IvyArtifactRepository>() {
|
||||
@Override
|
||||
public void execute(IvyArtifactRepository repository) {
|
||||
repository.setUrl("https://github.com/");
|
||||
repository.patternLayout(new Action<IvyPatternRepositoryLayout>() {
|
||||
@Override
|
||||
public void execute(IvyPatternRepositoryLayout layout) {
|
||||
layout.artifact("[organization]/[artifact]/releases/download/v[revision]/[artifact].[ext]");
|
||||
}
|
||||
});
|
||||
repository.getMetadataSources().artifact();
|
||||
}
|
||||
});
|
||||
project.getRepositories().exclusiveContent(new Action<ExclusiveContentRepository>() {
|
||||
@Override
|
||||
public void execute(ExclusiveContentRepository exclusiveContentRepository) {
|
||||
exclusiveContentRepository.forRepositories(repository);
|
||||
exclusiveContentRepository.filter(descriptor -> descriptor.includeGroup("spring-io"));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* 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 java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import org.springframework.gradle.github.RepositoryRef;
|
||||
|
||||
public class GitHubMilestoneApi {
|
||||
private String baseUrl = "https://api.github.com";
|
||||
|
||||
private OkHttpClient client;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
public GitHubMilestoneApi(String gitHubToken) {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new AuthorizationInterceptor(gitHubToken))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
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 retrieve milestones for repository " + repositoryRef + ". Response " + response);
|
||||
}
|
||||
return this.gson.fromJson(response.body().charStream(), new TypeToken<List<Milestone>>(){}.getType());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Could not retrieve milestones for repository " + repositoryRef, e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOpenIssuesForMilestoneNumber(RepositoryRef repositoryRef, long milestoneNumber) {
|
||||
String url = this.baseUrl + "/repos/" + repositoryRef.getOwner() + "/" + repositoryRef.getName() + "/issues?per_page=1&milestone=" + milestoneNumber;
|
||||
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 issues for milestone number " + milestoneNumber + " for repository " + repositoryRef + ". Response " + response);
|
||||
}
|
||||
List<Object> issues = this.gson.fromJson(response.body().charStream(), new TypeToken<List<Object>>(){}.getType());
|
||||
return !issues.isEmpty();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Could not find issues for milestone number " + milestoneNumber + " for repository " + repositoryRef, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private final String token;
|
||||
|
||||
public AuthorizationInterceptor(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public okhttp3.Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request().newBuilder()
|
||||
.addHeader("Authorization", "Bearer " + this.token).build();
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 GitHubMilestoneHasNoOpenIssuesTask extends DefaultTask {
|
||||
@Input
|
||||
private RepositoryRef repository = new RepositoryRef();
|
||||
|
||||
@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() 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.writeString(isOpenIssuesPath, String.valueOf(isOpenIssues));
|
||||
if (isOpenIssues) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 getMilestoneTitle() {
|
||||
return milestoneTitle;
|
||||
}
|
||||
|
||||
public void setMilestoneTitle(String milestoneTitle) {
|
||||
this.milestoneTitle = milestoneTitle;
|
||||
}
|
||||
|
||||
public String getGitHubAccessToken() {
|
||||
return gitHubAccessToken;
|
||||
}
|
||||
|
||||
public void setGitHubAccessToken(String gitHubAccessToken) {
|
||||
this.gitHubAccessToken = gitHubAccessToken;
|
||||
this.milestones = new GitHubMilestoneApi(gitHubAccessToken);
|
||||
}
|
||||
}
|
||||
+93
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+102
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
|
||||
import org.springframework.gradle.github.RepositoryRef;
|
||||
|
||||
public class GitHubMilestonePlugin implements Plugin<Project> {
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
+23
@@ -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());
|
||||
}
|
||||
}
|
||||
+25
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class Milestone {
|
||||
private String title;
|
||||
|
||||
private Long number;
|
||||
|
||||
@SerializedName("due_on")
|
||||
private LocalDateTime dueOn;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Long getNumber() {
|
||||
return 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 + '\'' +
|
||||
", dueOn='" + dueOn + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
+29
@@ -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;
|
||||
}
|
||||
}
|
||||
+147
@@ -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;
|
||||
}
|
||||
}
|
||||
+136
@@ -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);
|
||||
}
|
||||
}
|
||||
+205
@@ -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) {
|
||||
this.train = switch (train) {
|
||||
case 1 -> Train.ONE;
|
||||
case 2 -> Train.TWO;
|
||||
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) {
|
||||
this.weekOfMonth = switch (weekOfMonth) {
|
||||
case 1 -> WeekOfMonth.FIRST;
|
||||
case 2 -> WeekOfMonth.SECOND;
|
||||
case 3 -> WeekOfMonth.THIRD;
|
||||
case 4 -> WeekOfMonth.FOURTH;
|
||||
default -> throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth);
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder weekOfMonth(WeekOfMonth weekOfMonth) {
|
||||
this.weekOfMonth = weekOfMonth;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder dayOfWeek(int dayOfWeek) {
|
||||
this.dayOfWeek = switch (dayOfWeek) {
|
||||
case 1 -> DayOfWeek.MONDAY;
|
||||
case 2 -> DayOfWeek.TUESDAY;
|
||||
case 3 -> DayOfWeek.WEDNESDAY;
|
||||
case 4 -> DayOfWeek.THURSDAY;
|
||||
case 5 -> DayOfWeek.FRIDAY;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -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;
|
||||
}
|
||||
}
|
||||
+84
@@ -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;
|
||||
}
|
||||
}
|
||||
+98
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -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,82 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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.user;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class GitHubUserApi {
|
||||
private String baseUrl = "https://api.github.com";
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public GitHubUserApi(String gitHubAccessToken) {
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.addInterceptor(new AuthorizationInterceptor(gitHubAccessToken))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a GitHub user by the personal access token.
|
||||
*
|
||||
* @return The GitHub user
|
||||
*/
|
||||
public User getUser() {
|
||||
String url = this.baseUrl + "/user";
|
||||
Request request = new Request.Builder().url(url).get().build();
|
||||
try (Response response = this.httpClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
throw new RuntimeException(
|
||||
String.format("Unable to retrieve GitHub user." +
|
||||
" Please check the personal access token and try again." +
|
||||
" Got response %s", response));
|
||||
}
|
||||
return this.gson.fromJson(response.body().charStream(), User.class);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Unable to retrieve GitHub user.", 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,53 @@
|
||||
package org.springframework.gradle.github.user;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class User {
|
||||
private Long id;
|
||||
private String login;
|
||||
private String name;
|
||||
private String url;
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLogin() {
|
||||
return this.login;
|
||||
}
|
||||
|
||||
public void setLogin(String login) {
|
||||
this.login = login;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return this.url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "User{" +
|
||||
"id=" + id +
|
||||
", login='" + login + '\'' +
|
||||
", name='" + name + '\'' +
|
||||
", url='" + url + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.publish.Publication;
|
||||
import org.gradle.api.publish.PublishingExtension;
|
||||
import org.gradle.api.publish.plugins.PublishingPlugin;
|
||||
import org.gradle.plugins.signing.SigningExtension;
|
||||
import org.gradle.plugins.signing.SigningPlugin;
|
||||
|
||||
@@ -44,12 +43,7 @@ public class SpringSigningPlugin implements Plugin<Project> {
|
||||
|
||||
private void sign(Project project) {
|
||||
SigningExtension signing = project.getExtensions().findByType(SigningExtension.class);
|
||||
signing.setRequired(new Callable<Boolean>() {
|
||||
@Override
|
||||
public Boolean call() throws Exception {
|
||||
return project.getGradle().getTaskGraph().hasTask("publishArtifacts");
|
||||
}
|
||||
});
|
||||
signing.setRequired((Callable<Boolean>) () -> project.getGradle().getTaskGraph().hasTask("publishArtifacts"));
|
||||
String signingKeyId = (String) project.findProperty("signingKeyId");
|
||||
String signingKey = (String) project.findProperty("signingKey");
|
||||
String signingPassword = (String) project.findProperty("signingPassword");
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.sagan;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Domain object for creating a new release version.
|
||||
*/
|
||||
public class Release {
|
||||
private String version;
|
||||
|
||||
private ReleaseStatus status;
|
||||
|
||||
private boolean current;
|
||||
|
||||
private String referenceDocUrl;
|
||||
|
||||
private String apiDocUrl;
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public ReleaseStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(ReleaseStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public boolean isCurrent() {
|
||||
return current;
|
||||
}
|
||||
|
||||
public void setCurrent(boolean current) {
|
||||
this.current = current;
|
||||
}
|
||||
|
||||
public String getReferenceDocUrl() {
|
||||
return referenceDocUrl;
|
||||
}
|
||||
|
||||
public void setReferenceDocUrl(String referenceDocUrl) {
|
||||
this.referenceDocUrl = referenceDocUrl;
|
||||
}
|
||||
|
||||
public String getApiDocUrl() {
|
||||
return apiDocUrl;
|
||||
}
|
||||
|
||||
public void setApiDocUrl(String apiDocUrl) {
|
||||
this.apiDocUrl = apiDocUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Release{" +
|
||||
"version='" + version + '\'' +
|
||||
", status=" + status +
|
||||
", current=" + current +
|
||||
", referenceDocUrl='" + referenceDocUrl + '\'' +
|
||||
", apiDocUrl='" + apiDocUrl + '\'' +
|
||||
'}';
|
||||
}
|
||||
|
||||
public enum ReleaseStatus {
|
||||
/**
|
||||
* Unstable version with limited support
|
||||
*/
|
||||
SNAPSHOT,
|
||||
/**
|
||||
* Pre-Release version meant to be tested by the community
|
||||
*/
|
||||
PRERELEASE,
|
||||
/**
|
||||
* Release Generally Available on public artifact repositories and enjoying full support from maintainers
|
||||
*/
|
||||
GENERAL_AVAILABILITY;
|
||||
|
||||
private static final Pattern PRERELEASE_PATTERN = Pattern.compile("[A-Za-z0-9\\.\\-]+?(M|RC)\\d+");
|
||||
|
||||
private static final String SNAPSHOT_SUFFIX = "SNAPSHOT";
|
||||
|
||||
/**
|
||||
* Parse the ReleaseStatus from a String
|
||||
* @param version a project version
|
||||
* @return the release status for this version
|
||||
*/
|
||||
public static ReleaseStatus parse(String version) {
|
||||
if (version == null) {
|
||||
throw new IllegalArgumentException("version cannot be null");
|
||||
}
|
||||
if (version.endsWith(SNAPSHOT_SUFFIX)) {
|
||||
return SNAPSHOT;
|
||||
}
|
||||
if (PRERELEASE_PATTERN.matcher(version).matches()) {
|
||||
return PRERELEASE;
|
||||
}
|
||||
return GENERAL_AVAILABILITY;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.sagan;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import okhttp3.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Implements necessary calls to the Sagan API See https://spring.io/restdocs/index.html
|
||||
*/
|
||||
public class SaganApi {
|
||||
private String baseUrl = "https://api.spring.io";
|
||||
|
||||
private OkHttpClient client;
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public SaganApi(String username, String gitHubToken) {
|
||||
this.client = new OkHttpClient.Builder()
|
||||
.addInterceptor(new BasicInterceptor(username, gitHubToken))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setBaseUrl(String baseUrl) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
public void createReleaseForProject(Release release, String projectName) {
|
||||
String url = this.baseUrl + "/projects/" + projectName + "/releases";
|
||||
String releaseJsonString = gson.toJson(release);
|
||||
RequestBody body = RequestBody.create(MediaType.parse("application/json"), releaseJsonString);
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(body)
|
||||
.build();
|
||||
try {
|
||||
Response response = this.client.newCall(request).execute();
|
||||
if (!response.isSuccessful()) {
|
||||
throw new RuntimeException("Could not create release " + release + ". Got response " + response);
|
||||
}
|
||||
} catch (IOException fail) {
|
||||
throw new RuntimeException("Could not create release " + release, fail);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteReleaseForProject(String release, String projectName) {
|
||||
String url = this.baseUrl + "/projects/" + projectName + "/releases/" + release;
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.delete()
|
||||
.build();
|
||||
try {
|
||||
Response response = this.client.newCall(request).execute();
|
||||
if (!response.isSuccessful()) {
|
||||
throw new RuntimeException("Could not delete release " + release + ". Got response " + response);
|
||||
}
|
||||
} catch (IOException fail) {
|
||||
throw new RuntimeException("Could not delete release " + release, fail);
|
||||
}
|
||||
}
|
||||
|
||||
private static class BasicInterceptor implements Interceptor {
|
||||
|
||||
private final String token;
|
||||
|
||||
public BasicInterceptor(String username, String token) {
|
||||
this.token = Base64.getEncoder().encodeToString((username + ":" + token).getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public okhttp3.Response intercept(Chain chain) throws IOException {
|
||||
Request request = chain.request().newBuilder()
|
||||
.addHeader("Authorization", "Basic " + this.token).build();
|
||||
return chain.proceed(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.sagan;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.gradle.github.user.GitHubUserApi;
|
||||
import org.springframework.gradle.github.user.User;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
public class SaganCreateReleaseTask extends DefaultTask {
|
||||
|
||||
private static final Pattern VERSION_PATTERN = Pattern.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-.+)?$");
|
||||
|
||||
@Input
|
||||
private String gitHubAccessToken;
|
||||
@Input
|
||||
private String version;
|
||||
@Input
|
||||
private String apiDocUrl;
|
||||
@Input
|
||||
private String referenceDocUrl;
|
||||
@Input
|
||||
private String projectName;
|
||||
|
||||
@TaskAction
|
||||
public void saganCreateRelease() {
|
||||
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
|
||||
User user = github.getUser();
|
||||
|
||||
// Antora reference docs URLs for snapshots do not contain -SNAPSHOT
|
||||
String referenceDocUrl = this.referenceDocUrl;
|
||||
if (this.version.endsWith("-SNAPSHOT")) {
|
||||
Matcher versionMatcher = VERSION_PATTERN.matcher(this.version);
|
||||
Assert.isTrue(versionMatcher.matches(), "Version " + this.version + " does not match expected pattern");
|
||||
String majorVersion = versionMatcher.group(1);
|
||||
String minorVersion = versionMatcher.group(2);
|
||||
String majorMinorVersion = String.format("%s.%s-SNAPSHOT", majorVersion, minorVersion);
|
||||
referenceDocUrl = this.referenceDocUrl.replace("{version}", majorMinorVersion);
|
||||
}
|
||||
|
||||
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
|
||||
Release release = new Release();
|
||||
release.setVersion(this.version);
|
||||
release.setApiDocUrl(this.apiDocUrl);
|
||||
release.setReferenceDocUrl(referenceDocUrl);
|
||||
sagan.createReleaseForProject(release, this.projectName);
|
||||
}
|
||||
|
||||
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 getApiDocUrl() {
|
||||
return apiDocUrl;
|
||||
}
|
||||
|
||||
public void setApiDocUrl(String apiDocUrl) {
|
||||
this.apiDocUrl = apiDocUrl;
|
||||
}
|
||||
|
||||
public String getReferenceDocUrl() {
|
||||
return referenceDocUrl;
|
||||
}
|
||||
|
||||
public void setReferenceDocUrl(String referenceDocUrl) {
|
||||
this.referenceDocUrl = referenceDocUrl;
|
||||
}
|
||||
|
||||
public String getProjectName() {
|
||||
return projectName;
|
||||
}
|
||||
|
||||
public void setProjectName(String projectName) {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.sagan;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import org.springframework.gradle.github.user.GitHubUserApi;
|
||||
import org.springframework.gradle.github.user.User;
|
||||
|
||||
public class SaganDeleteReleaseTask extends DefaultTask {
|
||||
|
||||
@Input
|
||||
private String gitHubAccessToken;
|
||||
@Input
|
||||
private String version;
|
||||
@Input
|
||||
private String projectName;
|
||||
|
||||
@TaskAction
|
||||
public void saganCreateRelease() {
|
||||
GitHubUserApi github = new GitHubUserApi(this.gitHubAccessToken);
|
||||
User user = github.getUser();
|
||||
|
||||
SaganApi sagan = new SaganApi(user.getLogin(), this.gitHubAccessToken);
|
||||
sagan.deleteReleaseForProject(this.version, this.projectName);
|
||||
}
|
||||
|
||||
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 getProjectName() {
|
||||
return projectName;
|
||||
}
|
||||
|
||||
public void setProjectName(String projectName) {
|
||||
this.projectName = projectName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.sagan;
|
||||
|
||||
import io.spring.gradle.convention.Utils;
|
||||
import org.gradle.api.*;
|
||||
|
||||
public class SaganPlugin implements Plugin<Project> {
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getTasks().register("saganCreateRelease", SaganCreateReleaseTask.class, new Action<SaganCreateReleaseTask>() {
|
||||
@Override
|
||||
public void execute(SaganCreateReleaseTask saganCreateVersion) {
|
||||
saganCreateVersion.setGroup("Release");
|
||||
saganCreateVersion.setDescription("Creates a new version for the specified project on spring.io");
|
||||
saganCreateVersion.setVersion((String) project.findProperty("nextVersion"));
|
||||
saganCreateVersion.setProjectName(Utils.getProjectName(project));
|
||||
saganCreateVersion.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken"));
|
||||
}
|
||||
});
|
||||
project.getTasks().register("saganDeleteRelease", SaganDeleteReleaseTask.class, new Action<SaganDeleteReleaseTask>() {
|
||||
@Override
|
||||
public void execute(SaganDeleteReleaseTask saganDeleteVersion) {
|
||||
saganDeleteVersion.setGroup("Release");
|
||||
saganDeleteVersion.setDescription("Delete a version for the specified project on spring.io");
|
||||
saganDeleteVersion.setVersion((String) project.findProperty("previousVersion"));
|
||||
saganDeleteVersion.setProjectName(Utils.getProjectName(project));
|
||||
saganDeleteVersion.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+44
@@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+63
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+68
@@ -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,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,6 +17,7 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -33,11 +34,18 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import com.github.mustachejava.DefaultMustacheFactory;
|
||||
import com.github.mustachejava.Mustache;
|
||||
import com.github.mustachejava.MustacheFactory;
|
||||
@@ -63,10 +71,6 @@ public class S101Configurer {
|
||||
|
||||
private final Path licenseDirectory;
|
||||
|
||||
private final String repository;
|
||||
|
||||
private final String version;
|
||||
|
||||
private final Project project;
|
||||
private final Logger logger;
|
||||
|
||||
@@ -86,9 +90,6 @@ public class S101Configurer {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
this.licenseDirectory = new File(System.getProperty("user.home") + "/.Structure101/java").toPath();
|
||||
S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class);
|
||||
this.repository = extension.getRepository().get();
|
||||
this.version = extension.getVersion().get();
|
||||
}
|
||||
|
||||
public void license(String licenseId) {
|
||||
@@ -128,7 +129,25 @@ public class S101Configurer {
|
||||
|
||||
public void configure(File installationDirectory, File configurationDirectory) {
|
||||
deleteDirectory(configurationDirectory);
|
||||
configureProject(this.version, configurationDirectory);
|
||||
String version = computeVersionFromInstallation(installationDirectory);
|
||||
configureProject(version, configurationDirectory);
|
||||
}
|
||||
|
||||
private String computeVersionFromInstallation(File installationDirectory) {
|
||||
File buildJar = new File(installationDirectory, "structure101-java-build.jar");
|
||||
try (JarInputStream input = new JarInputStream(new FileInputStream(buildJar))) {
|
||||
JarEntry entry;
|
||||
while ((entry = input.getNextJarEntry()) != null) {
|
||||
if (entry.getName().contains("structure101-build.properties")) {
|
||||
Properties properties = new Properties();
|
||||
properties.load(input);
|
||||
return properties.getProperty("s101-build");
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
throw new IllegalStateException("Unable to determine Structure101 version");
|
||||
}
|
||||
|
||||
private boolean deleteDirectory(File directoryToBeDeleted) {
|
||||
@@ -142,8 +161,24 @@ public class S101Configurer {
|
||||
}
|
||||
|
||||
private String installBuildTool(File installationDirectory, File configurationDirectory) {
|
||||
copyZipToFilesystem(this.repository, installationDirectory, "structure101-build-java-all-" + this.version);
|
||||
return this.version;
|
||||
String source = "https://structure101.com/binaries/v6";
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(source);
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher == null) {
|
||||
return null;
|
||||
}
|
||||
copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2));
|
||||
return matcher.group(2);
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyZipToFilesystem(String source, File destination, String name) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -17,12 +17,7 @@
|
||||
package s101;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.WebClient;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.Input;
|
||||
@@ -30,11 +25,6 @@ import org.gradle.api.tasks.InputDirectory;
|
||||
|
||||
public class S101PluginExtension {
|
||||
private final Property<String> licenseId;
|
||||
|
||||
private final Property<String> repository;
|
||||
|
||||
private final Property<String> version;
|
||||
|
||||
private final Property<File> installationDirectory;
|
||||
private final Property<File> configurationDirectory;
|
||||
private final Property<String> label;
|
||||
@@ -75,24 +65,6 @@ public class S101PluginExtension {
|
||||
this.label.set(label);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public void setRepository(String repository) {
|
||||
this.repository.set(repository);
|
||||
}
|
||||
|
||||
@Input
|
||||
public Property<String> getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version.set(version);
|
||||
}
|
||||
|
||||
public S101PluginExtension(Project project) {
|
||||
this.licenseId = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.licenseId")) {
|
||||
@@ -106,31 +78,5 @@ public class S101PluginExtension {
|
||||
if (project.hasProperty("s101.label")) {
|
||||
setLabel((String) project.findProperty("s101.label"));
|
||||
}
|
||||
this.repository = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.repository")) {
|
||||
setRepository((String) project.findProperty("s101.repository"));
|
||||
} else {
|
||||
setRepository("https://structure101.com/binaries/v6");
|
||||
}
|
||||
this.version = project.getObjects().property(String.class);
|
||||
if (project.hasProperty("s101.version")) {
|
||||
setVersion((String) project.findProperty("s101.version"));
|
||||
} else {
|
||||
try (final WebClient webClient = new WebClient()) {
|
||||
HtmlPage page = webClient.getPage(getRepository().get());
|
||||
Matcher matcher = null;
|
||||
for (HtmlAnchor anchor : page.getAnchors()) {
|
||||
Matcher candidate = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
|
||||
if (candidate.find()) {
|
||||
matcher = candidate;
|
||||
}
|
||||
}
|
||||
if (matcher != null) {
|
||||
setVersion(matcher.group(2));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2019-2020 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.sagan;
|
||||
|
||||
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.sagan.Release;
|
||||
import org.springframework.gradle.sagan.SaganApi;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
|
||||
public class SaganApiTests {
|
||||
private MockWebServer server;
|
||||
|
||||
private SaganApi sagan;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
this.sagan = new SaganApi("user", "mock-oauth-token");
|
||||
this.baseUrl = this.server.url("/api").toString();
|
||||
this.sagan.setBaseUrl(this.baseUrl);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() throws Exception {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenValidThenNoException() throws Exception {
|
||||
this.server.enqueue(new MockResponse());
|
||||
Release release = new Release();
|
||||
release.setVersion("5.6.0-SNAPSHOT");
|
||||
release.setApiDocUrl("https://docs.spring.io/spring-security/site/docs/{version}/api/");
|
||||
release.setReferenceDocUrl("https://docs.spring.io/spring-security/reference/{version}/index.html");
|
||||
this.sagan.createReleaseForProject(release, "spring-security");
|
||||
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
|
||||
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases");
|
||||
assertThat(request.getMethod()).isEqualToIgnoringCase("post");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
|
||||
assertThat(request.getBody().readString(Charset.defaultCharset())).isEqualToIgnoringWhitespace("{\n" +
|
||||
" \"version\":\"5.6.0-SNAPSHOT\",\n" +
|
||||
" \"current\":false,\n" +
|
||||
" \"referenceDocUrl\":\"https://docs.spring.io/spring-security/reference/{version}/index.html\",\n" +
|
||||
" \"apiDocUrl\":\"https://docs.spring.io/spring-security/site/docs/{version}/api/\"\n" +
|
||||
"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteWhenValidThenNoException() throws Exception {
|
||||
this.server.enqueue(new MockResponse());
|
||||
this.sagan.deleteReleaseForProject("5.6.0-SNAPSHOT", "spring-security");
|
||||
RecordedRequest request = this.server.takeRequest(1, TimeUnit.SECONDS);
|
||||
assertThat(request.getRequestUrl().toString()).isEqualTo(this.baseUrl + "/projects/spring-security/releases/5.6.0-SNAPSHOT");
|
||||
assertThat(request.getMethod()).isEqualToIgnoringCase("delete");
|
||||
assertThat(request.getHeaders().get("Authorization")).isEqualTo("Basic dXNlcjptb2NrLW9hdXRoLXRva2Vu");
|
||||
assertThat(request.getBody().readString(Charset.defaultCharset())).isEmpty();
|
||||
}
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
package org.springframework.gradle.antora;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.testfixtures.ProjectBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
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 AntoraVersionPluginTests {
|
||||
|
||||
@Test
|
||||
void defaultsPropertiesWhenSnapshot() {
|
||||
String expectedVersion = "1.0.0-SNAPSHOT";
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
project.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0");
|
||||
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT");
|
||||
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse();
|
||||
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultsPropertiesWhenMilestone() {
|
||||
String expectedVersion = "1.0.0-M1";
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
project.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-M1");
|
||||
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true");
|
||||
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get());
|
||||
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultsPropertiesWhenRc() {
|
||||
String expectedVersion = "1.0.0-RC1";
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
project.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0-RC1");
|
||||
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("true");
|
||||
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().get()).isEqualTo(checkAntoraVersionTask.getAntoraVersion().get());
|
||||
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultsPropertiesWhenRelease() {
|
||||
String expectedVersion = "1.0.0";
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
project.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0");
|
||||
assertThat(checkAntoraVersionTask.getAntoraPrerelease().isPresent()).isFalse();
|
||||
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse();
|
||||
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitProperties() {
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
checkAntoraVersionTask.getAntoraVersion().set("1.0.0");
|
||||
checkAntoraVersionTask.getAntoraPrerelease().set("-SNAPSHOT");
|
||||
assertThat(checkAntoraVersionTask.getAntoraVersion().get()).isEqualTo("1.0.0");
|
||||
assertThat(checkAntoraVersionTask.getAntoraPrerelease().get()).isEqualTo("-SNAPSHOT");
|
||||
assertThat(checkAntoraVersionTask.getAntoraDisplayVersion().isPresent()).isFalse();
|
||||
assertThat(checkAntoraVersionTask.getAntoraYmlFile().getAsFile().get()).isEqualTo(project.file("antora.yml"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void versionNotDefined() throws Exception {
|
||||
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(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check());
|
||||
}
|
||||
|
||||
@Test
|
||||
void antoraFileNotFound() throws Exception {
|
||||
String expectedVersion = "1.0.0-SNAPSHOT";
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
project.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
assertThatIOException().isThrownBy(() -> checkAntoraVersionTask.check());
|
||||
}
|
||||
|
||||
@Test
|
||||
void actualAntoraPrereleaseNull() throws Exception {
|
||||
String expectedVersion = "1.0.0-SNAPSHOT";
|
||||
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.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
assertThatExceptionOfType(GradleException.class).isThrownBy(() -> checkAntoraVersionTask.check());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenSnapshot() throws Exception {
|
||||
String expectedVersion = "1.0.0-SNAPSHOT";
|
||||
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.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
checkAntoraVersionTask.check();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenMilestone() throws Exception {
|
||||
String expectedVersion = "1.0.0-M1";
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
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(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
checkAntoraVersionTask.check();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenRc() throws Exception {
|
||||
String expectedVersion = "1.0.0-RC1";
|
||||
Project project = ProjectBuilder.builder().build();
|
||||
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(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
checkAntoraVersionTask.check();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenReleaseAndPrereleaseUndefined() throws Exception {
|
||||
String expectedVersion = "1.0.0";
|
||||
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.setVersion(expectedVersion);
|
||||
project.getPluginManager().apply(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
checkAntoraVersionTask.check();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenExplicitRelease() throws Exception {
|
||||
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(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0");
|
||||
checkAntoraVersionTask.check();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenExplicitPrerelease() throws Exception {
|
||||
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(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0");
|
||||
((CheckAntoraVersionTask) task).getAntoraPrerelease().set("-SNAPSHOT");
|
||||
checkAntoraVersionTask.check();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWhenMissingPropertyDefined() throws Exception {
|
||||
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(AntoraVersionPlugin.class);
|
||||
|
||||
Task task = project.getTasks().findByName(AntoraVersionPlugin.ANTORA_CHECK_VERSION_TASK_NAME);
|
||||
|
||||
assertThat(task).isInstanceOf(CheckAntoraVersionTask.class);
|
||||
CheckAntoraVersionTask checkAntoraVersionTask = (CheckAntoraVersionTask) task;
|
||||
((CheckAntoraVersionTask) task).getAntoraVersion().set("1.0.0");
|
||||
checkAntoraVersionTask.check();
|
||||
}
|
||||
|
||||
}
|
||||
+1226
File diff suppressed because it is too large
Load Diff
+245
@@ -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);
|
||||
}
|
||||
}
|
||||
+89
@@ -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));
|
||||
}
|
||||
}
|
||||
+155
@@ -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()));
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2020-2023 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.user;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class GitHubUserApiTests {
|
||||
private GitHubUserApi gitHubUserApi;
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
this.server = new MockWebServer();
|
||||
this.server.start();
|
||||
this.baseUrl = this.server.url("/api").toString();
|
||||
this.gitHubUserApi = new GitHubUserApi("mock-oauth-token");
|
||||
this.gitHubUserApi.setBaseUrl(this.baseUrl);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() throws Exception {
|
||||
this.server.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserWhenValidParametersThenSuccess() {
|
||||
// @formatter:off
|
||||
String responseJson = "{\n" +
|
||||
" \"avatar_url\": \"https://avatars.githubusercontent.com/u/583231?v=4\",\n" +
|
||||
" \"bio\": null,\n" +
|
||||
" \"blog\": \"https://github.blog\",\n" +
|
||||
" \"company\": \"@github\",\n" +
|
||||
" \"created_at\": \"2011-01-25T18:44:36Z\",\n" +
|
||||
" \"email\": null,\n" +
|
||||
" \"events_url\": \"https://api.github.com/users/octocat/events{/privacy}\",\n" +
|
||||
" \"followers\": 8481,\n" +
|
||||
" \"followers_url\": \"https://api.github.com/users/octocat/followers\",\n" +
|
||||
" \"following\": 9,\n" +
|
||||
" \"following_url\": \"https://api.github.com/users/octocat/following{/other_user}\",\n" +
|
||||
" \"gists_url\": \"https://api.github.com/users/octocat/gists{/gist_id}\",\n" +
|
||||
" \"gravatar_id\": \"\",\n" +
|
||||
" \"hireable\": null,\n" +
|
||||
" \"html_url\": \"https://github.com/octocat\",\n" +
|
||||
" \"id\": 583231,\n" +
|
||||
" \"location\": \"San Francisco\",\n" +
|
||||
" \"login\": \"octocat\",\n" +
|
||||
" \"name\": \"The Octocat\",\n" +
|
||||
" \"node_id\": \"MDQ6VXNlcjU4MzIzMQ==\",\n" +
|
||||
" \"organizations_url\": \"https://api.github.com/users/octocat/orgs\",\n" +
|
||||
" \"public_gists\": 8,\n" +
|
||||
" \"public_repos\": 8,\n" +
|
||||
" \"received_events_url\": \"https://api.github.com/users/octocat/received_events\",\n" +
|
||||
" \"repos_url\": \"https://api.github.com/users/octocat/repos\",\n" +
|
||||
" \"site_admin\": false,\n" +
|
||||
" \"starred_url\": \"https://api.github.com/users/octocat/starred{/owner}{/repo}\",\n" +
|
||||
" \"subscriptions_url\": \"https://api.github.com/users/octocat/subscriptions\",\n" +
|
||||
" \"twitter_username\": null,\n" +
|
||||
" \"type\": \"User\",\n" +
|
||||
" \"updated_at\": \"2023-02-25T12:14:58Z\",\n" +
|
||||
" \"url\": \"https://api.github.com/users/octocat\"\n" +
|
||||
"}";
|
||||
// @formatter:on
|
||||
this.server.enqueue(new MockResponse().setBody(responseJson));
|
||||
|
||||
User user = this.gitHubUserApi.getUser();
|
||||
assertThat(user.getId()).isEqualTo(583231);
|
||||
assertThat(user.getLogin()).isEqualTo("octocat");
|
||||
assertThat(user.getName()).isEqualTo("The Octocat");
|
||||
assertThat(user.getUrl()).isEqualTo("https://api.github.com/users/octocat");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUserWhenErrorResponseThenException() {
|
||||
this.server.enqueue(new MockResponse().setResponseCode(400));
|
||||
// @formatter:off
|
||||
assertThatExceptionOfType(RuntimeException.class)
|
||||
.isThrownBy(() -> this.gitHubUserApi.getUser());
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
+1
@@ -31,6 +31,7 @@ import org.springframework.security.authentication.AccountStatusUserDetailsCheck
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.cas.web.authentication.ServiceAuthenticationDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.SpringSecurityMessageSource;
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2011-2024 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.cas.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* In order for the {@link CasAuthenticationProvider} to provide the correct service url
|
||||
* to authenticate the ticket, the returned value of {@link Authentication#getDetails()}
|
||||
* should implement this interface when tickets can be sent to any URL rather than only
|
||||
* {@link ServiceProperties#getService()}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @see org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource
|
||||
*/
|
||||
public interface ServiceAuthenticationDetails extends Serializable {
|
||||
|
||||
/**
|
||||
* Gets the absolute service url (i.e. https://example.com/service/).
|
||||
* @return the service url. Cannot be <code>null</code>.
|
||||
*/
|
||||
String getServiceUrl();
|
||||
|
||||
}
|
||||
+10
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -27,9 +27,8 @@ import org.apereo.cas.client.util.WebUtils;
|
||||
import org.apereo.cas.client.validation.TicketValidator;
|
||||
|
||||
import org.springframework.core.log.LogMessage;
|
||||
import org.springframework.security.authentication.AnonymousAuthenticationToken;
|
||||
import org.springframework.security.authentication.AuthenticationDetailsSource;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolver;
|
||||
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
|
||||
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.cas.authentication.CasServiceTicketAuthenticationToken;
|
||||
@@ -39,6 +38,7 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
|
||||
@@ -196,7 +196,8 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
|
||||
private SecurityContextRepository securityContextRepository = new HttpSessionSecurityContextRepository();
|
||||
|
||||
private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
|
||||
.getContextHolderStrategy();
|
||||
|
||||
public CasAuthenticationFilter() {
|
||||
super("/login/cas");
|
||||
@@ -214,9 +215,10 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
}
|
||||
this.logger.debug(
|
||||
LogMessage.format("Authentication success. Updating SecurityContextHolder to contain: %s", authResult));
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
|
||||
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
|
||||
context.setAuthentication(authResult);
|
||||
SecurityContextHolder.setContext(context);
|
||||
this.securityContextHolderStrategy.setContext(context);
|
||||
this.securityContextRepository.saveContext(context, request, response);
|
||||
if (this.eventPublisher != null) {
|
||||
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
|
||||
@@ -301,12 +303,6 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
this.authenticateAllArtifacts = serviceProperties.isAuthenticateAllArtifacts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSecurityContextRepository(SecurityContextRepository securityContextRepository) {
|
||||
super.setSecurityContextRepository(securityContextRepository);
|
||||
this.securityContextRepository = securityContextRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the request is elgible to process a service ticket. This method exists
|
||||
* for readability.
|
||||
@@ -340,7 +336,8 @@ public class CasAuthenticationFilter extends AbstractAuthenticationProcessingFil
|
||||
*/
|
||||
private boolean authenticated() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return this.trustResolver.isAuthenticated(authentication);
|
||||
return authentication != null && authentication.isAuthenticated()
|
||||
&& !(authentication instanceof AnonymousAuthenticationToken);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2011-2024 the original author or authors.
|
||||
* Copyright 2011-2023 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,24 +16,22 @@
|
||||
|
||||
package org.springframework.security.cas.web.authentication;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.security.cas.ServiceProperties;
|
||||
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
|
||||
/**
|
||||
* In order for the
|
||||
* {@link org.springframework.security.cas.authentication.CasAuthenticationProvider} to
|
||||
* provide the correct service url to authenticate the ticket, the returned value of
|
||||
* {@link Authentication#getDetails()} should implement this interface when tickets can be
|
||||
* sent to any URL rather than only {@link ServiceProperties#getService()}.
|
||||
* In order for the {@link CasAuthenticationProvider} to provide the correct service url
|
||||
* to authenticate the ticket, the returned value of {@link Authentication#getDetails()}
|
||||
* should implement this interface when tickets can be sent to any URL rather than only
|
||||
* {@link ServiceProperties#getService()}.
|
||||
*
|
||||
* @author Rob Winch
|
||||
* @see ServiceAuthenticationDetailsSource
|
||||
* @deprecated Please use
|
||||
* org.springframework.security.cas.authentication.ServiceAuthenticationDetails
|
||||
*/
|
||||
@Deprecated
|
||||
public interface ServiceAuthenticationDetails
|
||||
extends org.springframework.security.cas.authentication.ServiceAuthenticationDetails {
|
||||
public interface ServiceAuthenticationDetails extends Serializable {
|
||||
|
||||
/**
|
||||
* Gets the absolute service url (i.e. https://example.com/service/).
|
||||
|
||||
+1
-14
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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,10 +16,7 @@
|
||||
|
||||
package org.springframework.security.cas.web;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.apereo.cas.client.proxy.ProxyGrantingTicketStorage;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -222,14 +219,4 @@ public class CasAuthenticationFilterTests {
|
||||
verify(securityContextRepository).saveContext(any(SecurityContext.class), eq(request), eq(response));
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfulAuthenticationWhenSecurityContextRepositorySetThenUses() throws ServletException, IOException {
|
||||
SecurityContextRepository securityContextRepository = mock(SecurityContextRepository.class);
|
||||
CasAuthenticationFilter filter = new CasAuthenticationFilter();
|
||||
filter.setSecurityContextRepository(securityContextRepository);
|
||||
filter.successfulAuthentication(new MockHttpServletRequest(), new MockHttpServletResponse(),
|
||||
new MockFilterChain(), mock(Authentication.class));
|
||||
verify(securityContextRepository).saveContext(any(SecurityContext.class), any(), any());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -80,7 +80,6 @@ dependencies {
|
||||
testImplementation "org.hibernate.orm:hibernate-core"
|
||||
testImplementation 'org.hsqldb:hsqldb'
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
testImplementation "org.mockito:mockito-inline"
|
||||
testImplementation('org.seleniumhq.selenium:htmlunit-driver') {
|
||||
exclude group: 'commons-logging', module: 'commons-logging'
|
||||
exclude group: 'xml-apis', module: 'xml-apis'
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ public class RSocketMessageHandlerITests {
|
||||
|
||||
@MessageMapping({ "secure.send", "send" })
|
||||
Mono<Void> send(Mono<String> payload) {
|
||||
return payload.doOnNext(this::add).then(Mono.fromRunnable(() -> doNotifyAll()));
|
||||
return payload.doOnNext(this::add).then(Mono.fromRunnable(this::doNotifyAll));
|
||||
}
|
||||
|
||||
private synchronized void doNotifyAll() {
|
||||
|
||||
+2
-2
@@ -87,7 +87,7 @@ public class LdapUserServiceBeanDefinitionParserTests {
|
||||
|
||||
Set<String> authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities());
|
||||
assertThat(authorities).hasSize(3);
|
||||
assertThat(authorities.contains("ROLE_DEVELOPERS")).isTrue();
|
||||
assertThat(authorities).contains("ROLE_DEVELOPERS");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -128,7 +128,7 @@ public class LdapUserServiceBeanDefinitionParserTests {
|
||||
|
||||
Set<String> authorities = AuthorityUtils.authorityListToSet(ben.getAuthorities());
|
||||
assertThat(authorities).hasSize(3);
|
||||
assertThat(authorities.contains("ROLE_DEVELOPER")).isTrue();
|
||||
assertThat(authorities).contains("ROLE_DEVELOPER");
|
||||
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -96,7 +96,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
|
||||
pc.getReaderContext()
|
||||
.fatal("You cannot use a spring-security-2.0.xsd or spring-security-3.0.xsd or "
|
||||
+ "spring-security-3.1.xsd schema or spring-security-3.2.xsd schema or spring-security-4.0.xsd schema "
|
||||
+ "with Spring Security 6.1. Please update your schema declarations to the 6.1 schema.",
|
||||
+ "with Spring Security 6.2. Please update your schema declarations to the 6.2 schema.",
|
||||
element);
|
||||
}
|
||||
String name = pc.getDelegate().getLocalName(element);
|
||||
@@ -221,7 +221,7 @@ public final class SecurityNamespaceHandler implements NamespaceHandler {
|
||||
|
||||
private boolean matchesVersionInternal(Element element) {
|
||||
String schemaLocation = element.getAttributeNS("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation");
|
||||
return schemaLocation.matches("(?m).*spring-security-6\\.1.*.xsd.*")
|
||||
return schemaLocation.matches("(?m).*spring-security-6\\.2.*.xsd.*")
|
||||
|| schemaLocation.matches("(?m).*spring-security.xsd.*")
|
||||
|| !schemaLocation.matches("(?m).*spring-security.*");
|
||||
}
|
||||
|
||||
+22
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -27,6 +27,7 @@ import java.util.Map;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.filter.DelegatingFilterProxy;
|
||||
@@ -117,7 +118,10 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
|
||||
* @param configurer
|
||||
* @return the {@link SecurityConfigurerAdapter} for further customizations
|
||||
* @throws Exception
|
||||
* @deprecated For removal in 7.0. Use
|
||||
* {@link #with(SecurityConfigurerAdapter, Customizer)} instead.
|
||||
*/
|
||||
@Deprecated(since = "6.2", forRemoval = true)
|
||||
@SuppressWarnings("unchecked")
|
||||
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
|
||||
configurer.addObjectPostProcessor(this.objectPostProcessor);
|
||||
@@ -139,6 +143,23 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
|
||||
return configurer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a {@link SecurityConfigurerAdapter} to this {@link SecurityBuilder} and
|
||||
* invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}.
|
||||
* @param configurer
|
||||
* @return the {@link SecurityBuilder} for further customizations
|
||||
* @throws Exception
|
||||
* @since 6.2
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <C extends SecurityConfigurerAdapter<O, B>> B with(C configurer, Customizer<C> customizer) throws Exception {
|
||||
configurer.addObjectPostProcessor(this.objectPostProcessor);
|
||||
configurer.setBuilder((B) this);
|
||||
add(configurer);
|
||||
customizer.customize(configurer);
|
||||
return (B) this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an object that is shared by multiple {@link SecurityConfigurer}.
|
||||
* @param sharedType the Class to key the shared object by.
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2013 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.
|
||||
@@ -149,7 +149,7 @@ public class AuthenticationManagerBuilder
|
||||
* {@link #getDefaultUserDetailsService()} method. Note that additional
|
||||
* {@link UserDetailsService}'s may override this {@link UserDetailsService} as the
|
||||
* default. See the <a href=
|
||||
* "https://docs.spring.io/spring-security/reference/servlet/appendix/database-schema.html"
|
||||
* "https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#user-schema"
|
||||
* >User Schema</a> section of the reference for the default schema.
|
||||
* </p>
|
||||
* @return a {@link JdbcUserDetailsManagerConfigurer} to allow customization of the
|
||||
|
||||
+3
-35
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2018 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.
|
||||
@@ -22,14 +22,11 @@ import java.util.List;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.Aware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.core.NativeDetector;
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -58,13 +55,14 @@ final class AutowireBeanFactoryObjectPostProcessor
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T postProcess(T object) {
|
||||
if (object == null) {
|
||||
return null;
|
||||
}
|
||||
T result = null;
|
||||
try {
|
||||
result = initializeBeanIfNeeded(object);
|
||||
result = (T) this.autowireBeanFactory.initializeBean(object, object.toString());
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
Class<?> type = object.getClass();
|
||||
@@ -80,36 +78,6 @@ final class AutowireBeanFactoryObjectPostProcessor
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes {@link AutowireCapableBeanFactory#initializeBean(Object, String)} only if
|
||||
* needed, i.e when the application is not a native image or the object is not a CGLIB
|
||||
* proxy.
|
||||
* @param object the object to initialize
|
||||
* @param <T> the type of the object
|
||||
* @return the initialized bean or an existing bean if the object is a CGLIB proxy and
|
||||
* the application is a native image
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-projects/spring-security/issues/14825">Issue
|
||||
* gh-14825</a>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T initializeBeanIfNeeded(T object) {
|
||||
if (!NativeDetector.inNativeImage() || !AopUtils.isCglibProxy(object)) {
|
||||
return (T) this.autowireBeanFactory.initializeBean(object, object.toString());
|
||||
}
|
||||
ObjectProvider<?> provider = this.autowireBeanFactory.getBeanProvider(object.getClass());
|
||||
Object bean = provider.getIfUnique();
|
||||
if (bean == null) {
|
||||
String msg = """
|
||||
Failed to resolve an unique bean (single or primary) of type [%s] from the BeanFactory.
|
||||
Because the object is a CGLIB Proxy, a raw bean cannot be initialized during runtime in a native image.
|
||||
"""
|
||||
.formatted(object.getClass());
|
||||
throw new IllegalStateException(msg);
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
for (SmartInitializingSingleton singleton : this.smartSingletons) {
|
||||
|
||||
+2
-1
@@ -41,7 +41,8 @@ final class MethodSecuritySelector implements ImportSelector {
|
||||
|
||||
@Override
|
||||
public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
|
||||
if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())) {
|
||||
if (!importMetadata.hasAnnotation(EnableMethodSecurity.class.getName())
|
||||
&& !importMetadata.hasMetaAnnotation(EnableMethodSecurity.class.getName())) {
|
||||
return new String[0];
|
||||
}
|
||||
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
|
||||
|
||||
+7
-10
@@ -43,7 +43,6 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
* @since 5.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
private int advisorOrder;
|
||||
@@ -52,17 +51,16 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMethodSecurityMetadataSource source,
|
||||
ReactiveMethodSecurityConfiguration configuration) {
|
||||
MethodSecurityMetadataSourceAdvisor methodSecurityInterceptor(AbstractMethodSecurityMetadataSource source) {
|
||||
MethodSecurityMetadataSourceAdvisor advisor = new MethodSecurityMetadataSourceAdvisor(
|
||||
"securityMethodInterceptor", source, "methodMetadataSource");
|
||||
advisor.setOrder(configuration.advisorOrder);
|
||||
advisor.setOrder(this.advisorOrder);
|
||||
return advisor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static DelegatingMethodSecurityMetadataSource methodMetadataSource(
|
||||
DelegatingMethodSecurityMetadataSource methodMetadataSource(
|
||||
MethodSecurityExpressionHandler methodSecurityExpressionHandler) {
|
||||
ExpressionBasedAnnotationAttributeFactory attributeFactory = new ExpressionBasedAnnotationAttributeFactory(
|
||||
methodSecurityExpressionHandler);
|
||||
@@ -72,7 +70,7 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
}
|
||||
|
||||
@Bean
|
||||
static PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(AbstractMethodSecurityMetadataSource source,
|
||||
PrePostAdviceReactiveMethodInterceptor securityMethodInterceptor(AbstractMethodSecurityMetadataSource source,
|
||||
MethodSecurityExpressionHandler handler) {
|
||||
ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(handler);
|
||||
ExpressionBasedPreInvocationAdvice preAdvice = new ExpressionBasedPreInvocationAdvice();
|
||||
@@ -82,11 +80,10 @@ class ReactiveMethodSecurityConfiguration implements ImportAware {
|
||||
|
||||
@Bean
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler(
|
||||
ReactiveMethodSecurityConfiguration configuration) {
|
||||
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler() {
|
||||
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
|
||||
if (configuration.grantedAuthorityDefaults != null) {
|
||||
handler.setDefaultRolePrefix(configuration.grantedAuthorityDefaults.getRolePrefix());
|
||||
if (this.grantedAuthorityDefaults != null) {
|
||||
handler.setDefaultRolePrefix(this.grantedAuthorityDefaults.getRolePrefix());
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
+2
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -27,7 +27,6 @@ import org.springframework.security.oauth2.server.resource.web.authentication.Be
|
||||
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||
import org.springframework.security.web.access.ExceptionTranslationFilter;
|
||||
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
|
||||
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
@@ -38,11 +37,7 @@ import org.springframework.security.web.authentication.rememberme.RememberMeAuth
|
||||
import org.springframework.security.web.authentication.switchuser.SwitchUserFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.www.DigestAuthenticationFilter;
|
||||
import org.springframework.security.web.context.SecurityContextHolderFilter;
|
||||
import org.springframework.security.web.context.SecurityContextPersistenceFilter;
|
||||
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
|
||||
import org.springframework.security.web.csrf.CsrfFilter;
|
||||
import org.springframework.security.web.header.HeaderWriterFilter;
|
||||
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
|
||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
|
||||
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
|
||||
@@ -50,7 +45,6 @@ import org.springframework.security.web.session.ConcurrentSessionFilter;
|
||||
import org.springframework.security.web.session.DisableEncodeUrlFilter;
|
||||
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
|
||||
import org.springframework.security.web.session.SessionManagementFilter;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
/**
|
||||
* @param <H>
|
||||
@@ -132,24 +126,15 @@ public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
|
||||
* The ordering of the Filters is:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link DisableEncodeUrlFilter}</li>
|
||||
* <li>{@link ForceEagerSessionCreationFilter}</li>
|
||||
* <li>{@link DisableEncodeUrlFilter}</li>
|
||||
* <li>{@link ChannelProcessingFilter}</li>
|
||||
* <li>{@link WebAsyncManagerIntegrationFilter}</li>
|
||||
* <li>{@link SecurityContextHolderFilter}</li>
|
||||
* <li>{@link SecurityContextPersistenceFilter}</li>
|
||||
* <li>{@link HeaderWriterFilter}</li>
|
||||
* <li>{@link CorsFilter}</li>
|
||||
* <li>{@link CsrfFilter}</li>
|
||||
* <li>{@link LogoutFilter}</li>
|
||||
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter}</li>
|
||||
* <li>{@link org.springframework.security.saml2.provider.service.web.Saml2WebSsoAuthenticationRequestFilter}</li>
|
||||
* <li>{@link X509AuthenticationFilter}</li>
|
||||
* <li>{@link AbstractPreAuthenticatedProcessingFilter}</li>
|
||||
* <li><a href="
|
||||
* {@docRoot}/org/springframework/security/cas/web/CasAuthenticationFilter.html">CasAuthenticationFilter</a></li>
|
||||
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter}</li>
|
||||
* <li>{@link org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter}</li>
|
||||
* <li>{@link UsernamePasswordAuthenticationFilter}</li>
|
||||
* <li>{@link org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter}</li>
|
||||
* <li>{@link org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter}</li>
|
||||
@@ -162,11 +147,9 @@ public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
|
||||
* <li>{@link JaasApiIntegrationFilter}</li>
|
||||
* <li>{@link RememberMeAuthenticationFilter}</li>
|
||||
* <li>{@link AnonymousAuthenticationFilter}</li>
|
||||
* <li>{@link org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter}</li>
|
||||
* <li>{@link SessionManagementFilter}</li>
|
||||
* <li>{@link ExceptionTranslationFilter}</li>
|
||||
* <li>{@link FilterSecurityInterceptor}</li>
|
||||
* <li>{@link AuthorizationFilter}</li>
|
||||
* <li>{@link SwitchUserFilter}</li>
|
||||
* </ul>
|
||||
* @param filter the {@link Filter} to add
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
||||
+11
@@ -70,6 +70,7 @@ import org.springframework.security.config.annotation.web.configurers.SessionMan
|
||||
import org.springframework.security.config.annotation.web.configurers.X509Configurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2ClientConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.client.OidcLogoutConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LoginConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.saml2.Saml2LogoutConfigurer;
|
||||
@@ -2835,6 +2836,16 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
|
||||
return HttpSecurity.this;
|
||||
}
|
||||
|
||||
public OidcLogoutConfigurer<HttpSecurity> oidcLogout() throws Exception {
|
||||
return getOrApply(new OidcLogoutConfigurer<>());
|
||||
}
|
||||
|
||||
public HttpSecurity oidcLogout(Customizer<OidcLogoutConfigurer<HttpSecurity>> oidcLogoutCustomizer)
|
||||
throws Exception {
|
||||
oidcLogoutCustomizer.customize(getOrApply(new OidcLogoutConfigurer<>()));
|
||||
return HttpSecurity.this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures OAuth 2.0 Client support.
|
||||
* @return the {@link OAuth2ClientConfigurer} for further customizations
|
||||
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -47,6 +47,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
|
||||
import org.springframework.web.accept.ContentNegotiationStrategy;
|
||||
import org.springframework.web.accept.HeaderContentNegotiationStrategy;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
|
||||
import static org.springframework.security.config.Customizer.withDefaults;
|
||||
|
||||
@@ -124,10 +125,18 @@ class HttpSecurityConfiguration {
|
||||
.apply(new DefaultLoginPageConfigurer<>());
|
||||
http.logout(withDefaults());
|
||||
// @formatter:on
|
||||
applyCorsIfAvailable(http);
|
||||
applyDefaultConfigurers(http);
|
||||
return http;
|
||||
}
|
||||
|
||||
private void applyCorsIfAvailable(HttpSecurity http) throws Exception {
|
||||
String[] beanNames = this.context.getBeanNamesForType(CorsConfigurationSource.class);
|
||||
if (beanNames.length == 1) {
|
||||
http.cors(withDefaults());
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationManager authenticationManager() throws Exception {
|
||||
return this.authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
+301
-51
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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,19 +16,46 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.BeanInitializationException;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.annotation.AnnotationBeanNameGenerator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportSelector;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.oauth2.client.AuthorizationCodeOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.ClientCredentialsOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.DelegatingOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.JwtBearerOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
|
||||
import org.springframework.security.oauth2.client.PasswordOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.RefreshTokenOAuth2AuthorizedClientProvider;
|
||||
import org.springframework.security.oauth2.client.endpoint.JwtBearerGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2PasswordGrantRequest;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGrantRequest;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
@@ -48,7 +75,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
* @since 5.1
|
||||
* @see OAuth2ImportSelector
|
||||
*/
|
||||
@Import(OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class)
|
||||
@Import({ OAuth2ClientConfiguration.OAuth2ClientWebMvcImportSelector.class,
|
||||
OAuth2ClientConfiguration.OAuth2AuthorizedClientManagerConfiguration.class })
|
||||
final class OAuth2ClientConfiguration {
|
||||
|
||||
private static final boolean webMvcPresent;
|
||||
@@ -65,8 +93,22 @@ final class OAuth2ClientConfiguration {
|
||||
if (!webMvcPresent) {
|
||||
return new String[0];
|
||||
}
|
||||
return new String[] { "org.springframework.security.config.annotation.web.configuration."
|
||||
+ "OAuth2ClientConfiguration.OAuth2ClientWebMvcSecurityConfiguration" };
|
||||
return new String[] {
|
||||
OAuth2ClientConfiguration.class.getName() + ".OAuth2ClientWebMvcSecurityConfiguration" };
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
* @since 6.2.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class OAuth2AuthorizedClientManagerConfiguration {
|
||||
|
||||
@Bean(name = OAuth2AuthorizedClientManagerRegistrar.BEAN_NAME)
|
||||
OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar() {
|
||||
return new OAuth2AuthorizedClientManagerRegistrar();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -74,16 +116,12 @@ final class OAuth2ClientConfiguration {
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class OAuth2ClientWebMvcSecurityConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private ClientRegistrationRepository clientRegistrationRepository;
|
||||
|
||||
private OAuth2AuthorizedClientRepository authorizedClientRepository;
|
||||
|
||||
private OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient;
|
||||
|
||||
private OAuth2AuthorizedClientManager authorizedClientManager;
|
||||
|
||||
private SecurityContextHolderStrategy securityContextHolderStrategy;
|
||||
|
||||
private OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar;
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
||||
OAuth2AuthorizedClientManager authorizedClientManager = getAuthorizedClientManager();
|
||||
@@ -97,26 +135,6 @@ final class OAuth2ClientConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setClientRegistrationRepository(List<ClientRegistrationRepository> clientRegistrationRepositories) {
|
||||
if (clientRegistrationRepositories.size() == 1) {
|
||||
this.clientRegistrationRepository = clientRegistrationRepositories.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientRepository(List<OAuth2AuthorizedClientRepository> authorizedClientRepositories) {
|
||||
if (authorizedClientRepositories.size() == 1) {
|
||||
this.authorizedClientRepository = authorizedClientRepositories.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAccessTokenResponseClient(
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient) {
|
||||
this.accessTokenResponseClient = accessTokenResponseClient;
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
void setAuthorizedClientManager(List<OAuth2AuthorizedClientManager> authorizedClientManagers) {
|
||||
if (authorizedClientManagers.size() == 1) {
|
||||
@@ -129,33 +147,265 @@ final class OAuth2ClientConfiguration {
|
||||
this.securityContextHolderStrategy = strategy;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
void setAuthorizedClientManagerRegistrar(
|
||||
OAuth2AuthorizedClientManagerRegistrar authorizedClientManagerRegistrar) {
|
||||
this.authorizedClientManagerRegistrar = authorizedClientManagerRegistrar;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
if (this.authorizedClientManager != null) {
|
||||
return this.authorizedClientManager;
|
||||
}
|
||||
OAuth2AuthorizedClientManager authorizedClientManager = null;
|
||||
if (this.clientRegistrationRepository != null && this.authorizedClientRepository != null) {
|
||||
if (this.accessTokenResponseClient != null) {
|
||||
// @formatter:off
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
|
||||
.builder()
|
||||
.authorizationCode()
|
||||
.refreshToken()
|
||||
.clientCredentials((configurer) -> configurer.accessTokenResponseClient(this.accessTokenResponseClient))
|
||||
.password()
|
||||
.build();
|
||||
// @formatter:on
|
||||
DefaultOAuth2AuthorizedClientManager defaultAuthorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||
defaultAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
authorizedClientManager = defaultAuthorizedClientManager;
|
||||
return this.authorizedClientManagerRegistrar.getAuthorizedClientManagerIfAvailable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A registrar for registering the default {@link OAuth2AuthorizedClientManager} bean
|
||||
* definition, if not already present.
|
||||
*
|
||||
* @author Joe Grandja
|
||||
* @author Steve Riesenberg
|
||||
* @since 6.2.0
|
||||
*/
|
||||
static final class OAuth2AuthorizedClientManagerRegistrar
|
||||
implements BeanDefinitionRegistryPostProcessor, BeanFactoryAware {
|
||||
|
||||
static final String BEAN_NAME = "authorizedClientManagerRegistrar";
|
||||
|
||||
static final String FACTORY_METHOD_NAME = "getAuthorizedClientManager";
|
||||
|
||||
// @formatter:off
|
||||
private static final Set<Class<?>> KNOWN_AUTHORIZED_CLIENT_PROVIDERS = Set.of(
|
||||
AuthorizationCodeOAuth2AuthorizedClientProvider.class,
|
||||
RefreshTokenOAuth2AuthorizedClientProvider.class,
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider.class,
|
||||
PasswordOAuth2AuthorizedClientProvider.class,
|
||||
JwtBearerOAuth2AuthorizedClientProvider.class
|
||||
);
|
||||
// @formatter:on
|
||||
|
||||
private final AnnotationBeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();
|
||||
|
||||
private ListableBeanFactory beanFactory;
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
if (getBeanNamesForType(OAuth2AuthorizedClientManager.class).length != 0
|
||||
|| getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|
||||
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(OAuth2AuthorizedClientManager.class)
|
||||
.setFactoryMethodOnBean(FACTORY_METHOD_NAME, BEAN_NAME)
|
||||
.getBeanDefinition();
|
||||
|
||||
registry.registerBeanDefinition(this.beanNameGenerator.generateBeanName(beanDefinition, registry),
|
||||
beanDefinition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = (ListableBeanFactory) beanFactory;
|
||||
}
|
||||
|
||||
OAuth2AuthorizedClientManager getAuthorizedClientManagerIfAvailable() {
|
||||
if (getBeanNamesForType(ClientRegistrationRepository.class).length != 1
|
||||
|| getBeanNamesForType(OAuth2AuthorizedClientRepository.class).length != 1) {
|
||||
return null;
|
||||
}
|
||||
return getAuthorizedClientManager();
|
||||
}
|
||||
|
||||
OAuth2AuthorizedClientManager getAuthorizedClientManager() {
|
||||
ClientRegistrationRepository clientRegistrationRepository = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, ClientRegistrationRepository.class, true, true);
|
||||
|
||||
OAuth2AuthorizedClientRepository authorizedClientRepository = BeanFactoryUtils
|
||||
.beanOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientRepository.class, true, true);
|
||||
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviderBeans = BeanFactoryUtils
|
||||
.beansOfTypeIncludingAncestors(this.beanFactory, OAuth2AuthorizedClientProvider.class, true, true)
|
||||
.values();
|
||||
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider;
|
||||
if (hasDelegatingAuthorizedClientProvider(authorizedClientProviderBeans)) {
|
||||
authorizedClientProvider = authorizedClientProviderBeans.iterator().next();
|
||||
}
|
||||
else {
|
||||
List<OAuth2AuthorizedClientProvider> authorizedClientProviders = new ArrayList<>();
|
||||
authorizedClientProviders
|
||||
.add(getAuthorizationCodeAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getRefreshTokenAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders
|
||||
.add(getClientCredentialsAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
authorizedClientProviders.add(getPasswordAuthorizedClientProvider(authorizedClientProviderBeans));
|
||||
|
||||
OAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = getJwtBearerAuthorizedClientProvider(
|
||||
authorizedClientProviderBeans);
|
||||
if (jwtBearerAuthorizedClientProvider != null) {
|
||||
authorizedClientProviders.add(jwtBearerAuthorizedClientProvider);
|
||||
}
|
||||
else {
|
||||
authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
this.clientRegistrationRepository, this.authorizedClientRepository);
|
||||
|
||||
authorizedClientProviders.addAll(getAdditionalAuthorizedClientProviders(authorizedClientProviderBeans));
|
||||
authorizedClientProvider = new DelegatingOAuth2AuthorizedClientProvider(authorizedClientProviders);
|
||||
}
|
||||
|
||||
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
|
||||
clientRegistrationRepository, authorizedClientRepository);
|
||||
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
|
||||
|
||||
Consumer<DefaultOAuth2AuthorizedClientManager> authorizedClientManagerConsumer = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(Consumer.class, DefaultOAuth2AuthorizedClientManager.class));
|
||||
if (authorizedClientManagerConsumer != null) {
|
||||
authorizedClientManagerConsumer.accept(authorizedClientManager);
|
||||
}
|
||||
|
||||
return authorizedClientManager;
|
||||
}
|
||||
|
||||
private boolean hasDelegatingAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
if (authorizedClientProviders.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
return authorizedClientProviders.iterator().next() instanceof DelegatingOAuth2AuthorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getAuthorizationCodeAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
AuthorizationCodeOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, AuthorizationCodeOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new AuthorizationCodeOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getRefreshTokenAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
RefreshTokenOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, RefreshTokenOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new RefreshTokenOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2RefreshTokenGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getClientCredentialsAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
ClientCredentialsOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, ClientCredentialsOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new ClientCredentialsOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2ClientCredentialsGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getPasswordAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
PasswordOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, PasswordOAuth2AuthorizedClientProvider.class);
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new PasswordOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2PasswordGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private OAuth2AuthorizedClientProvider getJwtBearerAuthorizedClientProvider(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
JwtBearerOAuth2AuthorizedClientProvider authorizedClientProvider = getAuthorizedClientProviderByType(
|
||||
authorizedClientProviders, JwtBearerOAuth2AuthorizedClientProvider.class);
|
||||
|
||||
OAuth2AccessTokenResponseClient<JwtBearerGrantRequest> accessTokenResponseClient = getBeanOfType(
|
||||
ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
JwtBearerGrantRequest.class));
|
||||
if (accessTokenResponseClient != null) {
|
||||
if (authorizedClientProvider == null) {
|
||||
authorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider();
|
||||
}
|
||||
|
||||
authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);
|
||||
}
|
||||
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private List<OAuth2AuthorizedClientProvider> getAdditionalAuthorizedClientProviders(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders) {
|
||||
List<OAuth2AuthorizedClientProvider> additionalAuthorizedClientProviders = new ArrayList<>(
|
||||
authorizedClientProviders);
|
||||
additionalAuthorizedClientProviders
|
||||
.removeIf((provider) -> KNOWN_AUTHORIZED_CLIENT_PROVIDERS.contains(provider.getClass()));
|
||||
return additionalAuthorizedClientProviders;
|
||||
}
|
||||
|
||||
private <T extends OAuth2AuthorizedClientProvider> T getAuthorizedClientProviderByType(
|
||||
Collection<OAuth2AuthorizedClientProvider> authorizedClientProviders, Class<T> providerClass) {
|
||||
T authorizedClientProvider = null;
|
||||
for (OAuth2AuthorizedClientProvider current : authorizedClientProviders) {
|
||||
if (providerClass.isInstance(current)) {
|
||||
assertAuthorizedClientProviderIsNull(authorizedClientProvider);
|
||||
authorizedClientProvider = providerClass.cast(current);
|
||||
}
|
||||
}
|
||||
return authorizedClientManager;
|
||||
return authorizedClientProvider;
|
||||
}
|
||||
|
||||
private static void assertAuthorizedClientProviderIsNull(
|
||||
OAuth2AuthorizedClientProvider authorizedClientProvider) {
|
||||
if (authorizedClientProvider != null) {
|
||||
// @formatter:off
|
||||
throw new BeanInitializationException(String.format(
|
||||
"Unable to create an %s bean. Expected one bean of type %s, but found multiple. " +
|
||||
"Please consider defining only a single bean of this type, or define an %s bean yourself.",
|
||||
OAuth2AuthorizedClientManager.class.getName(),
|
||||
authorizedClientProvider.getClass().getName(),
|
||||
OAuth2AuthorizedClientManager.class.getName()));
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
||||
|
||||
private <T> String[] getBeanNamesForType(Class<T> beanClass) {
|
||||
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanClass, true, true);
|
||||
}
|
||||
|
||||
private <T> T getBeanOfType(ResolvableType resolvableType) {
|
||||
ObjectProvider<T> objectProvider = this.beanFactory.getBeanProvider(resolvableType, true);
|
||||
return objectProvider.getIfAvailable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
-28
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -46,7 +46,6 @@ import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.HandlerMappingIntrospectorRequestTransformer;
|
||||
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
|
||||
import org.springframework.security.web.debug.DebugFilter;
|
||||
import org.springframework.security.web.firewall.HttpFirewall;
|
||||
import org.springframework.security.web.firewall.RequestRejectedHandler;
|
||||
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
|
||||
@@ -135,34 +134,30 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
|
||||
return;
|
||||
}
|
||||
|
||||
String hmiRequestTransformerBeanName = HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + "RequestTransformer";
|
||||
if (!registry.containsBeanDefinition(hmiRequestTransformerBeanName)) {
|
||||
BeanDefinition hmiRequestTransformer = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(HandlerMappingIntrospectorRequestTransformer.class)
|
||||
.addConstructorArgReference(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)
|
||||
.getBeanDefinition();
|
||||
registry.registerBeanDefinition(hmiRequestTransformerBeanName, hmiRequestTransformer);
|
||||
}
|
||||
BeanDefinition hmiRequestTransformer = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(HandlerMappingIntrospectorRequestTransformer.class)
|
||||
.addConstructorArgReference(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)
|
||||
.getBeanDefinition();
|
||||
registry.registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + "RequestTransformer",
|
||||
hmiRequestTransformer);
|
||||
|
||||
BeanDefinition filterChainProxy = registry
|
||||
.getBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
|
||||
|
||||
if (!filterChainProxy.getResolvableType().isInstance(CompositeFilterChainProxy.class)) {
|
||||
BeanDefinitionBuilder hmiCacheFilterBldr = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(HandlerMappingIntrospectorCacheFilterFactoryBean.class)
|
||||
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
BeanDefinitionBuilder hmiCacheFilterBldr = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(HandlerMappingIntrospectorCachFilterFactoryBean.class)
|
||||
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
|
||||
ManagedList<BeanMetadataElement> filters = new ManagedList<>();
|
||||
filters.add(hmiCacheFilterBldr.getBeanDefinition());
|
||||
filters.add(filterChainProxy);
|
||||
BeanDefinitionBuilder compositeSpringSecurityFilterChainBldr = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(CompositeFilterChainProxy.class)
|
||||
.addConstructorArgValue(filters);
|
||||
ManagedList<BeanMetadataElement> filters = new ManagedList<>();
|
||||
filters.add(hmiCacheFilterBldr.getBeanDefinition());
|
||||
filters.add(filterChainProxy);
|
||||
BeanDefinitionBuilder compositeSpringSecurityFilterChainBldr = BeanDefinitionBuilder
|
||||
.rootBeanDefinition(CompositeFilterChainProxy.class)
|
||||
.addConstructorArgValue(filters);
|
||||
|
||||
registry.removeBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
|
||||
registry.registerBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME,
|
||||
compositeSpringSecurityFilterChainBldr.getBeanDefinition());
|
||||
}
|
||||
registry.removeBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
|
||||
registry.registerBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME,
|
||||
compositeSpringSecurityFilterChainBldr.getBeanDefinition());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -171,7 +166,7 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
|
||||
* {@link FactoryBean} to defer creation of
|
||||
* {@link HandlerMappingIntrospector#createCacheFilter()}
|
||||
*/
|
||||
static class HandlerMappingIntrospectorCacheFilterFactoryBean
|
||||
static class HandlerMappingIntrospectorCachFilterFactoryBean
|
||||
implements ApplicationContextAware, FactoryBean<Filter> {
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
@@ -303,9 +298,6 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
|
||||
if (filter instanceof FilterChainProxy fcp) {
|
||||
return fcp;
|
||||
}
|
||||
if (filter instanceof DebugFilter debugFilter) {
|
||||
return debugFilter.getFilterChainProxy();
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Couldn't find FilterChainProxy in " + filters);
|
||||
}
|
||||
|
||||
+12
-2
@@ -35,6 +35,7 @@ import org.springframework.security.authorization.SpringAuthorizationEventPublis
|
||||
import org.springframework.security.config.annotation.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.core.GrantedAuthorityDefaults;
|
||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
|
||||
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
|
||||
@@ -62,6 +63,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
|
||||
private final Supplier<RoleHierarchy> roleHierarchy;
|
||||
|
||||
private String rolePrefix = "ROLE_";
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
* @param context the {@link ApplicationContext} to use
|
||||
@@ -76,6 +79,11 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
}
|
||||
this.roleHierarchy = SingletonSupplier.of(() -> (context.getBeanNamesForType(RoleHierarchy.class).length > 0)
|
||||
? context.getBean(RoleHierarchy.class) : new NullRoleHierarchy());
|
||||
String[] grantedAuthorityDefaultsBeanNames = context.getBeanNamesForType(GrantedAuthorityDefaults.class);
|
||||
if (grantedAuthorityDefaultsBeanNames.length > 0) {
|
||||
GrantedAuthorityDefaults grantedAuthorityDefaults = context.getBean(GrantedAuthorityDefaults.class);
|
||||
this.rolePrefix = grantedAuthorityDefaults.getRolePrefix();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,7 +287,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
* customizations
|
||||
*/
|
||||
public AuthorizationManagerRequestMatcherRegistry hasRole(String role) {
|
||||
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasRole(role)));
|
||||
return access(withRoleHierarchy(AuthorityAuthorizationManager
|
||||
.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, new String[] { role })));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,7 +300,8 @@ public final class AuthorizeHttpRequestsConfigurer<H extends HttpSecurityBuilder
|
||||
* customizations
|
||||
*/
|
||||
public AuthorizationManagerRequestMatcherRegistry hasAnyRole(String... roles) {
|
||||
return access(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(roles)));
|
||||
return access(withRoleHierarchy(
|
||||
AuthorityAuthorizationManager.hasAnyRole(AuthorizeHttpRequestsConfigurer.this.rolePrefix, roles)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-3
@@ -40,7 +40,6 @@ import org.springframework.security.web.csrf.CsrfLogoutHandler;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestHandler;
|
||||
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
|
||||
import org.springframework.security.web.session.InvalidSessionStrategy;
|
||||
@@ -83,7 +82,7 @@ import org.springframework.util.Assert;
|
||||
public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
extends AbstractHttpConfigurer<CsrfConfigurer<H>, H> {
|
||||
|
||||
private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());
|
||||
private CsrfTokenRepository csrfTokenRepository = new HttpSessionCsrfTokenRepository();
|
||||
|
||||
private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;
|
||||
|
||||
@@ -105,7 +104,7 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
|
||||
/**
|
||||
* Specify the {@link CsrfTokenRepository} to use. The default is an
|
||||
* {@link HttpSessionCsrfTokenRepository} wrapped by {@link LazyCsrfTokenRepository}.
|
||||
* {@link HttpSessionCsrfTokenRepository}.
|
||||
* @param csrfTokenRepository the {@link CsrfTokenRepository} to use
|
||||
* @return the {@link CsrfConfigurer} for further customizations
|
||||
*/
|
||||
|
||||
+1
-1
@@ -823,7 +823,7 @@ public class HeadersConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* replaced with "#". For example:
|
||||
*
|
||||
* <pre>
|
||||
* X-XSS-Protection: 1; mode=block
|
||||
* X-XSS-Protection: 1 ; mode=block
|
||||
* </pre>
|
||||
* @param headerValue the new header value
|
||||
* @since 5.8
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -335,6 +335,7 @@ public final class LogoutConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
LogoutFilter result = new LogoutFilter(getLogoutSuccessHandler(), handlers);
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result.setLogoutRequestMatcher(getLogoutRequestMatcher(http));
|
||||
result.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
|
||||
result = postProcess(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -296,7 +296,7 @@ public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
|
||||
* @param sessionAuthenticationStrategy
|
||||
* @return the {@link SessionManagementConfigurer} for further customizations
|
||||
*/
|
||||
SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
|
||||
public SessionManagementConfigurer<H> addSessionAuthenticationStrategy(
|
||||
SessionAuthenticationStrategy sessionAuthenticationStrategy) {
|
||||
this.sessionAuthenticationStrategies.add(sessionAuthenticationStrategy);
|
||||
return this;
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.config.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
|
||||
|
||||
final class DefaultOidcLogoutTokenValidatorFactory implements Function<ClientRegistration, OAuth2TokenValidator<Jwt>> {
|
||||
|
||||
@Override
|
||||
public OAuth2TokenValidator<Jwt> apply(ClientRegistration clientRegistration) {
|
||||
return new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
|
||||
new OidcBackChannelLogoutTokenValidator(clientRegistration));
|
||||
}
|
||||
|
||||
}
|
||||
+18
-1
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.security.config.annotation.web.configurers.oauth2.client;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
@@ -307,7 +309,22 @@ public final class OAuth2ClientConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
if (this.accessTokenResponseClient != null) {
|
||||
return this.accessTokenResponseClient;
|
||||
}
|
||||
return new DefaultAuthorizationCodeTokenResponseClient();
|
||||
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2AuthorizationCodeGrantRequest.class);
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> bean = getBeanOrNull(resolvableType);
|
||||
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getBeanOrNull(ResolvableType type) {
|
||||
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
|
||||
if (context != null) {
|
||||
String[] names = context.getBeanNamesForType(type);
|
||||
if (names.length == 1) {
|
||||
return (T) context.getBean(names[0]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
@@ -25,6 +25,8 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
|
||||
import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository;
|
||||
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
|
||||
@@ -112,4 +114,13 @@ final class OAuth2ClientConfigurerUtils {
|
||||
return (!authorizedClientServiceMap.isEmpty() ? authorizedClientServiceMap.values().iterator().next() : null);
|
||||
}
|
||||
|
||||
static <B extends HttpSecurityBuilder<B>> OidcSessionRegistry getOidcSessionRegistry(B builder) {
|
||||
OidcSessionRegistry sessionRegistry = builder.getSharedObject(OidcSessionRegistry.class);
|
||||
if (sessionRegistry == null) {
|
||||
sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
builder.setSharedObject(OidcSessionRegistry.class, sessionRegistry);
|
||||
}
|
||||
return sessionRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+152
-4
@@ -22,9 +22,18 @@ import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.GenericApplicationListenerAdapter;
|
||||
import org.springframework.context.event.SmartApplicationListener;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.config.Customizer;
|
||||
@@ -32,9 +41,14 @@ import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
|
||||
import org.springframework.security.context.DelegatingApplicationListener;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
|
||||
import org.springframework.security.core.session.AbstractSessionEvent;
|
||||
import org.springframework.security.core.session.SessionDestroyedEvent;
|
||||
import org.springframework.security.core.session.SessionIdChangedEvent;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken;
|
||||
@@ -42,6 +56,9 @@ import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationC
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
|
||||
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider;
|
||||
import org.springframework.security.oauth2.client.oidc.session.InMemoryOidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionInformation;
|
||||
import org.springframework.security.oauth2.client.oidc.session.OidcSessionRegistry;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
|
||||
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
|
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration;
|
||||
@@ -67,7 +84,10 @@ import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.RedirectStrategy;
|
||||
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
|
||||
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
|
||||
import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.savedrequest.RequestCache;
|
||||
import org.springframework.security.web.util.matcher.AndRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
@@ -124,6 +144,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
* <li>{@link DefaultLoginPageGeneratingFilter} - if {@link #loginPage(String)} is not
|
||||
* configured and {@code DefaultLoginPageGeneratingFilter} is available, then a default
|
||||
* login page will be made available</li>
|
||||
* <li>{@link OidcSessionRegistry}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Joe Grandja
|
||||
@@ -202,6 +223,18 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the registry for managing the OIDC client-provider session link
|
||||
* @param oidcSessionRegistry the {@link OidcSessionRegistry} to use
|
||||
* @return the {@link OAuth2LoginConfigurer} for further configuration
|
||||
* @since 6.2
|
||||
*/
|
||||
public OAuth2LoginConfigurer<B> oidcSessionRegistry(OidcSessionRegistry oidcSessionRegistry) {
|
||||
Assert.notNull(oidcSessionRegistry, "oidcSessionRegistry cannot be null");
|
||||
getBuilder().setSharedObject(OidcSessionRegistry.class, oidcSessionRegistry);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link AuthorizationEndpointConfig} for configuring the Authorization
|
||||
* Server's Authorization Endpoint.
|
||||
@@ -330,10 +363,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
super.init(http);
|
||||
}
|
||||
}
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = this.tokenEndpointConfig.accessTokenResponseClient;
|
||||
if (accessTokenResponseClient == null) {
|
||||
accessTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient = getAccessTokenResponseClient();
|
||||
OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService = getOAuth2UserService();
|
||||
OAuth2LoginAuthenticationProvider oauth2LoginAuthenticationProvider = new OAuth2LoginAuthenticationProvider(
|
||||
accessTokenResponseClient, oauth2UserService);
|
||||
@@ -400,6 +430,7 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
authenticationFilter
|
||||
.setAuthorizationRequestRepository(this.authorizationEndpointConfig.authorizationRequestRepository);
|
||||
}
|
||||
configureOidcSessionRegistry(http);
|
||||
super.configure(http);
|
||||
}
|
||||
|
||||
@@ -442,6 +473,16 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return (!grantedAuthoritiesMapperMap.isEmpty() ? grantedAuthoritiesMapperMap.values().iterator().next() : null);
|
||||
}
|
||||
|
||||
private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> getAccessTokenResponseClient() {
|
||||
if (this.tokenEndpointConfig.accessTokenResponseClient != null) {
|
||||
return this.tokenEndpointConfig.accessTokenResponseClient;
|
||||
}
|
||||
ResolvableType resolvableType = ResolvableType.forClassWithGenerics(OAuth2AccessTokenResponseClient.class,
|
||||
OAuth2AuthorizationCodeGrantRequest.class);
|
||||
OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> bean = getBeanOrNull(resolvableType);
|
||||
return (bean != null) ? bean : new DefaultAuthorizationCodeTokenResponseClient();
|
||||
}
|
||||
|
||||
private OAuth2UserService<OidcUserRequest, OidcUser> getOidcUserService() {
|
||||
if (this.userInfoEndpointConfig.oidcUserService != null) {
|
||||
return this.userInfoEndpointConfig.oidcUserService;
|
||||
@@ -540,6 +581,29 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
return AnyRequestMatcher.INSTANCE;
|
||||
}
|
||||
|
||||
private void configureOidcSessionRegistry(B http) {
|
||||
OidcSessionRegistry sessionRegistry = OAuth2ClientConfigurerUtils.getOidcSessionRegistry(http);
|
||||
SessionManagementConfigurer<B> sessionConfigurer = http.getConfigurer(SessionManagementConfigurer.class);
|
||||
if (sessionConfigurer != null) {
|
||||
OidcSessionRegistryAuthenticationStrategy sessionAuthenticationStrategy = new OidcSessionRegistryAuthenticationStrategy();
|
||||
sessionAuthenticationStrategy.setSessionRegistry(sessionRegistry);
|
||||
sessionConfigurer.addSessionAuthenticationStrategy(sessionAuthenticationStrategy);
|
||||
}
|
||||
OidcClientSessionEventListener listener = new OidcClientSessionEventListener();
|
||||
listener.setSessionRegistry(sessionRegistry);
|
||||
registerDelegateApplicationListener(listener);
|
||||
}
|
||||
|
||||
private void registerDelegateApplicationListener(ApplicationListener<?> delegate) {
|
||||
DelegatingApplicationListener delegating = getBeanOrNull(
|
||||
ResolvableType.forType(DelegatingApplicationListener.class));
|
||||
if (delegating == null) {
|
||||
return;
|
||||
}
|
||||
SmartApplicationListener smartListener = new GenericApplicationListenerAdapter(delegate);
|
||||
delegating.addListener(smartListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for the Authorization Server's Authorization Endpoint.
|
||||
*/
|
||||
@@ -787,4 +851,88 @@ public final class OAuth2LoginConfigurer<B extends HttpSecurityBuilder<B>>
|
||||
|
||||
}
|
||||
|
||||
private static final class OidcClientSessionEventListener implements ApplicationListener<AbstractSessionEvent> {
|
||||
|
||||
private final Log logger = LogFactory.getLog(OidcClientSessionEventListener.class);
|
||||
|
||||
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(AbstractSessionEvent event) {
|
||||
if (event instanceof SessionDestroyedEvent destroyed) {
|
||||
this.logger.debug("Received SessionDestroyedEvent");
|
||||
this.sessionRegistry.removeSessionInformation(destroyed.getId());
|
||||
return;
|
||||
}
|
||||
if (event instanceof SessionIdChangedEvent changed) {
|
||||
this.logger.debug("Received SessionIdChangedEvent");
|
||||
OidcSessionInformation information = this.sessionRegistry
|
||||
.removeSessionInformation(changed.getOldSessionId());
|
||||
if (information == null) {
|
||||
this.logger
|
||||
.debug("Failed to register new session id since old session id was not found in registry");
|
||||
return;
|
||||
}
|
||||
this.sessionRegistry.saveSessionInformation(information.withSessionId(changed.getNewSessionId()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The registry where OIDC Provider sessions are linked to the Client session.
|
||||
* Defaults to in-memory storage.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class OidcSessionRegistryAuthenticationStrategy implements SessionAuthenticationStrategy {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private OidcSessionRegistry sessionRegistry = new InMemoryOidcSessionRegistry();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onAuthentication(Authentication authentication, HttpServletRequest request,
|
||||
HttpServletResponse response) throws SessionAuthenticationException {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
if (!(authentication.getPrincipal() instanceof OidcUser user)) {
|
||||
return;
|
||||
}
|
||||
String sessionId = session.getId();
|
||||
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
Map<String, String> headers = (csrfToken != null) ? Map.of(csrfToken.getHeaderName(), csrfToken.getToken())
|
||||
: Collections.emptyMap();
|
||||
OidcSessionInformation registration = new OidcSessionInformation(sessionId, headers, user);
|
||||
if (this.logger.isTraceEnabled()) {
|
||||
this.logger
|
||||
.trace(String.format("Linking a provider [%s] session to this client's session", user.getIssuer()));
|
||||
}
|
||||
this.sessionRegistry.saveSessionInformation(registration);
|
||||
}
|
||||
|
||||
/**
|
||||
* The registration for linking OIDC Provider Session information to the Client's
|
||||
* session. Defaults to in-memory storage.
|
||||
* @param sessionRegistry the {@link OidcSessionRegistry} to use
|
||||
*/
|
||||
void setSessionRegistry(OidcSessionRegistry sessionRegistry) {
|
||||
Assert.notNull(sessionRegistry, "sessionRegistry cannot be null");
|
||||
this.sessionRegistry = sessionRegistry;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user