Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80314f22d0 | ||
|
|
c9c414d83f | ||
|
|
27c7455870 | ||
|
|
42d5eb286f | ||
|
|
25b04dd86b | ||
|
|
62e2f5536d | ||
|
|
1111f961dd | ||
|
|
95e5336e50 | ||
|
|
2c43861d49 | ||
|
|
ba0a204d75 | ||
|
|
405ebfec34 | ||
|
|
9102d88a13 | ||
|
|
573bdf39ba | ||
|
|
b08bed7977 | ||
|
|
48b9eec4be | ||
|
|
68dcb033a4 | ||
|
|
ca75291b39 | ||
|
|
1c3e6a927d | ||
|
|
3bb21e5315 | ||
|
|
19d17350fd | ||
|
|
75b2b87379 |
@@ -1,23 +1,20 @@
|
||||
{
|
||||
"name": "Tasknote Dev",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:20",
|
||||
"image": "mcr.microsoft.com/devcontainers/java:25-trixie",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/java:1": {
|
||||
"version": "25",
|
||||
"jdkDistro": "temurin"
|
||||
"installMaven": true,
|
||||
"version": "latest",
|
||||
"jdkDistro": "tem",
|
||||
"gradleVersion": "latest",
|
||||
"mavenVersion": "latest",
|
||||
"antVersion": "latest",
|
||||
"groovyVersion": "latest"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||
"ghcr.io/devcontainers/features/terraform:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"vscjava.vscode-java-pack",
|
||||
"hashicorp.terraform",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"installYarnUsingApt": true,
|
||||
"version": "lts",
|
||||
"pnpmVersion": "latest",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "npm install -g @nestjs/cli typescript fastify class-validator"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Build App Candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push-app:
|
||||
name: Build & Push App
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/app
|
||||
tags: |
|
||||
type=raw,value=candidate
|
||||
|
||||
- name: Generate version tag
|
||||
id: version
|
||||
run: |
|
||||
DATE=$(date +'%Y.%m.%d')
|
||||
TAG="app-v${DATE}.${{ github.run_number }}"
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "Generated tag: ${TAG}"
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./client
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VITE_BUILD=${{ steps.version.outputs.tag }}
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Build Server Candidate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push-server:
|
||||
name: Build & Push Server
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.ref }}
|
||||
|
||||
- name: Set lowercase repo name
|
||||
id: repo
|
||||
run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
cache: 'maven'
|
||||
cache-dependency-path: 'server/pom.xml'
|
||||
|
||||
- name: Log in to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cache Buildpack layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/reproducible-builds
|
||||
key: ${{ runner.os }}-buildpack-${{ hashFiles('server/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildpack-
|
||||
|
||||
- name: Build Docker image
|
||||
working-directory: ./server
|
||||
run: |
|
||||
# Use the dynamic repo name to prevent tagging errors
|
||||
./mvnw -Pnative -DskipTests spring-boot:build-image \
|
||||
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
|
||||
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
|
||||
|
||||
- name: Tag and push candidate
|
||||
run: |
|
||||
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
|
||||
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Deploy to K3s
|
||||
name: Main CD-Deploy to Prod
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -13,10 +13,8 @@ on:
|
||||
description: "Apply changes after plan"
|
||||
required: false
|
||||
default: "true"
|
||||
push:
|
||||
branches: [ main ]
|
||||
workflow_run:
|
||||
workflows: [ "Backend Build & Push", "Frontend Build & Push" ]
|
||||
workflows: [ "Main CI-Backend", "Main CI-Frontend" ]
|
||||
types: [ completed ]
|
||||
|
||||
jobs:
|
||||
@@ -118,6 +116,9 @@ jobs:
|
||||
echo "no_changes=true" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes to apply."
|
||||
exit 0
|
||||
else
|
||||
echo "Changes detected. Proceeding with apply"
|
||||
echo "no_changes=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Upload plan artifact
|
||||
@@ -134,6 +135,7 @@ jobs:
|
||||
&& needs.terraform-plan.outputs.no_changes == 'false'
|
||||
environment:
|
||||
name: production
|
||||
url: https://tasknote.darkroasted.vps-kinghost.net
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
@@ -0,0 +1,137 @@
|
||||
name: Pull Request CD-Deploy to Staging
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_run:
|
||||
workflows: [ "Pull Request CI-Backend", "Pull Request CI-Frontend" ]
|
||||
types: [ completed ]
|
||||
|
||||
jobs:
|
||||
terraform-plan-stg:
|
||||
name: Plan changs to staging
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
no_changes: ${{ steps.check-changes.outputs.no_changes }}
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v4
|
||||
|
||||
- name: Setup Kubeconfig
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
|
||||
- name: Validate cluster access
|
||||
run: |
|
||||
kubectl cluster-info
|
||||
kubectl get namespace tasknote-stg
|
||||
|
||||
- name: Determine deployment values
|
||||
id: deploy-vars
|
||||
run: |
|
||||
backend_image="ghcr.io/rmcampos/tasknote/api:candidate"
|
||||
frontend_image="ghcr.io/rmcampos/tasknote/app:candidate"
|
||||
|
||||
echo "backend_image=$backend_image" >> "$GITHUB_OUTPUT"
|
||||
echo "frontend_image=$frontend_image" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Terraform Fmt -check -diff
|
||||
working-directory: terraform-stg
|
||||
run: terraform fmt -check -diff
|
||||
|
||||
- name: Terraform Init
|
||||
working-directory: terraform-stg
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
run: terraform init -input=false
|
||||
|
||||
- name: Terraform Validate
|
||||
working-directory: terraform-stg
|
||||
run: terraform validate
|
||||
|
||||
- name: Terraform Plan
|
||||
id: check-changes
|
||||
working-directory: terraform-stg
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
run: |
|
||||
timeout 1m terraform plan -input=false -out=tfplan \
|
||||
-var="db_user=${{ secrets.DB_USER }}" \
|
||||
-var="db_password=${{ secrets.DB_PASSWORD }}" \
|
||||
-var="db_name=${{ secrets.DB_NAME }}" \
|
||||
-var="security_key=${{ secrets.JWT_SECURITY_KEY }}" \
|
||||
-var="mailgun_apikey=${{ secrets.MAILGUN_API_KEY }}" \
|
||||
-var="backend_image=${{ steps.deploy-vars.outputs.backend_image }}" \
|
||||
-var="frontend_image=${{ steps.deploy-vars.outputs.frontend_image }}" \
|
||||
-var="deploy_version=${{ github.run_id }}"
|
||||
terraform show -json tfplan > tfplan.json
|
||||
if jq -e '.resource_changes | length == 0' tfplan.json >/dev/null; then
|
||||
echo "no_changes=true" >> "$GITHUB_OUTPUT"
|
||||
echo "No changes to apply."
|
||||
exit 0
|
||||
else
|
||||
echo "Changes detected. Proceeding with apply"
|
||||
echo "no_changes=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Upload plan artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: tfplan
|
||||
path: terraform-stg/tfplan
|
||||
|
||||
terraform-apply:
|
||||
runs-on: ubuntu-latest
|
||||
needs: terraform-plan-stg
|
||||
if: needs.terraform-plan-stg.outputs.no_changes == 'false'
|
||||
environment:
|
||||
name: staging
|
||||
url: https://tasknote-stg.darkroasted.vps-kinghost.net
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Terraform
|
||||
uses: hashicorp/setup-terraform@v3
|
||||
|
||||
- name: Download plan artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: tfplan
|
||||
path: terraform-stg
|
||||
|
||||
- name: Setup Kubeconfig
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
|
||||
chmod 600 ~/.kube/config
|
||||
|
||||
- name: Terraform Init
|
||||
working-directory: terraform-stg
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
run: terraform init -input=false
|
||||
|
||||
- name: Terraform Apply
|
||||
working-directory: terraform-stg
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
run: timeout 1m terraform apply tfplan
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Backend Build & Push
|
||||
name: Main CI-Backend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Backend Build & Push
|
||||
name: Build & Push
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -69,18 +69,29 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image with Spring Boot
|
||||
working-directory: ./server
|
||||
run: |
|
||||
./mvnw -Pnative -DskipTests spring-boot:build-image \
|
||||
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
|
||||
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Tag and push Docker image
|
||||
- name: Find PR number
|
||||
id: find_pr
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }}
|
||||
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:latest
|
||||
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }}
|
||||
PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "No merged PR found for this commit. Falling back to 'candidate' tag."
|
||||
PR_NUMBER="candidate"
|
||||
else
|
||||
PR_NUMBER="pr-${PR_NUMBER}"
|
||||
fi
|
||||
echo "tag=${PR_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Promote Docker image
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
|
||||
--tag ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }} \
|
||||
ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.find_pr.outputs.tag }}
|
||||
|
||||
- name: Create and push Git tag
|
||||
run: |
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Frontend Build & Push
|
||||
name: Main CI-Frontend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Frontend Build & Push
|
||||
name: Build & Push
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -31,6 +31,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set lowercase repo name
|
||||
id: repo
|
||||
run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate version tag
|
||||
id: version
|
||||
run: |
|
||||
@@ -49,26 +53,26 @@ jobs:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/app
|
||||
tags: |
|
||||
type=raw,value=${{ steps.version.outputs.tag }}
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
- name: Find PR number
|
||||
id: find_pr
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
echo "No merged PR found for this commit. Falling back to 'candidate' tag."
|
||||
PR_NUMBER="candidate"
|
||||
else
|
||||
PR_NUMBER="pr-${PR_NUMBER}"
|
||||
fi
|
||||
echo "tag=${PR_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./client
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VITE_BUILD=v${{ steps.version.outputs.tag }}
|
||||
- name: Promote Docker image
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ steps.repo.outputs.name }}/app:latest \
|
||||
--tag ghcr.io/${{ steps.repo.outputs.name }}/app:${{ steps.version.outputs.tag }} \
|
||||
ghcr.io/${{ steps.repo.outputs.name }}/app:${{ steps.find_pr.outputs.tag }}
|
||||
|
||||
- name: Create and push Git tag
|
||||
run: |
|
||||
@@ -0,0 +1,129 @@
|
||||
name: Pull Request CI-Backend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'server/**/*.java'
|
||||
- 'server/**/*.xml'
|
||||
- 'server/pom.xml'
|
||||
- '.github/workflows/server-ci.yml'
|
||||
|
||||
jobs:
|
||||
run-checks:
|
||||
name: Checks
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
cache: 'maven'
|
||||
cache-dependency-path: 'server/pom.xml'
|
||||
|
||||
- name: Run Check Style
|
||||
working-directory: ./server
|
||||
run: ./mvnw --no-transfer-progress checkstyle:check -Dcheckstyle.skip=false
|
||||
|
||||
- name: Run build
|
||||
working-directory: ./server
|
||||
run: ./mvnw --no-transfer-progress clean compile -DskipTests
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./server
|
||||
run: ./mvnw --no-transfer-progress clean verify -P tests --file pom.xml
|
||||
|
||||
build-and-push:
|
||||
name: Build & Push
|
||||
runs-on: ubuntu-latest
|
||||
needs: ["run-checks"]
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set lowercase repo name
|
||||
id: repo
|
||||
run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
cache: 'maven'
|
||||
cache-dependency-path: 'server/pom.xml'
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Cache Buildpack layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/reproducible-builds
|
||||
key: ${{ runner.os }}-buildpack-${{ hashFiles('server/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildpack-
|
||||
|
||||
- name: Build Docker image with Spring Boot
|
||||
working-directory: ./server
|
||||
run: |
|
||||
./mvnw -Pnative -DskipTests spring-boot:build-image \
|
||||
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
|
||||
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
|
||||
|
||||
- name: Tag and push Docker image
|
||||
run: |
|
||||
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
|
||||
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:pr-${{ github.event.pull_request.number }}
|
||||
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
|
||||
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:pr-${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Create GitHub deployment for staging
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const ref = context.payload.pull_request.head.sha;
|
||||
const env = 'staging';
|
||||
const resp = await github.rest.repos.createDeployment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref,
|
||||
required_contexts: [],
|
||||
environment: env,
|
||||
description: `PR #${context.payload.pull_request.number} preview deployment`,
|
||||
transient_environment: true,
|
||||
auto_merge: false
|
||||
});
|
||||
// create a deployment status pointing to the staging URL
|
||||
await github.rest.repos.createDeploymentStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
deployment_id: resp.data.id,
|
||||
state: 'success',
|
||||
environment_url: 'https://tasknote-stg.darkroasted.vps-kinghost.net'
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
name: Pull Request CI-Frontend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'client/**/*.html'
|
||||
- 'client/**/*.png'
|
||||
- 'client/**/*.json'
|
||||
- 'client/**/*.txt'
|
||||
- 'client/**/*.ts'
|
||||
- 'client/**/*.tsx'
|
||||
- 'client/**/*.js'
|
||||
- 'client/Dockerfile'
|
||||
- 'client/Caddyfile'
|
||||
- '.github/workflows/client-ci.yml'
|
||||
|
||||
jobs:
|
||||
run-checks:
|
||||
name: Checks
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
working-directory: ./client
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
working-directory: ./client
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:no-watch
|
||||
working-directory: ./client
|
||||
|
||||
build-and-push:
|
||||
name: Build & Push
|
||||
runs-on: ubuntu-latest
|
||||
needs: ["run-checks"]
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
deployments: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}/app
|
||||
tags: |
|
||||
type=raw,value=candidate
|
||||
type=raw,value=pr-${{ github.event.pull_request.number }}
|
||||
|
||||
- name: Generate version tag
|
||||
id: version
|
||||
run: |
|
||||
DATE=$(date +'%Y.%m.%d')
|
||||
TAG="app-v${DATE}.${{ github.run_number }}"
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "Generated tag: ${TAG}"
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./client
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VITE_BUILD=${{ steps.version.outputs.tag }}
|
||||
|
||||
- name: Create GitHub deployment for staging
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const ref = context.payload.pull_request.head.sha;
|
||||
const env = 'staging';
|
||||
const resp = await github.rest.repos.createDeployment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref,
|
||||
required_contexts: [],
|
||||
environment: env,
|
||||
description: `PR #${context.payload.pull_request.number} preview deployment`,
|
||||
transient_environment: true,
|
||||
auto_merge: false
|
||||
});
|
||||
// create a deployment status pointing to the staging URL
|
||||
await github.rest.repos.createDeploymentStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
deployment_id: resp.data.id,
|
||||
state: 'success',
|
||||
environment_url: 'https://tasknote-stg.darkroasted.vps-kinghost.net'
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
name: Frontend CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- 'client/**/*.html'
|
||||
- 'client/**/*.png'
|
||||
- 'client/**/*.json'
|
||||
- 'client/**/*.txt'
|
||||
- 'client/**/*.ts'
|
||||
- 'client/**/*.tsx'
|
||||
- 'client/**/*.js'
|
||||
- 'client/Dockerfile'
|
||||
- 'client/Caddyfile'
|
||||
- '.github/workflows/client-ci.yml'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Frontend CI
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
working-directory: ./client
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint
|
||||
working-directory: ./client
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
working-directory: ./client
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:no-watch
|
||||
working-directory: ./client
|
||||
@@ -1,45 +0,0 @@
|
||||
name: Backend CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# run for all pushes, not only main
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- 'server/**/*.java'
|
||||
- 'server/**/*.xml'
|
||||
- 'server/pom.xml'
|
||||
- '.github/workflows/server-ci.yml'
|
||||
|
||||
jobs:
|
||||
run-checks:
|
||||
name: Backend CI
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
cache: 'maven'
|
||||
|
||||
- name: Run Check Style
|
||||
working-directory: ./server
|
||||
run: ./mvnw --no-transfer-progress checkstyle:check -Dcheckstyle.skip=false
|
||||
|
||||
- name: Run build
|
||||
working-directory: ./server
|
||||
run: ./mvnw --no-transfer-progress clean compile -DskipTests
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./server
|
||||
run: ./mvnw --no-transfer-progress clean verify -P tests --file pom.xml
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { isSafeUrl } from '../../utils/UrlUtils';
|
||||
|
||||
describe('UrlUtils', () => {
|
||||
it('should allow http:// URLs', () => {
|
||||
expect(isSafeUrl('http://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow https:// URLs', () => {
|
||||
expect(isSafeUrl('https://example.com')).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow # URLs', () => {
|
||||
expect(isSafeUrl('#section')).toBe(true);
|
||||
});
|
||||
|
||||
it('should disallow javascript: URLs', () => {
|
||||
expect(isSafeUrl('javascript:alert(1)')).toBe(false);
|
||||
});
|
||||
|
||||
it('should disallow data: URLs', () => {
|
||||
expect(isSafeUrl('data:text/html,<script>alert(1)</script>')).toBe(false);
|
||||
});
|
||||
|
||||
it('should disallow empty or null URLs', () => {
|
||||
expect(isSafeUrl('')).toBe(false);
|
||||
expect(isSafeUrl(null)).toBe(false);
|
||||
expect(isSafeUrl(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should be case insensitive for protocol', () => {
|
||||
expect(isSafeUrl('HTTP://example.com')).toBe(true);
|
||||
expect(isSafeUrl('HTTPS://example.com')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { env } from '../env';
|
||||
|
||||
const server = env.VITE_BACKEND_SERVER;
|
||||
const server = env.VITE_BACKEND_SERVER || '/api';
|
||||
|
||||
const ApiConfig = {
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ExternalLinkIcon from '../../assets/icons8-external-link-30.png';
|
||||
import { isSafeUrl } from '../../utils/UrlUtils';
|
||||
|
||||
interface Props {
|
||||
readonly title: string;
|
||||
@@ -18,8 +19,8 @@ function NoteTitle(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
<span className="task-title-icon">
|
||||
<span className="poppins-semibold">
|
||||
{props.title}
|
||||
{props.noteUrl && props.noteUrl.length > 0 && (
|
||||
<a href={props.noteUrl} target="_blank" rel="noreferrer" className="task-note-external-link">
|
||||
{isSafeUrl(props.noteUrl) && (
|
||||
<a href={props.noteUrl!} target="_blank" rel="noreferrer" className="task-note-external-link">
|
||||
<img src={ExternalLinkIcon} width={20} alt="external link" />
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import ExternalLinkIcon from '../../assets/icons8-external-link-30.png';
|
||||
import { isSafeUrl } from '../../utils/UrlUtils';
|
||||
import './style.css';
|
||||
|
||||
interface Props {
|
||||
@@ -24,7 +25,7 @@ function TaskTitle(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
data-testid={`task-title-text-${props.title}`}
|
||||
>
|
||||
{props.title}
|
||||
{props.taskUrl && props.taskUrl.length > 0 && (
|
||||
{props.taskUrl && props.taskUrl.length > 0 && isSafeUrl(props.taskUrl[0]) && (
|
||||
<a href={props.taskUrl[0]} target="_blank" rel="noreferrer" className="task-note-external-link">
|
||||
<img src={ExternalLinkIcon} width={20} alt="external link" />
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Validates if a URL is safe to be used in an <a> tag.
|
||||
* Only allows http, https, and # (for internal links/placeholders).
|
||||
*
|
||||
* @param {string | null | undefined} url The URL to validate.
|
||||
* @returns {boolean} True if the URL is safe, false otherwise.
|
||||
*/
|
||||
export function isSafeUrl(url: string | null | undefined): boolean {
|
||||
if (!url) {
|
||||
return false;
|
||||
}
|
||||
const safeProtocolRegex = /^(https?:\/\/|#)/i;
|
||||
return safeProtocolRegex.test(url);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import remarkGfm from 'remark-gfm';
|
||||
import { NoteResponse } from '../../types/NoteResponse';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
import { isSafeUrl } from '../../utils/UrlUtils';
|
||||
|
||||
/**
|
||||
* SharedNote component for displaying a publicly shared note.
|
||||
@@ -80,9 +81,9 @@ function SharedNote(): React.ReactNode {
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<Card.Title>{note.title}</Card.Title>
|
||||
{note.url && (
|
||||
{isSafeUrl(note.url) && (
|
||||
<p>
|
||||
<a href={note.url} target="_blank" rel="noopener noreferrer">
|
||||
<a href={note.url!} target="_blank" rel="noopener noreferrer">
|
||||
{note.url}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -11,8 +11,6 @@ services:
|
||||
context: ./client
|
||||
dockerfile: Dockerfile
|
||||
ports: ["5000:5000"]
|
||||
environment:
|
||||
VITE_BACKEND_SERVER: http://localhost:8585
|
||||
networks:
|
||||
- tasknote-network
|
||||
|
||||
@@ -27,7 +25,7 @@ services:
|
||||
POSTGRES_USER: tasknoteuser
|
||||
POSTGRES_PASSWORD: default
|
||||
POSTGRES_PORT: 5432
|
||||
CORS_ALLOWED_ORIGINS: http://tasknote-web:5000, http://localhost:5000
|
||||
CORS_ALLOWED_ORIGINS: http://tasknote-web:5000, http://localhost:5000, https://flattop-depth-dropper.ngrok-free.dev
|
||||
SERVER_SERVLET_CONTEXT_PATH: /
|
||||
TARGET_ENV: development
|
||||
SECURITY_KEY: this-is-a-very-long-security-key-for-dev
|
||||
@@ -62,4 +60,4 @@ services:
|
||||
|
||||
networks:
|
||||
tasknote-network:
|
||||
driver: bridge
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
events {}
|
||||
http {
|
||||
server {
|
||||
listen 8181;
|
||||
server_name _;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://tasknote-api:8585/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://tasknote-web:5000/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
ngrok http 8181 --log=stdout > ngrok-8181.log 2>&1 &
|
||||
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
docker run -d \
|
||||
--name ngrok-tasknote-proxy \
|
||||
-p 127.0.0.1:8181:8181 \
|
||||
-v ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
|
||||
--restart unless-stopped \
|
||||
--network tasknote-network \
|
||||
nginx:stable
|
||||
+2
-18
@@ -1,19 +1,3 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
wrapperVersion=3.3.2
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
|
||||
|
||||
Vendored
+44
-8
@@ -8,7 +8,7 @@
|
||||
# "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
|
||||
# http://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
|
||||
@@ -19,7 +19,7 @@
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
@@ -105,14 +105,17 @@ trim() {
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
@@ -130,7 +133,7 @@ maven-mvnd-*bin.*)
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
@@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
@@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
|
||||
Vendored
+189
-149
@@ -1,149 +1,189 @@
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
|
||||
+17
-10
@@ -5,13 +5,13 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.0.3</version>
|
||||
<version>4.0.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>br.com.tasknoteapp</groupId>
|
||||
<artifactId>server</artifactId>
|
||||
<version>15</version>
|
||||
<version>19</version>
|
||||
<name>tasknote-api</name>
|
||||
<description>Java backend REST API to serve TaskNote frontend client</description>
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
<jacoco.output.data>${project.build.directory}/coverage-reports</jacoco.output.data>
|
||||
<timestamp>${maven.build.timestamp}</timestamp>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
|
||||
<failsafe.version>3.5.5</failsafe.version>
|
||||
<surefire.version>3.5.5</surefire.version>
|
||||
<jacoco.version>0.8.14</jacoco.version>
|
||||
<checkstyle.version>3.6.0</checkstyle.version>
|
||||
<springboot.version>4.0.5</springboot.version>
|
||||
<jjwt.version>0.12.6</jjwt.version>
|
||||
</properties>
|
||||
|
||||
<!-- Profiles -->
|
||||
@@ -143,17 +149,17 @@
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-gson</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
@@ -181,7 +187,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>3.5.5</version>
|
||||
<version>${failsafe.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>integration-tests</id>
|
||||
@@ -203,7 +209,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.5</version>
|
||||
<version>${surefire.version}</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -Xmx1024m</argLine>
|
||||
<skipTests>${skip.unit.tests}</skipTests>
|
||||
@@ -215,7 +221,7 @@
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.14</version>
|
||||
<version>${jacoco.version}</version>
|
||||
<configuration>
|
||||
<skip>${jacoco.skip}</skip>
|
||||
<excludes>
|
||||
@@ -324,7 +330,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-checkstyle-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>${checkstyle.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
@@ -358,7 +364,8 @@
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${springboot.version}</version>
|
||||
<configuration>
|
||||
<image>
|
||||
<name>ghcr.io/rmcampos/tasknote/api:latest</name>
|
||||
|
||||
@@ -55,6 +55,9 @@ public class UserEntity implements UserDetails {
|
||||
@Column(name = "lang", nullable = true, length = 6)
|
||||
private String lang;
|
||||
|
||||
@Column(name = "last_password_change", nullable = false)
|
||||
private LocalDateTime lastPasswordChange;
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of();
|
||||
@@ -182,4 +185,12 @@ public class UserEntity implements UserDetails {
|
||||
public void setLang(String lang) {
|
||||
this.lang = lang;
|
||||
}
|
||||
|
||||
public LocalDateTime getLastPasswordChange() {
|
||||
return lastPasswordChange;
|
||||
}
|
||||
|
||||
public void setLastPasswordChange(LocalDateTime lastPasswordChange) {
|
||||
this.lastPasswordChange = lastPasswordChange;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
package br.com.tasknoteapp.server.request;
|
||||
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
/** This record represents a note patch payload. */
|
||||
public record NotePatchRequest(String title, String description, String url, String tag) {}
|
||||
public record NotePatchRequest(
|
||||
String title,
|
||||
String description,
|
||||
@Pattern(
|
||||
regexp = "^(https?://.*|#.*)?$",
|
||||
message = "URL must start with http://, https:// or #")
|
||||
String url,
|
||||
String tag) {}
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
package br.com.tasknoteapp.server.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
|
||||
/** This record represents a note request to be created. */
|
||||
public record NoteRequest(
|
||||
@NotNull String title, @NotNull String description, String url, String tag) {}
|
||||
@NotNull String title,
|
||||
@NotNull String description,
|
||||
@Pattern(
|
||||
regexp = "^(https?://.*|#.*)?$",
|
||||
message = "URL must start with http://, https:// or #")
|
||||
String url,
|
||||
String tag) {}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
package br.com.tasknoteapp.server.request;
|
||||
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.util.List;
|
||||
|
||||
/** This record represents a task patch payload. */
|
||||
public record TaskPatchRequest(
|
||||
String description,
|
||||
Boolean done,
|
||||
List<String> urls,
|
||||
List<
|
||||
@Pattern(
|
||||
regexp = "^(https?://.*|#.*)?$",
|
||||
message = "URL must start with http://, https:// or #")
|
||||
String>
|
||||
urls,
|
||||
String dueDate,
|
||||
Boolean highPriority,
|
||||
String tag) {}
|
||||
|
||||
@@ -2,12 +2,18 @@ package br.com.tasknoteapp.server.request;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.util.List;
|
||||
|
||||
/** This record represents a task request to be created. */
|
||||
public record TaskRequest(
|
||||
@NotNull @NotEmpty String description,
|
||||
List<String> urls,
|
||||
List<
|
||||
@Pattern(
|
||||
regexp = "^(https?://.*|#.*)?$",
|
||||
message = "URL must start with http://, https:// or #")
|
||||
String>
|
||||
urls,
|
||||
String dueDate,
|
||||
Boolean highPriority,
|
||||
String tag) {}
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -131,7 +132,8 @@ public class AuthService {
|
||||
user.setEmail(newUser.email());
|
||||
user.setPassword(passwordEncoder.encode(newUser.password()));
|
||||
user.setAdmin(false);
|
||||
user.setCreatedAt(LocalDateTime.now());
|
||||
user.setCreatedAt(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
||||
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
||||
user.setEmailUuid(emailUuid);
|
||||
user.setLang(newUser.lang());
|
||||
userRepository.save(user);
|
||||
@@ -332,6 +334,7 @@ public class AuthService {
|
||||
}
|
||||
|
||||
currentUser.setPassword(passwordEncoder.encode(patchRequest.password()));
|
||||
currentUser.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
||||
shouldUpdate = true;
|
||||
}
|
||||
|
||||
@@ -433,7 +436,8 @@ public class AuthService {
|
||||
|
||||
UserEntity user = userOptional.get();
|
||||
user.setResetToken(resetToken);
|
||||
user.setResetPasswordExpiration(LocalDateTime.now().plusHours(2L));
|
||||
user.setResetPasswordExpiration(
|
||||
LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS).plusHours(2L));
|
||||
|
||||
userRepository.save(user);
|
||||
if (hasValidMailgunApiKey()) {
|
||||
@@ -477,6 +481,7 @@ public class AuthService {
|
||||
user.setResetToken(null);
|
||||
user.setResetPasswordExpiration(null);
|
||||
user.setPassword(passwordEncoder.encode(request.password()));
|
||||
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
||||
|
||||
userRepository.save(user);
|
||||
if (hasValidMailgunApiKey()) {
|
||||
|
||||
@@ -29,10 +29,10 @@ import org.springframework.web.client.RestTemplate;
|
||||
@Service
|
||||
public class MailgunEmailService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MailgunEmailService.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(MailgunEmailService.class.getName());
|
||||
private final RestTemplate restTemplate;
|
||||
private final String targetEnv;
|
||||
private String domain;
|
||||
private final String domain;
|
||||
private String senderEmail;
|
||||
|
||||
/**
|
||||
@@ -73,6 +73,8 @@ public class MailgunEmailService {
|
||||
String subject = "TaskNote App confirmation email";
|
||||
String link = getBaseUrl() + "/email-confirmation?identification=%s";
|
||||
|
||||
logger.info("New user link: {}", link);
|
||||
|
||||
MailgunTemplateSignUp signUpTemplate = new MailgunTemplateSignUp();
|
||||
signUpTemplate.setConfirmationLink(String.format(link, user.getEmailUuid().toString()));
|
||||
|
||||
@@ -91,6 +93,8 @@ public class MailgunEmailService {
|
||||
String subject = "TaskNote App password reset";
|
||||
String link = getBaseUrl() + "/finish-reset-password?token=%s";
|
||||
|
||||
logger.info("Password reset link: {}", link);
|
||||
|
||||
MailgunTemplateResetPwd resetTemplate = new MailgunTemplateResetPwd();
|
||||
resetTemplate.setResetLink(String.format(link, user.getResetToken()));
|
||||
|
||||
@@ -184,7 +188,10 @@ public class MailgunEmailService {
|
||||
if ("development".equals(targetEnv) || Objects.isNull(targetEnv)) {
|
||||
return "http://localhost:5000";
|
||||
}
|
||||
String stage = targetEnv.equals("stage") ? "stage." : "";
|
||||
return String.format("https://%s%s", stage, domain);
|
||||
String baseUrl = domain;
|
||||
if (targetEnv.equals("staging")) {
|
||||
baseUrl = "tasknote-stg" + domain.substring(8);
|
||||
}
|
||||
return String.format("https://%s", baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,14 @@ class JwtServiceImpl implements JwtService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private LocalDateTime extractIssuedAt(String token) {
|
||||
Date date = extractClaim(token, Claims::getIssuedAt);
|
||||
if (!Objects.isNull(date)) {
|
||||
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateToken(UserEntity user) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
@@ -91,7 +99,18 @@ class JwtServiceImpl implements JwtService {
|
||||
@Override
|
||||
public boolean validateTokenAndUser(String token, UserDetails user) {
|
||||
final String email = user.getUsername();
|
||||
return !isTokenExpired(token) && email.equals(getEmailFromToken(token));
|
||||
boolean basicValid = !isTokenExpired(token) && email.equals(getEmailFromToken(token));
|
||||
|
||||
if (basicValid && user instanceof UserEntity userEntity) {
|
||||
LocalDateTime iat = extractIssuedAt(token);
|
||||
if (iat != null && userEntity.getLastPasswordChange() != null) {
|
||||
// Token must be issued after or at the same time as last password change
|
||||
// We use isBefore to invalidate tokens issued BEFORE the change
|
||||
return !iat.isBefore(userEntity.getLastPasswordChange());
|
||||
}
|
||||
}
|
||||
|
||||
return basicValid;
|
||||
}
|
||||
|
||||
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE tasknote.users ADD COLUMN last_password_change TIMESTAMP WITHOUT TIME ZONE;
|
||||
|
||||
-- Initialize for existing users
|
||||
UPDATE tasknote.users SET last_password_change = created_at WHERE last_password_change IS NULL;
|
||||
|
||||
ALTER TABLE tasknote.users ALTER COLUMN last_password_change SET NOT NULL;
|
||||
@@ -276,7 +276,7 @@ class TaskControllerTest {
|
||||
"""
|
||||
{
|
||||
"description": "Test task",
|
||||
"urls": ["www.url.com"],
|
||||
"urls": ["https://www.url.com"],
|
||||
"highPriority": true,
|
||||
"tag": "tag"
|
||||
}
|
||||
|
||||
@@ -166,6 +166,30 @@ class JwtServiceImplTest {
|
||||
assertFalse(valid);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateTokenAndUser_shouldReturnFalseIfTokenIssuedBeforeLastPasswordChange()
|
||||
throws InterruptedException {
|
||||
UserEntity user = new UserEntity();
|
||||
user.setId(testUserId);
|
||||
user.setEmail(testEmail);
|
||||
user.setAdmin(false);
|
||||
user.setName(testName);
|
||||
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
||||
|
||||
// Token issued NOW
|
||||
String token = jwtService.generateToken(user);
|
||||
|
||||
// Update lastPasswordChange to FUTURE (simulating a password change after token issuance)
|
||||
// We wait 1 second to ensure the new timestamp is strictly after token iat (which has second
|
||||
// precision)
|
||||
Thread.sleep(1100);
|
||||
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
|
||||
|
||||
boolean valid = jwtService.validateTokenAndUser(token, user);
|
||||
|
||||
assertFalse(valid, "Token issued before password change should be invalid");
|
||||
}
|
||||
|
||||
private Claims extractClaims(String token) {
|
||||
return Jwts.parser().verifyWith(getKey()).build().parseSignedClaims(token).getPayload();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Create test user
|
||||
insert into users (email, password, admin, created_at, inactivated_at)
|
||||
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null
|
||||
insert into users (email, password, admin, created_at, inactivated_at, last_password_change)
|
||||
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, current_timestamp
|
||||
where not exists (select 1 from users where email = 'test@domain.com');
|
||||
|
||||
-- Create some tasks
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Create test user
|
||||
insert into users (email, password, admin, created_at, inactivated_at)
|
||||
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null
|
||||
insert into users (email, password, admin, created_at, inactivated_at, last_password_change)
|
||||
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, current_timestamp
|
||||
where not exists (select 1 from users where email = 'test@domain.com');
|
||||
|
||||
-- Create a task
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
-- Create test user
|
||||
insert into users (email, password, admin, created_at, inactivated_at, email_uuid, reset_token)
|
||||
select 'testuuid@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, 'cc2b5506-83ed-5764-985e-611ad4ce8050', 'abc123456'
|
||||
insert into users (email, password, admin, created_at, inactivated_at, email_uuid, reset_token, last_password_change)
|
||||
select 'testuuid@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, 'cc2b5506-83ed-5764-985e-611ad4ce8050', 'abc123456', current_timestamp
|
||||
where not exists (select 1 from users where email = 'testuuid@domain.com');
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = ">= 2.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {
|
||||
bucket = "tasknote-stg"
|
||||
key = "kubernetes/terraform.tfstate"
|
||||
region = "auto"
|
||||
endpoints = { s3 = "https://d17eb09b6bce2f90e16e800bb2a6baf9.r2.cloudflarestorage.com" }
|
||||
skip_credentials_validation = true
|
||||
skip_region_validation = true
|
||||
skip_requesting_account_id = true
|
||||
skip_metadata_api_check = true
|
||||
skip_s3_checksum = true
|
||||
}
|
||||
}
|
||||
|
||||
provider "kubernetes" {
|
||||
config_path = "~/.kube/config"
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "db_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "security_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "mailgun_apikey" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cors_allowed_origins" {
|
||||
type = string
|
||||
default = "https://tasknote-stg.darkroasted.vps-kinghost.net"
|
||||
}
|
||||
|
||||
variable "root_log_level" {
|
||||
type = string
|
||||
default = "INFO"
|
||||
}
|
||||
|
||||
variable "backend_image" {
|
||||
type = string
|
||||
default = "ghcr.io/rmcampos/tasknote/api:candidate"
|
||||
}
|
||||
|
||||
variable "frontend_image" {
|
||||
type = string
|
||||
default = "ghcr.io/rmcampos/tasknote/app:candidate"
|
||||
}
|
||||
|
||||
variable "deploy_version" {
|
||||
type = string
|
||||
default = "manual"
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace_v1" "tasknote_stg" {
|
||||
metadata {
|
||||
name = "tasknote-stg"
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_secret_v1" "tasknote_stg_secrets" {
|
||||
metadata {
|
||||
name = "tasknote-stg-secrets"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
|
||||
data = {
|
||||
postgres_user = var.db_user
|
||||
postgres_password = var.db_password
|
||||
postgres_db = var.db_name
|
||||
security_key = var.security_key
|
||||
mailgun_apikey = var.mailgun_apikey
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim_v1" "tasknote_stg_db_data" {
|
||||
metadata {
|
||||
name = "postgres-data-pvc"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
resources {
|
||||
requests = {
|
||||
storage = "1Gi"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment_v1" "tasknote_stg_db" {
|
||||
metadata {
|
||||
name = "tasknote-stg-db"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
replicas = 1
|
||||
selector { match_labels = { app = "tasknote-stg-db" } }
|
||||
template {
|
||||
metadata { labels = { app = "tasknote-stg-db" } }
|
||||
spec {
|
||||
container {
|
||||
image = "postgres:15.8-bookworm"
|
||||
name = "postgres"
|
||||
volume_mount {
|
||||
name = "postgres-storage"
|
||||
mount_path = "/var/lib/postgresql/data"
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_USER"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "postgres_user"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_PASSWORD"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "postgres_password"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_DB"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "postgres_db"
|
||||
}
|
||||
}
|
||||
}
|
||||
port { container_port = 5432 }
|
||||
}
|
||||
volume {
|
||||
name = "postgres-storage"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim_v1.tasknote_stg_db_data.metadata[0].name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service_v1" "tasknote_stg_db_svc" {
|
||||
metadata {
|
||||
name = "tasknote-stg-db-svc"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
selector = { app = "tasknote-stg-db" }
|
||||
port { port = 5432 }
|
||||
type = "ClusterIP"
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment_v1" "tasknote_stg_backend" {
|
||||
metadata {
|
||||
name = "tasknote-stg-backend"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
replicas = 1
|
||||
selector { match_labels = { app = "tasknote-stg-backend" } }
|
||||
template {
|
||||
metadata {
|
||||
labels = { app = "tasknote-stg-backend" }
|
||||
annotations = {
|
||||
"deploy_id" = var.deploy_version
|
||||
}
|
||||
}
|
||||
spec {
|
||||
container {
|
||||
image = var.backend_image
|
||||
name = "backend"
|
||||
image_pull_policy = "Always"
|
||||
env {
|
||||
name = "POSTGRES_DB"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "postgres_db"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_HOST"
|
||||
value = "tasknote-stg-db-svc"
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_USER"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "postgres_user"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_PASSWORD"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "postgres_password"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_PORT"
|
||||
value = "5432"
|
||||
}
|
||||
env {
|
||||
name = "CORS_ALLOWED_ORIGINS"
|
||||
value = var.cors_allowed_origins
|
||||
}
|
||||
env {
|
||||
name = "SERVER_SERVLET_CONTEXT_PATH"
|
||||
value = "/"
|
||||
}
|
||||
env {
|
||||
name = "ROOT_LOG_LEVEL"
|
||||
value = var.root_log_level
|
||||
}
|
||||
env {
|
||||
name = "SECURITY_KEY"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "security_key"
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "TARGET_ENV"
|
||||
value = "staging"
|
||||
}
|
||||
env {
|
||||
name = "MAILGUN_APIKEY"
|
||||
value_from {
|
||||
secret_key_ref {
|
||||
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
|
||||
key = "mailgun_apikey"
|
||||
}
|
||||
}
|
||||
}
|
||||
resources {
|
||||
limits = { memory = "256Mi", cpu = "500m" }
|
||||
requests = { memory = "256Mi", cpu = "250m" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service_v1" "tasknote_stg_backend_svc" {
|
||||
metadata {
|
||||
name = "tasknote-stg-backend-svc"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
selector = { app = "tasknote-stg-backend" }
|
||||
port {
|
||||
port = 8585
|
||||
target_port = 8585
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment_v1" "tasknote_stg_frontend" {
|
||||
metadata {
|
||||
name = "tasknote-stg-frontend"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
replicas = 1
|
||||
selector { match_labels = { app = "tasknote-stg-app" } }
|
||||
template {
|
||||
metadata {
|
||||
labels = { app = "tasknote-stg-app" }
|
||||
annotations = {
|
||||
"deploy_id" = var.deploy_version
|
||||
}
|
||||
}
|
||||
spec {
|
||||
container {
|
||||
image = var.frontend_image
|
||||
name = "frontend"
|
||||
image_pull_policy = "Always"
|
||||
port { container_port = 5000 }
|
||||
env {
|
||||
name = "VITE_BACKEND_SERVER"
|
||||
value = "https://tasknoteapi-stg.darkroasted.vps-kinghost.net"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service_v1" "tasknote_stg_frontend_svc" {
|
||||
metadata {
|
||||
name = "tasknote-stg-frontend-svc"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
}
|
||||
spec {
|
||||
selector = { app = "tasknote-stg-app" }
|
||||
port {
|
||||
port = 5000
|
||||
target_port = 5000
|
||||
}
|
||||
type = "ClusterIP"
|
||||
}
|
||||
}
|
||||
|
||||
# Unified Ingress for App and API
|
||||
resource "kubernetes_ingress_v1" "tasknote_stg_ingress" {
|
||||
metadata {
|
||||
name = "tasknote-stg-ingress"
|
||||
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
|
||||
annotations = {
|
||||
"kubernetes.io/ingress.class" = "traefik"
|
||||
"cert-manager.io/cluster-issuer" = "letsencrypt-prod"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
tls {
|
||||
hosts = ["tasknote-stg.darkroasted.vps-kinghost.net", "tasknoteapi-stg.darkroasted.vps-kinghost.net"]
|
||||
secret_name = "tasknote-stg-tls-certs"
|
||||
}
|
||||
rule {
|
||||
host = "tasknote-stg.darkroasted.vps-kinghost.net"
|
||||
http {
|
||||
path {
|
||||
path = "/"
|
||||
path_type = "Prefix"
|
||||
backend {
|
||||
service {
|
||||
name = kubernetes_service_v1.tasknote_stg_frontend_svc.metadata[0].name
|
||||
port { number = 5000 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rule {
|
||||
host = "tasknoteapi-stg.darkroasted.vps-kinghost.net"
|
||||
http {
|
||||
path {
|
||||
path = "/"
|
||||
path_type = "Prefix"
|
||||
backend {
|
||||
service {
|
||||
name = kubernetes_service_v1.tasknote_stg_backend_svc.metadata[0].name
|
||||
port { number = 8585 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+119
@@ -0,0 +1,119 @@
|
||||
#!/bin/bash
|
||||
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
if [ ! -f "$CURRENT_DIR/pom.xml" ]; then
|
||||
cd "$CURRENT_DIR/server"
|
||||
fi
|
||||
|
||||
#
|
||||
# Get latest version of the `maven-failsafe-plugin` plugin
|
||||
#
|
||||
LATEST_FAILSAFE=$(
|
||||
curl -s https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-failsafe-plugin/maven-metadata.xml \
|
||||
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
|
||||
| sed -E 's/<\/?version>//g' \
|
||||
| sort -V \
|
||||
| tail -n 1
|
||||
)
|
||||
|
||||
# Get current version from pom.xml, reading from the `failsafe.version` property
|
||||
CURRENT_FAILSAFE=$(./mvnw help:evaluate -Dexpression=failsafe.version -q -DforceStdout 2>/dev/null)
|
||||
|
||||
if [ "$LATEST_FAILSAFE" != "$CURRENT_FAILSAFE" ]; then
|
||||
echo "The maven-failsafe-plugin is outdated. Current version: $CURRENT_FAILSAFE, Latest version: $LATEST_FAILSAFE"
|
||||
exit 1
|
||||
else
|
||||
echo "The maven-failsafe-plugin is up to date. Current version: $CURRENT_FAILSAFE"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Get latest version of the `maven-surefire-plugin` plugin
|
||||
#
|
||||
LATEST_SUREFIRE=$(
|
||||
curl -s https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-surefire-plugin/maven-metadata.xml \
|
||||
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
|
||||
| sed -E 's/<\/?version>//g' \
|
||||
| sort -V \
|
||||
| tail -n 1
|
||||
)
|
||||
|
||||
# Get current version from pom.xml, reading from the `surefire.version` property
|
||||
CURRENT_SUREFIRE=$(./mvnw help:evaluate -Dexpression=surefire.version -q -DforceStdout 2>/dev/null)
|
||||
|
||||
if [ "$LATEST_SUREFIRE" != "$CURRENT_SUREFIRE" ]; then
|
||||
echo "The maven-surefire-plugin is outdated. Current version: $CURRENT_SUREFIRE, Latest version: $LATEST_SUREFIRE"
|
||||
exit 1
|
||||
else
|
||||
echo "The maven-surefire-plugin is up to date. Current version: $CURRENT_SUREFIRE"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Get latest version of the `maven-jacoco-plugin` plugin
|
||||
#
|
||||
LATEST_JACOCO=$(
|
||||
curl -s https://repo1.maven.org/maven2/org/jacoco/jacoco-maven-plugin/maven-metadata.xml \
|
||||
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
|
||||
| sed -E 's/<\/?version>//g' \
|
||||
| sort -V \
|
||||
| tail -n 1
|
||||
)
|
||||
|
||||
# Get current version from pom.xml, reading from the `jacoco.version` property
|
||||
CURRENT_JACOCO=$(./mvnw help:evaluate -Dexpression=jacoco.version -q -DforceStdout 2>/dev/null)
|
||||
if [ "$LATEST_JACOCO" != "$CURRENT_JACOCO" ]; then
|
||||
echo "The maven-jacoco-plugin is outdated. Current version: $CURRENT_JACOCO, Latest version: $LATEST_JACOCO"
|
||||
exit 1
|
||||
else
|
||||
echo "The maven-jacoco-plugin is up to date. Current version: $CURRENT_JACOCO"
|
||||
fi
|
||||
|
||||
|
||||
# --
|
||||
#
|
||||
# Get latest version of the `maven-checkstyle-plugin` plugin
|
||||
#
|
||||
LATEST_CHECKSTYLE=$(
|
||||
curl -s https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-checkstyle-plugin/maven-metadata.xml \
|
||||
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
|
||||
| sed -E 's/<\/?version>//g' \
|
||||
| sort -V \
|
||||
| tail -n 1
|
||||
)
|
||||
|
||||
# Get current version from pom.xml, reading from the `checkstyle.version` property
|
||||
CURRENT_CHECKSTYLE=$(./mvnw help:evaluate -Dexpression=checkstyle.version -q -DforceStdout 2> /dev/null)
|
||||
if [ "$LATEST_CHECKSTYLE" != "$CURRENT_CHECKSTYLE" ]; then
|
||||
echo "The maven-checksytle-plugin is outdated. Current version: $CURRENT_CHECKSTYLE, Latest version: $LATEST_CHECKSTYLE"
|
||||
exit 1
|
||||
else
|
||||
echo "The maven-cehckstyle-plugin is up to date. Current version: $CURRENT_CHECKSTYLE"
|
||||
fi
|
||||
|
||||
|
||||
# --
|
||||
|
||||
#
|
||||
# Get latest version of the Spring Boot Starter Web plugin
|
||||
#
|
||||
LATEST_SPRINGBOOT=$(
|
||||
curl -s https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-maven-plugin/maven-metadata.xml \
|
||||
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
|
||||
| sed -E 's/<\/?version>//g' \
|
||||
| sort -V \
|
||||
| tail -n 1
|
||||
)
|
||||
|
||||
# Get current version from pom.xml, reading from the `springboot.version` property
|
||||
CURRENT_SPRINGBOOT=$(./mvnw help:evaluate -Dexpression=springboot.version -q -DforceStdout 2>/dev/null)
|
||||
if [ "$LATEST_SPRINGBOOT" != "$CURRENT_SPRINGBOOT" ]; then
|
||||
echo "Spring Boot is outdated. Current version: $CURRENT_SPRINGBOOT, Latest version: $LATEST_SPRINGBOOT"
|
||||
exit 1
|
||||
else
|
||||
echo "Spring Boot is up to date. Current version: $CURRENT_SPRINGBOOT"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user