Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
749ddbcdad | ||
|
|
294e1b7a6a | ||
|
|
58dabc75a6
|
||
|
|
dd15837d4d | ||
|
|
a79e79c04f
|
||
|
|
488586be48
|
||
|
|
8901084fa9
|
||
|
|
466e255594
|
||
|
|
300b2e0576 | ||
|
|
0250b558cf |
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: client-updater
|
||||
description: A bot that updates the client software to the latest version.
|
||||
tools: [run_shell_command, read_file, write_file, replace, list_directory, glob, grep_search, ask_user]
|
||||
---
|
||||
# Instructions
|
||||
1. Navigate to the `client` directory.
|
||||
2. Execute the following command to check for updates:
|
||||
`npx npm-check-updates --target minor`
|
||||
3. Display the resulting list of available minor updates to the user.
|
||||
4. If there are packages to update:
|
||||
- Run `npx npm-check-updates --target minor -u` to update `package.json`.
|
||||
- Run `npm install` to update the `package-lock.json` and install the new versions.
|
||||
5. Run the validation script to ensure stability:
|
||||
`../tools/check-frontend.sh`
|
||||
6. If the validation passes:
|
||||
- Create a new branch (e.g., `update-deps-[date]`).
|
||||
- Commit the changes to `package.json` and `package-lock.json`.
|
||||
- Push and create a Pull Request (using `gh pr create` if available).
|
||||
7. If validation fails (lint, build, or test issues):
|
||||
- Diagnose the failure using `grep_search` and `read_file`.
|
||||
- Fix the issues, then retry the validation and PR steps.
|
||||
8. Report the final status and the PR link to the user.
|
||||
@@ -1,12 +0,0 @@
|
||||
description = "Create a formatted GitHub issue"
|
||||
prompt = """
|
||||
Act as a project manager. Based on the following input: {{args}}
|
||||
Create a GitHub issue using the `gh` CLI tool with this exact structure:
|
||||
- Title: [Feat/Bug] <short summary>
|
||||
- Body: A detailed description, technical requirements, and acceptance criteria.
|
||||
- Label: Automatically determine if it's 'bug', 'enhencement', or 'task'.
|
||||
|
||||
Once the user confirms the plan, execute the command:
|
||||
`gh issue create --title "[Type] Title" --body "..." --label "..."
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
name: Backend CD
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'server/**'
|
||||
- '.github/workflows/cd-backend.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: graalvm-25
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache Maven dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/server/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
|
||||
- name: Generate version tag
|
||||
id: version
|
||||
run: |
|
||||
DATE=$(date +'%Y.%m.%d')
|
||||
TAG="api-v${DATE}.${{ github.run_number }}"
|
||||
echo "tag=${TAG}" >> $GITHUB_OUTPUT
|
||||
echo "Generated tag: ${TAG}"
|
||||
|
||||
- name: Build Docker image with Spring Boot
|
||||
working-directory: ./server
|
||||
run: |
|
||||
./mvnw -Pnative -DskipTests spring-boot:build-image \
|
||||
-Dspring-boot.build-image.imageName=rmcampos/tasknote-api:latest \
|
||||
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:0.0.505
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: rmcampos/tasknote-api
|
||||
tags: |
|
||||
type=raw,value=${{ steps.version.outputs.tag }}
|
||||
type=raw,value=latest,enable={{ is_default_branch }}
|
||||
|
||||
- name: Tag and push Docker image
|
||||
run: |
|
||||
docker tag docker.io/rmcampos/tasknote-api:latest docker.io/rmcampos/tasknote-api:${{ steps.version.outputs.tag }}
|
||||
docker push docker.io/rmcampos/tasknote-api:latest
|
||||
docker push docker.io/rmcampos/tasknote-api:${{ steps.version.outputs.tag }}
|
||||
|
||||
- name: Output Docker image URLs
|
||||
run: |
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "🚀 Docker Images Published Successfully!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "API Image:"
|
||||
echo " rmcampos/tasknote-api:${{ steps.version.outputs.tag }}"
|
||||
echo " rmcampos/tasknote-api:latest"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
|
||||
- name: Create and push Git tag
|
||||
run: |
|
||||
git config user.name "gitea-actions[bot]"
|
||||
git config user.email "gitea-actions[bot]@noreply.lightroasted.vps-kinghost.net"
|
||||
git tag -a ${{ steps.version.outputs.tag }} -m "Release ${{ steps.version.outputs.tag }}"
|
||||
git push origin ${{ steps.version.outputs.tag }}
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Frontend CD
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths:
|
||||
- 'client/**'
|
||||
- '.github/workflows/frontend-cd.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: easynode-debian
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/client/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-npm-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
working-directory: ./client
|
||||
|
||||
- name: Run build
|
||||
run: npm run build
|
||||
working-directory: ./client
|
||||
|
||||
- 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: Set up Docker Buildx
|
||||
run: docker buildx inspect --bootstrap
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: rmcampos/tasknote-app
|
||||
tags: |
|
||||
type=raw,value=${{ steps.version.outputs.tag }}
|
||||
type=raw,value=latest,enable={{ is_default_branch }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./client
|
||||
push: true
|
||||
tags: |
|
||||
${{ steps.meta.outputs.tags }}
|
||||
rmcampos/tasknote-app:${{ steps.version.outputs.tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=registry,ref=rmcampos/tasknote-app:buildcache
|
||||
cache-to: type=registry,ref=rmcampos/tasknote-app:buildcache,mode=max
|
||||
build-args: |
|
||||
VITE_BUILD=${{ steps.version.outputs.tag }}
|
||||
|
||||
- name: Output Docker image URLs
|
||||
run: |
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "🚀 Docker Images Published Successfully!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "App Image:"
|
||||
echo " rmcampos/tasknote-app:${{ steps.version.outputs.tag }}"
|
||||
echo " rmcampos/tasknote-app:latest"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
|
||||
- name: Create and push Git tag
|
||||
run: |
|
||||
git config user.name "gitea-actions[bot]"
|
||||
git config user.email "gitea-actions[bot]@noreply.lightroasted.vps-kinghost.net"
|
||||
git tag -a ${{ steps.version.outputs.tag }} -m "Release ${{ steps.version.outputs.tag }}"
|
||||
git push origin ${{ steps.version.outputs.tag }}
|
||||
@@ -0,0 +1,74 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['**/*']
|
||||
|
||||
jobs:
|
||||
build-backend:
|
||||
name: Build Backend
|
||||
runs-on: graalvm-25
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache Maven dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/server/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-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
|
||||
|
||||
build-frontend:
|
||||
name: Build Frontend
|
||||
runs-on: easynode-debian
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache npm dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-npm-${{ hashFiles('**/client/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-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,4 +1,8 @@
|
||||
name: Main CD-Deploy to Prod
|
||||
name: Deploy to Prod
|
||||
|
||||
concurrency:
|
||||
group: deploy-production
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -14,7 +18,7 @@ on:
|
||||
required: false
|
||||
default: "true"
|
||||
workflow_run:
|
||||
workflows: [ "Main CI-Backend", "Main CI-Frontend" ]
|
||||
workflows: [ "Backend CD", "Frontend CD" ]
|
||||
types: [ completed ]
|
||||
|
||||
jobs:
|
||||
@@ -37,10 +41,15 @@ jobs:
|
||||
- name: Setup kubectl
|
||||
uses: azure/setup-kubectl@v4
|
||||
|
||||
- name: Setup Doppler CLI
|
||||
uses: dopplerhq/cli-action@v4
|
||||
|
||||
- name: Setup Kubeconfig
|
||||
env:
|
||||
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
|
||||
doppler run --config prd -- bash -c 'echo "$KUBECONFIG_DATA" | base64 -d > ~/.kube/config'
|
||||
chmod 600 ~/.kube/config
|
||||
|
||||
- name: Validate cluster access
|
||||
@@ -54,25 +63,17 @@ jobs:
|
||||
backend_image="${{ github.event.inputs.backend_image }}"
|
||||
frontend_image="${{ github.event.inputs.frontend_image }}"
|
||||
|
||||
latest_backend_tag_tmp="$(git tag --list 'api-v*' | sort -V | tail -n1)"
|
||||
latest_backend_tag="${latest_backend_tag_tmp#api-v}"
|
||||
latest_backend_tag="$(git tag --list 'api-v*' | sort -V | tail -n1)"
|
||||
echo "latest backend tag=$latest_backend_tag"
|
||||
|
||||
latest_frontend_tag="$(git tag --list 'app-v*' | sort -V | tail -n1)"
|
||||
|
||||
if [ -z "$latest_backend_tag" ]; then
|
||||
echo "No backend tag found matching [0-9]*" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$latest_frontend_tag" ]; then
|
||||
echo "No frontend tag found matching app-v*" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "latest frontend tag=$latest_frontend_tag"
|
||||
|
||||
if [ -z "$backend_image" ]; then
|
||||
backend_image="rmcampos/tasknote-api:$latest_backend_tag"
|
||||
backend_image="docker.io/rmcampos/tasknote-api:$latest_backend_tag"
|
||||
fi
|
||||
if [ -z "$frontend_image" ]; then
|
||||
frontend_image="rmcampos/tasknote-app:$latest_frontend_tag"
|
||||
frontend_image="docker.io/rmcampos/tasknote-app:$latest_frontend_tag"
|
||||
fi
|
||||
|
||||
echo "Resolved backend_image=$backend_image"
|
||||
@@ -88,9 +89,8 @@ jobs:
|
||||
- name: Terraform Init
|
||||
working-directory: terraform
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
run: terraform init -input=false
|
||||
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
|
||||
run: doppler run --config prd -- terraform init -input=false
|
||||
|
||||
- name: Terraform Validate
|
||||
working-directory: terraform
|
||||
@@ -100,19 +100,22 @@ jobs:
|
||||
id: check-changes
|
||||
working-directory: terraform
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
|
||||
BACKEND_IMAGE: ${{ steps.deploy-vars.outputs.backend_image }}
|
||||
FRONTEND_IMAGE: ${{ steps.deploy-vars.outputs.frontend_image }}
|
||||
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="r2_access_key=${{ secrets.R2_ACCESS_KEY_ID }}" \
|
||||
-var="r2_secret_key=${{ secrets.R2_SECRET_ACCESS_KEY }}" \
|
||||
-var="backend_image=${{ steps.deploy-vars.outputs.backend_image }}" \
|
||||
-var="frontend_image=${{ steps.deploy-vars.outputs.frontend_image }}"
|
||||
doppler run --config prd -- bash -c '
|
||||
TF_VAR_db_user="$DB_USER" \
|
||||
TF_VAR_db_password="$DB_PASSWORD" \
|
||||
TF_VAR_db_name="$DB_NAME" \
|
||||
TF_VAR_security_key="$SECURITY_KEY" \
|
||||
TF_VAR_mailgun_apikey="$MAILGUN_APIKEY" \
|
||||
TF_VAR_r2_access_key="$AWS_ACCESS_KEY_ID" \
|
||||
TF_VAR_r2_secret_key="$AWS_SECRET_ACCESS_KEY" \
|
||||
TF_VAR_backend_image="$BACKEND_IMAGE" \
|
||||
TF_VAR_frontend_image="$FRONTEND_IMAGE" \
|
||||
timeout 1m terraform plan -input=false -out=tfplan
|
||||
'
|
||||
terraform show -json tfplan > tfplan.json
|
||||
if jq -e '.resource_changes | length == 0' tfplan.json >/dev/null; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -127,6 +130,5 @@ jobs:
|
||||
working-directory: terraform
|
||||
if: steps.check-changes.outputs.has_changes == 'true'
|
||||
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
|
||||
DOPPLER_TOKEN: ${{ secrets.DOPPLER_AT_SECRETS }}
|
||||
run: doppler run --config prd -- timeout 2m terraform apply tfplan
|
||||
@@ -2,9 +2,6 @@ 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:
|
||||
@@ -2,14 +2,6 @@ name: Main CI-Backend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'server/**/*.java'
|
||||
- 'server/**/*.xml'
|
||||
- 'server/pom.xml'
|
||||
- '.github/workflows/ci-main-backend.yml'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -2,20 +2,6 @@ name: Main CI-Frontend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'client/**/*.html'
|
||||
- 'client/**/*.png'
|
||||
- 'client/**/*.json'
|
||||
- 'client/**/*.txt'
|
||||
- 'client/**/*.ts'
|
||||
- 'client/**/*.tsx'
|
||||
- 'client/**/*.js'
|
||||
- 'client/Dockerfile'
|
||||
- 'client/Caddyfile'
|
||||
- '.github/workflows/ci-main-frontend.yml'
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
@@ -2,16 +2,6 @@ name: Pull Request CI-Backend
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'server/**/*.java'
|
||||
- 'server/**/*.xml'
|
||||
- 'server/pom.xml'
|
||||
- 'server/**/*.yml'
|
||||
- '.github/workflows/ci-pr-backend.yml'
|
||||
|
||||
jobs:
|
||||
run-checks:
|
||||
@@ -2,21 +2,6 @@ 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/ci-pr-frontend.yml'
|
||||
|
||||
jobs:
|
||||
run-checks:
|
||||
@@ -25,10 +25,10 @@
|
||||
- Frontend local dev: run from `client/` with `npm start`; backend local dev: run from `server/` with `./mvnw spring-boot:run`.
|
||||
|
||||
## CI/CD and release behavior
|
||||
- PR workflows (`.github/workflows/ci-pr-frontend.yml`, `.github/workflows/ci-pr-backend.yml`) run checks then push `:candidate` and `:pr-<N>` images to GHCR.
|
||||
- Main workflows (`.github/workflows/ci-main-frontend.yml`, `.github/workflows/ci-main-backend.yml`) push versioned tags (`app-v<date>.<run>` / `api-v<pom-version>`) + `latest`; backend workflow also increments `server/pom.xml` version.
|
||||
- Staging deploy workflow (`.github/workflows/cd-pr.yml`) triggers on completion of either PR CI workflow and applies Terraform in `terraform-stg/` using a plan→apply split.
|
||||
- Production deploy workflow (`.github/workflows/cd-main.yml`) triggers on completion of either Main CI workflow and applies Terraform in `terraform/` using a plan→apply split; `apply` can be skipped if there are no Terraform changes.
|
||||
- PR workflows (`.github/workflows/drop-ci-pr-frontend.yml`, `.github/workflows/drop-ci-pr-backend.yml`) run checks then push `:candidate` and `:pr-<N>` images to GHCR.
|
||||
- Main workflows (`.github/workflows/drop-ci-main-frontend.yml`, `.github/workflows/drop-ci-main-backend.yml`) push versioned tags (`app-v<date>.<run>` / `api-v<pom-version>`) + `latest`; backend workflow also increments `server/pom.xml` version.
|
||||
- Staging deploy workflow (`.github/workflows/drop-cd-pr.yml`) triggers on completion of either PR CI workflow and applies Terraform in `terraform-stg/` using a plan→apply split.
|
||||
- Production deploy workflow (`.github/workflows/deploy.yml`) triggers on completion of either Main CI workflow and applies Terraform in `terraform/` using a plan→apply split; `apply` can be skipped if there are no Terraform changes.
|
||||
- Infra wiring (secrets, services, ingress, image vars) is defined in `terraform/main.tf`; an alternative GCP target is under `terraform-gcp/`.
|
||||
|
||||
## Project conventions to preserve
|
||||
|
||||
+36
-2
@@ -7,7 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## app-v2026.07.01.140 - 2026-07-01
|
||||
## 2026-07-22
|
||||
|
||||
### Added
|
||||
- Button to save notes from the preview modal. Closes [#15](https://lightroasted.vps-kinghost.net/rmcampos/tasknote/issues/15)
|
||||
|
||||
### Changed
|
||||
- Removed deployments to staging in PR pipelines. PR only runs CI now. Closes [#14](https://lightroasted.vps-kinghost.net/rmcampos/tasknote/issues/14)
|
||||
|
||||
### Removed
|
||||
- Old files from project and moved scripts to `tools` folder.
|
||||
|
||||
```bash
|
||||
# Docker images
|
||||
docker pull rmcampos/tasknote-app:app-v2026.07.22.161
|
||||
docker pull rmcampos/tasknote-api:api-v2026.07.22.169
|
||||
```
|
||||
|
||||
## 2026-07-20
|
||||
|
||||
### Changed
|
||||
- Labels in tasks due date to use the time ago format.
|
||||
- Bumped all minor deps in the frontend.
|
||||
|
||||
### Fixed
|
||||
- Background image position in landing, login and register pages.
|
||||
|
||||
### Removed
|
||||
- React Date Picker dependency in favor of regular browser input date UI.
|
||||
|
||||
```bash
|
||||
# Docker images
|
||||
docker pull rmcampos/tasknote-app:app-v2026.07.20.161
|
||||
```
|
||||
|
||||
## 2026-07-01
|
||||
|
||||
### Added
|
||||
- Support for `Draft` notes and tasks.
|
||||
@@ -26,7 +60,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Bumped client minor and major dependencies.
|
||||
|
||||
### Docker images
|
||||
- `docker.io/rmcampos/tasknote-app:app-v2026.06.24.?`
|
||||
- `rmcampos/tasknote-app:app-v2026.06.25.102`
|
||||
|
||||
## api-v32 && app-v2026.06.15.97 - 2026-06-15
|
||||
|
||||
|
||||
+10
-4
@@ -9,7 +9,7 @@ If you want to contribute, please create a fork and a Merge Request. Take a look
|
||||
## Steps to Contribute
|
||||
|
||||
1. Fork the Project
|
||||
2. Clone it on your local (`git clone https://github.com/ricardo-campos-org/react-typescript-todolist`)
|
||||
2. Clone it on your local (`git clone https://lightroasted.vps-kinghost.net/rmcampos/tasknote.git`)
|
||||
3. Develop your amazing feature/changes
|
||||
4. Make sure your name is set (`git config user.name 'YOUR NAME'; git config user.email 'YOUR EMAIL'`)
|
||||
5. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
@@ -24,19 +24,25 @@ The easiest way of having the app up and running is using [Docker](https://www.d
|
||||
|
||||
1. Start the database engine (PostgreSQL)
|
||||
```sh
|
||||
bash tools/run-docker-db.sh
|
||||
task dev-run-db
|
||||
```
|
||||
2. Start the back-end engine (Java & Spring Boot)
|
||||
```sh
|
||||
bash tools/run-docker-server.sh
|
||||
task dev-run-api
|
||||
```
|
||||
3. Start the app server
|
||||
```sh
|
||||
bash tools/run-docker-client.sh
|
||||
task dev-run-web
|
||||
```
|
||||
|
||||
> Remember to follow up logs with 'docker ps' and 'docker logs -f <name>'
|
||||
|
||||
Or start all services at once with Docker Compose:
|
||||
|
||||
```sh
|
||||
docker compose -f docker-compose.dev.yml up -d
|
||||
```
|
||||
|
||||
If everything went well, you can head to [http://localhost:5000](http://localhost:5000) and create your user.
|
||||
|
||||
## 🦾 Automation
|
||||
|
||||
+2
-5
@@ -2,17 +2,14 @@
|
||||
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
|
||||
tasks:
|
||||
docker-build-web:
|
||||
desc: Build the tasknote-web prod-ready docker image, tagging it as candidate
|
||||
cmd: docker build --no-cache --build-arg VITE_BUILD="v999-$(date '+%Y-%m-%d-%H%M%S')" --build-arg SOURCE_PR="v999-123456789-$(date '+%Y-%m-%d-%H%M%S')" -t ghcr.io/rmcampos/tasknote/app:latest ./client
|
||||
cmd: docker build --no-cache --build-arg VITE_BUILD="v999-$(date '+%Y-%m-%d-%H%M%S')" --build-arg SOURCE_PR="v999-123456789-$(date '+%Y-%m-%d-%H%M%S')" -t rmcampos/tasknote-app:latest ./client
|
||||
|
||||
docker-build-api:
|
||||
desc: Build the tasknote-api prod-ready docker image, tagging it as candidate
|
||||
cmd: cd server && mvn -Pnative -DskipTests spring-boot:build-image -Dspring-boot.build-image.imageName=ghcr.io/rmcampos/tasknote/api:latest -Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:0.0.505
|
||||
cmd: cd server && mvn -Pnative -DskipTests spring-boot:build-image -Dspring-boot.build-image.imageName=rmcampos/tasknote-api:latest -Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:0.0.505
|
||||
|
||||
prod-up-web:
|
||||
desc: Speed up the tasknote-web prod-like image, building it if required
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ LABEL org.opencontainers.image.authors="Ricardo Campos <ricardompcampos@gmail.co
|
||||
org.opencontainers.image.title="TaskNoteApp client" \
|
||||
org.opencontainers.image.description="React Web app application" \
|
||||
org.opencontainers.image.version="${SOURCE_PR}" \
|
||||
org.opencontainers.image.source="https://github.com/ricardo-campos-org/react-typescript-todolist"
|
||||
org.opencontainers.image.source="https://lightroasted.vps-kinghost.net/rmcampos/tasknote"
|
||||
|
||||
# Copy files and run formatting
|
||||
COPY --from=build /app/dist/ /app/dist
|
||||
|
||||
Generated
+281
-450
File diff suppressed because it is too large
Load Diff
+18
-19
@@ -9,29 +9,28 @@
|
||||
"node",
|
||||
"nestjs"
|
||||
],
|
||||
"repository": "https://github.com/ricardo-campos-org/react-typescript-todolist",
|
||||
"repository": "https://lightroasted.vps-kinghost.net/rmcampos/tasknote",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@types/node": "^26.0.1",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"bootstrap": "^5.3.8",
|
||||
"dompurify": "^3.4.11",
|
||||
"i18next": "^26.3.2",
|
||||
"dompurify": "^3.4.12",
|
||||
"i18next": "^26.3.6",
|
||||
"react": "^19.2.7",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-bootstrap-icons": "^1.11.6",
|
||||
"react-charts": "^3.0.0-beta.57",
|
||||
"react-datepicker": "^9.1.0",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-i18next": "^17.0.10",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^8.0.1",
|
||||
"react-router": "^8.2.0",
|
||||
"react-router-bootstrap": "^0.26.3",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.0"
|
||||
"vite": "^8.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "vite --host",
|
||||
@@ -66,8 +65,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^2.1.0",
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@eslint/eslintrc": "^3.3.6",
|
||||
"@eslint/js": "^9.39.5",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
@@ -75,21 +74,21 @@
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-router-bootstrap": "^0.26.8",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
"cypress": "^15.18.0",
|
||||
"eslint": "^9.39.4",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"cypress": "^15.18.1",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import-x": "^4.17.0",
|
||||
"eslint-plugin-jsdoc": "^63.0.7",
|
||||
"eslint-plugin-n": "^18.1.0",
|
||||
"eslint-plugin-import-x": "^4.17.1",
|
||||
"eslint-plugin-jsdoc": "^63.2.0",
|
||||
"eslint-plugin-n": "^18.2.2",
|
||||
"eslint-plugin-promise": "^7.3.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^17.7.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"prettier": "^3.8.4",
|
||||
"prettier": "^3.9.5",
|
||||
"sass": "^1.101.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript-eslint": "^8.62.0",
|
||||
"vitest": "^4.1.9"
|
||||
"typescript-eslint": "^8.64.0",
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# Must be unique in a given SonarQube instance
|
||||
sonar.projectKey=ricardo-campos-org_react-typescript-todolist_client
|
||||
sonar.organization=ricardo-campos-org
|
||||
|
||||
# This is the name and version displayed in the SonarQube UI.
|
||||
# Was mandatory prior to SonarQube 6.1.
|
||||
sonar.projectName=tasknote-webapp
|
||||
#sonar.projectVersion=1.0
|
||||
|
||||
# Path is relative to the sonar-project.properties file.
|
||||
# Replace "\" by "/" on Windows.
|
||||
# This property is optional if sonar.modules is set.
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.typescript.tsconfigPaths=tsconfig.json
|
||||
sonar.sources=src/
|
||||
sonar.exclusions=src/__test__/**
|
||||
sonar.tests=src/__test__/
|
||||
sonar.verbose=false
|
||||
|
||||
# Encoding of the source code. Default is default system encoding
|
||||
sonar.sourceEncoding=UTF-8
|
||||
@@ -28,6 +28,8 @@ describe('Portuguese Utils unit tests', () => {
|
||||
expect(translateTimeMessage('1 month left', 'pt_br')).toBe('1 mês restante');
|
||||
expect(translateTimeMessage('2 days left', 'pt_br')).toBe('2 dias restantes');
|
||||
expect(translateTimeMessage('1 day left', 'pt_br')).toBe('1 dia restante');
|
||||
expect(translateTimeMessage('Due tomorrow', 'pt_br')).toBe('Vence amanhã');
|
||||
expect(translateTimeMessage('Due today', 'pt_br')).toBe('Vence hoje');
|
||||
expect(translateTimeMessage('lala', 'pt_br')).toBe('lala');
|
||||
expect(translateTimeMessage('null', 'pt_br')).toBe('null');
|
||||
});
|
||||
|
||||
@@ -49,6 +49,8 @@ describe('Russian Utils unit tests', () => {
|
||||
expect(translateTimeMessage('7 days left', 'ru')).toBe('осталось 7 дней');
|
||||
expect(translateTimeMessage('8 days left', 'ru')).toBe('осталось 8 дней');
|
||||
expect(translateTimeMessage('9 days left', 'ru')).toBe('осталось 9 дней');
|
||||
expect(translateTimeMessage('Due tomorrow', 'ru')).toBe('Срок завтра');
|
||||
expect(translateTimeMessage('Due today', 'ru')).toBe('Срок сегодня');
|
||||
expect(translateTimeMessage('lala', 'ru')).toBe('lala');
|
||||
expect(translateTimeMessage('null', 'ru')).toBe('null');
|
||||
});
|
||||
|
||||
@@ -28,6 +28,8 @@ describe('Spanish Utils unit tests', () => {
|
||||
expect(translateTimeMessage('1 month left', 'es')).toBe('Falta 1 mes');
|
||||
expect(translateTimeMessage('2 days left', 'es')).toBe('Faltan 2 días');
|
||||
expect(translateTimeMessage('1 day left', 'es')).toBe('Falta 1 día');
|
||||
expect(translateTimeMessage('Due tomorrow', 'es')).toBe('Vence mañana');
|
||||
expect(translateTimeMessage('Due today', 'es')).toBe('Vence hoy');
|
||||
expect(translateTimeMessage('lala', 'es')).toBe('lala');
|
||||
expect(translateTimeMessage('null', 'es')).toBe('null');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
@import '../../styles/theme.scss';
|
||||
|
||||
.form-control[type="date"] {
|
||||
font-size: 16px; /* Prevents iOS zoom on focus */
|
||||
border-left: none;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
@import '../../styles/theme.scss';
|
||||
|
||||
/* custom-datepicker.scss */
|
||||
.react-datepicker-wrapper,
|
||||
.react-datepicker__input-container {
|
||||
flex: 1 1 auto !important;
|
||||
width: 1% !important;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.react-datepicker__input-container {
|
||||
/* Ensure it takes full width of the flex item */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.react-datepicker__input-container input {
|
||||
font-size: 16px; /* Prevents iOS zoom on focus */
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
.react-datepicker__day {
|
||||
margin: 0.2rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Larger touch targets on mobile */
|
||||
@media (max-width: 768px) {
|
||||
.react-datepicker__day,
|
||||
.react-datepicker__month-text,
|
||||
.react-datepicker__quarter-text,
|
||||
.react-datepicker__year-text {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
margin: 0.2rem;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Col, Form, InputGroup, Row } from 'react-bootstrap';
|
||||
import * as Icons from 'react-bootstrap-icons';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { MiddlewareReturn } from '@floating-ui/core';
|
||||
import { MiddlewareState } from '@floating-ui/dom';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import './custom-datepicker.scss';
|
||||
import './FormInput.scss';
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
|
||||
@@ -17,9 +13,7 @@ interface Props {
|
||||
name: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
valueDate?: Date | null;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onChangeDate?: (date: Date | null) => void;
|
||||
dataTestId?: string;
|
||||
pwdHideText?: string;
|
||||
pwdShowText?: string;
|
||||
@@ -67,36 +61,16 @@ function FormInput(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
<InputGroup.Text>
|
||||
<Icon />
|
||||
</InputGroup.Text>
|
||||
{props.type == 'date'
|
||||
{props.type === 'date'
|
||||
? (
|
||||
<DatePicker
|
||||
selected={props?.valueDate}
|
||||
onChange={(date: Date | null) => {
|
||||
if (props.onChangeDate) {
|
||||
props.onChangeDate(date);
|
||||
}
|
||||
}}
|
||||
dateFormat="MMMM d, yyyy"
|
||||
className="form-control"
|
||||
id="date-input"
|
||||
placeholderText={props.placeholder}
|
||||
popperPlacement="bottom"
|
||||
popperModifiers={[
|
||||
{
|
||||
name: 'preventOverflow',
|
||||
options: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport'
|
||||
},
|
||||
fn: function (state: MiddlewareState): MiddlewareReturn | Promise<MiddlewareReturn> {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
]}
|
||||
withPortal
|
||||
showYearDropdown
|
||||
showMonthDropdown
|
||||
dropdownMode="select"
|
||||
<Form.Control
|
||||
required={props.required}
|
||||
type="date"
|
||||
name={props.name}
|
||||
placeholder={props.placeholder ? props.placeholder : ''}
|
||||
value={props?.value}
|
||||
onChange={props.onChange}
|
||||
data-testid={props.dataTestId}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
|
||||
@@ -10,6 +10,8 @@ type Props = {
|
||||
title: string;
|
||||
markdownText: string;
|
||||
onHide: () => void;
|
||||
onSave?: () => Promise<boolean>;
|
||||
saveButtonLabel?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -73,13 +75,18 @@ const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer className="d-flex flex-wrap gap-2 justify-content-end">
|
||||
<Button variant="outline-secondary" onClick={handleHide}>
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={handleHide}
|
||||
className="modal-action-btn"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant={showSource ? 'info' : 'outline-info'}
|
||||
onClick={handleToggleSource}
|
||||
data-testid="modal-source-button"
|
||||
className="modal-action-btn"
|
||||
>
|
||||
Source
|
||||
</Button>
|
||||
@@ -87,9 +94,23 @@ const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
variant="outline-primary"
|
||||
onClick={handleCopy}
|
||||
data-testid="modal-copy-button"
|
||||
className="modal-action-btn"
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</Button>
|
||||
{props.onSave && (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={async () => {
|
||||
handleHide();
|
||||
await props.onSave!();
|
||||
}}
|
||||
data-testid="modal-save-button"
|
||||
className="home-new-item modal-action-btn"
|
||||
>
|
||||
{props.saveButtonLabel ?? 'Save note'}
|
||||
</Button>
|
||||
)}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@@ -168,6 +168,11 @@
|
||||
max-height: 60vh;
|
||||
}
|
||||
|
||||
.modal-action-btn {
|
||||
border-radius: 3px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.markdown-source {
|
||||
font-size: 12px;
|
||||
|
||||
@@ -19,6 +19,8 @@ export const timeAgoTranslations: Record<string, string> = {
|
||||
'month left_pt_br': '{X} mês restante',
|
||||
'days left_pt_br': '{X} dias restantes',
|
||||
'day left_pt_br': '{X} dia restante',
|
||||
'due tomorrow_pt_br': 'Vence amanhã',
|
||||
'due today_pt_br': 'Vence hoje',
|
||||
|
||||
'years ago_es': 'Hace {X} años',
|
||||
'year ago_es': 'Hace {X} año',
|
||||
@@ -40,6 +42,8 @@ export const timeAgoTranslations: Record<string, string> = {
|
||||
'month left_es': 'Falta {X} mes',
|
||||
'days left_es': 'Faltan {X} días',
|
||||
'day left_es': 'Falta {X} día',
|
||||
'due tomorrow_es': 'Vence mañana',
|
||||
'due today_es': 'Vence hoy',
|
||||
|
||||
'years ago_ru': '{X} года назад',
|
||||
'year ago_ru': '{X} год назад',
|
||||
@@ -60,7 +64,9 @@ export const timeAgoTranslations: Record<string, string> = {
|
||||
'months left_ru': 'осталось {X} месяца',
|
||||
'month left_ru': 'Остался {X} месяц',
|
||||
'days left_ru': 'осталось {X} дня',
|
||||
'day left_ru': 'Остался {X} день'
|
||||
'day left_ru': 'Остался {X} день',
|
||||
'due tomorrow_ru': 'Срок завтра',
|
||||
'due today_ru': 'Срок сегодня'
|
||||
};
|
||||
|
||||
export const serverResponsesTranslations: Record<string, string> = {
|
||||
|
||||
@@ -277,8 +277,7 @@ code {
|
||||
padding: 0.375rem 0.10rem 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.input-group > .form-control,
|
||||
.react-datepicker__input-container > .form-control {
|
||||
.input-group > .form-control {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,14 @@ function translateTimeMessage(message: string, target: string): string {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (message === 'Due tomorrow') {
|
||||
return timeAgoTranslations[`due tomorrow_${target}`] ?? message;
|
||||
}
|
||||
|
||||
if (message === 'Due today') {
|
||||
return timeAgoTranslations[`due today_${target}`] ?? message;
|
||||
}
|
||||
|
||||
const firstSpace = message.indexOf(' ');
|
||||
const numberValue = message.substring(0, firstSpace);
|
||||
const textValue = message.substring(firstSpace).trim();
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: $dark-text;
|
||||
background: var(--bs-landing-bg) no-repeat center center;
|
||||
background: var(--bs-landing-bg) no-repeat center center fixed;
|
||||
background-size: cover; // Ensures the image covers the whole background
|
||||
min-height: 100vh;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
@import '../../styles/theme.scss';
|
||||
|
||||
.login-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: var(--bs-landing-bg) no-repeat center center;
|
||||
background: var(--bs-landing-bg) no-repeat center center fixed;
|
||||
background-size: cover;
|
||||
|
||||
&::before {
|
||||
|
||||
@@ -201,6 +201,51 @@ function NoteAdd(): React.ReactNode {
|
||||
saveDraft(noteTitle, noteContent, noteUrl, newTags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the note, either adding a new one or editing an existing one.
|
||||
*
|
||||
* @returns {Promise<boolean>} True if the note was saved successfully, false otherwise.
|
||||
*/
|
||||
const saveNote = async (): Promise<boolean> => {
|
||||
setValidated(true);
|
||||
|
||||
if (!noteTitle.trim() || !noteContent.trim()) {
|
||||
setErrorMessage(translateServerResponse('Please fill in all the fields', i18n.language));
|
||||
return false;
|
||||
}
|
||||
|
||||
const finalTags = [...selectedTags];
|
||||
if (currentTag.trim()) {
|
||||
const normalized = currentTag.trim().toLowerCase();
|
||||
if (!finalTags.includes(normalized)) {
|
||||
finalTags.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
const payload: NoteResponse = {
|
||||
id: action === 'edit' ? noteId : 0,
|
||||
title: noteTitle,
|
||||
description: noteContent,
|
||||
url: noteUrl,
|
||||
tags: finalTags,
|
||||
lastUpdate: '',
|
||||
shared: false,
|
||||
shareToken: null
|
||||
};
|
||||
|
||||
const saved = action === 'add'
|
||||
? await addNote(payload)
|
||||
: await submitEditNote(payload);
|
||||
|
||||
if (saved) {
|
||||
clearDraft();
|
||||
resetInputs();
|
||||
navigate('/home');
|
||||
}
|
||||
|
||||
return saved;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the form submission.
|
||||
*
|
||||
@@ -217,54 +262,7 @@ function NoteAdd(): React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
const finalTags = [...selectedTags];
|
||||
if (currentTag.trim()) {
|
||||
const normalized = currentTag.trim().toLowerCase();
|
||||
if (!finalTags.includes(normalized)) {
|
||||
finalTags.push(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
if (action === 'add') {
|
||||
const payload: NoteResponse = {
|
||||
id: 0,
|
||||
title: noteTitle,
|
||||
description: noteContent,
|
||||
url: noteUrl,
|
||||
tags: finalTags,
|
||||
lastUpdate: '',
|
||||
shared: false,
|
||||
shareToken: null
|
||||
};
|
||||
|
||||
const added: boolean = await addNote(payload);
|
||||
if (added) {
|
||||
clearDraft();
|
||||
form.reset();
|
||||
resetInputs();
|
||||
navigate('/home');
|
||||
}
|
||||
}
|
||||
else if (action === 'edit') {
|
||||
const payload: NoteResponse = {
|
||||
id: noteId,
|
||||
title: noteTitle,
|
||||
description: noteContent,
|
||||
url: noteUrl,
|
||||
tags: finalTags,
|
||||
lastUpdate: '',
|
||||
shared: false,
|
||||
shareToken: null
|
||||
};
|
||||
|
||||
const edited: boolean = await submitEditNote(payload);
|
||||
if (edited) {
|
||||
clearDraft();
|
||||
form.reset();
|
||||
resetInputs();
|
||||
navigate('/home');
|
||||
}
|
||||
}
|
||||
await saveNote();
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -569,6 +567,8 @@ function NoteAdd(): React.ReactNode {
|
||||
onHide={handleCloseModal}
|
||||
title={noteTitle}
|
||||
markdownText={noteContent}
|
||||
onSave={saveNote}
|
||||
saveButtonLabel={t('note_form_submit')}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ function TaskAdd(): React.ReactNode {
|
||||
const [taskUrl, setTaskUrl] = useState<string>('');
|
||||
const [taskDone, setTaskDone] = useState<boolean>(false);
|
||||
const [action, setAction] = useState<TaskAction>('add');
|
||||
const [dueDate, setDueDate] = useState<Date | null>(null);
|
||||
const [dueDate, setDueDate] = useState<string>('');
|
||||
const [highPriority, setHighPriority] = useState<boolean>(false);
|
||||
const [currentTag, setCurrentTag] = useState<string>('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
@@ -127,7 +127,7 @@ function TaskAdd(): React.ReactNode {
|
||||
setTaskDescription('');
|
||||
setTaskDone(false);
|
||||
setTaskUrl('');
|
||||
setDueDate(null);
|
||||
setDueDate('');
|
||||
setHighPriority(false);
|
||||
setCurrentTag('');
|
||||
setSelectedTags([]);
|
||||
@@ -138,7 +138,7 @@ function TaskAdd(): React.ReactNode {
|
||||
const saveDraft = (
|
||||
description: string,
|
||||
taskUrl: string,
|
||||
due: Date | null,
|
||||
due: string,
|
||||
priority: boolean,
|
||||
draftTags: string[]
|
||||
): void => {
|
||||
@@ -148,7 +148,7 @@ function TaskAdd(): React.ReactNode {
|
||||
const draft: TaskDraft = {
|
||||
description,
|
||||
taskUrl,
|
||||
dueDate: due ? due.toISOString() : null,
|
||||
dueDate: due || null,
|
||||
highPriority: priority,
|
||||
tags: draftTags
|
||||
};
|
||||
@@ -168,8 +168,8 @@ function TaskAdd(): React.ReactNode {
|
||||
const draft: TaskDraft = JSON.parse(raw);
|
||||
setTaskDescription(draft.description ?? '');
|
||||
setTaskUrl(draft.taskUrl ?? '');
|
||||
const parsedDate = draft.dueDate ? new Date(draft.dueDate) : null;
|
||||
setDueDate(parsedDate && !isNaN(parsedDate.getTime()) ? parsedDate : null);
|
||||
const parsedDate = draft.dueDate ? draft.dueDate : '';
|
||||
setDueDate(parsedDate);
|
||||
setHighPriority(draft.highPriority ?? false);
|
||||
setSelectedTags(draft.tags ?? []);
|
||||
setDraftBanner(true);
|
||||
@@ -185,7 +185,7 @@ function TaskAdd(): React.ReactNode {
|
||||
setTaskUrl(task.urls.length ? task.urls[0] : '');
|
||||
setTaskDone(task.done);
|
||||
if (task.dueDateFmt) {
|
||||
setDueDate(new Date(task.dueDate));
|
||||
setDueDate(task.dueDate);
|
||||
}
|
||||
setHighPriority(task.highPriority);
|
||||
if (task.tags) {
|
||||
@@ -249,10 +249,7 @@ function TaskAdd(): React.ReactNode {
|
||||
return;
|
||||
}
|
||||
|
||||
let dueDateFormatted: string = '';
|
||||
if (dueDate) {
|
||||
dueDateFormatted = dueDate.toISOString().substring(0, 10);
|
||||
}
|
||||
const dueDateFormatted: string = dueDate;
|
||||
|
||||
const finalTags = [...selectedTags];
|
||||
if (currentTag.trim()) {
|
||||
@@ -427,11 +424,10 @@ function TaskAdd(): React.ReactNode {
|
||||
type="date"
|
||||
name="dueDate"
|
||||
placeholder={t('task_form_duedate_placeholder')}
|
||||
valueDate={dueDate}
|
||||
onChangeDate={(date: Date | null) => {
|
||||
setDueDate(date);
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDueDate(e.target.value);
|
||||
hasUserEdited.current = true;
|
||||
saveDraft(taskDescription, taskUrl, date, highPriority, selectedTags);
|
||||
saveDraft(taskDescription, taskUrl, e.target.value, highPriority, selectedTags);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
|
||||
PR="$1"
|
||||
echo "PR: $PR"
|
||||
|
||||
echo "Getting env vars..."
|
||||
export $(cat .env | xargs)
|
||||
|
||||
docker run -d --rm \
|
||||
--name server \
|
||||
--network=host \
|
||||
-e POSTGRES_DB=$POSTGRES_DB \
|
||||
-e POSTGRES_USER=$POSTGRES_USER \
|
||||
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
|
||||
-e POSTGRES_PORT=$POSTGRES_PORT \
|
||||
-e POSTGRES_HOST=$POSTGRES_HOST \
|
||||
-e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \
|
||||
-e SERVER_SERVLET_CONTEXT_PATH=$SERVER_SERVLET_CONTEXT_PATH \
|
||||
-e MAILGUN_APIKEY=$MAILGUN_APIKEY \
|
||||
-e SECURITY_KEY=$SECURITY_KEY \
|
||||
-e BUILD=ghcr.io/ricardo-campos-org/react-typescript-todolist/server:$PR \
|
||||
ghcr.io/ricardo-campos-org/react-typescript-todolist/server:$PR
|
||||
@@ -6,7 +6,7 @@ services:
|
||||
depends_on:
|
||||
tasknote-api:
|
||||
condition: service_healthy
|
||||
image: ghcr.io/rmcampos/tasknote/app:latest
|
||||
image: rmcampos/tasknote-app:latest
|
||||
build:
|
||||
context: ./client
|
||||
dockerfile: Dockerfile
|
||||
@@ -31,7 +31,7 @@ services:
|
||||
SECURITY_KEY: ${SECURITY_KEY}
|
||||
MAILGUN_APIKEY: ${MAILGUN_APIKEY}
|
||||
ports: ["8585:8585"]
|
||||
image: ghcr.io/rmcampos/tasknote/api:latest
|
||||
image: rmcampos/tasknote-api:latest
|
||||
healthcheck:
|
||||
test: ["CMD", "/layers/local_healthcheck/healthcheck/bin/healthcheck"]
|
||||
interval: 10s
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
setup:
|
||||
project: tasknote
|
||||
config: prd
|
||||
+2
-2
@@ -11,7 +11,7 @@
|
||||
|
||||
<groupId>br.com.tasknoteapp</groupId>
|
||||
<artifactId>server</artifactId>
|
||||
<version>34</version>
|
||||
<version>35</version>
|
||||
<name>tasknote-api</name>
|
||||
<description>Java backend REST API to serve TaskNote frontend client</description>
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
<dependency>
|
||||
<groupId>com.puppycrawl.tools</groupId>
|
||||
<artifactId>checkstyle</artifactId>
|
||||
<version>13.3.0</version>
|
||||
<version>13.7.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
|
||||
@@ -89,9 +89,9 @@ public class TimeAgoUtil {
|
||||
} else if (period.getDays() > 1) {
|
||||
sb.append(String.format("%d days left", period.getDays()));
|
||||
} else if (period.getDays() > 0) {
|
||||
sb.append(String.format("%d day left", period.getDays()));
|
||||
sb.append("Due tomorrow");
|
||||
} else if (period.getDays() == 0) {
|
||||
sb.append("0 days left");
|
||||
sb.append("Due today");
|
||||
} else {
|
||||
sb.append("Due");
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class TimeAgoUtilTest {
|
||||
Assertions.assertNull(TimeAgoUtil.formatDueDate(null));
|
||||
|
||||
LocalDate localDate1 = LocalDate.now().plusDays(1L);
|
||||
String expected1 = "1 day left" + getFormattedSuffix(localDate1);
|
||||
String expected1 = "Due tomorrow" + getFormattedSuffix(localDate1);
|
||||
Assertions.assertEquals(expected1, TimeAgoUtil.formatDueDate(localDate1));
|
||||
|
||||
LocalDate localDate2 = LocalDate.now().plusDays(12L);
|
||||
@@ -74,7 +74,7 @@ class TimeAgoUtilTest {
|
||||
void formatDueDateEdgeCasesTest() {
|
||||
// Test for today
|
||||
LocalDate today = LocalDate.now();
|
||||
String expectedToday = "0 days left" + getFormattedSuffix(today);
|
||||
String expectedToday = "Due today" + getFormattedSuffix(today);
|
||||
Assertions.assertEquals(expectedToday, TimeAgoUtil.formatDueDate(today));
|
||||
|
||||
// Test for a past date
|
||||
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/google" {
|
||||
version = "5.45.2"
|
||||
constraints = "~> 5.0"
|
||||
hashes = [
|
||||
"h1:k8taQAdfHrv2F/AiGV5BZBZfI+1uaq8g6O8dWzjx42c=",
|
||||
"zh:0d09c8f20b556305192cdbe0efa6d333ceebba963a8ba91f9f1714b5a20c4b7a",
|
||||
"zh:117143fc91be407874568df416b938a6896f94cb873f26bba279cedab646a804",
|
||||
"zh:16ccf77d18dd2c5ef9c0625f9cf546ebdf3213c0a452f432204c69feed55081e",
|
||||
"zh:3e555cf22a570a4bd247964671f421ed7517970cd9765ceb46f335edc2c6f392",
|
||||
"zh:688bd5b05a75124da7ae6e885b2b92bd29f4261808b2b78bd5f51f525c1052ca",
|
||||
"zh:6db3ef37a05010d82900bfffb3261c59a0c247e0692049cb3eb8c2ef16c9d7bf",
|
||||
"zh:70316fde75f6a15d72749f66d994ccbdde5f5ed4311b6d06b99850f698c9bbf9",
|
||||
"zh:84b8e583771a4f2bd514e519d98ed7fd28dce5efe0634e973170e1cfb5556fb4",
|
||||
"zh:9d4b8ef0a9b6677935c604d94495042e68ff5489932cfd1ec41052e094a279d3",
|
||||
"zh:a2089dd9bd825c107b148dd12d6b286f71aa37dfd4ca9c35157f2dcba7bc19d8",
|
||||
"zh:f03d795c0fd9721e59839255ee7ba7414173017dc530b4ce566daf3802a0d6dd",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
]
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
resource "google_cloud_run_v2_service" "backend" {
|
||||
name = "tasknote-api"
|
||||
location = var.region
|
||||
ingress = "INGRESS_TRAFFIC_ALL"
|
||||
|
||||
depends_on = [google_project_service.run]
|
||||
|
||||
template {
|
||||
service_account = google_service_account.cloudrun_sa.email
|
||||
vpc_access {
|
||||
connector = google_vpc_access_connector.connector.id
|
||||
egress = "PRIVATE_RANGES_ONLY"
|
||||
}
|
||||
|
||||
containers {
|
||||
image = var.backend_image
|
||||
ports {
|
||||
container_port = 8585
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_DB"
|
||||
value = var.db_name
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_HOST"
|
||||
value = google_sql_database_instance.instance.private_ip_address
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_USER"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.db_user.secret_id
|
||||
version = google_secret_manager_secret_version.db_user_version.version
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "POSTGRES_PASSWORD"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.db_password.secret_id
|
||||
version = google_secret_manager_secret_version.db_password_version.version
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = "TARGET_ENV"
|
||||
value = "production"
|
||||
}
|
||||
env {
|
||||
name = "SECURITY_KEY"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.security_key.secret_id
|
||||
version = google_secret_manager_secret_version.security_key_version.version
|
||||
}
|
||||
}
|
||||
}
|
||||
env {
|
||||
name = "MAILGUN_APIKEY"
|
||||
value_source {
|
||||
secret_key_ref {
|
||||
secret = google_secret_manager_secret.mailgun_apikey.secret_id
|
||||
version = google_secret_manager_secret_version.mailgun_apikey_version.version
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_cloud_run_v2_service" "frontend" {
|
||||
name = "tasknote-app"
|
||||
location = var.region
|
||||
ingress = "INGRESS_TRAFFIC_ALL"
|
||||
|
||||
depends_on = [google_project_service.run]
|
||||
|
||||
template {
|
||||
service_account = google_service_account.cloudrun_sa.email
|
||||
|
||||
containers {
|
||||
image = var.frontend_image
|
||||
ports {
|
||||
container_port = 5000
|
||||
}
|
||||
env {
|
||||
name = "VITE_BACKEND_SERVER"
|
||||
value = google_cloud_run_v2_service.backend.uri
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Allow unauthenticated access to both services
|
||||
resource "google_cloud_run_service_iam_member" "backend_public" {
|
||||
location = google_cloud_run_v2_service.backend.location
|
||||
service = google_cloud_run_v2_service.backend.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
resource "google_cloud_run_service_iam_member" "frontend_public" {
|
||||
location = google_cloud_run_v2_service.frontend.location
|
||||
service = google_cloud_run_v2_service.frontend.name
|
||||
role = "roles/run.invoker"
|
||||
member = "allUsers"
|
||||
}
|
||||
|
||||
# Grant Cloud SQL Client role to the service account
|
||||
resource "google_project_iam_member" "cloudsql_client" {
|
||||
project = var.project_id
|
||||
role = "roles/cloudsql.client"
|
||||
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
resource "google_sql_database_instance" "instance" {
|
||||
name = "tasknote-db-instance"
|
||||
region = var.region
|
||||
database_version = "POSTGRES_15"
|
||||
|
||||
depends_on = [
|
||||
google_service_networking_connection.private_vpc_connection,
|
||||
google_project_service.sqladmin,
|
||||
google_project_service.servicenetworking
|
||||
]
|
||||
|
||||
settings {
|
||||
tier = "db-f1-micro" # Smallest tier for dev/small app
|
||||
ip_configuration {
|
||||
ipv4_enabled = false
|
||||
private_network = google_compute_network.vpc_network.id
|
||||
}
|
||||
}
|
||||
|
||||
deletion_protection = false # Set to true for production
|
||||
}
|
||||
|
||||
resource "google_sql_database" "database" {
|
||||
name = var.db_name
|
||||
instance = google_sql_database_instance.instance.name
|
||||
}
|
||||
|
||||
resource "google_sql_user" "users" {
|
||||
name = var.db_user
|
||||
instance = google_sql_database_instance.instance.name
|
||||
password = var.db_password
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
PROJECT_ID="replace-me"
|
||||
VERSION="23"
|
||||
SOURCE_IMG="ghcr.io/rmcampos/tasknote/api:$VERSION"
|
||||
DESTINATION_IMG="us-central1-docker.pkg.dev/$PROJECT_ID/tasknote-repo/api:$VERSION"
|
||||
GHCR_PAT="replace-me"
|
||||
|
||||
# Login
|
||||
echo $GHCR_PAT | docker login ghcr.io -u user --password-stdin
|
||||
echo "Logged in on ghcr.io"
|
||||
|
||||
gcloud auth configure-docker us-central1-docker.pkg.dev
|
||||
docker login us-central1-docker.pkg.dev
|
||||
echo "Logged in on GCP Container Registry"
|
||||
|
||||
crane auth login ghcr.io -u RMCampos -p $GHCR_PAT
|
||||
echo "Logged crane in on ghcr.io"
|
||||
|
||||
# Check if image already exists in destination
|
||||
echo "Checking if $DESTINATION_IMG already exists in destination registry..."
|
||||
if crane manifest "$DESTINATION_IMG" > /dev/null 2>&1; then
|
||||
echo "Image $DESTINATION_IMG already exists. Skipping push."
|
||||
else
|
||||
echo "Image not found in destination. Proceeding with push..."
|
||||
|
||||
docker pull $SOURCE_IMG
|
||||
docker tag $SOURCE_IMG $DESTINATION_IMG
|
||||
docker push $DESTINATION_IMG
|
||||
echo "Pushed image to GCP Container Registry"
|
||||
|
||||
crane copy $SOURCE_IMG $DESTINATION_IMG
|
||||
echo "Copied image to GCP Container Registry with crane"
|
||||
fi
|
||||
|
||||
# Verification
|
||||
echo "Verifying push..."
|
||||
if crane manifest "$DESTINATION_IMG" > /dev/null 2>&1; then
|
||||
echo "SUCCESS: Image $DESTINATION_IMG is properly pushed and available in the registry."
|
||||
gcloud artifacts docker images list us-central1-docker.pkg.dev/project-9f22294e-b6f5-41de-83c/tasknote-repo | grep "api:$VERSION"
|
||||
else
|
||||
echo "FAILURE: Image $DESTINATION_IMG was not found in the destination registry."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
resource "google_compute_network" "vpc_network" {
|
||||
name = "tasknote-vpc"
|
||||
auto_create_subnetworks = false
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "subnet" {
|
||||
name = "tasknote-subnet"
|
||||
ip_cidr_range = "10.0.0.0/24"
|
||||
region = var.region
|
||||
network = google_compute_network.vpc_network.id
|
||||
}
|
||||
|
||||
# Private IP for Cloud SQL
|
||||
resource "google_compute_global_address" "private_ip_address" {
|
||||
name = "tasknote-private-ip"
|
||||
purpose = "VPC_PEERING"
|
||||
address_type = "INTERNAL"
|
||||
prefix_length = 16
|
||||
network = google_compute_network.vpc_network.id
|
||||
}
|
||||
|
||||
resource "google_service_networking_connection" "private_vpc_connection" {
|
||||
network = google_compute_network.vpc_network.id
|
||||
service = "servicenetworking.googleapis.com"
|
||||
reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
|
||||
}
|
||||
|
||||
# Serverless VPC Access Connector
|
||||
resource "google_vpc_access_connector" "connector" {
|
||||
name = "tasknote-vpc-connector"
|
||||
region = var.region
|
||||
ip_cidr_range = "10.8.0.0/28"
|
||||
network = google_compute_network.vpc_network.name
|
||||
depends_on = [google_project_service.vpcaccess, google_project_service.compute]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
output "backend_url" {
|
||||
value = google_cloud_run_v2_service.backend.uri
|
||||
}
|
||||
|
||||
output "frontend_url" {
|
||||
value = google_cloud_run_v2_service.frontend.uri
|
||||
}
|
||||
|
||||
output "cloud_sql_instance_ip" {
|
||||
value = google_sql_database_instance.instance.private_ip_address
|
||||
}
|
||||
|
||||
output "artifact_registry_repo" {
|
||||
value = google_artifact_registry_repository.repo.name
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {
|
||||
bucket = "tasknote"
|
||||
key = "gcp/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 "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
resource "google_artifact_registry_repository" "repo" {
|
||||
location = var.region
|
||||
repository_id = "tasknote-repo"
|
||||
description = "Docker repository for TaskNote images"
|
||||
format = "DOCKER"
|
||||
depends_on = [google_project_service.artifactregistry]
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
resource "google_service_account" "cloudrun_sa" {
|
||||
account_id = "tasknote-cloudrun-sa"
|
||||
display_name = "TaskNote Cloud Run Service Account"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "db_password" {
|
||||
secret_id = "db-password"
|
||||
replication {
|
||||
user_managed {
|
||||
replicas {
|
||||
location = var.region
|
||||
}
|
||||
}
|
||||
}
|
||||
depends_on = [google_project_service.secretmanager]
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "db_password_version" {
|
||||
secret = google_secret_manager_secret.db_password.id
|
||||
secret_data = var.db_password
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "db_user" {
|
||||
secret_id = "db-user"
|
||||
replication {
|
||||
user_managed {
|
||||
replicas {
|
||||
location = var.region
|
||||
}
|
||||
}
|
||||
}
|
||||
depends_on = [google_project_service.secretmanager]
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "db_user_version" {
|
||||
secret = google_secret_manager_secret.db_user.id
|
||||
secret_data = var.db_user
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "db_password_access" {
|
||||
secret_id = google_secret_manager_secret.db_password.id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "db_user_access" {
|
||||
secret_id = google_secret_manager_secret.db_user.id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "security_key" {
|
||||
secret_id = "security-key"
|
||||
replication {
|
||||
user_managed {
|
||||
replicas {
|
||||
location = var.region
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "security_key_version" {
|
||||
secret = google_secret_manager_secret.security_key.id
|
||||
secret_data = var.security_key
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret" "mailgun_apikey" {
|
||||
secret_id = "mailgun-apikey"
|
||||
replication {
|
||||
user_managed {
|
||||
replicas {
|
||||
location = var.region
|
||||
}
|
||||
}
|
||||
}
|
||||
depends_on = [google_project_service.secretmanager]
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_version" "mailgun_apikey_version" {
|
||||
secret = google_secret_manager_secret.mailgun_apikey.id
|
||||
secret_data = var.mailgun_apikey
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "security_key_access" {
|
||||
secret_id = google_secret_manager_secret.security_key.id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
|
||||
}
|
||||
|
||||
resource "google_secret_manager_secret_iam_member" "mailgun_apikey_access" {
|
||||
secret_id = google_secret_manager_secret.mailgun_apikey.id
|
||||
role = "roles/secretmanager.secretAccessor"
|
||||
member = "serviceAccount:${google_service_account.cloudrun_sa.email}"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
resource "google_project_service" "compute" {
|
||||
service = "compute.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "sqladmin" {
|
||||
service = "sqladmin.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "run" {
|
||||
service = "run.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "secretmanager" {
|
||||
service = "secretmanager.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "vpcaccess" {
|
||||
service = "vpcaccess.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "artifactregistry" {
|
||||
service = "artifactregistry.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
|
||||
resource "google_project_service" "servicenetworking" {
|
||||
service = "servicenetworking.googleapis.com"
|
||||
disable_on_destroy = false
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
project_id = "your-gcp-project-id"
|
||||
region = "us-central1"
|
||||
db_user = "tasknoteuser"
|
||||
db_password = "your-db-password"
|
||||
db_name = "tasknote"
|
||||
security_key = "your-security-key"
|
||||
mailgun_apikey = "your-mailgun-apikey"
|
||||
backend_image = "ghcr.io/rmcampos/tasknote/api:latest"
|
||||
frontend_image = "ghcr.io/rmcampos/tasknote/app:latest"
|
||||
cors_allowed_origins = "*"
|
||||
@@ -1,50 +0,0 @@
|
||||
variable "project_id" {
|
||||
type = string
|
||||
description = "The GCP Project ID"
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us-central1"
|
||||
description = "The GCP region to deploy resources"
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "db_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
type = string
|
||||
default = "tasknote"
|
||||
}
|
||||
|
||||
variable "security_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "mailgun_apikey" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "backend_image" {
|
||||
type = string
|
||||
default = "ghcr.io/rmcampos/tasknote/api:latest"
|
||||
}
|
||||
|
||||
variable "frontend_image" {
|
||||
type = string
|
||||
default = "ghcr.io/rmcampos/tasknote/app:latest"
|
||||
}
|
||||
|
||||
variable "cors_allowed_origins" {
|
||||
type = string
|
||||
default = "*"
|
||||
}
|
||||
Generated
-22
@@ -1,22 +0,0 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/hashicorp/kubernetes" {
|
||||
version = "2.38.0"
|
||||
constraints = "2.38.0"
|
||||
hashes = [
|
||||
"h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=",
|
||||
"zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0",
|
||||
"zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f",
|
||||
"zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b",
|
||||
"zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12",
|
||||
"zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2",
|
||||
"zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc",
|
||||
"zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15",
|
||||
"zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396",
|
||||
"zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d",
|
||||
"zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4",
|
||||
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
|
||||
"zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db",
|
||||
]
|
||||
}
|
||||
@@ -1,387 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "= 2.38.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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -80,12 +80,12 @@ variable "root_log_level" {
|
||||
|
||||
variable "backend_image" {
|
||||
type = string
|
||||
default = "pull ghcr.io/rmcampos/tasknote/api:15"
|
||||
default = "rmcampos/tasknote-api:latest"
|
||||
}
|
||||
|
||||
variable "frontend_image" {
|
||||
type = string
|
||||
default = "ghcr.io/rmcampos/tasknote/app:app-v2026.03.17.18"
|
||||
default = "rmcampos/tasknote-app:latest"
|
||||
}
|
||||
|
||||
resource "kubernetes_namespace_v1" "tasknote" {
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@ All kind of tools and useful links and commands can be found here!
|
||||
|
||||
## Links
|
||||
|
||||
- **GitHub Container Registry:** https://ghcr.io/
|
||||
- **Docker Hub Container Registry:** https://hub.docker.io/
|
||||
- **Time tracking:** https://track.toggl.com/timer
|
||||
|
||||
## Building locally
|
||||
@@ -74,7 +74,7 @@ docker run -d -p 8585:8585 --rm \
|
||||
-e POSTGRES_PORT=$POSTGRES_PORT \
|
||||
-e POSTGRES_HOST=$POSTGRES_HOST \
|
||||
-e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \
|
||||
ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:<PR-Number>
|
||||
rmcampos/tasknote-api:<tag>
|
||||
```
|
||||
|
||||
Build Cloud Native: `./mvnw -B package -Pnative -DskipTests`
|
||||
@@ -89,19 +89,19 @@ The frontend app will run on Nginx.
|
||||
|
||||
```
|
||||
export CR_PAT=YOUR_TOKEN
|
||||
echo $CR_PAT | docker login ghcr.io -u RMCampos --password-stdin
|
||||
echo $CR_PAT | docker login -u RMCampos --password-stdin
|
||||
```
|
||||
|
||||
**Pulling images:**
|
||||
|
||||
```sh
|
||||
docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:50
|
||||
docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:50
|
||||
docker pull rmcampos/tasknote-app:latest
|
||||
docker pull rmcampos/tasknote-api:latest
|
||||
```
|
||||
|
||||
**Pushing images:**
|
||||
```sh
|
||||
docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:316
|
||||
docker push rmcampos/tasknote-api:latest
|
||||
```
|
||||
|
||||
- Get container IP
|
||||
|
||||
Reference in New Issue
Block a user