diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..ae0394d9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @hmcts/api-marketplace diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..e407d1cb --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contribution guidelines + +We're happy to accept 3rd-party contributions. Please make sure you read this document before you do any work though, +as we have some expectations related to the content and quality of change sets. + +## Before contributing + +Any ideas on the user journeys and general service experience you may have **should be first consulted +with us by submitting a new issue** to this repository. Ideas are always welcome, but if something is divergent or unrelated +to what we're trying to achieve we won't be able to accept it. Please keep this in mind as we don't want to waste anybody's time. + +In the interest of creating a friendly collaboration environment, please read and adhere to an open source contributor's +[code of conduct](http://contributor-covenant.org/version/1/4/). + +## Making a contribution + +After your idea has been accepted you can implement it. We don't allow direct changes to the codebase from the public, +they have to go through a review first. + +Here's what you should do: +1. [fork](https://help.github.com/articles/fork-a-repo/) this repository and clone it to your machine, +2. create a new branch for your change: + * use the latest *main* to branch from, +3. implement the change in your branch: + * if the change is non-trivial it's a good practice to split it into several logically independent units and deliver + each one as a separate commit, + * make sure the commit messages use proper language and accurately describe commit's content, e.g. *"Unify postcode lookup elements spacing"*. + More information on good commit messages can be found [here](http://chris.beams.io/posts/git-commit/), +4. test if your feature works as expected and does not break any existing features, this may include implementing additional automated tests or amending existing ones, +5. push the change to your GitHub fork, +6. submit a [pull request](https://help.github.com/articles/creating-a-pull-request-from-a-fork/) to our repository: + * ensure that the pull request and related GitHub issue reference each other. + +At this point the pull request will wait for someone from our team to review. It may be accepted straight away, +or we may ask you to make some additional amendments before incorporating it into the main branch. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..d1f90693 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,5 @@ +### What would you like to change? + +### How do you think that would improve the project? + +### If this entry is related to a bug, please provide the steps to reproduce it diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..dfadfd11 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +**Before creating a pull request make sure that:** + +- [ ] commit messages are meaningful and follow good commit message guidelines +- [ ] README and other documentation has been updated / added (if needed) +- [ ] tests have been updated / new tests has been added (if needed) + +Please remove this line and everything above and fill the following sections: + + +### JIRA link (if applicable) ### + + + +### Change description ### + + + +**Does this PR introduce a breaking change?** (check one with "x") + +``` +[ ] Yes +[ ] No +``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..3ce9f9df --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: "gradle" + directory: "/" # location of build.gradle + schedule: + interval: "daily" # or "weekly", "monthly" + open-pull-requests-limit: 5 + commit-message: + prefix: "chore(deps)" + groups: + all-dependencies: + patterns: + - "*" + + - package-ecosystem: "github-actions" + directory: "/" # location of your workflow files + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/pmd-ruleset.xml b/.github/pmd-ruleset.xml new file mode 100644 index 00000000..35916718 --- /dev/null +++ b/.github/pmd-ruleset.xml @@ -0,0 +1,34 @@ + + + + Custom ruleset including exclusions + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/workflows/ci-build-publish.yml b/.github/workflows/ci-build-publish.yml new file mode 100644 index 00000000..50f80634 --- /dev/null +++ b/.github/workflows/ci-build-publish.yml @@ -0,0 +1,188 @@ +name: CI Build and Publish + +on: + workflow_call: + secrets: + AZURE_DEVOPS_ARTIFACT_USERNAME: + required: true + AZURE_DEVOPS_ARTIFACT_TOKEN: + required: true + HMCTS_ADO_PAT: + required: true + inputs: + is_release: + required: false + type: boolean + default: false + is_publish: + required: true + type: boolean + trigger_docker: + required: true + type: boolean + trigger_deploy: + required: true + type: boolean + + +jobs: + Artefact-Version: + runs-on: ubuntu-latest + outputs: + artefact_version: ${{ inputs.is_release && steps.artefact.outputs.release_version || steps.artefact.outputs.draft_version }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Generate Artefact Version + id: artefact + uses: hmcts/artefact-version-action@v1 + with: + release: ${{ inputs.is_release }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + Build: + needs: [Artefact-Version] + runs-on: ubuntu-latest + outputs: + repo_name: ${{ steps.repo_vars.outputs.repo_name }} + artefact_name: ${{ steps.repo_vars.outputs.artefact_name }} + steps: + - name: Checkout source code + uses: actions/checkout@v6 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v5 + with: + gradle-version: current + + - name: Gradle Build + env: + ARTEFACT_VERSION: ${{ needs.Artefact-Version.outputs.artefact_version }} + run: | + echo "Building with ARTEFACT_VERSION=$ARTEFACT_VERSION" + gradle build -DARTEFACT_VERSION=$ARTEFACT_VERSION + + - name: Extract repo name + id: repo_vars + run: | + repo_name=${GITHUB_REPOSITORY##*/} + echo "repo_name=${repo_name}" >> $GITHUB_OUTPUT + echo "artefact_name=${repo_name}-${{ needs.Artefact-Version.outputs.artefact_version }}" >> $GITHUB_OUTPUT + + - name: Upload JAR Artefact + uses: actions/upload-artifact@v5 + with: + name: app-jar + path: build/libs/${{ steps.repo_vars.outputs.artefact_name }}.jar + + Provider-Deploy: + needs: [ Artefact-Version, Build ] + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v6 + + - name: Set up JDK + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '21' + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v5 + with: + gradle-version: current + + - name: Gradle Publish + if: ${{ inputs.is_publish }} + env: + ARTEFACT_VERSION: ${{ needs.Artefact-Version.outputs.artefact_version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + AZURE_DEVOPS_ARTIFACT_USERNAME: ${{ secrets.AZURE_DEVOPS_ARTIFACT_USERNAME }} + AZURE_DEVOPS_ARTIFACT_TOKEN: ${{ secrets.AZURE_DEVOPS_ARTIFACT_TOKEN }} + run: | + if [ -z "AZURE_DEVOPS_ARTIFACT_USERNAME" ]; then + echo "::warning::AZURE_DEVOPS_ARTIFACT_USERNAME is null or not set" + fi + + if [ -z "$AZURE_DEVOPS_ARTIFACT_TOKEN" ]; then + echo "::warning::AZURE_DEVOPS_ARTIFACT_TOKEN is null or not set" + fi + + echo "Publishing artefact for version: $ARTEFACT_VERSION" + + gradle publish \ + -DARTEFACT_VERSION=$ARTEFACT_VERSION \ + -DGITHUB_REPOSITORY=${{ github.repository }} \ + -DGITHUB_ACTOR=${{ github.actor }} \ + -DGITHUB_TOKEN=$GITHUB_TOKEN \ + -DAZURE_DEVOPS_ARTIFACT_USERNAME=$AZURE_DEVOPS_ARTIFACT_USERNAME \ + -DAZURE_DEVOPS_ARTIFACT_TOKEN=$AZURE_DEVOPS_ARTIFACT_TOKEN + + Build-Docker: + needs: [ Provider-Deploy, Build, Artefact-Version ] + if: ${{ inputs.trigger_docker }} + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Download JAR Artefact + uses: actions/download-artifact@v6 + with: + name: app-jar + path: build/libs + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Packages + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and Push Docker Image to GitHub + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile + push: true + tags: | + ghcr.io/${{ github.repository }}:${{ needs.Artefact-Version.outputs.artefact_version }} + build-args: | + BASE_IMAGE=openjdk:21-jdk-slim + JAR_FILENAME=${{ needs.Build.outputs.artefact_name }}.jar + + Deploy: + needs: [ Provider-Deploy, Build, Artefact-Version ] + if: ${{ inputs.trigger_deploy }} + runs-on: ubuntu-latest + steps: + - name: Extract repo name + run: echo "REPO_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Trigger ADO pipeline + uses: hmcts/trigger-ado-pipeline@v1 + with: + pipeline_id: 460 + ado_pat: ${{ secrets.HMCTS_ADO_PAT }} + template_parameters: > + { + "GROUP_ID": "uk.gov.hmcts.cp", + "ARTIFACT_ID": "${{ env.REPO_NAME }}", + "ARTIFACT_VERSION": "${{ needs.Artefact-Version.outputs.artefact_version }}", + "TARGET_REPOSITORY": "${{ github.repository }}" + } + diff --git a/.github/workflows/ci-draft.yml b/.github/workflows/ci-draft.yml new file mode 100644 index 00000000..5316ed78 --- /dev/null +++ b/.github/workflows/ci-draft.yml @@ -0,0 +1,23 @@ +name: Build and Publish (Non-Release) + +on: + pull_request: + branches: + - master + - main + push: + branches: + - master + - main + +jobs: + ci-draft: + uses: ./.github/workflows/ci-build-publish.yml + secrets: + AZURE_DEVOPS_ARTIFACT_USERNAME: ${{ secrets.AZURE_DEVOPS_ARTIFACT_USERNAME }} + AZURE_DEVOPS_ARTIFACT_TOKEN: ${{ secrets.AZURE_DEVOPS_ARTIFACT_TOKEN }} + HMCTS_ADO_PAT: ${{ secrets.HMCTS_CP_ADO_PAT }} + with: + is_publish: ${{ github.event_name == 'push' }} + trigger_docker: ${{ github.event_name == 'push' }} + trigger_deploy: ${{ github.event_name == 'push' }} diff --git a/.github/workflows/ci-released.yml b/.github/workflows/ci-released.yml new file mode 100644 index 00000000..81d9e3cd --- /dev/null +++ b/.github/workflows/ci-released.yml @@ -0,0 +1,19 @@ +name: CI Build and Publish – Release + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + ci-release: + uses: ./.github/workflows/ci-build-publish.yml + secrets: + AZURE_DEVOPS_ARTIFACT_USERNAME: ${{ secrets.AZURE_DEVOPS_ARTIFACT_USERNAME }} + AZURE_DEVOPS_ARTIFACT_TOKEN: ${{ secrets.AZURE_DEVOPS_ARTIFACT_TOKEN }} + HMCTS_ADO_PAT: ${{ secrets.HMCTS_CP_ADO_PAT }} + with: + is_release: true + is_publish: true + trigger_docker: true + trigger_deploy: true diff --git a/.github/workflows/code-analysis.yml b/.github/workflows/code-analysis.yml new file mode 100644 index 00000000..86b6f381 --- /dev/null +++ b/.github/workflows/code-analysis.yml @@ -0,0 +1,36 @@ +name: Code analysis + +on: + pull_request: + branches: + - master + - main + +jobs: + pmd-analysis: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '21' + + - uses: pmd/pmd-github-action@v2 + id: pmd + with: + rulesets: '.github/pmd-ruleset.xml' + sourcePath: 'src/main/java' + analyzeModifiedFilesOnly: false + + - name: Fail build if there are violations + if: always() + run: | + VIOLATIONS="${{ steps.pmd.outputs.violations }}" + if [ -n "$VIOLATIONS" ] && [ "$VIOLATIONS" != "0" ]; then + echo "PMD found $VIOLATIONS violations" + exit 1 + fi diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..c0fe9c93 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,121 @@ +name: CodeQL + +on: + pull_request: + branches: + - master + - main + + schedule: + - cron: '36 5 * * 4' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + language: [ 'java' ] + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + with: + languages: ${{ matrix.language }} + queries: security-extended + + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v5 + with: + gradle-version: current + + - name: Gradle Build + run: | + gradle build cyclonedxBom -x test + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + # If the (auto)build fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" + + - name: Log generated SBOM Hash + run: sha256sum build/resources/main/META-INF/sbom/application.cdx.json || true + + # This ensures: + # - The SBOM is archived with the CodeQL scan output + # - It's available to download and inspect from the GitHub Actions UI + - name: Upload SBOM + if: always() + uses: actions/upload-artifact@v5 + with: + name: sbom + path: build/resources/main/META-INF/sbom/application.cdx.json + + DAST: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: 21 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v5 + with: + gradle-version: current + + - name: Gradle Build + run: gradle build -x test -x api + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: DAST - Build and run containerised app + run: | + docker compose -f docker/docker-compose.yml up -d + + echo "Waiting for health endpoint..." + for i in {1..30}; do + if curl -s http://localhost:8082/health > /dev/null; then + echo "App is healthy" + break + fi + echo "Waiting for app to be healthy ($i)..." + sleep 2 + done + + - name: Run OWASP ZAP DAST Scan + uses: zaproxy/action-baseline@v0.15.0 + with: + target: "http://localhost:8082" + cmd_options: "-a -J zap_report.json -r zap_report.html" + + - name: Upload ZAP HTML Report + uses: actions/upload-artifact@v5 + with: + name: zap-html-report + path: zap_report.html diff --git a/.github/workflows/secrets-scanner.yml b/.github/workflows/secrets-scanner.yml new file mode 100644 index 00000000..121e0713 --- /dev/null +++ b/.github/workflows/secrets-scanner.yml @@ -0,0 +1,23 @@ +name: Secrets Scanner +on: + pull_request: + branches: + - master + - main + schedule: + - cron: '0 4 * * 4' # Every Thursday at 04:00 + workflow_dispatch: + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: hmcts/secrets-scanner@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + gitleaks_license: ${{ secrets.GITLEAKS_LICENSE }} + gitleaks_regex_internal_url: ${{ secrets.HMCTS_CP_GITLEAKS_REGEX_INTERNAL_URL }} diff --git a/README.md b/README.md index 812ee308..90af7556 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # API HMCTS -The Court Hearing Cases API provides details of a criminal court case results +The Court Hearing Cases API publishes events relating criminal court cases ### Contribute to This Repository diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..44052908 --- /dev/null +++ b/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'application' + id 'java' + id 'org.springframework.boot' version '4.0.0' + id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' + id 'maven-publish' + id 'com.github.ben-manes.versions' version '0.53.0' + id 'org.cyclonedx.bom' version '2.4.1' + id 'com.gorylenko.gradle-git-properties' version '2.5.3' + id 'com.avast.gradle.docker-compose' version '0.17.12' +} + +group = 'uk.gov.hmcts.cp' +version = System.getProperty('ARTEFACT_VERSION') ?: '0.0.999' + +apply { + from("$rootDir/gradle/dependencies/java-core.gradle") + from("$rootDir/gradle/dependencies/spring-core.gradle") + + from("$rootDir/gradle/github/repositories.gradle") + from("$rootDir/gradle/github/java.gradle") + from("$rootDir/gradle/github/dependency.gradle") + from("$rootDir/gradle/github/pmd.gradle") + from("$rootDir/gradle/github/test.gradle") + from("$rootDir/gradle/github/jar.gradle") + + from("$rootDir/gradle/tasks/apitest.gradle") + from("$rootDir/gradle/tasks/docker.gradle") +} + +springBoot { + buildInfo { + properties { + name = project.name + version = project.version.toString() + } + } +} + +dependencies { + implementation('uk.gov.hmcts.cp:api-cp-crime-courthearing-cases-results:0.0.0-a65d290') +} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 00000000..b78ded9c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,10 @@ +# Dockerfile (project root) +FROM eclipse-temurin:21 + +WORKDIR /app + +COPY build/libs/*.jar /app/ + +EXPOSE ${SERVER_PORT:-8082} + +ENTRYPOINT ["sh","-c","exec java -jar $(ls /app/*.jar | grep -v 'plain' | head -n1)"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..c8116301 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,10 @@ +services: + app: + container_name: app + build: + context: .. + dockerfile: docker/Dockerfile + ports: + - "8082:8082" + environment: + SERVER_PORT: 8082 diff --git a/gradle/dependencies/java-core.gradle b/gradle/dependencies/java-core.gradle new file mode 100644 index 00000000..27b07839 --- /dev/null +++ b/gradle/dependencies/java-core.gradle @@ -0,0 +1,21 @@ +ext { + log4JVersion = "2.24.3" + logbackVersion = "1.5.18" + lombokVersion = "1.18.38" + mapstructVersion = "1.5.5.Final" +} + +dependencies { + implementation 'net.logstash.logback:logstash-logback-encoder:8.1' + implementation 'org.apache.logging.log4j:log4j-to-slf4j' + implementation 'ch.qos.logback:logback-classic' + implementation 'ch.qos.logback:logback-core' + + compileOnly group: 'org.projectlombok', name: 'lombok', version: lombokVersion + annotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion + testCompileOnly group: 'org.projectlombok', name: 'lombok', version: lombokVersion + testAnnotationProcessor group: 'org.projectlombok', name: 'lombok', version: lombokVersion + + implementation "org.mapstruct:mapstruct:$mapstructVersion" + annotationProcessor "org.mapstruct:mapstruct-processor:$mapstructVersion" +} diff --git a/gradle/dependencies/spring-core.gradle b/gradle/dependencies/spring-core.gradle new file mode 100644 index 00000000..6c6d3f8c --- /dev/null +++ b/gradle/dependencies/spring-core.gradle @@ -0,0 +1,11 @@ +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-aspectj' + + testImplementation "org.springframework.boot:spring-boot-starter-webmvc-test" + + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'junit', module: 'junit' + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } +} \ No newline at end of file diff --git a/gradle/github/dependency.gradle b/gradle/github/dependency.gradle new file mode 100644 index 00000000..09621b82 --- /dev/null +++ b/gradle/github/dependency.gradle @@ -0,0 +1,11 @@ +// check dependencies upon release ONLY +tasks.named("dependencyUpdates").configure { + def isNonStable = { String version -> + def stableKeyword = ['RELEASE', 'FINAL', 'GA'].any { qualifier -> version.toUpperCase().contains(qualifier) } + def regex = /^[0-9,.v-]+$/ + return !stableKeyword && !(version ==~ regex) + } + rejectVersionIf { + isNonStable(it.candidate.version) && !isNonStable(it.currentVersion) + } +} diff --git a/gradle/github/jar.gradle b/gradle/github/jar.gradle new file mode 100644 index 00000000..ab1810db --- /dev/null +++ b/gradle/github/jar.gradle @@ -0,0 +1,34 @@ +jar { + enabled = true + archiveClassifier.set('plain') + manifest { + attributes( + 'Implementation-Title': project.name, + 'Implementation-Version': project.version.toString() + ) + } + if (file("CHANGELOG.md").exists()) { + from('CHANGELOG.md') { + into 'META-INF' + } + } else { + println "âš ī¸ CHANGELOG.md not found, skipping inclusion in JAR" + } +} + +bootJar { + archiveFileName = "${rootProject.name}-${project.version}.jar" + + manifest { + attributes('Implementation-Version': project.version.toString()) + } +} + +tasks.named('composeBuild') { + dependsOn tasks.named('bootJar') +} + +tasks.withType(AbstractArchiveTask).configureEach { + preserveFileTimestamps = false + reproducibleFileOrder = true +} diff --git a/gradle/github/java.gradle b/gradle/github/java.gradle new file mode 100644 index 00000000..d7b4c7d2 --- /dev/null +++ b/gradle/github/java.gradle @@ -0,0 +1,12 @@ +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} +tasks.withType(JavaCompile).configureEach { + options.compilerArgs << "-Xlint:unchecked" << "-Werror" +} + +// https://github.com/gradle/gradle/issues/16791 +tasks.withType(JavaExec).configureEach { + javaLauncher.set(javaToolchains.launcherFor(java.toolchain)) +} diff --git a/gradle/github/pmd.gradle b/gradle/github/pmd.gradle new file mode 100644 index 00000000..3804534c --- /dev/null +++ b/gradle/github/pmd.gradle @@ -0,0 +1,29 @@ +apply plugin: 'pmd' + +pmd { + ruleSets = [] + ruleSetFiles = files(".github/pmd-ruleset.xml") + ignoreFailures = false +} + +tasks.named("pmdMain").configure { + onlyIf { gradle.startParameter.taskNames.contains(name) } +} + +tasks.named("pmdTest").configure { + enabled = false +} + +tasks.withType(Pmd) { + reports { + xml.required.set(true) + html.required.set(true) + } +} + +tasks.withType(Checkstyle).configureEach { + def generatedDir = file("${layout.buildDirectory.get().asFile.absolutePath}/generated/src/main/java").canonicalPath + source = source.filter { file -> + !file.canonicalPath.startsWith(generatedDir) + } +} diff --git a/gradle/github/repositories.gradle b/gradle/github/repositories.gradle new file mode 100644 index 00000000..c149d032 --- /dev/null +++ b/gradle/github/repositories.gradle @@ -0,0 +1,46 @@ +def githubActor = project.findProperty("github.actor") ?: System.getenv("GITHUB_ACTOR") +def githubToken = project.findProperty("github.token") ?: System.getenv("GITHUB_TOKEN") +def githubRepo = System.getenv("GITHUB_REPOSITORY") + +def azureADOArtifactRepository = 'https://pkgs.dev.azure.com/hmcts/Artifacts/_packaging/hmcts-lib/maven/v1' +def azureADOArtifactActor = System.getenv("AZURE_DEVOPS_ARTIFACT_USERNAME") +def azureADOArtifactToken = System.getenv("AZURE_DEVOPS_ARTIFACT_TOKEN") + +repositories { + mavenLocal() + mavenCentral() + maven { + url = azureADOArtifactRepository + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifact(tasks.named('bootJar')) + artifact(tasks.named('jar')) + pom { + name = project.name + url = "https://github.com/${githubRepo ?: 'org/repo'}" + } + } + } + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/$githubRepo") + credentials { + username = githubActor + password = githubToken + } + } + maven { + name = "AzureArtifacts" + url = uri(azureADOArtifactRepository) + credentials { + username = azureADOArtifactActor + password = azureADOArtifactToken + } + } + } +} \ No newline at end of file diff --git a/gradle/github/test.gradle b/gradle/github/test.gradle new file mode 100644 index 00000000..a37c3592 --- /dev/null +++ b/gradle/github/test.gradle @@ -0,0 +1,33 @@ +tasks.named('test') { + useJUnitPlatform { + } + systemProperty 'API_SPEC_VERSION', project.version + failFast = true + // Mockito must be added as an agent, see: + // https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3 + jvmArgs += [ + "-javaagent:${configurations.testRuntimeClasspath.find { it.name.contains('mockito-core') }}", '-Xshare:off' + ] + testLogging { + events "passed", "skipped", "failed" + exceptionFormat = 'full' + showStandardStreams = true + } + reports { + junitXml.required.set(true) + html.required.set(true) + } +} + +tasks.named('jacocoTestReport') { + dependsOn tasks.named('test') + executionData fileTree( + dir: layout.buildDirectory.dir("jacoco").get().asFile, + include: ["*.exec"] + ) + reports { + xml.required.set(true) + csv.required.set(false) + html.required.set(true) + } +} \ No newline at end of file diff --git a/gradle/tasks/apitest.gradle b/gradle/tasks/apitest.gradle new file mode 100644 index 00000000..6d23937e --- /dev/null +++ b/gradle/tasks/apitest.gradle @@ -0,0 +1,34 @@ +tasks.register('api', Test) { + description = "Runs api tests against docker-compose stack" + group = "Verification" + + mustRunAfter tasks.named('test') + + testClassesDirs = sourceSets.apiTest.output.classesDirs + classpath = sourceSets.apiTest.runtimeClasspath + useJUnitPlatform() + + dependsOn tasks.composeUp + finalizedBy tasks.composeDown +} + +tasks.named('check') { + dependsOn tasks.named('api') +} + +tasks.named('build') { + dependsOn tasks.named('test') + dependsOn tasks.named('api') +} + +sourceSets { + apiTest { + java { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + } + } +} + +dependencies { +} diff --git a/gradle/tasks/docker.gradle b/gradle/tasks/docker.gradle new file mode 100644 index 00000000..97085af8 --- /dev/null +++ b/gradle/tasks/docker.gradle @@ -0,0 +1,18 @@ +dockerCompose { + useComposeFiles = ['docker/docker-compose.yml'] + startedServices = ['app'] + + buildBeforeUp = true + waitForTcpPorts = true + upAdditionalArgs = ['--wait', '--wait-timeout', '30'] + + captureContainersOutput = true + removeOrphans = true + stopContainers = true + removeContainers = true + + projectName = "${rootProject.name}" + + useDockerComposeV2 = true + dockerExecutable = 'docker' +} \ No newline at end of file diff --git a/src/main/java/uk/gov/hmcts/cp/Application.java b/src/main/java/uk/gov/hmcts/cp/Application.java new file mode 100644 index 00000000..20276053 --- /dev/null +++ b/src/main/java/uk/gov/hmcts/cp/Application.java @@ -0,0 +1,13 @@ +package uk.gov.hmcts.cp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication + +public class Application { + + public static void main(final String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/uk/gov/hmcts/cp/controllers/RootController.java b/src/main/java/uk/gov/hmcts/cp/controllers/RootController.java new file mode 100644 index 00000000..09bd774c --- /dev/null +++ b/src/main/java/uk/gov/hmcts/cp/controllers/RootController.java @@ -0,0 +1,19 @@ +package uk.gov.hmcts.cp.controllers; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@Slf4j +public class RootController { + + @GetMapping("/") + public ResponseEntity getRoot() { + log.info("START"); + return ResponseEntity.ok("Hello"); + } +} diff --git a/src/test/java/uk/gov/hmcts/cp/controllers/RootControllerTest.java b/src/test/java/uk/gov/hmcts/cp/controllers/RootControllerTest.java new file mode 100644 index 00000000..96392b5a --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/controllers/RootControllerTest.java @@ -0,0 +1,7 @@ +package uk.gov.hmcts.cp.controllers; + +import static org.junit.jupiter.api.Assertions.*; + +class RootControllerTest { + +} \ No newline at end of file diff --git a/src/test/java/uk/gov/hmcts/cp/controllers/integration/IntegrationTestBase.java b/src/test/java/uk/gov/hmcts/cp/controllers/integration/IntegrationTestBase.java new file mode 100644 index 00000000..1ffe987b --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/controllers/integration/IntegrationTestBase.java @@ -0,0 +1,16 @@ +package uk.gov.hmcts.cp.controllers.integration; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc +@Slf4j +public abstract class IntegrationTestBase { + + @Resource + protected MockMvc mockMvc; +} diff --git a/src/test/java/uk/gov/hmcts/cp/controllers/integration/RootControllerIntegrationTest.java b/src/test/java/uk/gov/hmcts/cp/controllers/integration/RootControllerIntegrationTest.java new file mode 100644 index 00000000..8ae04ed5 --- /dev/null +++ b/src/test/java/uk/gov/hmcts/cp/controllers/integration/RootControllerIntegrationTest.java @@ -0,0 +1,15 @@ +package uk.gov.hmcts.cp.controllers.integration; + +import org.junit.jupiter.api.Test; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class RootControllerIntegrationTest extends IntegrationTestBase { + + @Test + void root_endpoint_should_be_ok() throws Exception { + mockMvc.perform(get("/")) + .andExpect(status().isOk()); + } +} \ No newline at end of file