Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
name: CI

on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} # italia/developers-cms
CHART_PATH: deploy/helm/developers-cms
CHART_REGISTRY: oci://ghcr.io/${{ github.repository_owner }}/charts

jobs:
# ── Test applicativi: integrazione (Payload Local API su Postgres) ──────────
test:
name: Integration tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: developers_cms_test
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/developers_cms_test
PAYLOAD_SECRET: ci-test-secret
CI: 'true'
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3'
- run: bun install --frozen-lockfile
- name: Run integration tests
run: bun run test:int

# ── Test applicativi: end-to-end (Playwright sull'admin Payload) ────────────
e2e:
name: E2E tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: developers_cms_e2e
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/developers_cms_e2e
PAYLOAD_SECRET: ci-e2e-secret
CI: 'true'
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3'
- run: bun install --frozen-lockfile
- name: Install Playwright (chromium)
run: bunx playwright install --with-deps chromium
- name: Start app (dev) in background
run: |
nohup bun run dev > dev-server.log 2>&1 &
echo "Attendo l'avvio su http://localhost:3000 ..."
for i in $(seq 1 60); do
if curl -fsS http://localhost:3000/api/access >/dev/null 2>&1; then
echo "App pronta"; exit 0
fi
sleep 3
done
echo "Timeout avvio app"; cat dev-server.log; exit 1
- name: Run E2E tests
run: bun run test:e2e
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: |
playwright-report/
dev-server.log
retention-days: 7

# ── Build immagine container e push su GHCR ─────────────────────────────────
build-push:
name: Build & push image
runs-on: ubuntu-latest
needs: [test, e2e]
if: github.event_name != 'pull_request'
permissions:
contents: read
packages: write
outputs:
version: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=sha,format=long
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

# ── Lint, package e push del chart Helm su GHCR (OCI) ───────────────────────
helm:
name: Package & push Helm chart
runs-on: ubuntu-latest
needs: [build-push]
if: github.event_name != 'pull_request'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v4
with:
version: v3.16.2
- name: Lint chart
run: |
helm lint "$CHART_PATH" \
--set app.databaseUrl=postgres://u:p@db:5432/x \
--set app.payloadSecret=dummy
- name: Compute versions
id: ver
run: |
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
CHART_VERSION="${GITHUB_REF#refs/tags/v}"
APP_VERSION="${GITHUB_REF#refs/tags/v}"
else
CHART_VERSION="0.1.0-main.${GITHUB_SHA::7}"
APP_VERSION="sha-${GITHUB_SHA}"
fi
echo "chart=$CHART_VERSION" >> "$GITHUB_OUTPUT"
echo "app=$APP_VERSION" >> "$GITHUB_OUTPUT"
- name: Helm registry login
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: Package & push
run: |
helm package "$CHART_PATH" \
--version "${{ steps.ver.outputs.chart }}" \
--app-version "${{ steps.ver.outputs.app }}"
helm push "developers-cms-${{ steps.ver.outputs.chart }}.tgz" "$CHART_REGISTRY"
98 changes: 37 additions & 61 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,71 +1,47 @@
# To use this Dockerfile, you have to set `output: 'standalone'` in your next.config.mjs file.
# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile

FROM node:22.17.0-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
# Immagine di produzione per Developers CMS (Payload 3 / Next.js).
#
# Una sola immagine usata per tre scopi (vedi chart Helm in deploy/helm/developers-cms):
# - web: `next start` (default CMD)
# - migrate: `node node_modules/.bin/payload migrate` (job pre-deploy)
# - seed: `node /seed/seed.mjs` (job post-deploy: utente + API key)
#
# Runtime: node (la CLI `payload` richiede node anche se lanciata da bun → spawn node).
# Le dipendenze si installano con bun (lockfile bun.lock); build e runtime girano su node.
# Tutte le stage sono glibc (debian) per compatibilità dei moduli nativi (es. sharp).
# NON è una build "standalone" trimmata: servono node_modules + src (config Payload +
# migrations) per poter eseguire la CLI `payload` in cluster.

FROM oven/bun:1.3-debian AS deps
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi


# Rebuild the source code only when needed
FROM base AS builder
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_OPTIONS=--max-old-space-size=4000
RUN node_modules/.bin/next build

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi

# Production image, copy all the files and run next
FROM base AS runner
FROM node:22-bookworm-slim AS runner
WORKDIR /app

ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Remove this line if you do not have this folder
COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0
RUN groupadd -g 1001 nodejs && useradd -u 1001 -g nodejs -m nextjs

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/next.config.ts ./next.config.ts
COPY --from=builder /app/tsconfig.json ./tsconfig.json
COPY --from=builder /app/src ./src

# La collection Media scrive i media qui (montare un PVC su /app/media in produzione).
RUN mkdir -p /app/media && chown -R nextjs:nodejs /app/media /app/.next

USER nextjs

EXPOSE 3000

ENV PORT 3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js
CMD ["node_modules/.bin/next", "start"]
10 changes: 10 additions & 0 deletions deploy/helm/developers-cms/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Patterns to ignore when building packages.
.DS_Store
.git/
.gitignore
*.tmp
*.bak
*.orig
.vscode/
.idea/
README.md
6 changes: 6 additions & 0 deletions deploy/helm/developers-cms/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v2
name: developers-cms
description: Developers CMS (Payload 3 / Next.js) — PostgreSQL e media gestiti esternamente al chart
type: application
version: 0.1.0
appVersion: "1.0.0"
82 changes: 82 additions & 0 deletions deploy/helm/developers-cms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# developers-cms (Helm chart)

Chart per il deploy di **Developers CMS** (Payload 3 / Next.js) su Kubernetes.
PostgreSQL e media (PVC) sono gestiti esternamente al chart.

I valori reali per ogni ambiente (DATABASE_URL, PAYLOAD_SECRET, seed/apiKey, ingress)
**non** stanno qui: vivono nel repo `k8s-configuration` sotto
`helm/developers-cms/{test,prod}/values.yaml` e si passano con `-f`.

## Cosa fa il chart

| Risorsa | Note |
|---|---|
| Deployment + Service | Next/Payload sulla porta `app.port` (3000). `SITE_URL` per CORS/CSRF derivato dall'ingress se non impostato. |
| Ingress | opzionale, con annotazioni cert-manager (HTTP-01). |
| Secret `-env` | `DATABASE_URL`, `PAYLOAD_SECRET` (hook pre-install, montato come `envFrom`). |
| PVC `-media` | persistenza dei media (`upload: true`). `resource-policy: keep`. |
| Job `-migrate` | hook **pre-install/pre-upgrade**: esegue `payload migrate`. |
| Job `-seed` | hook **post-install/post-upgrade**: crea il primo utente e gli assegna una **API key** deterministica. Idempotente. |

## Installazione

```sh
helm upgrade --install developers-cms-test deploy/helm/developers-cms \
-n developers-cms-test --create-namespace \
-f /path/to/k8s-configuration/helm/developers-cms/test/values.yaml
```

## Seed: utente + API key

Con `seed.enabled=true` il job post-deploy:

1. crea il primo utente (`POST /api/users/first-register`) con `seed.user.email` / `seed.user.password` — no-op se esiste già;
2. fa login e abilita la API key sull'utente impostando **`seed.apiKey`** come valore in chiaro.

Quella `apiKey` è la credenziale che il repo di import (e qualunque client) usa per le API
in lettura e scrittura:

```
Authorization: users API-Key <seed.apiKey>
```

```sh
curl -H "Authorization: users API-Key <apiKey>" https://<host>/api/articles # lettura
curl -X POST -H "Authorization: users API-Key <apiKey>" -H "Content-Type: application/json" \
-d '{"title":"..."}' https://<host>/api/articles # scrittura
```

## Migrations (baseline)

Lo schema viene creato dal job `-migrate` (`payload migrate`) come hook pre-deploy.

> Nota storica: le migration originali contenevano solo `ALTER TABLE` (idType → uuid),
> senza `CREATE TABLE`, e fallivano su DB vuoto. Sono state sostituite da una **baseline
> completa** (`src/migrations/2026..._initial.ts`, 283 `CREATE TABLE`) verificata in locale
> su Postgres vuoto. Per rigenerarla da zero:
>
> ```sh
> # contro un Postgres VUOTO dedicato
> DATABASE_URL=postgres://... node node_modules/.bin/payload migrate:create initial --force-accept-warning
> ```

## ⚠️ Path dei media

`media.persistence.mountPath` (default `/app/media`) deve coincidere con la `staticDir`
effettiva della collection `Media`. Il Dockerfile in questo repo crea `/app/media` e l'app
gira con WORKDIR `/app`. Se in futuro si imposta una `staticDir` esplicita nella collection,
aggiornare di conseguenza il `mountPath`. Per scenari multi-replica passare a object storage
(`@payloadcms/storage-azure` / `-s3`) invece del PVC `ReadWriteOnce`.

## Immagine

L'immagine (`Dockerfile` in root) NON è una build standalone: include `node_modules` + `src`
così la CLI `payload` può girare in cluster (migrate/seed). Runtime: **node** (la CLI `payload`
fa comunque `spawn node`, quindi node deve essere presente). Le dipendenze si installano con
bun (lockfile `bun.lock`); build e runtime girano su node. Tutte le stage sono glibc per
compatibilità dei moduli nativi (sharp).

## Valori principali

Vedi `values.yaml`. I più rilevanti: `app.image.*`, `app.databaseUrl`, `app.payloadSecret`,
`app.siteUrl`, `media.persistence.*`, `migrate.*`, `seed.*`, `ingress.*`, `imagePullSecrets`.
Loading
Loading