diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1384575..cb0519a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [master] +permissions: + contents: read + jobs: backend-tests: name: Backend Tests @@ -96,38 +99,83 @@ jobs: run: go build -v ./cmd/server docker-build: - name: Docker Build Test + name: Docker Build (${{ matrix.app }}) runs-on: ubuntu-latest - + strategy: + fail-fast: false + matrix: + app: [backend, auth, frontend] steps: - uses: actions/checkout@v4 - + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - - name: Build backend image + + - name: Build ${{ matrix.app }} image uses: docker/build-push-action@v6 with: context: . - file: apps/backend/Dockerfile + file: apps/${{ matrix.app }}/Dockerfile push: false - tags: primora-backend:test - - - name: Build auth image + load: true + tags: primora-${{ matrix.app }}:test + cache-from: type=gha,scope=${{ matrix.app }} + cache-to: type=gha,mode=max,scope=${{ matrix.app }} + + - name: Smoke check ${{ matrix.app }} image contents + run: | + case "${{ matrix.app }}" in + backend) docker run --rm --entrypoint sh primora-backend:test -c 'test -x /usr/local/bin/primora-backend && test -x /usr/local/bin/dbx-mcp' ;; + auth) docker run --rm --entrypoint sh primora-auth:test -c 'test -f /workspace/apps/auth/dist/index.js' ;; + frontend) docker run --rm --entrypoint sh primora-frontend:test -c 'test -f /usr/share/nginx/html/index.html' ;; + esac + + - name: Scan ${{ matrix.app }} image + uses: aquasecurity/trivy-action@v0.33.1 + with: + image-ref: primora-${{ matrix.app }}:test + severity: CRITICAL,HIGH + ignore-unfixed: true + exit-code: '0' + format: table + + docker-publish: + name: Publish edge images + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: [docker-build, e2e-smoke] + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + strategy: + fail-fast: false + matrix: + app: [backend, auth, frontend] + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + + # ghcr.io requires lowercase repo paths; the owner login may be mixed-case. + - name: Compute lowercase GHCR owner + run: echo "GHCR_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_ENV" + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push edge + sha tags uses: docker/build-push-action@v6 with: context: . - file: apps/auth/Dockerfile - push: false - tags: primora-auth:test - - - name: Build frontend image - uses: docker/build-push-action@v6 - with: - context: . - file: apps/frontend/Dockerfile - push: false - tags: primora-frontend:test + file: apps/${{ matrix.app }}/Dockerfile + push: true + tags: | + ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:edge + ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:sha-${{ github.sha }} + cache-from: type=gha,scope=${{ matrix.app }} + cache-to: type=gha,mode=max,scope=${{ matrix.app }} migrations: name: Migration Up/Down @@ -186,6 +234,12 @@ jobs: - name: Prepare env run: cp .env.example .env + - name: Validate compose files + run: >- + docker compose + -f docker-compose.yml -f docker-compose.dev.yml + config -q + - name: Start stack run: >- docker compose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d07679..88a2f73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,9 @@ jobs: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 + # ghcr.io requires lowercase repo paths; the owner login may be mixed-case. + - name: Compute lowercase GHCR owner + run: echo "GHCR_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_ENV" - uses: docker/login-action@v3 with: registry: ghcr.io @@ -52,8 +55,8 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: | - ghcr.io/${{ github.repository_owner }}/primora-${{ matrix.app }}:${{ env.VERSION }} - ghcr.io/${{ github.repository_owner }}/primora-${{ matrix.app }}:latest + ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:${{ env.VERSION }} + ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:latest cache-from: type=gha,scope=${{ matrix.app }} cache-to: type=gha,mode=max,scope=${{ matrix.app }} diff --git a/README.md b/README.md index 161e83b..251e246 100644 --- a/README.md +++ b/README.md @@ -21,11 +21,30 @@ Browser ──▶ Nginx ──▶ Frontend (SolidJS + Vite) - **Generated client** — the TypeScript client is generated from `apps/backend/openapi/openapi.yaml`, so the API contract is the source of truth. - **Demo mode** — a fully client-side workspace (`?demo=true` or `VITE_DEMO_MODE=true`) for trying the UI without a backend. -## Quick start (Docker) +## Install Prerequisites: Docker with the Compose plugin. +### One-liner (no clone) + ```bash +curl -fsSL https://raw.githubusercontent.com/Dvorinka/Primora/master/install.sh | bash +``` + +Installs into `./primora`, generates secrets, and starts the stack on port 80. +Non-interactive — configure with env vars: + +```bash +DOMAIN=example.com NGINX_PORT=8080 PRIMORA_DIR=/opt/primora bash install.sh +``` + +The installer pulls prebuilt images from GHCR when they are published; +otherwise it downloads the source tarball and builds locally. + +### From a clone + +```bash +git clone https://github.com/Dvorinka/Primora.git && cd Primora ./scripts/setup.sh # creates .env, generates secrets, starts the stack ``` @@ -33,13 +52,31 @@ or manually: ```bash cp .env.example .env # fill JWT_SECRET and BETTER_AUTH_SECRET -docker compose up -d --build +docker compose up -d # builds the three app images from source ``` -Then open `http://localhost`. Mailpit is at `http://localhost/mailpit/` for local email. +To run published images instead of building: + +```bash +docker pull ghcr.io/dvorinka/primora-{backend,auth,frontend}:latest +docker compose up -d --no-build +``` + +Then open `http://localhost`. For local email capture, add the dev overlay +(`-f docker-compose.dev.yml`) and Mailpit is at `http://localhost/mailpit/`. If ports `5432` or `6379` are already in use on your machine, set `POSTGRES_PORT` and `DRAGONFLY_PORT` in `.env` — only the host bindings change. +### Useful commands + +```bash +docker compose ps # service status + health +docker compose logs -f backend # follow one service +docker compose pull && docker compose up -d # update to latest release +docker compose down # stop (data persists in volumes) +docker compose down -v # stop and delete all data +``` + ## Verification | Check | Endpoint | diff --git a/docker-compose.yml b/docker-compose.yml index 076630b..eb3123a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: restart: unless-stopped backend: + image: ghcr.io/dvorinka/primora-backend:latest build: context: . dockerfile: apps/backend/Dockerfile @@ -56,6 +57,7 @@ services: restart: unless-stopped auth: + image: ghcr.io/dvorinka/primora-auth:latest build: context: . dockerfile: apps/auth/Dockerfile @@ -81,6 +83,7 @@ services: restart: unless-stopped frontend: + image: ghcr.io/dvorinka/primora-frontend:latest build: context: . dockerfile: apps/frontend/Dockerfile diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..312d981 --- /dev/null +++ b/install.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Primora one-line installer. +# +# curl -fsSL https://raw.githubusercontent.com/Dvorinka/Primora/master/install.sh | bash +# +# Installs into ./primora (override with PRIMORA_DIR). Non-interactive: +# DOMAIN=example.com NGINX_PORT=8080 bash install.sh +# +# Pulls prebuilt GHCR images when available; otherwise downloads the source +# tarball and builds locally. Either way the result is the same compose stack. + +set -euo pipefail + +REPO="Dvorinka/Primora" +REF="${PRIMORA_REF:-master}" +DIR="${PRIMORA_DIR:-primora}" +DOMAIN="${DOMAIN:-localhost}" +NGINX_PORT="${NGINX_PORT:-80}" + +info() { echo "==> $*"; } +die() { echo "ERROR: $*" >&2; exit 1; } + +command -v docker >/dev/null 2>&1 || die "docker is not installed" +docker compose version >/dev/null 2>&1 || die "docker compose plugin is not installed" +command -v curl >/dev/null 2>&1 || die "curl is not installed" + +mkdir -p "$DIR/infra/nginx" +cd "$DIR" + +# The compose stack needs exactly these three files from the repo. +info "Fetching compose files ($REF)" +for f in docker-compose.yml .env.example infra/nginx/default.conf; do + curl -fsSL "https://raw.githubusercontent.com/$REPO/$REF/$f" -o "$f" +done + +if [ ! -f .env ]; then + info "Generating .env with random secrets" + cp .env.example .env + + JWT_SECRET=$(openssl rand -base64 32) + BETTER_AUTH_SECRET=$(openssl rand -base64 32) + ENCRYPTION_KEY=$(openssl rand -hex 32) + DB_PASSWORD=$(openssl rand -hex 16) + + # '|' delimiter: base64 output can contain '/', '+', '='. + SED=(sed -i) + [[ "$OSTYPE" == darwin* ]] && SED=(sed -i '') + + "${SED[@]}" "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$DB_PASSWORD|" .env + "${SED[@]}" "s|postgres://primora:primora@|postgres://primora:$DB_PASSWORD@|" .env + "${SED[@]}" "s|^JWT_SECRET=.*|JWT_SECRET=$JWT_SECRET|" .env + "${SED[@]}" "s|^BETTER_AUTH_SECRET=.*|BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET|" .env + "${SED[@]}" "s|^PRIMORA_ENCRYPTION_KEY=.*|PRIMORA_ENCRYPTION_KEY=$ENCRYPTION_KEY|" .env + "${SED[@]}" "s|^NGINX_PORT=.*|NGINX_PORT=$NGINX_PORT|" .env + + if [ "$DOMAIN" != "localhost" ]; then + "${SED[@]}" "s|http://localhost|http://$DOMAIN|g" .env + "${SED[@]}" "s|^COOKIE_DOMAIN=.*|COOKIE_DOMAIN=$DOMAIN|" .env + fi +else + info "Existing .env kept" +fi + +# Compose v5 skips `pull` for services that declare build:, so fetch the +# prebuilt images directly. --no-build keeps `up` from rebuilding them. +GHCR_OWNER=$(echo "$REPO" | cut -d/ -f1 | tr '[:upper:]' '[:lower:]') +if docker pull "ghcr.io/$GHCR_OWNER/primora-backend:latest" \ + && docker pull "ghcr.io/$GHCR_OWNER/primora-auth:latest" \ + && docker pull "ghcr.io/$GHCR_OWNER/primora-frontend:latest"; then + info "Starting prebuilt images" + docker compose up -d --no-build +else + # GHCR packages are private until first published release — build from source. + info "Prebuilt images unavailable; building from source" + curl -fsSL "https://codeload.github.com/$REPO/tar.gz/refs/heads/$REF" | tar xz --strip-components=1 + docker compose up -d --build +fi + +DISPLAY_URL="http://$DOMAIN" +[ "$NGINX_PORT" != "80" ] && DISPLAY_URL="$DISPLAY_URL:$NGINX_PORT" + +info "Primora is starting" +echo " Dashboard: $DISPLAY_URL" +echo " Health: $DISPLAY_URL/api/v1/health/liveness" +echo " Logs: cd $DIR && docker compose logs -f" +echo " Stop: cd $DIR && docker compose down"