Compare commits
7 Commits
a606983632
...
v0.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90b381823a | ||
|
|
52f29038d8 | ||
|
|
5dabf3f4ca | ||
|
|
281004434d | ||
|
|
c8b96ab8e7 | ||
|
|
37ae9dbe53 | ||
|
|
bbd3beb22a |
@@ -1,4 +1,4 @@
|
|||||||
name: Build and Push Docker Images
|
name: Release and Build Docker Images
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -12,11 +12,92 @@ env:
|
|||||||
OWNER: ${{ gitea.repository_owner }}
|
OWNER: ${{ gitea.repository_owner }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
release-and-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Git Configuration
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Generate Version and Changelog
|
||||||
|
id: generate
|
||||||
|
run: |
|
||||||
|
# 1. Fetch the latest tag
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||||
|
echo "Latest tag: $LATEST_TAG"
|
||||||
|
|
||||||
|
# 2. Calculate the next patch version
|
||||||
|
if [[ $LATEST_TAG == v* ]]; then
|
||||||
|
VERSION_NUM=${LATEST_TAG:1}
|
||||||
|
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION_NUM"
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
NEW_TAG="v$MAJOR.$MINOR.$PATCH"
|
||||||
|
else
|
||||||
|
# Fallback if the tag format is unrecognized
|
||||||
|
NEW_TAG="v0.0.1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "New tag: $NEW_TAG"
|
||||||
|
echo "new_tag=$NEW_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# 3. Create a temporary file for the new changelog entry
|
||||||
|
TEMP_CHANGELOG=$(mktemp)
|
||||||
|
DATE=$(date +'%Y-%m-%d')
|
||||||
|
|
||||||
|
echo "## [$NEW_TAG] - $DATE" > "$TEMP_CHANGELOG"
|
||||||
|
echo "" >> "$TEMP_CHANGELOG"
|
||||||
|
|
||||||
|
# Fetch all commit messages since the latest tag to HEAD
|
||||||
|
if [ "$LATEST_TAG" = "v0.0.0" ]; then
|
||||||
|
git log --pretty=format:"* %s (%h)" >> "$TEMP_CHANGELOG"
|
||||||
|
else
|
||||||
|
git log ${LATEST_TAG}..HEAD --pretty=format:"* %s (%h)" >> "$TEMP_CHANGELOG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> "$TEMP_CHANGELOG"
|
||||||
|
echo "" >> "$TEMP_CHANGELOG"
|
||||||
|
|
||||||
|
# 4. Prepend the new entry to the existing CHANGELOG.md
|
||||||
|
if [ -f CHANGELOG.md ]; then
|
||||||
|
cat CHANGELOG.md >> "$TEMP_CHANGELOG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "$TEMP_CHANGELOG" CHANGELOG.md
|
||||||
|
|
||||||
|
echo "Changelog generated successfully."
|
||||||
|
|
||||||
|
- name: Commit and Push Changes
|
||||||
|
run: |
|
||||||
|
NEW_TAG=${{ steps.generate.outputs.new_tag }}
|
||||||
|
|
||||||
|
# Add the updated changelog
|
||||||
|
git add CHANGELOG.md
|
||||||
|
|
||||||
|
# Only commit if there are changes
|
||||||
|
if ! git diff-index --quiet HEAD; then
|
||||||
|
git commit -m "chore(release): bump version to $NEW_TAG and update changelog [skip ci]"
|
||||||
|
|
||||||
|
# Create the new tag
|
||||||
|
git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
|
||||||
|
|
||||||
|
# Push the commit and the new tag
|
||||||
|
git push origin main
|
||||||
|
git push origin "$NEW_TAG"
|
||||||
|
else
|
||||||
|
echo "No changes to commit."
|
||||||
|
# If we haven't already tagged this commit, do it now
|
||||||
|
if ! git ls-remote --tags origin | grep -q "refs/tags/$NEW_TAG"; then
|
||||||
|
git tag -a "$NEW_TAG" -m "Release $NEW_TAG"
|
||||||
|
git push origin "$NEW_TAG"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -35,7 +116,7 @@ jobs:
|
|||||||
images: ${{ env.REGISTRY }}/${{ env.OWNER }}/todo-backend
|
images: ${{ env.REGISTRY }}/${{ env.OWNER }}/todo-backend
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=sha,prefix={{branch}}-
|
type=raw,value=${{ steps.generate.outputs.new_tag }}
|
||||||
|
|
||||||
- name: Build and push Backend image
|
- name: Build and push Backend image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
@@ -44,6 +125,8 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta-backend.outputs.tags }}
|
tags: ${{ steps.meta-backend.outputs.tags }}
|
||||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=${{ steps.generate.outputs.new_tag }}
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Frontend
|
- name: Extract metadata (tags, labels) for Frontend
|
||||||
id: meta-frontend
|
id: meta-frontend
|
||||||
@@ -52,7 +135,7 @@ jobs:
|
|||||||
images: ${{ env.REGISTRY }}/${{ env.OWNER }}/todo-frontend
|
images: ${{ env.REGISTRY }}/${{ env.OWNER }}/todo-frontend
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
type=raw,value=latest
|
||||||
type=sha,prefix={{branch}}-
|
type=raw,value=${{ steps.generate.outputs.new_tag }}
|
||||||
|
|
||||||
- name: Build and push Frontend image
|
- name: Build and push Frontend image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
@@ -61,3 +144,5 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta-frontend.outputs.tags }}
|
tags: ${{ steps.meta-frontend.outputs.tags }}
|
||||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
labels: ${{ steps.meta-frontend.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
VITE_APP_VERSION=${{ steps.generate.outputs.new_tag }}
|
||||||
|
|||||||
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
## [v0.0.3] - 2026-02-21
|
||||||
|
|
||||||
|
* feat: add custom favicon and update page title (52f2903)
|
||||||
|
|
||||||
|
## [v0.0.2] - 2026-02-21
|
||||||
|
|
||||||
|
* feat: Implement responsive styling for various screen sizes and refactor the main todo list container class. (2810044)
|
||||||
|
|
||||||
|
## [v0.0.1] - 2026-02-21
|
||||||
|
|
||||||
|
* feat: Introduce application version display in the frontend footer, showing both frontend and backend versions with integrated build arguments. (37ae9db)
|
||||||
|
* feat: Automate versioning, changelog updates, and Git tagging, using the generated version for Docker image tags. (bbd3beb)
|
||||||
|
* feat: Add `https://todo.almazlar.com` to allowed CORS origins. (a606983)
|
||||||
|
* refactor: Update TodoController base request mapping from `/todos` to `/api/todos`. (e8be570)
|
||||||
|
* refactor: remove `/api` prefix from TodoController request mapping. (d118222)
|
||||||
|
* feat: Broaden backend CORS mapping to all paths and update frontend API base URL to production. (ca838b1)
|
||||||
|
* refactor: switch to using docker/login-action for container registry authentication (568f662)
|
||||||
|
* build: Update Gitea workflow to use `docker login` with `password-stdin` instead of `docker/login-action`. (dd0c2e2)
|
||||||
|
* ci: Replace github context variables with gitea context variables in Docker build workflow. (082abfc)
|
||||||
|
* feat: Add initial frontend dependencies, base CSS, and Gitea Docker build workflow. (cf58ccb)
|
||||||
|
* Initial commit (c8572c5)
|
||||||
|
|
||||||
@@ -8,6 +8,8 @@ RUN mvn clean package -DskipTests
|
|||||||
# Run stage
|
# Run stage
|
||||||
FROM eclipse-temurin:25-jre
|
FROM eclipse-temurin:25-jre
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
ARG APP_VERSION=dev
|
||||||
|
ENV APP_VERSION=${APP_VERSION}
|
||||||
COPY --from=build /app/target/*.jar app.jar
|
COPY --from=build /app/target/*.jar app.jar
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.todo.backend.controller;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/version")
|
||||||
|
public class VersionController {
|
||||||
|
|
||||||
|
@Value("${APP_VERSION:dev}")
|
||||||
|
private String appVersion;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<Map<String, String>> getVersion() {
|
||||||
|
Map<String, String> response = new HashMap<>();
|
||||||
|
response.put("version", appVersion);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ WORKDIR /app
|
|||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
|
ARG VITE_APP_VERSION=dev
|
||||||
|
ENV VITE_APP_VERSION=${VITE_APP_VERSION}
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Run stage
|
# Run stage
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<title>To-Do Tasks</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
10
frontend/public/favicon.svg
Normal file
10
frontend/public/favicon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="4" stroke="none" fill="url(#grad)" />
|
||||||
|
<path d="M8 12.5l2.5 2.5 5.5-5.5" stroke="#ffffff" stroke-width="2.5" />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="grad" x1="3" y1="3" x2="21" y2="21" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#8b5cf6" />
|
||||||
|
<stop offset="100%" stop-color="#ec4899" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 557 B |
@@ -1,4 +1,7 @@
|
|||||||
.app-container {
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 90vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
background: var(--panel-bg);
|
background: var(--panel-bg);
|
||||||
@@ -11,6 +14,27 @@
|
|||||||
animation: slideIn 0.5s ease-out;
|
animation: slideIn 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.85em;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer code {
|
||||||
|
background: rgba(139, 92, 246, 0.1);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0 0.2rem;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideIn {
|
@keyframes slideIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -30,6 +54,7 @@
|
|||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
-webkit-text-fill-color: transparent;
|
-webkit-text-fill-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,3 +209,89 @@
|
|||||||
.error {
|
.error {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Responsive Design --- */
|
||||||
|
|
||||||
|
/* Tablets and small desktops */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
padding: 1.75rem;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile phones */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.app-container {
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-list {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-form {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
padding: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-item {
|
||||||
|
padding: 0.85rem 0.75rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.todo-text {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer code {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,34 @@
|
|||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import TodoList from './components/TodoList';
|
import TodoList from './components/TodoList';
|
||||||
|
import { getVersion } from './services/api';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [backendVersion, setBackendVersion] = useState('...');
|
||||||
|
const frontendVersion = import.meta.env.VITE_APP_VERSION || 'dev';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchVersion = async () => {
|
||||||
|
try {
|
||||||
|
const data = await getVersion();
|
||||||
|
setBackendVersion(data.version || 'dev');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching backend version:', error);
|
||||||
|
setBackendVersion('unknown');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchVersion();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<div className="app-container">
|
||||||
<TodoList />
|
<main className="main-content">
|
||||||
</main>
|
<TodoList />
|
||||||
|
</main>
|
||||||
|
<footer className="app-footer">
|
||||||
|
<p>Frontend <code>{frontendVersion}</code> | Backend <code>{backendVersion}</code></p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const TodoList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-container">
|
<div className="todo-wrapper">
|
||||||
<h1 className="title">Tasks</h1>
|
<h1 className="title">Tasks</h1>
|
||||||
|
|
||||||
<form className="todo-form" onSubmit={handleCreate}>
|
<form className="todo-form" onSubmit={handleCreate}>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ body {
|
|||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
background-image: radial-gradient(circle at top right, rgba(139, 92, 246, 0.15), transparent 40%),
|
background-image: radial-gradient(circle at top right, rgba(139, 92, 246, 0.15), transparent 40%),
|
||||||
radial-gradient(circle at bottom left, rgba(236, 72, 153, 0.15), transparent 40%);
|
radial-gradient(circle at bottom left, rgba(236, 72, 153, 0.15), transparent 40%);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -31,6 +31,19 @@ body {
|
|||||||
padding: 4rem 1rem;
|
padding: 4rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||
@@ -33,3 +33,11 @@ export const deleteTodo = async (id) => {
|
|||||||
if (!response.ok) throw new Error('Failed to delete todo');
|
if (!response.ok) throw new Error('Failed to delete todo');
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getVersion = async () => {
|
||||||
|
// Use the origin to ensure it accesses the /api/version correctly regardless of environment
|
||||||
|
const VERSION_URL = BASE_URL.replace('/todos', '/version');
|
||||||
|
const response = await fetch(VERSION_URL);
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch version');
|
||||||
|
return response.json();
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user