mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: unit test, push packages with ci/cd
This commit is contained in:
parent
e97aa2c6c3
commit
0300f7fef0
6 changed files with 165 additions and 5 deletions
41
.github/workflows/ci.yml
vendored
Normal file
41
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
name: CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm test
|
||||||
|
|
||||||
|
build-and-push:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ghcr.io/${{ github.repository_owner }}/octane-website:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
70
README.md
70
README.md
|
|
@ -149,9 +149,12 @@ Remplir `.env` :
|
||||||
Puis démarrer :
|
Puis démarrer :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up --build
|
docker compose pull
|
||||||
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`docker-compose.yml` référence l'image publiée par la CI (`ghcr.io/nfonteyne/octane-website:latest`, voir [CI/CD](#cicd-et-mises-à-jour) ci-dessous) plutôt que de la construire localement — c'est ce fichier que vous utilisez sur votre serveur.
|
||||||
|
|
||||||
Les migrations SQL (`src/db/migrations/*.sql`) sont exécutées automatiquement au démarrage du conteneur `app`, de façon idempotente (une table `schema_migrations` garde la trace des fichiers déjà appliqués).
|
Les migrations SQL (`src/db/migrations/*.sql`) sont exécutées automatiquement au démarrage du conteneur `app`, de façon idempotente (une table `schema_migrations` garde la trace des fichiers déjà appliqués).
|
||||||
|
|
||||||
## Tester en local sans Authentik (ex: dans WSL)
|
## Tester en local sans Authentik (ex: dans WSL)
|
||||||
|
|
@ -192,17 +195,78 @@ Ouvrir `http://localhost:3000` : vous serez redirigé vers `/auth/login`, qui af
|
||||||
|
|
||||||
Une fois satisfait, repassez `DEV_BYPASS_AUTH=false` et configurez les variables `AUTHENTIK_*`/`OIDC_*` avant de déployer avec `docker-compose.yml` (celui avec le réseau Authentik).
|
Une fois satisfait, repassez `DEV_BYPASS_AUTH=false` et configurez les variables `AUTHENTIK_*`/`OIDC_*` avant de déployer avec `docker-compose.yml` (celui avec le réseau Authentik).
|
||||||
|
|
||||||
|
## CI/CD et mises à jour
|
||||||
|
|
||||||
|
Le workflow `.github/workflows/ci.yml` se déclenche **uniquement sur push vers `main`** :
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Push["push sur main"] --> Test["Job test\nnpm ci + npm test"]
|
||||||
|
Test -- succès --> Build["Job build-and-push\ndocker build (app seule)"]
|
||||||
|
Build --> Push2["push vers ghcr.io\n:latest"]
|
||||||
|
Push2 -. "détecte le nouveau digest" .-> Watchtower["Watchtower (sur votre serveur)"]
|
||||||
|
Watchtower --> Redeploy["redéploie le conteneur app"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Job `test`** : installe les dépendances et lance `npm test` (tests unitaires avec le test runner natif de Node — `node --test`, aucune dépendance de test supplémentaire). Actuellement couvre la validation des liens YouTube/Spotify (`test/*.test.js`).
|
||||||
|
2. **Job `build-and-push`** (uniquement si les tests passent) : construit **uniquement l'image de l'app** (le `Dockerfile` ne contient que Node/Express, jamais Postgres) et la publie sur `ghcr.io/nfonteyne/octane-website:latest`.
|
||||||
|
|
||||||
|
Sur votre serveur, `docker-compose.yml` référence cette image directement (`image: ghcr.io/nfonteyne/octane-website:latest`) au lieu de la construire — Watchtower peut donc la surveiller et la mettre à jour automatiquement dès qu'un nouveau push sur `main` produit une nouvelle image.
|
||||||
|
|
||||||
|
Si le repo GitHub est privé, le package `ghcr.io` publié le sera aussi : sur le serveur, faites une fois :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo "$GITHUB_TOKEN" | docker login ghcr.io -u nfonteyne --password-stdin
|
||||||
|
```
|
||||||
|
|
||||||
|
avec un token GitHub (classic PAT ou fine-grained) ayant le scope `read:packages`.
|
||||||
|
|
||||||
|
Pour lancer les tests en local :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sauvegarde de la base de données
|
||||||
|
|
||||||
|
Aucune sauvegarde automatique n'est intégrée à l'application — c'est volontaire, pour que vous gardiez la main sur votre solution de backup externe. Les données Postgres vivent entièrement dans le volume Docker nommé **`pgdata`** (déclaré dans `docker-compose.yml`, monté sur `/var/lib/postgresql/data` du service `postgres`).
|
||||||
|
|
||||||
|
Repérer le nom réel du volume (préfixé par le nom du projet Compose) :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume ls | grep pgdata
|
||||||
|
docker volume inspect <nom_du_volume> # donne le Mountpoint sur le disque de l'hôte
|
||||||
|
```
|
||||||
|
|
||||||
|
Deux façons de sauvegarder depuis l'extérieur :
|
||||||
|
|
||||||
|
- **Backup logique (`pg_dump`)**, recommandé, portable entre versions de Postgres :
|
||||||
|
```bash
|
||||||
|
docker compose exec postgres pg_dump -U octane -d octane -F c -f /tmp/octane.dump
|
||||||
|
docker compose cp postgres:/tmp/octane.dump ./octane_$(date +%Y%m%d).dump
|
||||||
|
```
|
||||||
|
- **Backup brut du volume**, via le `Mountpoint` renvoyé par `docker volume inspect`, ou avec un conteneur utilitaire :
|
||||||
|
```bash
|
||||||
|
docker run --rm -v <nom_du_volume>:/data -v "$(pwd)/backups":/backup alpine \
|
||||||
|
tar czf /backup/pgdata_$(date +%Y%m%d).tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
Branchez l'une de ces commandes sur votre outil de backup externe habituel (cron, Veeam, Borg, etc.).
|
||||||
|
|
||||||
## Structure du projet
|
## Structure du projet
|
||||||
|
|
||||||
```
|
```
|
||||||
octane-website/
|
octane-website/
|
||||||
├── Dockerfile, docker-compose.yml
|
├── .github/workflows/ci.yml # tests + build/push de l'image Docker sur push main
|
||||||
|
├── Dockerfile, docker-compose.yml, docker-compose.dev.yml
|
||||||
|
├── test/ # tests unitaires (node --test)
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── server.js, app.js, config.js
|
│ ├── server.js, app.js, config.js
|
||||||
│ ├── db/ # pool Postgres, migration runner, migrations SQL
|
│ ├── db/ # pool Postgres, migration runner, migrations SQL
|
||||||
│ ├── auth/ # OIDC (Authentik), session, middleware, routes /auth
|
│ ├── auth/ # OIDC (Authentik), session, middleware, routes /auth
|
||||||
│ ├── routes/ # routes API /api/*
|
│ ├── routes/ # routes API /api/*
|
||||||
│ └── repositories/ # accès SQL par table
|
│ ├── repositories/ # accès SQL par table
|
||||||
|
│ └── lib/ # helpers purs (validation YouTube/Spotify) — couverts par les tests
|
||||||
└── public/
|
└── public/
|
||||||
├── *.html # une page par fonctionnalité
|
├── *.html # une page par fonctionnalité
|
||||||
├── css/style.css
|
├── css/style.css
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: .
|
# Built and pushed by .github/workflows/ci.yml on every push to main.
|
||||||
|
# Watchtower on the server picks up new digests on this tag automatically.
|
||||||
|
image: ${APP_IMAGE:-ghcr.io/nfonteyne/octane-website:latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file: .env
|
env_file: .env
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/server.js",
|
"start": "node src/server.js",
|
||||||
"migrate": "node src/db/migrate.js"
|
"migrate": "node src/db/migrate.js",
|
||||||
|
"test": "node --test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"connect-pg-simple": "^10.0.0",
|
"connect-pg-simple": "^10.0.0",
|
||||||
|
|
|
||||||
16
test/spotify.test.js
Normal file
16
test/spotify.test.js
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { isValidSpotifyUrl } = require('../src/lib/spotify');
|
||||||
|
|
||||||
|
test('isValidSpotifyUrl: true for open.spotify.com links', () => {
|
||||||
|
assert.equal(isValidSpotifyUrl('https://open.spotify.com/track/4uLU6hMCjMI75M1A2tKUQC'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isValidSpotifyUrl: false for other hosts', () => {
|
||||||
|
assert.equal(isValidSpotifyUrl('https://www.youtube.com/watch?v=abc'), false);
|
||||||
|
assert.equal(isValidSpotifyUrl('https://spotify.com/track/abc'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isValidSpotifyUrl: false for malformed URL', () => {
|
||||||
|
assert.equal(isValidSpotifyUrl('not a url'), false);
|
||||||
|
});
|
||||||
36
test/youtube.test.js
Normal file
36
test/youtube.test.js
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
const { test } = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { extractVideoId, isValidYoutubeUrl } = require('../src/lib/youtube');
|
||||||
|
|
||||||
|
test('extractVideoId: standard watch URL', () => {
|
||||||
|
assert.equal(extractVideoId('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extractVideoId: watch URL with extra query params', () => {
|
||||||
|
assert.equal(
|
||||||
|
extractVideoId('https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=30s&list=abc'),
|
||||||
|
'dQw4w9WgXcQ'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extractVideoId: short youtu.be URL', () => {
|
||||||
|
assert.equal(extractVideoId('https://youtu.be/dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extractVideoId: embed URL', () => {
|
||||||
|
assert.equal(extractVideoId('https://www.youtube.com/embed/dQw4w9WgXcQ'), 'dQw4w9WgXcQ');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extractVideoId: non-YouTube host returns null', () => {
|
||||||
|
assert.equal(extractVideoId('https://vimeo.com/12345'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extractVideoId: malformed URL returns null', () => {
|
||||||
|
assert.equal(extractVideoId('not a url'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isValidYoutubeUrl: true for valid links, false otherwise', () => {
|
||||||
|
assert.equal(isValidYoutubeUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), true);
|
||||||
|
assert.equal(isValidYoutubeUrl('https://open.spotify.com/track/abc'), false);
|
||||||
|
assert.equal(isValidYoutubeUrl(''), false);
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue