merge(main): packaging Docker Compose et push Gitea Registry (ticket #52)
Fusionne feature/server-52-docker-packaging — dart analyze clean, dart test 24/24 vert, docker compose config valide. Dockerfile et scripts relus manuellement et jugés corrects ; pas de docker build/compose up réel faute d'accès au daemon Docker dans ce sandbox. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#51"
|
issueRef: "#51"
|
||||||
version: 1
|
version: 3
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784412007538
|
updatedAt: 1784450081385
|
||||||
---
|
---
|
||||||
|
Endpoints `POST /shares`, `GET /shares/inbox`, `POST /shares/{id}/accept|decline|revoke` implémentés, protégés par le middleware Bearer. Acceptation crée une copie `synced_resources` (nouveaux IDs) pour le destinataire sans jamais modifier la ressource source de l'émetteur — relu et confirmé correct. Gestion des conflits : 404 si partage/destinataire introuvable ou révocation par non-émetteur, 409 si acceptation d'un partage révoqué ou déjà répondu. Emails destinataires inconnus retournés dans `unresolvedEmails` sans échouer toute la requête. Tests unitaires avec fakes en mémoire verts (24/24 sur toute la suite serveur), `dart analyze` clean. Non vérifié : aucun test de bout en bout contre un vrai PostgreSQL (pas d'accès Docker dans le sandbox Main).
|
||||||
@ -2,15 +2,15 @@
|
|||||||
id: "850ce975-99c3-4ee4-8d39-759ecc55139c"
|
id: "850ce975-99c3-4ee4-8d39-759ecc55139c"
|
||||||
number: 51
|
number: 51
|
||||||
title: "[Server] Partage ciblé de programmes et séances entre comptes"
|
title: "[Server] Partage ciblé de programmes et séances entre comptes"
|
||||||
status: "open"
|
status: "qa"
|
||||||
priority: "low"
|
priority: "low"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#46","kind":"relatesTo"},{"target":"#48","kind":"dependsOn"},{"target":"#49","kind":"dependsOn"}]
|
links: [{"target":"#46","kind":"relatesTo"},{"target":"#48","kind":"dependsOn"},{"target":"#49","kind":"dependsOn"}]
|
||||||
agentRefs: []
|
agentRefs: []
|
||||||
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784412007538
|
createdAt: 1784412007538
|
||||||
updatedAt: 1784412007538
|
updatedAt: 1784450081385
|
||||||
version: 1
|
version: 3
|
||||||
---
|
---
|
||||||
Implémenter le domaine de partage ciblé : `Share`, destinataires par compte utilisateur, statut `pending|accepted|declined|revoked`, payload snapshot autonome de Program ou WorkoutTemplate. Endpoints : `POST /shares`, `GET /shares/inbox`, `POST /shares/{id}/accept`, `POST /shares/{id}/decline`, `POST /shares/{id}/revoke`. À l'acceptation, créer une copie importable dans l'espace du destinataire, sans lien public ouvert.
|
Implémenter le domaine de partage ciblé : `Share`, destinataires par compte utilisateur, statut `pending|accepted|declined|revoked`, payload snapshot autonome de Program ou WorkoutTemplate. Endpoints : `POST /shares`, `GET /shares/inbox`, `POST /shares/{id}/accept`, `POST /shares/{id}/decline`, `POST /shares/{id}/revoke`. À l'acceptation, créer une copie importable dans l'espace du destinataire, sans lien public ouvert.
|
||||||
@ -577,11 +577,11 @@
|
|||||||
"issueRef": "#51",
|
"issueRef": "#51",
|
||||||
"path": "51",
|
"path": "51",
|
||||||
"title": "[Server] Partage ciblé de programmes et séances entre comptes",
|
"title": "[Server] Partage ciblé de programmes et séances entre comptes",
|
||||||
"status": "open",
|
"status": "qa",
|
||||||
"priority": "low",
|
"priority": "low",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [],
|
"assignedAgentIds": [],
|
||||||
"updatedAt": 1784412007538
|
"updatedAt": 1784450081385
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#52",
|
"issueRef": "#52",
|
||||||
|
|||||||
5
server/.dockerignore
Normal file
5
server/.dockerignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.dart_tool/
|
||||||
|
build/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
pubspec_overrides.yaml
|
||||||
23
server/.env.example
Normal file
23
server/.env.example
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# API container.
|
||||||
|
# Set API_BIND_ADDRESS to the Docker host interface reachable by the external
|
||||||
|
# reverse proxy. Use 0.0.0.0 for LAN-wide exposure, or a specific host IP to
|
||||||
|
# restrict where Docker publishes the port.
|
||||||
|
API_BIND_ADDRESS=0.0.0.0
|
||||||
|
API_PORT=8080
|
||||||
|
MIGRATE_ON_STARTUP=true
|
||||||
|
|
||||||
|
# PostgreSQL container bootstrap values. Replace the password before deploying.
|
||||||
|
POSTGRES_DB=gametime
|
||||||
|
POSTGRES_USER=gametime
|
||||||
|
POSTGRES_PASSWORD=change-me
|
||||||
|
|
||||||
|
# API database connection values. With this docker-compose.yaml, the database
|
||||||
|
# host is the Compose service name `postgres`.
|
||||||
|
DATABASE_HOST=postgres
|
||||||
|
DATABASE_PORT=5432
|
||||||
|
DATABASE_NAME=gametime
|
||||||
|
DATABASE_USER=gametime
|
||||||
|
DATABASE_PASSWORD=change-me
|
||||||
|
|
||||||
|
# External HTTPS is terminated by a reverse proxy running outside this Compose
|
||||||
|
# stack. Point that proxy to http://<docker-host>:${API_PORT}.
|
||||||
33
server/Dockerfile
Normal file
33
server/Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
FROM dart:stable AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY pubspec.* ./
|
||||||
|
RUN dart pub get
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN dart compile exe bin/server.dart -o build/server \
|
||||||
|
&& dart compile exe bin/migrate.dart -o build/migrate
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV PORT=8080
|
||||||
|
ENV MIGRATE_ON_STARTUP=true
|
||||||
|
|
||||||
|
COPY --from=build /app/build/server /app/bin/server
|
||||||
|
COPY --from=build /app/build/migrate /app/bin/migrate
|
||||||
|
COPY migrations/ /app/migrations/
|
||||||
|
COPY scripts/docker-entrypoint.sh /app/scripts/docker-entrypoint.sh
|
||||||
|
|
||||||
|
RUN chmod +x /app/bin/server /app/bin/migrate /app/scripts/docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/scripts/docker-entrypoint.sh"]
|
||||||
|
CMD ["/app/bin/server"]
|
||||||
@ -125,13 +125,73 @@ without failing the whole request. Accepting a share creates a new
|
|||||||
`409` for revoked or already answered shares, while missing shares or recipients
|
`409` for revoked or already answered shares, while missing shares or recipients
|
||||||
return `404`.
|
return `404`.
|
||||||
|
|
||||||
|
## Docker Deployment
|
||||||
|
|
||||||
|
Ticket #52 adds Docker packaging for a headless Linux deployment. TLS is not
|
||||||
|
handled by the API container: terminate HTTPS in an external reverse proxy and
|
||||||
|
forward traffic to the Docker host on `API_PORT`.
|
||||||
|
|
||||||
|
Create a local environment file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit `.env` before deployment:
|
||||||
|
|
||||||
|
- `API_BIND_ADDRESS`: Docker host interface to publish. Use `0.0.0.0` for LAN
|
||||||
|
access by the external reverse proxy, or a specific host IP to restrict the
|
||||||
|
bind address.
|
||||||
|
- `API_PORT`: host port exposed for the reverse proxy.
|
||||||
|
- `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD`: PostgreSQL bootstrap
|
||||||
|
values.
|
||||||
|
- `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_NAME`, `DATABASE_USER`,
|
||||||
|
`DATABASE_PASSWORD`: database connection used by the API. With the provided
|
||||||
|
Compose file, `DATABASE_HOST=postgres`.
|
||||||
|
- `MIGRATE_ON_STARTUP`: when `true`, the API container applies SQL migrations
|
||||||
|
before starting the server.
|
||||||
|
|
||||||
|
Start the stack from `server/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
The `api` service waits for the PostgreSQL healthcheck, runs `/app/bin/migrate`,
|
||||||
|
then launches `/app/bin/server`. Migrations are copied into the image under
|
||||||
|
`/app/migrations`.
|
||||||
|
|
||||||
|
To inspect the fully interpolated Compose configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gitea Registry
|
||||||
|
|
||||||
|
Build and push the server image to a Gitea Container Registry with no registry
|
||||||
|
or credential value stored in the repository:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GITEA_REGISTRY=gitea.example.com \
|
||||||
|
GITEA_OWNER=my-org \
|
||||||
|
GITEA_IMAGE_NAME=gametime-server \
|
||||||
|
GITEA_IMAGE_TAG=latest \
|
||||||
|
GITEA_USERNAME=my-user \
|
||||||
|
GITEA_TOKEN='replace-with-token' \
|
||||||
|
./scripts/push-gitea-image.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`GITEA_IMAGE_TAG` defaults to `latest` when omitted. The script uses
|
||||||
|
`docker login --password-stdin` so the token is not printed by the command line.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
Ticket #47 only scaffolds the Dart server, the hexagonal directory layout and
|
Ticket #47 only scaffolds the Dart server, the hexagonal directory layout and
|
||||||
the `/health` endpoint. Ticket #49 adds the first PostgreSQL schema. Ticket #48
|
the `/health` endpoint. Ticket #49 adds the first PostgreSQL schema. Ticket #48
|
||||||
adds authentication. Ticket #50 adds sync. Ticket #51 adds targeted sharing.
|
adds authentication. Ticket #50 adds sync. Ticket #51 adds targeted sharing.
|
||||||
|
Ticket #52 adds Docker Compose packaging and Gitea registry publishing.
|
||||||
|
|
||||||
Upcoming tickets will fill the empty adapters and use cases:
|
Upcoming tickets will fill the empty adapters and use cases:
|
||||||
|
|
||||||
- #52: Docker and registry packaging.
|
|
||||||
- #53: API, contract and integration tests.
|
- #53: API, contract and integration tests.
|
||||||
|
|||||||
42
server/docker-compose.yaml
Normal file
42
server/docker-compose.yaml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
PORT: 8080
|
||||||
|
MIGRATE_ON_STARTUP: ${MIGRATE_ON_STARTUP:?set MIGRATE_ON_STARTUP}
|
||||||
|
DATABASE_HOST: ${DATABASE_HOST:?set DATABASE_HOST}
|
||||||
|
DATABASE_PORT: ${DATABASE_PORT:?set DATABASE_PORT}
|
||||||
|
DATABASE_NAME: ${DATABASE_NAME:?set DATABASE_NAME}
|
||||||
|
DATABASE_USER: ${DATABASE_USER:?set DATABASE_USER}
|
||||||
|
DATABASE_PASSWORD: ${DATABASE_PASSWORD:?set DATABASE_PASSWORD}
|
||||||
|
ports:
|
||||||
|
- "${API_BIND_ADDRESS:?set API_BIND_ADDRESS}:${API_PORT:?set API_PORT}:8080"
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""
|
||||||
|
]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
@ -2,4 +2,7 @@
|
|||||||
|
|
||||||
Operational scripts for the server package.
|
Operational scripts for the server package.
|
||||||
|
|
||||||
Docker image publishing is planned for ticket #52.
|
- `docker-entrypoint.sh`: container entrypoint; applies migrations when
|
||||||
|
`MIGRATE_ON_STARTUP=true`, then executes the server command.
|
||||||
|
- `push-gitea-image.sh`: builds and pushes the Docker image to a Gitea
|
||||||
|
Container Registry using environment variables only.
|
||||||
|
|||||||
8
server/scripts/docker-entrypoint.sh
Executable file
8
server/scripts/docker-entrypoint.sh
Executable file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "${MIGRATE_ON_STARTUP:-true}" = "true" ]; then
|
||||||
|
/app/bin/migrate
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
29
server/scripts/push-gitea-image.sh
Executable file
29
server/scripts/push-gitea-image.sh
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local name="$1"
|
||||||
|
if [[ -z "${!name:-}" ]]; then
|
||||||
|
echo "Missing required environment variable: ${name}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env GITEA_REGISTRY
|
||||||
|
require_env GITEA_OWNER
|
||||||
|
require_env GITEA_IMAGE_NAME
|
||||||
|
require_env GITEA_USERNAME
|
||||||
|
require_env GITEA_TOKEN
|
||||||
|
|
||||||
|
GITEA_IMAGE_TAG="${GITEA_IMAGE_TAG:-latest}"
|
||||||
|
IMAGE_REF="${GITEA_REGISTRY}/${GITEA_OWNER}/${GITEA_IMAGE_NAME}:${GITEA_IMAGE_TAG}"
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
SERVER_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
docker build -t "${IMAGE_REF}" "${SERVER_DIR}"
|
||||||
|
printf '%s' "${GITEA_TOKEN}" | docker login "${GITEA_REGISTRY}" \
|
||||||
|
--username "${GITEA_USERNAME}" \
|
||||||
|
--password-stdin
|
||||||
|
docker push "${IMAGE_REF}"
|
||||||
|
|
||||||
|
echo "Pushed ${IMAGE_REF}"
|
||||||
Reference in New Issue
Block a user