diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b54e63..9839b75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,3 +74,20 @@ jobs: run: npm run test - name: build run: npm run build + + # The Dockerfile ships to users from the release workflow, so it is built + # here on every push and pull request (single arch, nothing published) and + # the resulting image is asked for its version. + image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Build the image + run: | + docker build \ + --build-arg VERSION=0.0.0-ci \ + --build-arg COMMIT="$(git rev-parse --short HEAD)" \ + --build-arg DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --tag davcloud:ci . + - name: The image runs and reports its version + run: docker run --rm davcloud:ci -version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dede906 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,75 @@ +name: release + +# Tagging is what publishes: a v* tag on main produces a GitHub release with +# the static binaries for linux/amd64 and linux/arm64 (CGO disabled, version +# stamped) and a multi-arch image at ghcr.io//davcloud. +on: + push: + tags: ["v*"] + +permissions: + contents: write # create the release and upload the binaries + packages: write # push the image to GHCR + +jobs: + binaries: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v7 + with: + go-version-file: go.mod + cache: true + - name: Build the static binaries + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + ldflags="-s -w -X main.version=${version} -X main.commit=$(git rev-parse --short HEAD) -X main.date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" + mkdir -p dist + for arch in amd64 arm64; do + name="davcloud-linux-${arch}" + CGO_ENABLED=0 GOOS=linux GOARCH="${arch}" \ + go build -trimpath -ldflags "${ldflags}" -o "dist/${name}" ./cmd/server + (cd dist && sha256sum "${name}" > "${name}.sha256") + done + ./dist/davcloud-linux-amd64 -version + ls -l dist + - name: Publish the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + gh release create "${GITHUB_REF_NAME}" \ + --repo "${GITHUB_REPOSITORY}" \ + --title "${GITHUB_REF_NAME}" \ + --generate-notes \ + dist/davcloud-linux-amd64 \ + dist/davcloud-linux-amd64.sha256 \ + dist/davcloud-linux-arm64 \ + dist/davcloud-linux-arm64.sha256 + + image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: docker/setup-qemu-action@v4 + - uses: docker/setup-buildx-action@v4 + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push the multi-arch image + run: | + set -euo pipefail + image="ghcr.io/$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')" + version="${GITHUB_REF_NAME#v}" + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --build-arg VERSION="${version}" \ + --build-arg COMMIT="$(git rev-parse --short HEAD)" \ + --build-arg DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + --tag "${image}:${GITHUB_REF_NAME}" \ + --tag "${image}:latest" \ + --push . diff --git a/Dockerfile b/Dockerfile index 81596a0..1bdc1f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,15 +8,27 @@ RUN npm run build # ── Backend build ─────────────────────────────────────────────────────────── FROM golang:1.24-alpine AS build +# Version metadata as build args, so the binary inside the image reports the +# tag it was built from instead of "dev". +ARG VERSION=dev +ARG COMMIT=unknown +ARG DATE=unknown WORKDIR /src -COPY go.mod go.sum* ./ -# go.sum is generated by `go mod tidy` on first build; tidy also fetches deps. -RUN go mod tidy +COPY go.mod go.sum ./ +RUN go mod download COPY . . -RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/davcloud ./cmd/server +RUN CGO_ENABLED=0 go build -trimpath \ + -ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \ + -o /out/davcloud ./cmd/server # ── Runtime ───────────────────────────────────────────────────────────────── FROM alpine:3.21 +ARG VERSION=dev +LABEL org.opencontainers.image.title="davcloud" \ + org.opencontainers.image.description="CalDAV and CardDAV server over a Radicale-compatible data directory" \ + org.opencontainers.image.source="https://github.com/gnacho/davcloud" \ + org.opencontainers.image.version="${VERSION}" \ + org.opencontainers.image.licenses="AGPL-3.0-only" RUN apk add --no-cache ca-certificates tzdata COPY --from=build /out/davcloud /usr/local/bin/davcloud COPY --from=ui /src/dist /app/ui diff --git a/PLAN.md b/PLAN.md index 27ed301..1d64ca5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -7,8 +7,9 @@ opencloud-contacts) noten la diferencia. Estado al 18-Oct-2026: **fases 0-4 completadas** (issues #1-#9 y #21 cerrados; davcloud es el servidor DAV de producción desde el 17-Oct) y **fase 5 en curso** (soak en marcha; el **drill de rollback pasó el 18-Oct-2026**, issue #22). -**Nada de Docker**: el despliegue es un binario estático + systemd (ver -`deploy/README.md`). Repo público desde el 18-Oct-2026, licencia AGPL-3.0. +El despliegue propio **no usa Docker**: binario estático + systemd (ver +`deploy/README.md`); la imagen de contenedor existe para quien prefiera ese +camino (fase 6). Repo público desde el 18-Oct-2026, licencia AGPL-3.0. ## Hecho @@ -50,16 +51,30 @@ davcloud es el servidor DAV de producción desde el 17-Oct) y **fase 5 en curso* colección leídas y ninguno de los items borrados reapareciendo; vuelta a davcloud con los items byte a byte intactos (Radicale solo añade su `.Radicale.cache/item/`). +- **Endurecimiento** (issues #38 y #39, 18-Oct-2026): los nombres gestionados + por el servidor quedan fuera del alcance de los clientes DAV (404 al leer, + 400 al escribir, la misma semántica que Radicale) y hay límites explícitos de + tamaño (petición y objeto, con `max-resource-size` anunciado y timeouts de + HTTP); el tope de expansión de recurrencias deja acotadas las consultas + patológicas (un `calendar-query` `SECONDLY` que antes no terminaba en 25 s + responde 207 en 0,0016 s). Desplegado en producción el mismo día, con el + rechazo verificado en vivo. ## Pendiente (issues) +- **#43 - Fase 6: distribución** (abierta el 18-Oct-2026): binarios + `linux/amd64` y `linux/arm64` por tag e imagen multi-arch en + `ghcr.io/gnacho/davcloud`, para que probarlo no exija toolchain. El + despliegue propio no cambia. Versiones de `0.2` en `0.2` (`v0.2.0`, + `v0.4.0`, ...). + - **#22 - Fase 5: soak + drill de rollback** (soak en curso desde el 17-Oct: davcloud es el único servidor DAV; el drill ya pasó el 18-Oct-2026, ver arriba). Queda el uso diario con datos reales y, solo entonces, retirar Radicale del contenedor. - **Edición de recurrencias en la web UI**: sigue siendo un TODO documentado en `web/src/lib/ics.ts`. -- **Fase 6 - paridad extendida (opcional, por necesidad)**: iMIP +- **Fase 7 - paridad extendida (opcional, por necesidad)**: iMIP (invitaciones por email) y compartición de calendarios (requiere ACLs que el proxy no proporciona). Radicale tampoco cubre bien ninguno de los dos. diff --git a/README.es.md b/README.es.md index ca0fed9..3d2ddad 100644 --- a/README.es.md +++ b/README.es.md @@ -13,8 +13,9 @@ detrás de un despliegue de referencia trae Radicale exactamente así), pero funciona detrás de cualquier proxy que autentique e inyecte la identidad. -Un binario estático (~7 MB), sin dependencias de runtime, sin base de datos, -sin contenedores: el servidor y el volumen de datos son todo el despliegue. +Un binario estático (~7 MB), sin dependencias de runtime, sin base de datos: +el binario y el volumen de datos son todo el despliegue (también publicamos +una imagen de contenedor, para quien la prefiera). ## Qué hace @@ -90,8 +91,43 @@ volumen de datos, layout "multifilesystem" de Radicale ## Cómo instalarlo -Requisitos: Go 1.24+ para compilar. `npm` solo si quieres la web UI. Sin -base de datos ni dependencias de runtime. +Para compilarlo necesitas Go 1.24+ (`npm` solo si quieres la web UI). Sin base +de datos ni dependencias de runtime. Un binario de release o la imagen de +contenedor ya se bastan solos. + +**Descargar una release** (las etiquetas van de `0.2` en `0.2`: `v0.2.0`, +`v0.4.0`, ...): + +```bash +curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64 +curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64.sha256 +sha256sum -c davcloud-linux-amd64.sha256 +chmod +x davcloud-linux-amd64 +./davcloud-linux-amd64 -version +``` + +En máquinas ARM usa `arm64` en lugar de `amd64`. + +**Contenedor**: + +```bash +mkdir -p davcloud-data +docker run -d --name davcloud \ + --user "$(id -u):$(id -g)" \ + -p 127.0.0.1:5232:5232 \ + -v "$PWD/davcloud-data:/data" \ + ghcr.io/gnacho/davcloud:latest \ + -addr :5232 -data-dir /data -ui-dir /app/ui -dev-user alice +curl -s -o /dev/null -w '%{http_code}\n' -X PROPFIND -H 'Depth: 0' \ + http://127.0.0.1:5232/caldav/ +# 207 +``` + +La imagen corre como uid 1000, de ahí `--user` con tus propios ids para que +pueda escribir en el directorio montado. `-dev-user` es el atajo de +desarrollo que se fía de toda petición como ese usuario; en producción la +identidad llega en la cabecera `X-Remote-User` que inyecta el proxy, así que +el puerto no debe ser alcanzable por nadie más. **Compilar** diff --git a/README.md b/README.md index 3baf956..6f3343b 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,9 @@ same header a reverse proxy injects. Designed to sit behind an full-stack reference ships Radicale exactly like this), but it works behind any proxy that authenticates and injects the identity. -One static binary (~7 MB), no runtime dependencies, no database, no -containers: the server and the data volume are the whole deployment. +One static binary (~7 MB), no runtime dependencies, no database: the binary +and the data volume are the whole deployment (a container image is published +as well, for those who prefer one). ## What it does @@ -84,8 +85,42 @@ data volume, Radicale "multifilesystem" layout ## How to install it -Requirements: Go 1.24+ to build. `npm` only if you want the web UI. No -database, no runtime dependencies. +To build it you need Go 1.24+ (plus `npm` for the web UI). No database, no +runtime dependencies. A released binary or the container image is +self-contained. + +**Download a release** (tags move in steps of `0.2`: `v0.2.0`, `v0.4.0`, ...): + +```bash +curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64 +curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64.sha256 +sha256sum -c davcloud-linux-amd64.sha256 +chmod +x davcloud-linux-amd64 +./davcloud-linux-amd64 -version +``` + +Use `arm64` instead of `amd64` on ARM machines. + +**Container**: + +```bash +mkdir -p davcloud-data +docker run -d --name davcloud \ + --user "$(id -u):$(id -g)" \ + -p 127.0.0.1:5232:5232 \ + -v "$PWD/davcloud-data:/data" \ + ghcr.io/gnacho/davcloud:latest \ + -addr :5232 -data-dir /data -ui-dir /app/ui -dev-user alice +curl -s -o /dev/null -w '%{http_code}\n' -X PROPFIND -H 'Depth: 0' \ + http://127.0.0.1:5232/caldav/ +# 207 +``` + +The image runs as uid 1000, hence `--user` with your own ids so that it can +write into the bind-mounted directory. `-dev-user` is the development shortcut +that trusts every request as that user; in production the identity comes from +the `X-Remote-User` header injected by the proxy, so the port must stay +unreachable for anybody else. **Build** diff --git a/deploy/README.md b/deploy/README.md index 575ad59..f917008 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -57,7 +57,16 @@ sha256sum ./radicale-vol-.tgz # compara con el sha256 sacado dentro de ## 1. Binario estático -En cualquier máquina con Go (el CT no necesita toolchain): +Sin toolchain en el CT. Puedes descargar el binario de la release en vez de +compilarlo (comprueba el `sha256`, que va en el mismo asset): + +``` +curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64 +curl -LO https://github.com/gnacho/davcloud/releases/latest/download/davcloud-linux-amd64.sha256 +sha256sum -c davcloud-linux-amd64.sha256 +``` + +O compilarlo en cualquier máquina con Go: ``` CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath \