1
0
mirror of synced 2026-07-10 13:20:02 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Rob Winch ccb5f916e0 rm .github/workflows for unsupported branch 2025-05-02 12:35:23 -05:00
1939 changed files with 21193 additions and 157030 deletions
-1
View File
@@ -15,4 +15,3 @@
*.otg binary
*.png binary
*.hsx binary
*.serialized binary
-40
View File
@@ -1,40 +0,0 @@
version: 2
registries:
spring-milestones:
type: maven-repository
url: https://repo.spring.io/milestone
updates:
- package-ecosystem: "gradle"
target-branch: "main"
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" ]
# GitHub Actions
- package-ecosystem: github-actions
target-branch: "main"
directory: "/"
schedule:
interval: weekly
ignore:
- dependency-name: "spring-io/*"
- dependency-name: "spring-security-release-tools/*"
-110
View File
@@ -1,110 +0,0 @@
version: 2
registries:
spring-milestones:
type: maven-repository
url: https://repo.spring.io/milestone
updates:
- package-ecosystem: gradle
target-branch: 6.3.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
- dependency-name: org.python:jython
- dependency-name: org.apache.directory.server:*
- 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: main
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
- dependency-name: org.python:jython
- dependency-name: org.apache.directory.server:*
- 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: com.gradle.enterprise
update-types:
- version-update:semver-major
- version-update:semver-minor
- dependency-name: '*'
update-types:
- version-update:semver-major
- package-ecosystem: github-actions
target-branch: 6.3.x
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
- package-ecosystem: github-actions
target-branch: main
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
- package-ecosystem: github-actions
target-branch: docs-build
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
- package-ecosystem: npm
target-branch: docs-build
directory: /
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
- package-ecosystem: npm
target-branch: main
directory: /docs
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
- package-ecosystem: npm
target-branch: 6.3.x
directory: /docs
schedule:
interval: weekly
labels:
- 'type: task'
- 'in: build'
@@ -1,23 +0,0 @@
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;
@@ -1,125 +0,0 @@
name: CI
on:
push:
branches-ignore:
- "dependabot/**"
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch: # Manual trigger
env:
DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
permissions:
contents: read
jobs:
build:
name: Build
uses: spring-io/spring-security-release-tools/.github/workflows/build.yml@v1
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
jdk: [ 17 ]
with:
runs-on: ${{ matrix.os }}
java-version: ${{ matrix.jdk }}
distribution: temurin
secrets: inherit
test:
name: Test Against Snapshots
uses: spring-io/spring-security-release-tools/.github/workflows/test.yml@v1
strategy:
matrix:
include:
- java-version: 21-ea
toolchain: 21
- java-version: 17
toolchain: 17
with:
java-version: ${{ matrix.java-version }}
test-args: --refresh-dependencies -PforceMavenRepositories=snapshot -PisOverrideVersionCatalog -PtestToolchain=${{ matrix.toolchain }} -PspringFrameworkVersion=6.2.+ -PreactorVersion=2023.0.+ -PspringDataVersion=2024.0.+ --stacktrace
secrets: inherit
check-samples:
name: Check Samples
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'spring-projects' }}
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
run: |
# Extract version from gradle.properties
version=$(cat gradle.properties | grep "version=" | awk -F'=' '{print $2}')
# Extract samplesBranch from gradle.properties
samples_branch=$(cat gradle.properties | grep "samplesBranch=" | awk -F'=' '{print $2}')
./gradlew publishMavenJavaPublicationToLocalRepository
./gradlew cloneRepository -PrepositoryName="spring-projects/spring-security-samples" -Pref="$samples_branch" -PcloneOutputDirectory="$SAMPLES_DIR"
./gradlew --refresh-dependencies --project-dir "$SAMPLES_DIR" --init-script spring-security-ci.gradle -PlocalRepositoryPath="$LOCAL_REPOSITORY_PATH" -PspringSecurityVersion="$version" test integrationTest
check-tangles:
name: Check for Package Tangles
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'spring-projects' }}
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
env:
STRUCTURE101_LICENSEID: ${{ secrets.STRUCTURE101_LICENSEID }}
run: |
./gradlew check s101 -Ps101.licenseId="$STRUCTURE101_LICENSEID" --stacktrace
deploy-artifacts:
name: Deploy Artifacts
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-artifacts.yml@v1
with:
should-deploy-artifacts: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-docs:
name: Deploy Docs
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-docs.yml@v1
with:
should-deploy-docs: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
deploy-schema:
name: Deploy Schema
needs: [ build, test, check-samples, check-tangles ]
uses: spring-io/spring-security-release-tools/.github/workflows/deploy-schema.yml@v1
with:
should-deploy-schema: ${{ needs.build.outputs.should-deploy-artifacts }}
secrets: inherit
perform-release:
name: Perform Release
needs: [ deploy-artifacts, deploy-docs, deploy-schema ]
uses: spring-io/spring-security-release-tools/.github/workflows/perform-release.yml@v1
with:
should-perform-release: ${{ needs.deploy-artifacts.outputs.artifacts-deployed }}
project-version: ${{ needs.deploy-artifacts.outputs.project-version }}
milestone-repo-url: https://repo.spring.io/artifactory/milestone
release-repo-url: https://repo1.maven.org/maven2
artifact-path: org/springframework/security/spring-security-core
slack-announcing-id: spring-security-announcing
secrets: inherit
send-notification:
name: Send Notification
needs: [ perform-release ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -1,57 +0,0 @@
name: Auto Merge Forward Dependabot Commits
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: dependabot-auto-merge-forward
jobs:
get-supported-branches:
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
with:
project: spring-security
type: oss
repository_name: spring-projects/spring-security
auto-merge-forward-dependabot:
name: Auto Merge Forward Dependabot Commits
runs-on: ubuntu-latest
needs: [get-supported-branches]
permissions:
contents: write
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
- name: Setup GitHub User
id: setup-gh-user
run: |
git config user.name 'github-actions[bot]'
git config user.email 'github-actions[bot]@users.noreply.github.com'
- name: Run Auto Merge Forward
id: run-auto-merge-forward
uses: spring-io/spring-security-release-tools/.github/actions/auto-merge-forward@actions-v1
with:
branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
from-author: dependabot[bot]
notify_result:
name: Check for failures
needs: [ auto-merge-forward-dependabot ]
if: failure()
runs-on: ubuntu-latest
permissions:
actions: read
steps:
- name: Send Slack message
uses: Gamesight/slack-workflow-status@v1.3.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
slack_webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }}
channel: '#spring-security-ci'
name: 'CI Notifier'
-33
View File
@@ -1,33 +0,0 @@
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)
@@ -1,32 +0,0 @@
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@v3
- name: Upgrade Wrappers
run: ./gradlew clean upgradeGradleWrapperAll --continue -Porg.gradle.java.installations.auto-download=false
env:
WRAPPER_UPGRADE_GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -1,45 +0,0 @@
name: Mark Duplicate Dependabot PRs
on:
pull_request:
types: [closed]
jobs:
check_duplicate_prs:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Extract Dependency Name from PR Title
id: extract
run: |
PR_TITLE="${{ github.event.pull_request.title }}"
DEPENDENCY_NAME=$(echo "$PR_TITLE" | awk -F ' from ' '{print $1}')
echo "dependency_name=$DEPENDENCY_NAME" >> $GITHUB_OUTPUT
- name: Find PRs
id: find_duplicates
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PRS=$(gh pr list --search 'milestone:${{ github.event.pull_request.milestone.title }} is:merged in:title "${{ steps.extract.outputs.dependency_name }}"' --json number --jq 'map(.number) | join(",")')
echo "prs=$PRS" >> $GITHUB_OUTPUT
- name: Label Duplicate PRs
if: steps.find_duplicates.outputs.prs != ''
env:
PRS: ${{ steps.find_duplicates.outputs.prs }}
CURRENT_PR_NUMBER: ${{ github.event.pull_request.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
for i in ${PRS//,/ }
do
if [ ! $i -eq "$CURRENT_PR_NUMBER" ]; then
echo "Marking PR $i as duplicate"
gh pr edit "$i" --add-label "status: duplicate"
gh pr comment "$i" --body "Duplicate of #$CURRENT_PR_NUMBER"
fi
done
-63
View File
@@ -1,63 +0,0 @@
name: Merge Dependabot PR
on: pull_request_target
run-name: Merge Dependabot PR ${{ github.ref_name }}
permissions: write-all
jobs:
merge-dependabot-pr:
name: Merge Dependabot PR
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
ref: ${{ github.event.pull_request.head.sha }}
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Set Milestone to Dependabot Pull Request
id: set-milestone
run: |
if test -f pom.xml
then
CURRENT_VERSION=$(mvn help:evaluate -Dexpression="project.version" -q -DforceStdout)
else
CURRENT_VERSION=$(cat gradle.properties | sed -n '/^version=/ { s/^version=//;p }')
fi
export CANDIDATE_VERSION=${CURRENT_VERSION/-SNAPSHOT}
MILESTONE=$(gh api repos/$GITHUB_REPOSITORY/milestones --jq 'map(select(.due_on != null and (.title | startswith(env.CANDIDATE_VERSION)))) | .[0] | .title')
if [ -z $MILESTONE ]
then
gh run cancel ${{ github.run_id }}
echo "::warning title=Cannot merge::No scheduled milestone for $CURRENT_VERSION version"
else
gh pr edit ${{ github.event.pull_request.number }} --milestone $MILESTONE
echo mergeEnabled=true >> $GITHUB_OUTPUT
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Merge Dependabot pull request
if: steps.set-milestone.outputs.mergeEnabled
run: gh pr merge ${{ github.event.pull_request.number }} --auto --rebase
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
send-notification:
name: Send Notification
needs: [ merge-dependabot-pr ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
@@ -1,35 +0,0 @@
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
send-notification:
name: Send Notification
needs: [ spring-releasetrain-checks ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
-51
View File
@@ -1,51 +0,0 @@
name: PR Build
on: pull_request
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 -PskipCheckExpectedBranchVersion --continue --scan
generate-docs:
name: Generate Docs
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: Run Antora
run: ./gradlew -PbuildSrc.skipTests=true :spring-security-docs:antora
- name: Upload Docs
id: upload
uses: actions/upload-artifact@v4
with:
name: docs
path: docs/build/site
overwrite: true
send-notification:
name: Send Notification
needs: [ build, generate-docs ]
if: ${{ failure() && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
-24
View File
@@ -1,24 +0,0 @@
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.3.x, 6.2.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 }}
@@ -1,22 +0,0 @@
name: Trigger Dependabot Auto Merge Forward
on:
push:
branches:
- '*.x'
permissions: read-all
jobs:
trigger-worflow:
name: Trigger Workflow
runs-on: ubuntu-latest
if: ${{ github.event.commits[0].author.username == 'dependabot[bot]' && github.repository == 'spring-projects/spring-security' }}
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- id: trigger
env:
GH_TOKEN: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
run: gh workflow run dependabot-auto-merge-forward.yml -r main
@@ -1,35 +0,0 @@
name: Update Antora UI Spring
on:
schedule:
- cron: '0 10 * * *' # Once per day at 10am UTC
workflow_dispatch:
permissions:
pull-requests: write
issues: write
contents: write
jobs:
update-antora-ui-spring:
runs-on: ubuntu-latest
name: Update on Supported Branches
strategy:
matrix:
branch: [ '5.8.x', '6.2.x', '6.3.x', 'main' ]
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@852920ba3fb1f28b35a2f13201133bc00ef33677
name: Update
with:
docs-branch: ${{ matrix.branch }}
token: ${{ secrets.GITHUB_TOKEN }}
antora-file-path: 'docs/antora-playbook.yml'
update-antora-ui-spring-docs-build:
runs-on: ubuntu-latest
name: Update on docs-build
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@852920ba3fb1f28b35a2f13201133bc00ef33677
name: Update
with:
docs-branch: 'docs-build'
token: ${{ secrets.GITHUB_TOKEN }}
-36
View File
@@ -1,36 +0,0 @@
name: Update dependabot.yml
on:
workflow_dispatch:
permissions:
contents: read
jobs:
get-supported-branches:
uses: spring-io/spring-security-release-tools/.github/workflows/retrieve-spring-supported-versions.yml@actions-v1
with:
project: spring-security
type: oss
repository_name: spring-projects/spring-security
main:
runs-on: ubuntu-latest
needs: [get-supported-branches]
if: ${{ (github.repository == 'spring-projects/spring-security') && (github.ref == 'refs/heads/main') }}
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: spring-io/spring-security-release-tools/.github/actions/generate-dependabot-yml@actions-v1
name: Update dependabot.yml
with:
gradle-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main
github-actions-branches: ${{ needs.get-supported-branches.outputs.supported_versions }},main,docs-build
gh-token: ${{ secrets.GITHUB_TOKEN }}
- uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: Update dependabot.yml
@@ -1,23 +0,0 @@
name: Update Scheduled Release Version
on:
workflow_dispatch: # Manual trigger only. Triggered by release-scheduler.yml on main.
permissions:
contents: read
jobs:
update-scheduled-release-version:
name: Update Scheduled Release Version
uses: spring-io/spring-security-release-tools/.github/workflows/update-scheduled-release-version.yml@v1
secrets: inherit
send-notification:
name: Send Notification
needs: [ update-scheduled-release-version ]
if: ${{ failure() || cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Send Notification
uses: spring-io/spring-security-release-tools/.github/actions/send-notification@v1
with:
webhook-url: ${{ secrets.SPRING_SECURITY_CI_GCHAT_WEBHOOK_URL }}
-2
View File
@@ -28,5 +28,3 @@ s101plugin.state
!.idea/checkstyle-idea.xml
!.idea/externalDependencies.xml
node_modules
+196 -118
View File
@@ -1,154 +1,232 @@
= Contributing to Spring Security
_Have something you'd like to contribute to the framework? We welcome pull requests, but ask that you carefully read this document first to understand how best to submit them; what kind of changes are likely to be accepted; and what to expect from the Spring Security team when evaluating your submission._
First off, thank you for taking the time to contribute! :+1: :tada:
_Please refer back to this document as a checklist before issuing any pull request; this will save time for everyone!_
== Table of Contents
= Code of Conduct
* <<code-of-conduct>>
* <<how-to-contribute>>
* <<ask-questions>>
* <<find-an-issue>>
* <<create-an-issue>>
* <<issue-lifecycle>>
* <<submit-a-pull-request>>
* <<build-from-source>>
* <<code-style>>
Please see our https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[code of conduct].
[[code-of-conduct]]
== Code of Conduct
= Similar but different
This project is governed by the https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[Spring code of conduct].
By participating you are expected to uphold this code.
Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
Each Spring module is slightly different from one another in terms of team size, number of issues, etc. Therefore, each project is managed slightly different. You will notice that this document is very similar to the https://github.com/spring-projects/spring-framework/wiki/Contributor-guidelines[Spring Framework Contributor guidelines]. However, there are some subtle differences between the two documents, so please be sure to read this document thoroughly.
[[how-to-contribute]]
== How to Contribute
= Importing into IDE
[[ask-questions]]
=== Ask Questions
The following provides information on setting up a development environment that can run the sample in https://www.springsource.org/sts[Spring Tool Suite 3.6.0+]. Other IDE's should work using Gradle's IDE support, but have not been tested.
If you have a question, check Stack Overflow using
https://stackoverflow.com/questions/tagged/spring-security+or+spring-ldap+or+spring-authorization-server+or+spring-session?tab=Newest[this list of tags].
Find an existing discussion, or start a new one if necessary.
* IDE Setup
** Install Spring Tool Suite 3.6.0+
** You will need the following plugins installed (can be found on the Extensions Page)
*** Gradle Eclipse
*** Groovy Eclipse
* Importing the project into Spring Tool Suite
** File -> Import… -> Gradle Project
If you believe there is an issue, search through https://github.com/spring-projects/spring-security/issues[existing issues] trying a few different ways to find discussions, past or current, that are related to the issue.
Reading those discussions helps you to learn about the issue, and helps us to make a decision.
As of new versions of Spring Tool Suite, you might need to install Groovy Eclipse pointing directly to the updated plugin location. To install Groovy Eclipse on Spring Tool Suite based on Eclipse Oxigen you must do the following steps:
[[find-an-issue]]
=== Find an Existing Issue
Help -> Install New Software… -> Add the following URL into _Work with_ field:
https://dist.springsource.org/snapshot/GRECLIPSE/e4.7/[https://dist.springsource.org/snapshot/GRECLIPSE/e4.7/]
There are many issues in Spring Security with the labels https://github.com/spring-projects/spring-security/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+ideal-for-contribution%22[`ideal-for-contribution`] or https://github.com/spring-projects/spring-security/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+first-timers-only%22[`first-timers-only`] that are a great way to contribute to a discussion or <<submit-a-pull-request,to a PR>>.
You can volunteer by commenting on these tickets, and we will assign them to you.
= Understand the basics
[[create-an-issue]]
=== Create an Issue
Not sure what a pull request is, or how to submit one? Take a look at GitHub's excellent https://help.github.com/articles/using-pull-requests[help documentation first].
Reporting an issue or making a feature request is a great way to contribute.
Your feedback and the conversations that result from it provide a continuous flow of ideas.
However, before creating a ticket, please take the time to <<ask-questions,ask and research>> first.
= Search GitHub issues; create an issue if necessary
If you create an issue after a discussion on Stack Overflow, please provide a description in the issue instead of simply referring to Stack Overflow.
The issue tracker is an important place of record for design discussions and should be self-sufficient.
Is there already an issue that addresses your concern? Do a bit of searching in our https://github.com/spring-projects/spring-security/issues[GitHub issues] to see if you can find something similar. If not, please create a new issue before submitting a pull request unless the change is not a user facing issue.
Once you're ready, create an issue on https://github.com/spring-projects/spring-security/issues[GitHub].
= Discuss non-trivial contribution ideas with committers
Many issues are caused by subtle behavior, typos, and unintended configuration.
Creating a https://stackoverflow.com/help/minimal-reproducible-example[Minimal Reproducible Example] (starting with https://start.spring.io for example) of the problem helps the team quickly triage your issue and get to the core of the problem.
If you're considering anything more than correcting a typo or fixing a minor bug, please discuss it on the https://gitter.im/spring-projects/spring-security[Spring Security Gitter] before submitting a pull request. We're happy to provide guidance but please spend an hour or two researching the subject on your own including searching the forums for prior discussions.
We love contributors, and we may ask you to <<submit-a-pull-request,submit a PR with a fix>>.
= Sign the Contributor License Agreement
[[issue-lifecycle]]
=== Issue Lifecycle
If you have not previously done so, please fill out and submit the https://cla.pivotal.io/sign/spring[Contributor License Agreement].
When an issue is first created, it is flagged `waiting-for-triage` waiting for a team member to triage it.
Once the issue has been reviewed, the team may ask for further information if needed, and based on the findings, the issue is either assigned a target branch (or no branch if a feature) or is closed with a specific status.
The target branch is https://spring.io/projects/spring-security#support[the earliest supported branch] where <<choose-a-branch,the change will be applied>>.
= Create your branch from oldest maintenance branch
When a fix is ready, the issue is closed and may still be re-opened until the fix is released.
After that the issue will typically no longer be reopened.
In rare cases if the issue was not at all fixed, the issue may be re-opened.
In most cases however any follow-up reports will need to be created as new issues with a fresh description.
Create your topic branch to be submitted as a pull request from the oldest impacted and supported maintenance branch.
You can find the supported versions by looking at the https://github.com/spring-projects/spring-security/milestones[milestones page].
Switch to a branch named `<major>.<minor>.x` from the smallest milestone in the format of `<major>.<minor>.<patch>(-<prerelease>)`.
The spring team will ensure the code gets merged forward into additional branches.
[[build-from-source]]
=== Build from Source
= Use short branch names
See https://github.com/spring-projects/spring-security/tree/main#building-from-source[Build from Source] for instructions on how to check out, build, and import the Spring Security source code into your IDE.
Branches used when submitting pull requests should preferably be named according to GitHub issues, e.g. `gh-1234` or `gh-1234-fix-npe`. Otherwise, use succinct, lower-case, dash (`-`) delimited names, such as `fix-warnings` or `fix-typo`. This is important, because branch names show up in the merge commits that result from accepting pull requests, and should be as expressive and concise as possible.
[[code-style]]
=== Source Code Style
= Keep commits focused
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.
Remember each ticket should be focused on a single item of interest since the tickets are used to produce the changelog. Since each commit should be tied to a single GitHub issue, ensure that your commits are focused. For example, do not include an update to a transitive library in your commit unless the GitHub is to update the library. Reviewing your commits is essential before sending a pull request.
To format the code as well as check the style, run `./gradlew format check`.
= Mind the whitespace
[[submit-a-pull-request]]
=== Submit a Pull Request
Please carefully follow the whitespace and formatting conventions already present in the framework.
We are excited for your pull request! :heart:
. Tabs, not spaces
. Unix (LF), not dos (CRLF) line endings
. Eliminate all trailing whitespace
. Aim to wrap code at 120 characters, but favor readability over wrapping
. Preserve existing formatting; i.e. do not reformat code for its own sake
. Search the codebase using `git grep` and other tools to discover common naming conventions, etc.
. UTF-8 encoding for Java sources and XML files
Please do your best to follow these steps.
Don't worry if you don't get them all correct the first time, we will help you.
Whitespace management tips
. You can use the https://marketplace.eclipse.org/content/anyedit-tools[AnyEdit Eclipse plugin] to ensure spaces are used and to clean up trailing whitespaces.
. Use Git's `pre-commit.sample` hook to prevent invalid whitespace from being pushed out. You can enable it by moving `.git/hooks/pre-commit.sample` to `.git/hooks/pre-commit` and ensuring it is executable. For more information on hooks refer to https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks[https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks].
= Add Apache license header to all new classes
[[sign-cla]]
1. If you have not previously done so, please sign the https://cla.spring.io/sign/spring[Contributor License Agreement].
You will be reminded automatically when you submit the PR.
[[create-an-issue]]
1. Must you https://github.com/spring-projects/spring-security/issues/new/choose[create an issue] first? No, but it is recommended for features and larger bug fixes. It's easier discuss with the team first to determine the right fix or enhancement.
For typos and straightforward bug fixes, starting with a pull request is encouraged.
Please include a description for context and motivation.
Note that the team may close your pull request if it's not a fit for the project.
[[choose-a-branch]]
1. Always check out the branch indicated in the milestone and submit pull requests against it (for example, for milestone `5.8.3` use the `5.8.x` branch).
If there is no milestone, choose `main`.
Once merged, the fix will be forwarded-ported to applicable branches including `main`.
[[create-a-local-branch]]
1. Create a local branch
If this is for an issue, consider a branch name with the issue number, like `gh-22276`.
[[write-tests]]
1. Add documentation and JUnit Tests for your changes.
[[update-copyright]]
1. In all files you edited, if the copyright header is of the form 2002-20xx, update the final copyright year to the current year.
[[add-since]]
1. If on `main`, add `@since` JavaDoc attributes to new public APIs that your PR adds
[[change-rnc]]
1. If you are updating the XSD, please instead update the RNC file and then run `./gradlew :spring-security-config:rncToXsd`.
[[format-code]]
1. For each commit, build the code using `./gradlew format check`.
This command ensures the code meets most of <<code-style,the style guide>>; a notable exception is import order.
[[commit-atomically]]
1. Choose the granularity of your commits consciously and squash commits that represent
multiple edits or corrections of the same logical change.
See https://git-scm.com/book/en/Git-Tools-Rewriting-History[Rewriting History section of Pro Git] for an overview of streamlining the commit history.
[[format-commit-messages]]
1. Format commit messages using 55 characters for the subject line, 72 characters per line
for the description, followed by the issue fixed, for example, `Closes gh-22276`.
See the https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines[Commit Guidelines section of Pro Git] for best practices around commit messages, and use `git log` to see some examples.
Present tense is preferred.
+
[indent=0]
----
Address NullPointerException
/*
* 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.
* 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.
*/
Closes gh-22276
----
[[reference-issue]]
1. If there is a prior issue, reference the GitHub issue number in the description of the pull request.
+
[indent=0]
----
Closes gh-22276
package ...;
----
If accepted, your contribution may be heavily modified as needed prior to merging.
You will likely retain author attribution for your Git commits granted that the bulk of your changes remain intact.
You may also be asked to rework the submission.
= Update Apache license header to modified files as necessary
If asked to make corrections, simply push the changes against the same branch, and your pull request will be updated.
In other words, you do not need to create a new pull request when asked to make changes.
When it is time to merge, you'll be asked to squash your commits.
Always check the date range in the license header. For example, if you've modified a file in 2020 whose header still reads
==== Participate in Reviews
----
* Copyright 2002-2012 the original author or authors.
----
Helping to review pull requests is another great way to contribute.
Your feedback can help to shape the implementation of new features.
When reviewing pull requests, however, please refrain from approving or rejecting a PR unless you are a core committer for Spring Security.
then be sure to update it to the current year appropriately (e.g. 2020)
----
* Copyright 2002-2020 the original author or authors.
----
= Use @since tags for newly-added public API types and methods
Example:
----
/**
* …
*
* @author First Last
* @since 5.4
* @see …
*/
----
= Submit JUnit test cases for all behavior changes
Search the codebase to find related unit tests and add additional `@Test` methods within.
. Any new tests should end in the name `Tests` (note this is plural). For example, a valid name would be `FilterChainProxyTests`. An invalid name would be `FilterChainProxyTest`.
. New test methods should not start with test. This is an old JUnit3 convention and is not necessary since the method is annotated with `@Test`.
= Update spring-security-x.y.rnc for schema changes
Update the https://www.relaxng.org[RELAX NG] schema `spring-security-x.y.rnc` instead of `spring-security-x.y.xsd` if you contribute changes to supported XML configuration. The XML schema file can be generated the following Gradle task:
----
./gradlew :spring-security-config:rncToXsd
----
Changes to the XML schema will be overwritten by the Gradle build task.
= Squash commits
Use `git rebase --interactive`, `git add --patch` and other tools to "squash" multiple commits into atomic changes. In addition to the man pages for `git`, there are https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History[many resources online] to help you understand how these tools work.
= Use real name in git commits
Please configure Git to use your real first and last name for any commits you intend to submit as pull requests. Make sure the name is properly capitalized as submitted to the https://cla.pivotal.io[Pivotal Contributor License Agreement]:
----
First Last <user@mail.com>
----
This helps ensure traceability against the CLA, and also goes a long way to ensuring useful output from tools like Git shortlog and others.
You can configure this globally:
----
git config --global user.name "First Last"
git config --global user.email user@example.com
----
or locally for the current repository by omitting the `--global` flag:
----
git config user.name "First Last"
git config user.email user@example.com
----
= Format commit messages
. Keep the subject line to 50 characters or less if possible
. Do not end the subject line with a period
. In the body of the commit message, explain how things worked before this commit, what has changed, and how things work now
. Include `Closes gh-<issue-number>` at the end if this fixes a GitHub issue
. Avoid markdown, including back-ticks identifying code
Example:
----
Short (50 chars or less) summary of changes
More detailed explanatory text, if necessary. Wrap it to about 72
characters or so. In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body. The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase can get confused if you run the
two together.
Further paragraphs come after blank lines.
- Bullet points are okay, too
- Typically a hyphen or asterisk is used for the bullet, preceded by a
single space, with blank lines in between, but conventions vary here
Closes gh-123
----
= Run all tests prior to submission
----
./gradlew clean build integrationTest
----
= Submit your pull request
*Subject line:*
Follow the same conventions for pull request subject lines as mentioned above for commit message subject lines.
*In the body:*
. Explain your use case. What led you to submit this change? Why were existing mechanisms in the framework insufficient? Make a case that this is a general-purpose problem and that yours is a general-purpose solution, etc
. Add any additional information and ask questions; start a conversation, or continue one from GitHub Issues
. Mention any GitHub Issues
. Also mention that you have submitted the CLA as described above
Note that for pull requests containing a single commit, GitHub will default the subject line and body of the pull request to match the subject line and body of the commit message. This is fine, but please also include the items above in the body of the request.
= Mention your pull request on the associated GitHub issue
Add a comment to the associated GitHub issue(s) linking to your new pull request.
= Expect discussion and rework
The Spring team takes a very conservative approach to accepting contributions to the framework. This is to keep code quality and stability as high as possible, and to keep complexity at a minimum. Your changes, if accepted, may be heavily modified prior to merging. You will retain "Author:" attribution for your Git commits granted that the bulk of your changes remain intact. You may be asked to rework the submission for style (as explained above) and/or substance. Again, we strongly recommend discussing any serious submissions with the Spring Framework team prior to engaging in serious development work.
Note that you can always force push (`git push -f`) reworked / rebased commits against the branch used to submit your pull request. i.e. you do not need to issue a new pull request when asked to make changes.
+5 -7
View File
@@ -1,8 +1,8 @@
image::https://badges.gitter.im/Join%20Chat.svg[Gitter,link=https://gitter.im/spring-projects/spring-security?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge]
image:https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions/workflows/continuous-integration-workflow.yml"]
image:https://github.com/spring-projects/spring-security/workflows/CI/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-security/actions?query=workflow%3ACI"]
image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?search.rootProjectNames=spring-security"]
= Spring Security
@@ -15,16 +15,14 @@ For a detailed list of features and access to the latest release, please visit h
Please see our https://github.com/spring-projects/.github/blob/main/CODE_OF_CONDUCT.md[code of conduct]
== Downloading Artifacts
See https://docs.spring.io/spring-security/reference/getting-spring-security.html[Getting Spring Security] for how to obtain Spring Security.
See https://docs.spring.io/spring-security/site/docs/current/reference/html5/#getting[Getting Spring Security] for how to obtain Spring Security.
== Documentation
Be sure to read the https://docs.spring.io/spring-security/reference/[Spring Security Reference].
Be sure to read the https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/[Spring Security Reference].
Extensive JavaDoc for the Spring Security code is also available in the https://docs.spring.io/spring-security/site/docs/current/api/[Spring Security API Documentation].
You may also want to check out https://docs.spring.io/spring-security/reference/whats-new.html[what's new in the latest release].
== Quick Start
See https://docs.spring.io/spring-security/reference/servlet/getting-started.html[Hello Spring Security] to get started with a "Hello, World" application.
See https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-hello[Hello Spring Security] to get started with a "Hello, World" application.
== Building from Source
Spring Security uses a https://gradle.org[Gradle]-based build system.
+218 -205
View File
@@ -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)
* `5.8.x`
* `5.7.x`
* `5.6.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 dont make sense, update `build.gradle` to ensure that the version is excluded.
Run all the checks:
[source,bash]
----
$ ./gradlew check
----
If they dont 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 its 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,7 +118,8 @@ public class AclPermissionEvaluator implements PermissionEvaluator {
if (permission instanceof Permission[]) {
return Arrays.asList((Permission[]) permission);
}
if (permission instanceof String permString) {
if (permission instanceof String) {
String permString = (String) permission;
Permission p = buildPermission(permString);
if (p != null) {
return Arrays.asList(p);
@@ -56,9 +56,10 @@ public abstract class AbstractPermission implements Permission {
if (obj == null) {
return false;
}
if (!(obj instanceof Permission other)) {
if (!(obj instanceof Permission)) {
return false;
}
Permission other = (Permission) obj;
return (this.mask == other.getMask());
}
@@ -17,13 +17,10 @@
package org.springframework.security.acls.domain;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.acls.model.SidRetrievalStrategy;
@@ -62,8 +59,6 @@ public class AclAuthorizationStrategyImpl implements AclAuthorizationStrategy {
private SidRetrievalStrategy sidRetrievalStrategy = new SidRetrievalStrategyImpl();
private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
/**
* Constructor. The only mandatory parameter relates to the system-wide
* {@link GrantedAuthority} instances that can be held to always permit ACL changes.
@@ -105,9 +100,7 @@ public class AclAuthorizationStrategyImpl implements AclAuthorizationStrategy {
}
// Iterate this principal's authorities to determine right
Collection<? extends GrantedAuthority> reachableGrantedAuthorities = this.roleHierarchy
.getReachableGrantedAuthorities(authentication.getAuthorities());
Set<String> authorities = AuthorityUtils.authorityListToSet(reachableGrantedAuthorities);
Set<String> authorities = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
if (acl.getOwner() instanceof GrantedAuthoritySid
&& authorities.contains(((GrantedAuthoritySid) acl.getOwner()).getGrantedAuthority())) {
return;
@@ -169,14 +162,4 @@ public class AclAuthorizationStrategyImpl implements AclAuthorizationStrategy {
this.securityContextHolderStrategy = securityContextHolderStrategy;
}
/**
* Sets the {@link RoleHierarchy} to use. The default is to use a
* {@link NullRoleHierarchy}
* @since 6.4
*/
public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
Assert.notNull(roleHierarchy, "roleHierarchy cannot be null");
this.roleHierarchy = roleHierarchy;
}
}
@@ -202,7 +202,7 @@ public class AclImpl implements Acl, MutableAcl, AuditableAcl, OwnershipAcl {
public boolean isSidLoaded(List<Sid> sids) {
// If loadedSides is null, this indicates all SIDs were loaded
// Also return true if the caller didn't specify a SID to find
if ((this.loadedSids == null) || (sids == null) || sids.isEmpty()) {
if ((this.loadedSids == null) || (sids == null) || (sids.size() == 0)) {
return true;
}
@@ -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.
@@ -140,7 +140,7 @@ public class DefaultPermissionFactory implements PermissionFactory {
@Override
public List<Permission> buildFromNames(List<String> names) {
if ((names == null) || names.isEmpty()) {
if ((names == null) || (names.size() == 0)) {
return Collections.emptyList();
}
List<Permission> permissions = new ArrayList<>(names.size());
@@ -25,7 +25,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.acls.model.Acl;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
@@ -35,7 +34,6 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.context.SecurityContextImpl;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
@@ -88,15 +86,6 @@ public class AclAuthorizationStrategyImplTests {
this.strategy.securityCheck(this.acl, AclAuthorizationStrategy.CHANGE_GENERAL);
}
@Test
public void securityCheckWhenRoleReachableByHierarchyThenAuthorized() {
given(this.acl.getOwner()).willReturn(new GrantedAuthoritySid("ROLE_AUTH_B"));
this.strategy = new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_SYSTEM_ADMIN"));
this.strategy.setRoleHierarchy(RoleHierarchyImpl.fromHierarchy("ROLE_AUTH > ROLE_AUTH_B"));
assertThatNoException()
.isThrownBy(() -> this.strategy.securityCheck(this.acl, AclAuthorizationStrategy.CHANGE_GENERAL));
}
@Test
public void securityCheckWhenCustomSecurityContextHolderStrategyThenUses() {
given(this.securityContextHolderStrategy.getContext()).willReturn(this.context);
@@ -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).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(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).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(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).hasSize(2);
assertThat(objectIdentities.size()).isEqualTo(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).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(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).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(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).hasSize(1);
assertThat(objectIdentities.size()).isEqualTo(1);
assertThat(objectIdentities.get(0).getType()).isEqualTo("location");
assertThat(objectIdentities.get(0).getIdentifier()).isEqualTo("prefix:US-PAL");
}
@@ -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(Long.valueOf(1L)).isEqualTo(result);
assertThat(new Long(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").isEqualTo(principalSid.getPrincipal());
assertThat("johndoe".equals(principalSid.getPrincipal())).isTrue();
assertThat("scott".equals(principalSid.getPrincipal())).isFalse();
assertThat("ROLE_TEST").isEqualTo(gaSid.getGrantedAuthority());
assertThat("ROLE_TEST".equals(gaSid.getGrantedAuthority())).isTrue();
assertThat("ROLE_TEST2".equals(gaSid.getGrantedAuthority())).isFalse();
}
+6
View File
@@ -29,4 +29,10 @@ 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"
@@ -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");
assertThat(objects).contains("aubergine");
assertThat(objects.contains("apple")).isTrue();
assertThat(objects.contains("aubergine")).isTrue();
}
private void configureForElAnnotations() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -103,13 +103,6 @@ public class PostAuthorizeAspectTests {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
}
@Test
public void nestedDenyAllPostAuthorizeDeniesAccess() {
SecurityContextHolder.getContext().setAuthentication(this.anne);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
}
interface SecuredInterface {
@PostAuthorize("hasRole('X')")
@@ -141,10 +134,6 @@ public class PostAuthorizeAspectTests {
privateMethod();
}
NestedObject myObject() {
return new NestedObject();
}
}
static class SecuredImplSubclass extends SecuredImpl {
@@ -168,13 +157,4 @@ public class PostAuthorizeAspectTests {
}
static class NestedObject {
@PostAuthorize("denyAll")
void denyAllMethod() {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -54,11 +54,6 @@ public class PostFilterAspectTests {
assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine");
}
@Test
public void nestedDenyAllPostFilterDeniesAccess() {
assertThat(this.prePostSecured.myObject().denyAllMethod()).isEmpty();
}
static class PrePostSecured {
@PostFilter("filterObject.startsWith('a')")
@@ -66,19 +61,6 @@ public class PostFilterAspectTests {
return objects;
}
NestedObject myObject() {
return new NestedObject();
}
}
static class NestedObject {
@PostFilter("filterObject == null")
List<String> denyAllMethod() {
return new ArrayList<>(List.of("deny"));
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -47,8 +47,6 @@ public class PreAuthorizeAspectTests {
private PrePostSecured prePostSecured = new PrePostSecured();
private MultipleInterfaces multiple = new MultipleInterfaces();
@BeforeEach
public final void setUp() {
MockitoAnnotations.initMocks(this);
@@ -105,19 +103,6 @@ public class PreAuthorizeAspectTests {
assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod);
}
@Test
public void nestedDenyAllPreAuthorizeDeniesAccess() {
SecurityContextHolder.getContext().setAuthentication(this.anne);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.secured.myObject().denyAllMethod());
}
@Test
public void multipleInterfacesPreAuthorizeAllows() {
// aspectj doesn't inherit annotations
this.multiple.securedMethod();
}
interface SecuredInterface {
@PreAuthorize("hasRole('X')")
@@ -149,10 +134,6 @@ public class PreAuthorizeAspectTests {
privateMethod();
}
NestedObject myObject() {
return new NestedObject();
}
}
static class SecuredImplSubclass extends SecuredImpl {
@@ -176,28 +157,4 @@ public class PreAuthorizeAspectTests {
}
static class NestedObject {
@PreAuthorize("denyAll")
void denyAllMethod() {
}
}
interface AnotherSecuredInterface {
@PreAuthorize("hasRole('Y')")
void securedMethod();
}
static class MultipleInterfaces implements SecuredInterface, AnotherSecuredInterface {
@Override
public void securedMethod() {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
@@ -54,11 +54,6 @@ public class PreFilterAspectTests {
assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine");
}
@Test
public void nestedDenyAllPreFilterDeniesAccess() {
assertThat(this.prePostSecured.myObject().denyAllMethod(new ArrayList<>(List.of("deny")))).isEmpty();
}
static class PrePostSecured {
@PreFilter("filterObject.startsWith('a')")
@@ -66,19 +61,6 @@ public class PreFilterAspectTests {
return objects;
}
NestedObject myObject() {
return new NestedObject();
}
}
static class NestedObject {
@PreFilter("filterObject == null")
List<String> denyAllMethod(List<String> list) {
return list;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 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.
+62 -39
View File
@@ -14,18 +14,17 @@ 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: 'org.springframework.security.check-expected-branch-version'
apply plugin: 'io.spring.security.release'
group = 'org.springframework.security'
description = 'Spring Security'
@@ -36,42 +35,70 @@ 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
}
def toolchainVersion() {
if (project.hasProperty('testToolchain')) {
return project.property('testToolchain').toString().toInteger()
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"
}
return 17
}
subprojects {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(toolchainVersion())
plugins.withType(JavaPlugin) {
java {
sourceCompatibility=JavaVersion.VERSION_17
}
}
kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks.withType(JavaCompile).configureEach {
tasks.withType(JavaCompile) {
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'
@@ -96,17 +123,23 @@ 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'
}
}
nohttp {
source.exclude "buildSrc/build/**", "javascript/.gradle/**", "javascript/package-lock.json", "javascript/node_modules/**", "javascript/build/**", "javascript/dist/**"
source.exclude "buildSrc/build/**"
source.builtBy(project(':spring-security-config').tasks.withType(RncToXsd))
}
@@ -118,15 +151,5 @@ 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.2.x' // runs only on 6.2.x and the update is merged forward to main
}
}
}
+25 -10
View File
@@ -11,7 +11,6 @@ java {
repositories {
gradlePluginPortal()
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
sourceSets {
@@ -27,6 +26,10 @@ sourceSets {
gradlePlugin {
plugins {
checkAntoraVersion {
id = "org.springframework.antora.check-version"
implementationClass = "org.springframework.gradle.antora.AntoraVersionPlugin"
}
trang {
id = "trang"
implementationClass = "trang.TrangPlugin"
@@ -39,6 +42,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"
@@ -47,10 +70,6 @@ gradlePlugin {
id = "org.springframework.security.versions.verify-dependencies-versions"
implementationClass = "org.springframework.security.convention.versions.VerifyDependenciesVersionsPlugin"
}
checkExpectedBranchVersion {
id = "org.springframework.security.check-expected-branch-version"
implementationClass = "org.springframework.security.CheckExpectedBranchVersionPlugin"
}
}
}
@@ -76,14 +95,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)
@@ -97,7 +113,6 @@ dependencies {
testImplementation libs.com.squareup.okhttp3.mockwebserver
}
tasks.named('test', Test).configure {
onlyIf { !project.hasProperty("buildSrc.skipTests") }
useJUnitPlatform()
@@ -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 {
@@ -63,7 +63,6 @@ class EclipsePlugin implements Plugin<Project> {
// test sources. Relax those from error to warning
properties['org.eclipse.jdt.core.circularClasspath'] = 'warning'
properties['org.eclipse.jdt.core.incompleteClasspath'] = 'warning'
properties['org.eclipse.jdt.core.compiler.codegen.methodParameters'] = 'generate'
}
}
}
@@ -0,0 +1,71 @@
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.Task;
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(new Action<Task>() {
@Override
public void execute(Task 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;
}
}
}
@@ -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);
}
}
@@ -35,7 +35,9 @@ 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) {
@@ -34,6 +34,8 @@ 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);
}
}
}
@@ -0,0 +1,103 @@
/*
* 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.InclusiveRepositoryContentDescriptor;
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(new Action<InclusiveRepositoryContentDescriptor>() {
@Override
public void execute(InclusiveRepositoryContentDescriptor descriptor) {
descriptor.includeGroup("spring-io");
}
});
}
});
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -0,0 +1,93 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.milestones;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import org.springframework.gradle.github.RepositoryRef;
public abstract class GitHubMilestoneNextReleaseTask extends DefaultTask {
@Input
private RepositoryRef repository = new RepositoryRef();
@Input
@Optional
private String gitHubAccessToken;
private GitHubMilestoneApi milestones = new GitHubMilestoneApi();
@TaskAction
public void calculateNextReleaseMilestone() throws IOException {
String currentVersion = getProject().getVersion().toString();
String nextPreRelease = this.milestones.getNextReleaseMilestone(this.repository, currentVersion);
System.out.println("The next release milestone is: " + nextPreRelease);
NextVersionYml nextVersionYml = new NextVersionYml();
nextVersionYml.setVersion(nextPreRelease);
File outputFile = getNextReleaseFile().get().getAsFile();
FileWriter outputWriter = new FileWriter(outputFile);
Yaml yaml = getYaml();
yaml.dump(nextVersionYml, outputWriter);
}
@OutputFile
public abstract RegularFileProperty getNextReleaseFile();
public RepositoryRef getRepository() {
return repository;
}
public void repository(Action<RepositoryRef> repository) {
repository.execute(this.repository);
}
public void setRepository(RepositoryRef repository) {
this.repository = repository;
}
public String getGitHubAccessToken() {
return gitHubAccessToken;
}
public void setGitHubAccessToken(String gitHubAccessToken) {
this.gitHubAccessToken = gitHubAccessToken;
this.milestones = new GitHubMilestoneApi(gitHubAccessToken);
}
private Yaml getYaml() {
Representer representer = new Representer();
representer.addClassTag(NextVersionYml.class, Tag.MAP);
DumperOptions ymlOptions = new DumperOptions();
ymlOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
return new Yaml(representer, ymlOptions);
}
}
@@ -0,0 +1,102 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.milestones;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.work.DisableCachingByDefault;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.springframework.gradle.github.RepositoryRef;
@DisableCachingByDefault(because = "the due date needs to be checked every time in case it changes")
public abstract class GitHubMilestoneNextVersionDueTodayTask extends DefaultTask {
@Input
private RepositoryRef repository = new RepositoryRef();
@Input
@Optional
private String gitHubAccessToken;
@InputFile
public abstract RegularFileProperty getNextVersionFile();
@OutputFile
public abstract RegularFileProperty getIsDueTodayFile();
private GitHubMilestoneApi milestones = new GitHubMilestoneApi();
@TaskAction
public void checkReleaseDueToday() throws IOException {
File nextVersionFile = getNextVersionFile().getAsFile().get();
Yaml yaml = new Yaml(new Constructor(NextVersionYml.class));
NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile));
String nextVersion = nextVersionYml.getVersion();
if (nextVersion == null) {
throw new IllegalArgumentException(
"Could not find version property in provided file " + nextVersionFile.getName());
}
boolean milestoneDueToday = this.milestones.isMilestoneDueToday(this.repository, nextVersion);
Path isDueTodayPath = getIsDueTodayFile().getAsFile().get().toPath();
Files.writeString(isDueTodayPath, String.valueOf(milestoneDueToday));
if (milestoneDueToday) {
System.out.println("The milestone with the title " + nextVersion + " in the repository " + this.repository
+ " is due today");
}
else {
System.out.println("The milestone with the title " + nextVersion + " in the repository "
+ this.repository + " is not due yet");
}
}
public RepositoryRef getRepository() {
return repository;
}
public void repository(Action<RepositoryRef> repository) {
repository.execute(this.repository);
}
public void setRepository(RepositoryRef repository) {
this.repository = repository;
}
public String getGitHubAccessToken() {
return gitHubAccessToken;
}
public void setGitHubAccessToken(String gitHubAccessToken) {
this.gitHubAccessToken = gitHubAccessToken;
this.milestones = new GitHubMilestoneApi(gitHubAccessToken);
}
}
@@ -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"));
});
}
}
@@ -0,0 +1,23 @@
package org.springframework.gradle.github.milestones;
import java.io.IOException;
import java.time.LocalDate;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* @author Steve Riesenberg
*/
class LocalDateAdapter extends TypeAdapter<LocalDate> {
@Override
public void write(JsonWriter jsonWriter, LocalDate localDate) throws IOException {
jsonWriter.value(localDate.toString());
}
@Override
public LocalDate read(JsonReader jsonReader) throws IOException {
return LocalDate.parse(jsonReader.nextString());
}
}
@@ -0,0 +1,25 @@
package org.springframework.gradle.github.milestones;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* @author Steve Riesenberg
*/
class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {
@Override
public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException {
jsonWriter.value(localDateTime.atOffset(ZoneOffset.UTC).format(DateTimeFormatter.ISO_ZONED_DATE_TIME));
}
@Override
public LocalDateTime read(JsonReader jsonReader) throws IOException {
return LocalDateTime.parse(jsonReader.nextString(), DateTimeFormatter.ISO_ZONED_DATE_TIME);
}
}
@@ -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 + '\'' +
'}';
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,16 +14,16 @@
* limitations under the License.
*/
package org.springframework.security.config.annotation.method.configuration;
package org.springframework.gradle.github.milestones;
public class MyMasker {
public class NextVersionYml {
private String version;
public String getMask(String value) {
return value + "-masked";
public String getVersion() {
return version;
}
public String getMask() {
return "mask";
public void setVersion(String version) {
this.version = version;
}
}
@@ -0,0 +1,147 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.milestones;
import java.time.LocalDate;
import java.time.LocalTime;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
import org.springframework.gradle.github.RepositoryRef;
/**
* @author Steve Riesenberg
*/
public class ScheduleNextReleaseTask extends DefaultTask {
@Input
private RepositoryRef repository = new RepositoryRef();
@Input
private String gitHubAccessToken;
@Input
private String version;
@Input
private Integer weekOfMonth;
@Input
private Integer dayOfWeek;
@TaskAction
public void scheduleNextRelease() {
GitHubMilestoneApi gitHubMilestoneApi = new GitHubMilestoneApi(this.gitHubAccessToken);
String nextReleaseMilestone = gitHubMilestoneApi.getNextReleaseMilestone(this.repository, this.version);
// If the next release contains a dash (e.g. 5.6.0-RC1), it is already scheduled
if (nextReleaseMilestone.contains("-")) {
return;
}
// Check to see if a scheduled GA version already exists
boolean hasExistingMilestone = gitHubMilestoneApi.getMilestones(this.repository).stream()
.anyMatch(milestone -> nextReleaseMilestone.equals(milestone.getTitle()));
if (hasExistingMilestone) {
return;
}
// Next milestone is either a patch version or minor version
// Note: Major versions will be handled like minor and get a release
// train which can be manually updated to match the desired schedule.
if (nextReleaseMilestone.endsWith(".0")) {
// Create M1, M2, M3, RC1 and GA milestones for release train
getReleaseTrain(nextReleaseMilestone).getTrainDates().forEach((milestoneTitle, dueOn) -> {
Milestone milestone = new Milestone();
milestone.setTitle(milestoneTitle);
// Note: GitHub seems to store full date/time as UTC then displays
// as a date (no time) in your timezone, which means the date will
// not always be the same date as we intend.
// Using 12pm/noon UTC allows GitHub to schedule and display the
// correct date.
milestone.setDueOn(dueOn.atTime(LocalTime.NOON));
gitHubMilestoneApi.createMilestone(this.repository, milestone);
});
} else {
// Create GA milestone for patch release on the next even month
LocalDate startDate = LocalDate.now();
LocalDate dueOn = getReleaseTrain(nextReleaseMilestone).getNextReleaseDate(startDate);
Milestone milestone = new Milestone();
milestone.setTitle(nextReleaseMilestone);
milestone.setDueOn(dueOn.atTime(LocalTime.NOON));
gitHubMilestoneApi.createMilestone(this.repository, milestone);
}
}
private SpringReleaseTrain getReleaseTrain(String nextReleaseMilestone) {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.nextTrain()
.version(nextReleaseMilestone)
.weekOfMonth(this.weekOfMonth)
.dayOfWeek(this.dayOfWeek)
.build();
return new SpringReleaseTrain(releaseTrainSpec);
}
public RepositoryRef getRepository() {
return this.repository;
}
public void repository(Action<RepositoryRef> repository) {
repository.execute(this.repository);
}
public void setRepository(RepositoryRef repository) {
this.repository = repository;
}
public String getGitHubAccessToken() {
return this.gitHubAccessToken;
}
public void setGitHubAccessToken(String gitHubAccessToken) {
this.gitHubAccessToken = gitHubAccessToken;
}
public String getVersion() {
return this.version;
}
public void setVersion(String version) {
this.version = version;
}
public Integer getWeekOfMonth() {
return weekOfMonth;
}
public void setWeekOfMonth(Integer weekOfMonth) {
this.weekOfMonth = weekOfMonth;
}
public Integer getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(Integer dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
}
@@ -0,0 +1,136 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.milestones;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.Month;
import java.time.Year;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Spring release train generator based on rules contained in a specification.
* <p>
* The rules are:
* <ol>
* <li>Train 1 (January-May) or 2 (July-November)</li>
* <li>Version number (e.g. 0.1.2, 1.0.0, etc.)</li>
* <li>Week of month (1st, 2nd, 3rd, 4th)</li>
* <li>Day of week (Monday-Friday)</li>
* <li>Year (e.g. 2020, 2021, etc.)</li>
* </ol>
*
* The release train generated will contain M1, M2, M3, RC1 and GA versions
* mapped to their respective dates in the train.
*
* @author Steve Riesenberg
*/
public final class SpringReleaseTrain {
private final SpringReleaseTrainSpec releaseTrainSpec;
public SpringReleaseTrain(SpringReleaseTrainSpec releaseTrainSpec) {
this.releaseTrainSpec = releaseTrainSpec;
}
/**
* Calculate release train dates based on the release train specification.
*
* @return A mapping of release milestones to scheduled release dates
*/
public Map<String, LocalDate> getTrainDates() {
Map<String, LocalDate> releaseDates = new LinkedHashMap<>();
switch (this.releaseTrainSpec.getTrain()) {
case ONE:
addTrainDate(releaseDates, "M1", Month.JANUARY);
addTrainDate(releaseDates, "M2", Month.FEBRUARY);
addTrainDate(releaseDates, "M3", Month.MARCH);
addTrainDate(releaseDates, "RC1", Month.APRIL);
addTrainDate(releaseDates, null, Month.MAY);
break;
case TWO:
addTrainDate(releaseDates, "M1", Month.JULY);
addTrainDate(releaseDates, "M2", Month.AUGUST);
addTrainDate(releaseDates, "M3", Month.SEPTEMBER);
addTrainDate(releaseDates, "RC1", Month.OCTOBER);
addTrainDate(releaseDates, null, Month.NOVEMBER);
break;
}
return releaseDates;
}
/**
* Determine if a given date matches the due date of given version.
*
* @param version The version number (e.g. 5.6.0-M1, 5.6.0, etc.)
* @param expectedDate The expected date
* @return true if the given date matches the due date of the given version, false otherwise
*/
public boolean isTrainDate(String version, LocalDate expectedDate) {
return expectedDate.isEqual(getTrainDates().get(version));
}
/**
* Calculate the next release date following the given date.
* <p>
* The next release date is always on an even month so that a patch release
* is the month after the GA version of a release train. This method does
* not consider the year of the release train, only the given start date.
*
* @param startDate The start date
* @return The next release date following the given date
*/
public LocalDate getNextReleaseDate(LocalDate startDate) {
LocalDate trainDate;
LocalDate currentDate = startDate;
do {
trainDate = calculateReleaseDate(
Year.of(currentDate.getYear()),
currentDate.getMonth(),
this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(),
this.releaseTrainSpec.getWeekOfMonth().getDayOffset()
);
currentDate = currentDate.plusMonths(1);
} while (!trainDate.isAfter(startDate) || trainDate.getMonthValue() % 2 != 0);
return trainDate;
}
private void addTrainDate(Map<String, LocalDate> releaseDates, String milestone, Month month) {
LocalDate releaseDate = calculateReleaseDate(
this.releaseTrainSpec.getYear(),
month,
this.releaseTrainSpec.getDayOfWeek().getDayOfWeek(),
this.releaseTrainSpec.getWeekOfMonth().getDayOffset()
);
String suffix = (milestone == null) ? "" : "-" + milestone;
releaseDates.put(this.releaseTrainSpec.getVersion() + suffix, releaseDate);
}
private static LocalDate calculateReleaseDate(Year year, Month month, DayOfWeek dayOfWeek, int dayOffset) {
TemporalAdjuster nextMonday = TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY);
TemporalAdjuster nextDayOfWeek = TemporalAdjusters.nextOrSame(dayOfWeek);
LocalDate firstDayOfMonth = year.atMonth(month).atDay(1);
LocalDate firstMondayOfMonth = firstDayOfMonth.with(nextMonday);
return firstMondayOfMonth.with(nextDayOfWeek).plusDays(dayOffset);
}
}
@@ -0,0 +1,205 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.milestones;
import java.time.LocalDate;
import java.time.Month;
import java.time.Year;
import org.springframework.util.Assert;
/**
* A specification for a release train.
*
* @author Steve Riesenberg
* @see SpringReleaseTrain
*/
public final class SpringReleaseTrainSpec {
private final Train train;
private final String version;
private final WeekOfMonth weekOfMonth;
private final DayOfWeek dayOfWeek;
private final Year year;
public SpringReleaseTrainSpec(Train train, String version, WeekOfMonth weekOfMonth, DayOfWeek dayOfWeek, Year year) {
this.train = train;
this.version = version;
this.weekOfMonth = weekOfMonth;
this.dayOfWeek = dayOfWeek;
this.year = year;
}
public Train getTrain() {
return train;
}
public String getVersion() {
return version;
}
public WeekOfMonth getWeekOfMonth() {
return weekOfMonth;
}
public DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
public Year getYear() {
return year;
}
public static Builder builder() {
return new Builder();
}
public enum WeekOfMonth {
FIRST(0), SECOND(7), THIRD(14), FOURTH(21);
private final int dayOffset;
WeekOfMonth(int dayOffset) {
this.dayOffset = dayOffset;
}
public int getDayOffset() {
return dayOffset;
}
}
public enum DayOfWeek {
MONDAY(java.time.DayOfWeek.MONDAY),
TUESDAY(java.time.DayOfWeek.TUESDAY),
WEDNESDAY(java.time.DayOfWeek.WEDNESDAY),
THURSDAY(java.time.DayOfWeek.THURSDAY),
FRIDAY(java.time.DayOfWeek.FRIDAY);
private final java.time.DayOfWeek dayOfWeek;
DayOfWeek(java.time.DayOfWeek dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public java.time.DayOfWeek getDayOfWeek() {
return dayOfWeek;
}
}
public enum Train {
ONE, TWO
}
public static class Builder {
private Train train;
private String version;
private WeekOfMonth weekOfMonth;
private DayOfWeek dayOfWeek;
private Year year;
private Builder() {
}
public Builder train(int train) {
switch (train) {
case 1: this.train = Train.ONE; break;
case 2: this.train = Train.TWO; break;
default: throw new IllegalArgumentException("Invalid train: " + train);
}
return this;
}
public Builder train(Train train) {
this.train = train;
return this;
}
public Builder nextTrain() {
// Search for next train starting with this month
return nextTrain(LocalDate.now().withDayOfMonth(1));
}
public Builder nextTrain(LocalDate startDate) {
Train nextTrain = null;
// Search for next train from a given start date
LocalDate currentDate = startDate;
while (nextTrain == null) {
if (currentDate.getMonth() == Month.JANUARY) {
nextTrain = Train.ONE;
} else if (currentDate.getMonth() == Month.JULY) {
nextTrain = Train.TWO;
}
currentDate = currentDate.plusMonths(1);
}
return train(nextTrain).year(currentDate.getYear());
}
public Builder version(String version) {
this.version = version;
return this;
}
public Builder weekOfMonth(int weekOfMonth) {
switch (weekOfMonth) {
case 1: this.weekOfMonth = WeekOfMonth.FIRST; break;
case 2: this.weekOfMonth = WeekOfMonth.SECOND; break;
case 3: this.weekOfMonth = WeekOfMonth.THIRD; break;
case 4: this.weekOfMonth = WeekOfMonth.FOURTH; break;
default: throw new IllegalArgumentException("Invalid weekOfMonth: " + weekOfMonth);
}
return this;
}
public Builder weekOfMonth(WeekOfMonth weekOfMonth) {
this.weekOfMonth = weekOfMonth;
return this;
}
public Builder dayOfWeek(int dayOfWeek) {
switch (dayOfWeek) {
case 1: this.dayOfWeek = DayOfWeek.MONDAY; break;
case 2: this.dayOfWeek = DayOfWeek.TUESDAY; break;
case 3: this.dayOfWeek = DayOfWeek.WEDNESDAY; break;
case 4: this.dayOfWeek = DayOfWeek.THURSDAY; break;
case 5: this.dayOfWeek = DayOfWeek.FRIDAY; break;
default: throw new IllegalArgumentException("Invalid dayOfWeek: " + dayOfWeek);
}
return this;
}
public Builder dayOfWeek(DayOfWeek dayOfWeek) {
this.dayOfWeek = dayOfWeek;
return this;
}
public Builder year(int year) {
this.year = Year.of(year);
return this;
}
public SpringReleaseTrainSpec build() {
Assert.notNull(train, "train cannot be null");
Assert.notNull(version, "version cannot be null");
Assert.notNull(weekOfMonth, "weekOfMonth cannot be null");
Assert.notNull(dayOfWeek, "dayOfWeek cannot be null");
Assert.notNull(year, "year cannot be null");
return new SpringReleaseTrainSpec(train, version, weekOfMonth, dayOfWeek, year);
}
}
}
@@ -0,0 +1,130 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.Project;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.springframework.gradle.github.RepositoryRef;
import org.springframework.gradle.github.changelog.GitHubChangelogPlugin;
/**
* @author Steve Riesenberg
*/
public class CreateGitHubReleaseTask extends DefaultTask {
@Input
private RepositoryRef repository = new RepositoryRef();
@Input @Optional
private String gitHubAccessToken;
@Input
private String version;
@Input @Optional
private String branch = "main";
@Input
private boolean createRelease = false;
@TaskAction
public void createGitHubRelease() {
String body = readReleaseNotes();
Release release = Release.tag(this.version)
.commit(this.branch)
.name(this.version)
.body(body)
.preRelease(this.version.contains("-"))
.build();
System.out.printf("%sCreating GitHub release for %s/%s@%s\n",
this.createRelease ? "" : "[DRY RUN] ",
this.repository.getOwner(),
this.repository.getName(),
this.version
);
System.out.printf(" Release Notes:\n\n----\n%s\n----\n\n", body.trim());
if (this.createRelease) {
GitHubReleaseApi github = new GitHubReleaseApi(this.gitHubAccessToken);
github.publishRelease(this.repository, release);
}
}
private String readReleaseNotes() {
Project project = getProject();
File inputFile = project.file(Paths.get(project.getBuildDir().getPath(), GitHubChangelogPlugin.RELEASE_NOTES_PATH));
try {
return Files.readString(inputFile.toPath());
} catch (IOException ex) {
throw new RuntimeException("Unable to read release notes from " + inputFile, ex);
}
}
public RepositoryRef getRepository() {
return repository;
}
public void repository(Action<RepositoryRef> repository) {
repository.execute(this.repository);
}
public void setRepository(RepositoryRef repository) {
this.repository = repository;
}
public String getGitHubAccessToken() {
return gitHubAccessToken;
}
public void setGitHubAccessToken(String gitHubAccessToken) {
this.gitHubAccessToken = gitHubAccessToken;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getBranch() {
return branch;
}
public void setBranch(String branch) {
this.branch = branch;
}
public boolean isCreateRelease() {
return createRelease;
}
public void setCreateRelease(boolean createRelease) {
this.createRelease = createRelease;
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;
import org.springframework.gradle.github.RepositoryRef;
/**
* @author Steve Riesenberg
*/
public class DispatchGitHubWorkflowTask extends DefaultTask {
@Input
private RepositoryRef repository = new RepositoryRef();
@Input
private String gitHubAccessToken;
@Input
private String branch;
@Input
private String workflowId;
@TaskAction
public void dispatchGitHubWorkflow() {
GitHubActionsApi gitHubActionsApi = new GitHubActionsApi(this.gitHubAccessToken);
WorkflowDispatch workflowDispatch = new WorkflowDispatch(this.branch, null);
gitHubActionsApi.dispatchWorkflow(this.repository, this.workflowId, workflowDispatch);
}
public RepositoryRef getRepository() {
return repository;
}
public void repository(Action<RepositoryRef> repository) {
repository.execute(this.repository);
}
public void setRepository(RepositoryRef repository) {
this.repository = repository;
}
public String getGitHubAccessToken() {
return gitHubAccessToken;
}
public void setGitHubAccessToken(String gitHubAccessToken) {
this.gitHubAccessToken = gitHubAccessToken;
}
public String getBranch() {
return branch;
}
public void setBranch(String branch) {
this.branch = branch;
}
public String getWorkflowId() {
return workflowId;
}
public void setWorkflowId(String workflowId) {
this.workflowId = workflowId;
}
}
@@ -0,0 +1,98 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import java.io.IOException;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.gradle.github.RepositoryRef;
/**
* Manage GitHub Actions.
*
* @author Steve Riesenberg
*/
public class GitHubActionsApi {
private String baseUrl = "https://api.github.com";
private final OkHttpClient client;
private final Gson gson = new GsonBuilder().create();
public GitHubActionsApi() {
this.client = new OkHttpClient.Builder().build();
}
public GitHubActionsApi(String gitHubToken) {
this.client = new OkHttpClient.Builder()
.addInterceptor(new AuthorizationInterceptor(gitHubToken))
.build();
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
/**
* Create a workflow dispatch event.
*
* @param repository The repository owner/name
* @param workflowId The ID of the workflow or the name of the workflow file name
* @param workflowDispatch The workflow dispatch containing a ref (branch) and optional inputs
*/
public void dispatchWorkflow(RepositoryRef repository, String workflowId, WorkflowDispatch workflowDispatch) {
String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/actions/workflows/" + workflowId + "/dispatches";
String json = this.gson.toJson(workflowDispatch);
RequestBody body = RequestBody.create(MediaType.parse("application/json"), json);
Request request = new Request.Builder().url(url).post(body).build();
try {
Response response = this.client.newCall(request).execute();
if (!response.isSuccessful()) {
throw new RuntimeException(String.format("Could not create workflow dispatch %s for repository %s/%s. Got response %s",
workflowId, repository.getOwner(), repository.getName(), response));
}
} catch (IOException ex) {
throw new RuntimeException(String.format("Could not create workflow dispatch %s for repository %s/%s",
workflowId, repository.getOwner(), repository.getName()), ex);
}
}
private static class AuthorizationInterceptor implements Interceptor {
private final String token;
public AuthorizationInterceptor(String token) {
this.token = token;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + this.token)
.build();
return chain.proceed(request);
}
}
}
@@ -0,0 +1,91 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import java.io.IOException;
import com.google.gson.Gson;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.springframework.gradle.github.RepositoryRef;
/**
* Manage GitHub releases.
*
* @author Steve Riesenberg
*/
public class GitHubReleaseApi {
private String baseUrl = "https://api.github.com";
private final OkHttpClient httpClient;
private Gson gson = new Gson();
public GitHubReleaseApi(String gitHubAccessToken) {
this.httpClient = new OkHttpClient.Builder()
.addInterceptor(new AuthorizationInterceptor(gitHubAccessToken))
.build();
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
/**
* Publish a release with no binary attachments.
*
* @param repository The repository owner/name
* @param release The contents of the release
*/
public void publishRelease(RepositoryRef repository, Release release) {
String url = this.baseUrl + "/repos/" + repository.getOwner() + "/" + repository.getName() + "/releases";
String json = this.gson.toJson(release);
RequestBody body = RequestBody.create(MediaType.parse("application/json"), json);
Request request = new Request.Builder().url(url).post(body).build();
try {
Response response = this.httpClient.newCall(request).execute();
if (!response.isSuccessful()) {
throw new RuntimeException(String.format("Could not create release %s for repository %s/%s. Got response %s",
release.getName(), repository.getOwner(), repository.getName(), response));
}
} catch (IOException ex) {
throw new RuntimeException(String.format("Could not create release %s for repository %s/%s",
release.getName(), repository.getOwner(), repository.getName()), ex);
}
}
private static class AuthorizationInterceptor implements Interceptor {
private final String token;
public AuthorizationInterceptor(String token) {
this.token = token;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + this.token)
.build();
return chain.proceed(request);
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import groovy.lang.MissingPropertyException;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
/**
* @author Steve Riesenberg
*/
public class GitHubReleasePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("createGitHubRelease", CreateGitHubReleaseTask.class, (createGitHubRelease) -> {
createGitHubRelease.setGroup("Release");
createGitHubRelease.setDescription("Create a github release");
createGitHubRelease.dependsOn("generateChangelog");
createGitHubRelease.setCreateRelease("true".equals(project.findProperty("createRelease")));
createGitHubRelease.setVersion((String) project.findProperty("nextVersion"));
if (project.hasProperty("branch")) {
createGitHubRelease.setBranch((String) project.findProperty("branch"));
}
createGitHubRelease.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken"));
if (createGitHubRelease.isCreateRelease() && createGitHubRelease.getGitHubAccessToken() == null) {
throw new MissingPropertyException("Please provide an access token with -PgitHubAccessToken=...");
}
});
project.getTasks().register("dispatchGitHubWorkflow", DispatchGitHubWorkflowTask.class, (dispatchGitHubWorkflow) -> {
dispatchGitHubWorkflow.setGroup("Release");
dispatchGitHubWorkflow.setDescription("Create a workflow_dispatch event on a given branch");
dispatchGitHubWorkflow.setBranch((String) project.findProperty("branch"));
dispatchGitHubWorkflow.setWorkflowId((String) project.findProperty("workflowId"));
dispatchGitHubWorkflow.setGitHubAccessToken((String) project.findProperty("gitHubAccessToken"));
});
}
}
@@ -0,0 +1,156 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import com.google.gson.annotations.SerializedName;
/**
* @author Steve Riesenberg
*/
public class Release {
@SerializedName("tag_name")
private final String tag;
@SerializedName("target_commitish")
private final String commit;
@SerializedName("name")
private final String name;
@SerializedName("body")
private final String body;
@SerializedName("draft")
private final boolean draft;
@SerializedName("prerelease")
private final boolean preRelease;
@SerializedName("generate_release_notes")
private final boolean generateReleaseNotes;
private Release(String tag, String commit, String name, String body, boolean draft, boolean preRelease, boolean generateReleaseNotes) {
this.tag = tag;
this.commit = commit;
this.name = name;
this.body = body;
this.draft = draft;
this.preRelease = preRelease;
this.generateReleaseNotes = generateReleaseNotes;
}
public String getTag() {
return tag;
}
public String getCommit() {
return commit;
}
public String getName() {
return name;
}
public String getBody() {
return body;
}
public boolean isDraft() {
return draft;
}
public boolean isPreRelease() {
return preRelease;
}
public boolean isGenerateReleaseNotes() {
return generateReleaseNotes;
}
@Override
public String toString() {
return "Release{" +
"tag='" + tag + '\'' +
", commit='" + commit + '\'' +
", name='" + name + '\'' +
", body='" + body + '\'' +
", draft=" + draft +
", preRelease=" + preRelease +
", generateReleaseNotes=" + generateReleaseNotes +
'}';
}
public static Builder tag(String tag) {
return new Builder().tag(tag);
}
public static Builder commit(String commit) {
return new Builder().commit(commit);
}
public static final class Builder {
private String tag;
private String commit;
private String name;
private String body;
private boolean draft;
private boolean preRelease;
private boolean generateReleaseNotes;
private Builder() {
}
public Builder tag(String tag) {
this.tag = tag;
return this;
}
public Builder commit(String commit) {
this.commit = commit;
return this;
}
public Builder name(String name) {
this.name = name;
return this;
}
public Builder body(String body) {
this.body = body;
return this;
}
public Builder draft(boolean draft) {
this.draft = draft;
return this;
}
public Builder preRelease(boolean preRelease) {
this.preRelease = preRelease;
return this;
}
public Builder generateReleaseNotes(boolean generateReleaseNotes) {
this.generateReleaseNotes = generateReleaseNotes;
return this;
}
public Release build() {
return new Release(tag, commit, name, body, draft, preRelease, generateReleaseNotes);
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import java.util.Map;
/**
* @author Steve Riesenberg
*/
public class WorkflowDispatch {
private String ref;
private Map<String, Object> inputs;
public WorkflowDispatch() {
}
public WorkflowDispatch(String ref, Map<String, Object> inputs) {
this.ref = ref;
this.inputs = inputs;
}
public String getRef() {
return ref;
}
public void setRef(String ref) {
this.ref = ref;
}
public Map<String, Object> getInputs() {
return inputs;
}
public void setInputs(Map<String, Object> inputs) {
this.inputs = inputs;
}
}
@@ -0,0 +1,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 + '\'' +
'}';
}
}
@@ -7,6 +7,8 @@ import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlatformPlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.publish.PublishingExtension;
import org.gradle.api.publish.VariantVersionMappingStrategy;
import org.gradle.api.publish.VersionMappingStrategy;
import org.gradle.api.publish.maven.MavenPublication;
import org.gradle.api.publish.maven.plugins.MavenPublishPlugin;
@@ -21,6 +21,7 @@ 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;
@@ -43,7 +44,12 @@ public class SpringSigningPlugin implements Plugin<Project> {
private void sign(Project project) {
SigningExtension signing = project.getExtensions().findByType(SigningExtension.class);
signing.setRequired((Callable<Boolean>) () -> project.getGradle().getTaskGraph().hasTask("publishArtifacts"));
signing.setRequired(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
return 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"));
}
});
}
}
@@ -1,115 +0,0 @@
/*
* Copyright 2002-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;
import org.gradle.api.DefaultTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.VerificationException;
import java.io.IOException;
import java.nio.file.Files;
/**
* @author Marcus da Coregio
*/
public class CheckExpectedBranchVersionPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
TaskProvider<CheckExpectedBranchVersionTask> checkExpectedBranchVersionTask = project.getTasks().register("checkExpectedBranchVersion", CheckExpectedBranchVersionTask.class, (task) -> {
task.setGroup("Build");
task.setDescription("Check if the project version matches the branch version");
task.onlyIf("skipCheckExpectedBranchVersion property is false or not present", CheckExpectedBranchVersionPlugin::skipPropertyFalseOrNotPresent);
task.getVersion().convention(project.provider(() -> project.getVersion().toString()));
task.getBranchName().convention(project.getProviders().exec(execSpec -> execSpec.setCommandLine("git", "symbolic-ref", "--short", "HEAD")).getStandardOutput().getAsText());
task.getOutputFile().convention(project.getLayout().getBuildDirectory().file("check-expected-branch-version"));
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(checkExpectedBranchVersionTask));
}
private static boolean skipPropertyFalseOrNotPresent(Task task) {
return task.getProject()
.getProviders()
.gradleProperty("skipCheckExpectedBranchVersion")
.orElse("false")
.map("false"::equalsIgnoreCase)
.get();
}
@CacheableTask
public static abstract class CheckExpectedBranchVersionTask extends DefaultTask {
@Input
abstract Property<String> getVersion();
@Input
abstract Property<String> getBranchName();
@OutputFile
abstract RegularFileProperty getOutputFile();
@TaskAction
public void run() {
String version = getVersion().get();
String branchVersion = getBranchName().map(String::trim).get();
if (!branchVersion.matches("^[0-9]+\\.[0-9]+\\.x$")) {
String msg = String.format("Branch version [%s] does not match *.x, ignoring", branchVersion);
getLogger().warn(msg);
writeExpectedVersionOutput(msg);
return;
}
if (!versionsMatch(version, branchVersion)) {
String msg = String.format("Project version [%s] does not match branch version [%s]. " +
"Please verify that the branch contains the right version.", version, branchVersion);
writeExpectedVersionOutput(msg);
throw new VerificationException(msg);
}
writeExpectedVersionOutput(version);
}
private void writeExpectedVersionOutput(String fileContent) {
try {
Files.writeString(getOutputFile().get().getAsFile().toPath(), fileContent);
} catch (IOException e) {
throw new TaskExecutionException(this, e);
}
}
private boolean versionsMatch(String projectVersion, String branchVersion) {
String[] projectVersionParts = projectVersion.split("\\.");
String[] branchVersionParts = branchVersion.split("\\.");
if (projectVersionParts.length < 2 || branchVersionParts.length < 2) {
return false;
}
return projectVersionParts[0].equals(branchVersionParts[0]) && projectVersionParts[1].equals(branchVersionParts[1]);
}
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.convention.versions;
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class UpdateProjectVersionPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getTasks().register("updateProjectVersion", UpdateProjectVersionTask.class, new Action<UpdateProjectVersionTask>() {
@Override
public void execute(UpdateProjectVersionTask updateProjectVersionTask) {
updateProjectVersionTask.setGroup("Release");
updateProjectVersionTask.setDescription("Updates the project version to the next release in gradle.properties");
updateProjectVersionTask.dependsOn("gitHubNextReleaseMilestone");
updateProjectVersionTask.getNextVersionFile().fileProvider(project.provider(() -> project.file("next-release.yml")));
}
});
project.getTasks().register("updateToSnapshotVersion", UpdateToSnapshotVersionTask.class, new Action<UpdateToSnapshotVersionTask>() {
@Override
public void execute(UpdateToSnapshotVersionTask updateToSnapshotVersionTask) {
updateToSnapshotVersionTask.setGroup("Release");
updateToSnapshotVersionTask.setDescription(
"Updates the project version to the next snapshot in gradle.properties");
}
});
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.convention.versions;
import org.gradle.api.DefaultTask;
import org.gradle.api.Project;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.Constructor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import org.springframework.gradle.github.milestones.NextVersionYml;
public abstract class UpdateProjectVersionTask extends DefaultTask {
@InputFile
public abstract RegularFileProperty getNextVersionFile();
@TaskAction
public void checkReleaseDueToday() throws FileNotFoundException {
File nextVersionFile = getNextVersionFile().getAsFile().get();
Yaml yaml = new Yaml(new Constructor(NextVersionYml.class));
NextVersionYml nextVersionYml = yaml.load(new FileInputStream(nextVersionFile));
String nextVersion = nextVersionYml.getVersion();
if (nextVersion == null) {
throw new IllegalArgumentException(
"Could not find version property in provided file " + nextVersionFile.getName());
}
String currentVersion = getProject().getVersion().toString();
File gradlePropertiesFile = getProject().getRootProject().file(Project.GRADLE_PROPERTIES);
if (!gradlePropertiesFile.exists()) {
return;
}
System.out.println("Updating the project version in " + Project.GRADLE_PROPERTIES + " from " + currentVersion
+ " to " + nextVersion);
FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> {
gradlePropertiesText = gradlePropertiesText.replace("version=" + currentVersion, "version=" + nextVersion);
return gradlePropertiesText;
});
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2019-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.convention.versions;
import org.gradle.api.DefaultTask;
import org.gradle.api.Project;
import org.gradle.api.tasks.TaskAction;
import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class UpdateToSnapshotVersionTask extends DefaultTask {
private static final String RELEASE_VERSION_PATTERN = "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(-M\\d+|-RC\\d+)?$";
@TaskAction
public void updateToSnapshotVersion() {
String currentVersion = getProject().getVersion().toString();
File gradlePropertiesFile = getProject().getRootProject().file(Project.GRADLE_PROPERTIES);
if (!gradlePropertiesFile.exists()) {
return;
}
String nextVersion = calculateNextSnapshotVersion(currentVersion);
System.out.println("Updating the project version in " + Project.GRADLE_PROPERTIES + " from " + currentVersion
+ " to " + nextVersion);
FileUtils.replaceFileText(gradlePropertiesFile, (gradlePropertiesText) -> {
gradlePropertiesText = gradlePropertiesText.replace("version=" + currentVersion, "version=" + nextVersion);
return gradlePropertiesText;
});
}
private String calculateNextSnapshotVersion(String currentVersion) {
Pattern releaseVersionPattern = Pattern.compile(RELEASE_VERSION_PATTERN);
Matcher releaseVersion = releaseVersionPattern.matcher(currentVersion);
if (releaseVersion.find()) {
String majorSegment = releaseVersion.group(1);
String minorSegment = releaseVersion.group(2);
String patchSegment = releaseVersion.group(3);
String modifier = releaseVersion.group(4);
if (modifier == null) {
patchSegment = String.valueOf(Integer.parseInt(patchSegment) + 1);
}
System.out.println("modifier = " + modifier);
return String.format("%s.%s.%s-SNAPSHOT", majorSegment, minorSegment, patchSegment);
}
else {
throw new IllegalStateException(
"Cannot calculate next snapshot version because the current project version does not conform to the expected format");
}
}
}
@@ -1,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.
@@ -19,79 +19,52 @@ package org.springframework.security.convention.versions;
import org.gradle.api.DefaultTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
import org.gradle.api.artifacts.VersionCatalog;
import org.gradle.api.artifacts.VersionCatalogsExtension;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.VerificationException;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Optional;
public class VerifyDependenciesVersionsPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
VersionCatalog versionCatalog = project.getExtensions().getByType(VersionCatalogsExtension.class).named("libs");
Optional<Provider<MinimalExternalModuleDependency>> oauth2OidcSdk = versionCatalog.findLibrary("com-nimbusds-oauth2-oidc-sdk");
Optional<Provider<MinimalExternalModuleDependency>> nimbusJoseJwt = versionCatalog.findLibrary("com-nimbusds-nimbus-jose-jwt");
if (oauth2OidcSdk.isEmpty()) {
throw new VerificationException("Library [com-nimbusds-oauth2-oidc-sdk] does not exist in the version catalog named libs.");
}
if (nimbusJoseJwt.isEmpty()) {
throw new VerificationException("Library [com-nimbusds-nimbus-jose-jwt] does not exist in the version catalog named libs.");
}
TaskProvider<VerifyDependenciesVersionsTask> verifyDependenciesVersionsTaskProvider = project.getTasks().register("verifyDependenciesVersions", VerifyDependenciesVersionsTask.class, (task) -> {
task.setGroup("Verification");
task.setDescription("Verify that specific dependencies are using the same version");
task.getOauth2OidcSdkVersion().convention(oauth2OidcSdk.get().map(Dependency::getVersion));
task.getExpectedNimbusJoseJwtVersion().convention(nimbusJoseJwt.get().map(Dependency::getVersion));
task.getOutputFile().convention(project.getLayout().getBuildDirectory().file("verify-dependencies-versions"));
VersionCatalog versionCatalog = project.getExtensions().getByType(VersionCatalogsExtension.class).named("libs");
MinimalExternalModuleDependency oauth2OidcSdk = versionCatalog.findLibrary("com-nimbusds-oauth2-oidc-sdk").get().get();
MinimalExternalModuleDependency nimbusJoseJwt = versionCatalog.findLibrary("com-nimbusds-nimbus-jose-jwt").get().get();
task.setOauth2OidcSdkVersion(oauth2OidcSdk.getVersion());
task.setExpectedNimbusJoseJwtVersion(nimbusJoseJwt.getVersion());
});
project.getTasks().named(JavaBasePlugin.CHECK_TASK_NAME, checkTask -> checkTask.dependsOn(verifyDependenciesVersionsTaskProvider));
}
@CacheableTask
public abstract static class VerifyDependenciesVersionsTask extends DefaultTask {
public static class VerifyDependenciesVersionsTask extends DefaultTask {
@Input
abstract Property<String> getOauth2OidcSdkVersion();
private String oauth2OidcSdkVersion;
@Input
abstract Property<String> getExpectedNimbusJoseJwtVersion();
private String expectedNimbusJoseJwtVersion;
@OutputFile
abstract RegularFileProperty getOutputFile();
public void setOauth2OidcSdkVersion(String oauth2OidcSdkVersion) {
this.oauth2OidcSdkVersion = oauth2OidcSdkVersion;
}
public void setExpectedNimbusJoseJwtVersion(String expectedNimbusJoseJwtVersion) {
this.expectedNimbusJoseJwtVersion = expectedNimbusJoseJwtVersion;
}
@TaskAction
public void verify() {
String oauth2OidcSdkVersion = this.getOauth2OidcSdkVersion().get();
String transitiveNimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(oauth2OidcSdkVersion);
String expectedNimbusJoseJwtVersion = this.getExpectedNimbusJoseJwtVersion().get();
if (!transitiveNimbusJoseJwtVersion.equals(expectedNimbusJoseJwtVersion)) {
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, but the project contains a different version of nimbus-jose-jwt [%s]. Please align the versions.", transitiveNimbusJoseJwtVersion, oauth2OidcSdkVersion, expectedNimbusJoseJwtVersion);
throw new VerificationException(message);
}
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, the project contains expected version of nimbus-jose-jwt [%s]. Verified all versions align.", transitiveNimbusJoseJwtVersion, oauth2OidcSdkVersion, expectedNimbusJoseJwtVersion);
try {
Files.writeString(getOutputFile().get().getAsFile().toPath(), message);
} catch (IOException e) {
throw new TaskExecutionException(this, e);
public void verify() {
String transitiveNimbusJoseJwtVersion = TransitiveDependencyLookupUtils.lookupJwtVersion(this.oauth2OidcSdkVersion);
if (!transitiveNimbusJoseJwtVersion.equals(this.expectedNimbusJoseJwtVersion)) {
String message = String.format("Found transitive nimbus-jose-jwt:%s in oauth2-oidc-sdk:%s, but the project contains a different version of nimbus-jose-jwt [%s]. Please align the versions.", transitiveNimbusJoseJwtVersion, this.oauth2OidcSdkVersion, this.expectedNimbusJoseJwtVersion);
throw new IllegalStateException(message);
}
}
}
}
+46 -11
View File
@@ -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 -1
View File
@@ -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();
}
}
@@ -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();
}
}
@@ -0,0 +1,245 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.milestones;
import java.time.LocalDate;
import java.time.Year;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.gradle.github.milestones.SpringReleaseTrainSpec.Train;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Steve Riesenberg
*/
public class SpringReleaseTrainTests {
@ParameterizedTest
@CsvSource({
"2019-12-31, ONE, 2020",
"2020-01-01, ONE, 2020",
"2020-01-31, ONE, 2020",
"2020-02-01, TWO, 2020",
"2020-07-31, TWO, 2020",
"2020-08-01, ONE, 2021"
})
public void nextTrainWhenBoundaryConditionsThenSuccess(LocalDate startDate, Train expectedTrain, Year expectedYear) {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.nextTrain(startDate)
.version("1.0.0")
.weekOfMonth(2)
.dayOfWeek(2)
.build();
assertThat(releaseTrainSpec.getTrain()).isEqualTo(expectedTrain);
assertThat(releaseTrainSpec.getYear()).isEqualTo(expectedYear);
}
@Test
public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2020ThenSuccess() {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(1)
.version("1.0.0")
.weekOfMonth(2)
.dayOfWeek(2)
.year(2020)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
Map<String, LocalDate> trainDates = releaseTrain.getTrainDates();
assertThat(trainDates).hasSize(5);
assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 1, 14));
assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 2, 11));
assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 3, 10));
assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 4, 14));
assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 5, 12));
}
@Test
public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2020ThenSuccess() {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(2)
.version("1.0.0")
.weekOfMonth(2)
.dayOfWeek(2)
.year(2020)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
Map<String, LocalDate> trainDates = releaseTrain.getTrainDates();
assertThat(trainDates).hasSize(5);
assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2020, 7, 14));
assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2020, 8, 11));
assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2020, 9, 15));
assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2020, 10, 13));
assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2020, 11, 10));
}
@Test
public void getTrainDatesWhenTrainOneIsSecondTuesdayOf2022ThenSuccess() {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(1)
.version("1.0.0")
.weekOfMonth(2)
.dayOfWeek(2)
.year(2022)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
Map<String, LocalDate> trainDates = releaseTrain.getTrainDates();
assertThat(trainDates).hasSize(5);
assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 11));
assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 15));
assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 15));
assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 12));
assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 10));
}
@Test
public void getTrainDatesWhenTrainTwoIsSecondTuesdayOf2022ThenSuccess() {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(2)
.version("1.0.0")
.weekOfMonth(2)
.dayOfWeek(2)
.year(2022)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
Map<String, LocalDate> trainDates = releaseTrain.getTrainDates();
assertThat(trainDates).hasSize(5);
assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 12));
assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 9));
assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 13));
assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 11));
assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 15));
}
@Test
public void getTrainDatesWhenTrainOneIsThirdMondayOf2022ThenSuccess() {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(1)
.version("1.0.0")
.weekOfMonth(3)
.dayOfWeek(1)
.year(2022)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
Map<String, LocalDate> trainDates = releaseTrain.getTrainDates();
assertThat(trainDates).hasSize(5);
assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 1, 17));
assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 2, 21));
assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 3, 21));
assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 4, 18));
assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 5, 16));
}
@Test
public void getTrainDatesWhenTrainTwoIsThirdMondayOf2022ThenSuccess() {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(2)
.version("1.0.0")
.weekOfMonth(3)
.dayOfWeek(1)
.year(2022)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
Map<String, LocalDate> trainDates = releaseTrain.getTrainDates();
assertThat(trainDates).hasSize(5);
assertThat(trainDates.get("1.0.0-M1")).isEqualTo(LocalDate.of(2022, 7, 18));
assertThat(trainDates.get("1.0.0-M2")).isEqualTo(LocalDate.of(2022, 8, 15));
assertThat(trainDates.get("1.0.0-M3")).isEqualTo(LocalDate.of(2022, 9, 19));
assertThat(trainDates.get("1.0.0-RC1")).isEqualTo(LocalDate.of(2022, 10, 17));
assertThat(trainDates.get("1.0.0")).isEqualTo(LocalDate.of(2022, 11, 21));
}
@Test
public void isTrainDateWhenTrainOneIsThirdMondayOf2022ThenSuccess() {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(1)
.version("1.0.0")
.weekOfMonth(3)
.dayOfWeek(1)
.year(2022)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) {
assertThat(releaseTrain.isTrainDate("1.0.0-M1", LocalDate.of(2022, 1, dayOfMonth))).isEqualTo(dayOfMonth == 17);
}
for (int dayOfMonth = 1; dayOfMonth <= 28; dayOfMonth++) {
assertThat(releaseTrain.isTrainDate("1.0.0-M2", LocalDate.of(2022, 2, dayOfMonth))).isEqualTo(dayOfMonth == 21);
}
for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) {
assertThat(releaseTrain.isTrainDate("1.0.0-M3", LocalDate.of(2022, 3, dayOfMonth))).isEqualTo(dayOfMonth == 21);
}
for (int dayOfMonth = 1; dayOfMonth <= 30; dayOfMonth++) {
assertThat(releaseTrain.isTrainDate("1.0.0-RC1", LocalDate.of(2022, 4, dayOfMonth))).isEqualTo(dayOfMonth == 18);
}
for (int dayOfMonth = 1; dayOfMonth <= 31; dayOfMonth++) {
assertThat(releaseTrain.isTrainDate("1.0.0", LocalDate.of(2022, 5, dayOfMonth))).isEqualTo(dayOfMonth == 16);
}
}
@ParameterizedTest
@CsvSource({
"2022-01-01, 2022-02-21",
"2022-02-01, 2022-02-21",
"2022-02-21, 2022-04-18",
"2022-03-01, 2022-04-18",
"2022-04-01, 2022-04-18",
"2022-04-18, 2022-06-20",
"2022-05-01, 2022-06-20",
"2022-06-01, 2022-06-20",
"2022-06-20, 2022-08-15",
"2022-07-01, 2022-08-15",
"2022-08-01, 2022-08-15",
"2022-08-15, 2022-10-17",
"2022-09-01, 2022-10-17",
"2022-10-01, 2022-10-17",
"2022-10-17, 2022-12-19",
"2022-11-01, 2022-12-19",
"2022-12-01, 2022-12-19",
"2022-12-19, 2023-02-20"
})
public void getNextReleaseDateWhenBoundaryConditionsThenSuccess(LocalDate startDate, LocalDate expectedDate) {
SpringReleaseTrainSpec releaseTrainSpec =
SpringReleaseTrainSpec.builder()
.train(1)
.version("1.0.0")
.weekOfMonth(3)
.dayOfWeek(1)
.year(2022)
.build();
SpringReleaseTrain releaseTrain = new SpringReleaseTrain(releaseTrainSpec);
assertThat(releaseTrain.getNextReleaseDate(startDate)).isEqualTo(expectedDate);
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.gradle.github.RepositoryRef;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Steve Riesenberg
*/
public class GitHubActionsApiTests {
private GitHubActionsApi gitHubActionsApi;
private MockWebServer server;
private String baseUrl;
private RepositoryRef repository;
@BeforeEach
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
this.baseUrl = this.server.url("/api").toString();
this.gitHubActionsApi = new GitHubActionsApi("mock-oauth-token");
this.gitHubActionsApi.setBaseUrl(this.baseUrl);
this.repository = new RepositoryRef("spring-projects", "spring-security");
}
@AfterEach
public void cleanup() throws Exception {
this.server.shutdown();
}
@Test
public void dispatchWorkflowWhenValidParametersThenSuccess() throws Exception {
this.server.enqueue(new MockResponse().setResponseCode(204));
Map<String, Object> inputs = new LinkedHashMap<>();
inputs.put("input-1", "value");
inputs.put("input-2", false);
WorkflowDispatch workflowDispatch = new WorkflowDispatch("main", inputs);
this.gitHubActionsApi.dispatchWorkflow(this.repository, "test-workflow.yml", workflowDispatch);
RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post");
assertThat(recordedRequest.getRequestUrl().toString())
.isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/actions/workflows/test-workflow.yml/dispatches");
assertThat(recordedRequest.getBody().readString(Charset.defaultCharset()))
.isEqualTo("{\"ref\":\"main\",\"inputs\":{\"input-1\":\"value\",\"input-2\":false}}");
}
@Test
public void dispatchWorkflowWhenErrorResponseThenException() throws Exception {
this.server.enqueue(new MockResponse().setResponseCode(400));
WorkflowDispatch workflowDispatch = new WorkflowDispatch("main", null);
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> this.gitHubActionsApi.dispatchWorkflow(this.repository, "test-workflow.yml", workflowDispatch));
}
}
@@ -0,0 +1,155 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.gradle.github.release;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.gradle.github.RepositoryRef;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Steve Riesenberg
*/
public class GitHubReleaseApiTests {
private GitHubReleaseApi gitHubReleaseApi;
private MockWebServer server;
private String baseUrl;
private RepositoryRef repository;
@BeforeEach
public void setup() throws Exception {
this.server = new MockWebServer();
this.server.start();
this.baseUrl = this.server.url("/api").toString();
this.gitHubReleaseApi = new GitHubReleaseApi("mock-oauth-token");
this.gitHubReleaseApi.setBaseUrl(this.baseUrl);
this.repository = new RepositoryRef("spring-projects", "spring-security");
}
@AfterEach
public void cleanup() throws Exception {
this.server.shutdown();
}
@Test
public void publishReleaseWhenValidParametersThenSuccess() throws Exception {
String responseJson = "{\n" +
" \"url\": \"https://api.github.com/spring-projects/spring-security/releases/1\",\n" +
" \"html_url\": \"https://github.com/spring-projects/spring-security/releases/tags/v1.0.0\",\n" +
" \"assets_url\": \"https://api.github.com/spring-projects/spring-security/releases/1/assets\",\n" +
" \"upload_url\": \"https://uploads.github.com/spring-projects/spring-security/releases/1/assets{?name,label}\",\n" +
" \"tarball_url\": \"https://api.github.com/spring-projects/spring-security/tarball/v1.0.0\",\n" +
" \"zipball_url\": \"https://api.github.com/spring-projects/spring-security/zipball/v1.0.0\",\n" +
" \"discussion_url\": \"https://github.com/spring-projects/spring-security/discussions/90\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDc6UmVsZWFzZTE=\",\n" +
" \"tag_name\": \"v1.0.0\",\n" +
" \"target_commitish\": \"main\",\n" +
" \"name\": \"v1.0.0\",\n" +
" \"body\": \"Description of the release\",\n" +
" \"draft\": false,\n" +
" \"prerelease\": false,\n" +
" \"created_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"published_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"author\": {\n" +
" \"login\": \"sjohnr\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDQ6VXNlcjE=\",\n" +
" \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" +
" \"gravatar_id\": \"\",\n" +
" \"url\": \"https://api.github.com/users/sjohnr\",\n" +
" \"html_url\": \"https://github.com/sjohnr\",\n" +
" \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" +
" \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" +
" \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" +
" \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" +
" \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" +
" \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" +
" \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" +
" \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" +
" \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" +
" \"type\": \"User\",\n" +
" \"site_admin\": false\n" +
" },\n" +
" \"assets\": [\n" +
" {\n" +
" \"url\": \"https://api.github.com/spring-projects/spring-security/releases/assets/1\",\n" +
" \"browser_download_url\": \"https://github.com/spring-projects/spring-security/releases/download/v1.0.0/example.zip\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDEyOlJlbGVhc2VBc3NldDE=\",\n" +
" \"name\": \"example.zip\",\n" +
" \"label\": \"short description\",\n" +
" \"state\": \"uploaded\",\n" +
" \"content_type\": \"application/zip\",\n" +
" \"size\": 1024,\n" +
" \"download_count\": 42,\n" +
" \"created_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"updated_at\": \"2013-02-27T19:35:32Z\",\n" +
" \"uploader\": {\n" +
" \"login\": \"sjohnr\",\n" +
" \"id\": 1,\n" +
" \"node_id\": \"MDQ6VXNlcjE=\",\n" +
" \"avatar_url\": \"https://github.com/images/avatar.gif\",\n" +
" \"gravatar_id\": \"\",\n" +
" \"url\": \"https://api.github.com/users/sjohnr\",\n" +
" \"html_url\": \"https://github.com/sjohnr\",\n" +
" \"followers_url\": \"https://api.github.com/users/sjohnr/followers\",\n" +
" \"following_url\": \"https://api.github.com/users/sjohnr/following{/other_user}\",\n" +
" \"gists_url\": \"https://api.github.com/users/sjohnr/gists{/gist_id}\",\n" +
" \"starred_url\": \"https://api.github.com/users/sjohnr/starred{/owner}{/repo}\",\n" +
" \"subscriptions_url\": \"https://api.github.com/users/sjohnr/subscriptions\",\n" +
" \"organizations_url\": \"https://api.github.com/users/sjohnr/orgs\",\n" +
" \"repos_url\": \"https://api.github.com/users/sjohnr/repos\",\n" +
" \"events_url\": \"https://api.github.com/users/sjohnr/events{/privacy}\",\n" +
" \"received_events_url\": \"https://api.github.com/users/sjohnr/received_events\",\n" +
" \"type\": \"User\",\n" +
" \"site_admin\": false\n" +
" }\n" +
" }\n" +
" ]\n" +
"}";
this.server.enqueue(new MockResponse().setBody(responseJson));
this.gitHubReleaseApi.publishRelease(this.repository, Release.tag("1.0.0").build());
RecordedRequest recordedRequest = this.server.takeRequest(1, TimeUnit.SECONDS);
assertThat(recordedRequest.getMethod()).isEqualToIgnoringCase("post");
assertThat(recordedRequest.getRequestUrl().toString())
.isEqualTo(this.baseUrl + "/repos/spring-projects/spring-security/releases");
assertThat(recordedRequest.getBody().readString(Charset.defaultCharset()))
.isEqualTo("{\"tag_name\":\"1.0.0\",\"draft\":false,\"prerelease\":false,\"generate_release_notes\":false}");
}
@Test
public void publishReleaseWhenErrorResponseThenException() throws Exception {
this.server.enqueue(new MockResponse().setResponseCode(400));
assertThatExceptionOfType(RuntimeException.class)
.isThrownBy(() -> this.gitHubReleaseApi.publishRelease(this.repository, Release.tag("1.0.0").build()));
}
}
@@ -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
}
}
-25
View File
@@ -1,25 +0,0 @@
apply plugin: 'io.spring.convention.spring-module'
dependencies {
management platform(project(":spring-security-dependencies"))
api project(':spring-security-core')
api project(':spring-security-web')
api 'org.apereo.cas.client:cas-client-core'
api 'org.springframework:spring-beans'
api 'org.springframework:spring-context'
api 'org.springframework:spring-core'
api 'org.springframework:spring-web'
optional 'com.fasterxml.jackson.core:jackson-databind'
provided 'jakarta.servlet:jakarta.servlet-api'
testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"
testImplementation "org.junit.jupiter:junit-jupiter-params"
testImplementation "org.junit.jupiter:junit-jupiter-engine"
testImplementation "org.mockito:mockito-core"
testImplementation "org.mockito:mockito-junit-jupiter"
testImplementation "org.springframework:spring-test"
testImplementation 'org.skyscreamer:jsonassert'
}
@@ -1,37 +0,0 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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;
/**
* Sets the appropriate parameters for CAS's implementation of SAML (which is not
* guaranteed to be actually SAML compliant).
*
* @author Scott Battaglia
* @since 3.0
*/
public final class SamlServiceProperties extends ServiceProperties {
public static final String DEFAULT_SAML_ARTIFACT_PARAMETER = "SAMLart";
public static final String DEFAULT_SAML_SERVICE_PARAMETER = "TARGET";
public SamlServiceProperties() {
super.setArtifactParameter(DEFAULT_SAML_ARTIFACT_PARAMETER);
super.setServiceParameter(DEFAULT_SAML_SERVICE_PARAMETER);
}
}
@@ -1,132 +0,0 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
/**
* Stores properties related to this CAS service.
* <p>
* Each web application capable of processing CAS tickets is known as a service. This
* class stores the properties that are relevant to the local CAS service, being the
* application that is being secured by Spring Security.
*
* @author Ben Alex
*/
public class ServiceProperties implements InitializingBean {
public static final String DEFAULT_CAS_ARTIFACT_PARAMETER = "ticket";
public static final String DEFAULT_CAS_SERVICE_PARAMETER = "service";
private String service;
private boolean authenticateAllArtifacts;
private boolean sendRenew = false;
private String artifactParameter = DEFAULT_CAS_ARTIFACT_PARAMETER;
private String serviceParameter = DEFAULT_CAS_SERVICE_PARAMETER;
@Override
public void afterPropertiesSet() {
Assert.hasLength(this.service, "service cannot be empty.");
Assert.hasLength(this.artifactParameter, "artifactParameter cannot be empty.");
Assert.hasLength(this.serviceParameter, "serviceParameter cannot be empty.");
}
/**
* Represents the service the user is authenticating to.
* <p>
* This service is the callback URL belonging to the local Spring Security System for
* Spring secured application. For example,
*
* <pre>
* https://www.mycompany.com/application/login/cas
* </pre>
* @return the URL of the service the user is authenticating to
*/
public final String getService() {
return this.service;
}
/**
* Indicates whether the <code>renew</code> parameter should be sent to the CAS login
* URL and CAS validation URL.
* <p>
* If <code>true</code>, it will force CAS to authenticate the user again (even if the
* user has previously authenticated). During ticket validation it will require the
* ticket was generated as a consequence of an explicit login. High security
* applications would probably set this to <code>true</code>. Defaults to
* <code>false</code>, providing automated single sign on.
* @return whether to send the <code>renew</code> parameter to CAS
*/
public final boolean isSendRenew() {
return this.sendRenew;
}
public final void setSendRenew(final boolean sendRenew) {
this.sendRenew = sendRenew;
}
public final void setService(final String service) {
this.service = service;
}
public final String getArtifactParameter() {
return this.artifactParameter;
}
/**
* Configures the Request Parameter to look for when attempting to see if a CAS ticket
* was sent from the server.
* @param artifactParameter the id to use. Default is "ticket".
*/
public final void setArtifactParameter(final String artifactParameter) {
this.artifactParameter = artifactParameter;
}
/**
* Configures the Request parameter to look for when attempting to send a request to
* CAS.
* @return the service parameter to use. Default is "service".
*/
public final String getServiceParameter() {
return this.serviceParameter;
}
public final void setServiceParameter(final String serviceParameter) {
this.serviceParameter = serviceParameter;
}
public final boolean isAuthenticateAllArtifacts() {
return this.authenticateAllArtifacts;
}
/**
* If true, then any non-null artifact (ticket) should be authenticated. Additionally,
* the service will be determined dynamically in order to ensure the service matches
* the expected value for this artifact.
* @param authenticateAllArtifacts
*/
public final void setAuthenticateAllArtifacts(final boolean authenticateAllArtifacts) {
this.authenticateAllArtifacts = authenticateAllArtifacts;
}
}
@@ -1,60 +0,0 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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.util.ArrayList;
import org.apereo.cas.client.validation.Assertion;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.SpringSecurityCoreVersion;
/**
* Temporary authentication object needed to load the user details service.
*
* @author Scott Battaglia
* @since 3.0
*/
public final class CasAssertionAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Assertion assertion;
private final String ticket;
public CasAssertionAuthenticationToken(final Assertion assertion, final String ticket) {
super(new ArrayList<>());
this.assertion = assertion;
this.ticket = ticket;
}
@Override
public Object getPrincipal() {
return this.assertion.getPrincipal().getName();
}
@Override
public Object getCredentials() {
return this.ticket;
}
public Assertion getAssertion() {
return this.assertion;
}
}
@@ -1,246 +0,0 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apereo.cas.client.validation.Assertion;
import org.apereo.cas.client.validation.TicketValidationException;
import org.apereo.cas.client.validation.TicketValidator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.log.LogMessage;
import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;
/**
* An {@link AuthenticationProvider} implementation that integrates with JA-SIG Central
* Authentication Service (CAS).
* <p>
* This <code>AuthenticationProvider</code> is capable of validating
* {@link CasServiceTicketAuthenticationToken} requests which contain a
* <code>principal</code> name equal to either
* {@link CasServiceTicketAuthenticationToken#CAS_STATEFUL_IDENTIFIER} or
* {@link CasServiceTicketAuthenticationToken#CAS_STATELESS_IDENTIFIER}. It can also
* validate a previously created {@link CasAuthenticationToken}.
*
* @author Ben Alex
* @author Scott Battaglia
* @author Kim Youngwoong
*/
public class CasAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
private static final Log logger = LogFactory.getLog(CasAuthenticationProvider.class);
private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService;
private UserDetailsChecker userDetailsChecker = new AccountStatusUserDetailsChecker();
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private StatelessTicketCache statelessTicketCache = new NullStatelessTicketCache();
private String key;
private TicketValidator ticketValidator;
private ServiceProperties serviceProperties;
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
@Override
public void afterPropertiesSet() {
Assert.notNull(this.authenticationUserDetailsService, "An authenticationUserDetailsService must be set");
Assert.notNull(this.ticketValidator, "A ticketValidator must be set");
Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set");
Assert.hasText(this.key,
"A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated");
Assert.notNull(this.messages, "A message source must be set");
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
// If an existing CasAuthenticationToken, just check we created it
if (authentication instanceof CasAuthenticationToken) {
if (this.key.hashCode() != ((CasAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProvider.incorrectKey",
"The presented CasAuthenticationToken does not contain the expected key"));
}
return authentication;
}
// Ensure credentials are presented
if ((authentication.getCredentials() == null) || "".equals(authentication.getCredentials())) {
throw new BadCredentialsException(this.messages.getMessage("CasAuthenticationProvider.noServiceTicket",
"Failed to provide a CAS service ticket to validate"));
}
boolean stateless = (authentication instanceof CasServiceTicketAuthenticationToken token
&& token.isStateless());
CasAuthenticationToken result = null;
if (stateless) {
// Try to obtain from cache
result = this.statelessTicketCache.getByTicketId(authentication.getCredentials().toString());
}
if (result == null) {
result = this.authenticateNow(authentication);
result.setDetails(authentication.getDetails());
}
if (stateless) {
// Add to cache
this.statelessTicketCache.putTicketInCache(result);
}
return result;
}
private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException {
try {
Assertion assertion = this.ticketValidator.validate(authentication.getCredentials().toString(),
getServiceUrl(authentication));
UserDetails userDetails = loadUserByAssertion(assertion);
this.userDetailsChecker.check(userDetails);
return new CasAuthenticationToken(this.key, userDetails, authentication.getCredentials(),
this.authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion);
}
catch (TicketValidationException ex) {
throw new BadCredentialsException(ex.getMessage(), ex);
}
}
/**
* Gets the serviceUrl. If the {@link Authentication#getDetails()} is an instance of
* {@link ServiceAuthenticationDetails}, then
* {@link ServiceAuthenticationDetails#getServiceUrl()} is used. Otherwise, the
* {@link ServiceProperties#getService()} is used.
* @param authentication
* @return
*/
private String getServiceUrl(Authentication authentication) {
String serviceUrl;
if (authentication.getDetails() instanceof ServiceAuthenticationDetails) {
return ((ServiceAuthenticationDetails) authentication.getDetails()).getServiceUrl();
}
Assert.state(this.serviceProperties != null,
"serviceProperties cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
Assert.state(this.serviceProperties.getService() != null,
"serviceProperties.getService() cannot be null unless Authentication.getDetails() implements ServiceAuthenticationDetails.");
serviceUrl = this.serviceProperties.getService();
logger.debug(LogMessage.format("serviceUrl = %s", serviceUrl));
return serviceUrl;
}
/**
* Template method for retrieving the UserDetails based on the assertion. Default is
* to call configured userDetailsService and pass the username. Deployers can override
* this method and retrieve the user based on any criteria they desire.
* @param assertion The CAS Assertion.
* @return the UserDetails.
*/
protected UserDetails loadUserByAssertion(final Assertion assertion) {
final CasAssertionAuthenticationToken token = new CasAssertionAuthenticationToken(assertion, "");
return this.authenticationUserDetailsService.loadUserDetails(token);
}
@SuppressWarnings("unchecked")
/**
* Sets the UserDetailsService to use. This is a convenience method to invoke
*/
public void setUserDetailsService(final UserDetailsService userDetailsService) {
this.authenticationUserDetailsService = new UserDetailsByNameServiceWrapper(userDetailsService);
}
public void setAuthenticationUserDetailsService(
final AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService) {
this.authenticationUserDetailsService = authenticationUserDetailsService;
}
/**
* Sets the UserDetailsChecker to be used for checking the status of retrieved user
* details. This allows customization of the UserDetailsChecker implementation.
* @param userDetailsChecker the UserDetailsChecker to be set
* @since 6.4
*/
public void setUserDetailsChecker(final UserDetailsChecker userDetailsChecker) {
Assert.notNull(userDetailsChecker, "userDetailsChecker cannot be null");
this.userDetailsChecker = userDetailsChecker;
}
public void setServiceProperties(final ServiceProperties serviceProperties) {
this.serviceProperties = serviceProperties;
}
protected String getKey() {
return this.key;
}
public void setKey(String key) {
this.key = key;
}
public StatelessTicketCache getStatelessTicketCache() {
return this.statelessTicketCache;
}
protected TicketValidator getTicketValidator() {
return this.ticketValidator;
}
@Override
public void setMessageSource(final MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setStatelessTicketCache(final StatelessTicketCache statelessTicketCache) {
this.statelessTicketCache = statelessTicketCache;
}
public void setTicketValidator(final TicketValidator ticketValidator) {
this.ticketValidator = ticketValidator;
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
@Override
public boolean supports(final Class<?> authentication) {
return (CasServiceTicketAuthenticationToken.class.isAssignableFrom(authentication))
|| (CasAuthenticationToken.class.isAssignableFrom(authentication))
|| (CasAssertionAuthenticationToken.class.isAssignableFrom(authentication));
}
}
@@ -1,166 +0,0 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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 java.util.Collection;
import org.apereo.cas.client.validation.Assertion;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Represents a successful CAS <code>Authentication</code>.
*
* @author Ben Alex
* @author Scott Battaglia
*/
public class CasAuthenticationToken extends AbstractAuthenticationToken implements Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object credentials;
private final Object principal;
private final UserDetails userDetails;
private final int keyHash;
private final Assertion assertion;
/**
* Constructor.
* @param key to identify if this object made by a given
* {@link CasAuthenticationProvider}
* @param principal typically the UserDetails object (cannot be <code>null</code>)
* @param credentials the service/proxy ticket ID from CAS (cannot be
* <code>null</code>)
* @param authorities the authorities granted to the user (from the
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
* be <code>null</code>)
* @param userDetails the user details (from the
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
* be <code>null</code>)
* @param assertion the assertion returned from the CAS servers. It contains the
* principal and how to obtain a proxy ticket for the user.
* @throws IllegalArgumentException if a <code>null</code> was passed
*/
public CasAuthenticationToken(final String key, final Object principal, final Object credentials,
final Collection<? extends GrantedAuthority> authorities, final UserDetails userDetails,
final Assertion assertion) {
this(extractKeyHash(key), principal, credentials, authorities, userDetails, assertion);
}
/**
* Private constructor for Jackson Deserialization support
* @param keyHash hashCode of provided key to identify if this object made by a given
* {@link CasAuthenticationProvider}
* @param principal typically the UserDetails object (cannot be <code>null</code>)
* @param credentials the service/proxy ticket ID from CAS (cannot be
* <code>null</code>)
* @param authorities the authorities granted to the user (from the
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
* be <code>null</code>)
* @param userDetails the user details (from the
* {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot
* be <code>null</code>)
* @param assertion the assertion returned from the CAS servers. It contains the
* principal and how to obtain a proxy ticket for the user.
* @throws IllegalArgumentException if a <code>null</code> was passed
* @since 4.2
*/
private CasAuthenticationToken(final Integer keyHash, final Object principal, final Object credentials,
final Collection<? extends GrantedAuthority> authorities, final UserDetails userDetails,
final Assertion assertion) {
super(authorities);
if ((principal == null) || "".equals(principal) || (credentials == null) || "".equals(credentials)
|| (authorities == null) || (userDetails == null) || (assertion == null)) {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
this.keyHash = keyHash;
this.principal = principal;
this.credentials = credentials;
this.userDetails = userDetails;
this.assertion = assertion;
setAuthenticated(true);
}
private static Integer extractKeyHash(String key) {
Assert.hasLength(key, "key cannot be null or empty");
return key.hashCode();
}
@Override
public boolean equals(final Object obj) {
if (!super.equals(obj)) {
return false;
}
if (obj instanceof CasAuthenticationToken test) {
return this.assertion.equals(test.getAssertion()) && this.getKeyHash() == test.getKeyHash();
}
return false;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = 31 * result + this.credentials.hashCode();
result = 31 * result + this.principal.hashCode();
result = 31 * result + this.userDetails.hashCode();
result = 31 * result + this.keyHash;
result = 31 * result + ObjectUtils.nullSafeHashCode(this.assertion);
return result;
}
@Override
public Object getCredentials() {
return this.credentials;
}
public int getKeyHash() {
return this.keyHash;
}
@Override
public Object getPrincipal() {
return this.principal;
}
public Assertion getAssertion() {
return this.assertion;
}
public UserDetails getUserDetails() {
return this.userDetails;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString());
sb.append(" Assertion: ").append(this.assertion);
sb.append(" Credentials (Service/Proxy Ticket): ").append(this.credentials);
return (sb.toString());
}
}
@@ -1,112 +0,0 @@
/*
* 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.cas.authentication;
import java.io.Serial;
import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
/**
* An {@link org.springframework.security.core.Authentication} implementation that is
* designed to process CAS service ticket.
*
* @author Hal Deadman
* @since 6.1
*/
public class CasServiceTicketAuthenticationToken extends AbstractAuthenticationToken {
static final String CAS_STATELESS_IDENTIFIER = "_cas_stateless_";
static final String CAS_STATEFUL_IDENTIFIER = "_cas_stateful_";
@Serial
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final String identifier;
private Object credentials;
/**
* This constructor can be safely used by any code that wishes to create a
* <code>CasServiceTicketAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public CasServiceTicketAuthenticationToken(String identifier, Object credentials) {
super(null);
this.identifier = identifier;
this.credentials = credentials;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
* @param identifier
* @param credentials
* @param authorities
*/
public CasServiceTicketAuthenticationToken(String identifier, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.identifier = identifier;
this.credentials = credentials;
super.setAuthenticated(true);
}
public static CasServiceTicketAuthenticationToken stateful(Object credentials) {
return new CasServiceTicketAuthenticationToken(CAS_STATEFUL_IDENTIFIER, credentials);
}
public static CasServiceTicketAuthenticationToken stateless(Object credentials) {
return new CasServiceTicketAuthenticationToken(CAS_STATELESS_IDENTIFIER, credentials);
}
public boolean isStateless() {
return CAS_STATELESS_IDENTIFIER.equals(this.identifier);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.identifier;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
Assert.isTrue(!isAuthenticated,
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
this.credentials = null;
}
}
@@ -1,64 +0,0 @@
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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;
/**
* Implementation of @link {@link StatelessTicketCache} that has no backing cache. Useful
* in instances where storing of tickets for stateless session management is not required.
* <p>
* This is the default StatelessTicketCache of the @link {@link CasAuthenticationProvider}
* to eliminate the unnecessary dependency on EhCache that applications have even if they
* are not using the stateless session management.
*
* @author Scott Battaglia
* @see CasAuthenticationProvider
*/
public final class NullStatelessTicketCache implements StatelessTicketCache {
/**
* @return null since we are not storing any tickets.
*/
@Override
public CasAuthenticationToken getByTicketId(final String serviceTicket) {
return null;
}
/**
* This is a no-op since we are not storing tickets.
*/
@Override
public void putTicketInCache(final CasAuthenticationToken token) {
// nothing to do
}
/**
* This is a no-op since we are not storing tickets.
*/
@Override
public void removeTicketFromCache(final CasAuthenticationToken token) {
// nothing to do
}
/**
* This is a no-op since we are not storing tickets.
*/
@Override
public void removeTicketFromCache(final String serviceTicket) {
// nothing to do
}
}

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