ci: image caching, Trivy scans, edge publishing, standalone installer

- docker-build job is now a per-app matrix with GHA layer cache, a
  container contents smoke check, and a non-blocking Trivy scan.
- New docker-publish job pushes edge + sha tags to GHCR on master.
- Fixed release.yml tagging ghcr.io/<Owner>/... with a mixed-case
  owner — GHCR requires lowercase, so tagged releases would have
  failed at push time.
- compose files: image: keys name local builds after the GHCR repos.
- install.sh: curl|bash installer — fetches compose files, generates
  secrets, pulls prebuilt images (docker pull + up --no-build, since
  compose pull skips buildable services), falls back to a source
  tarball build when packages aren't public.
- README: install section covering the one-liner, clone, and prebuilt
  paths plus common compose commands.

Verified locally: all three images build, full dev-overlay stack
passes scripts/smoke-e2e.sh, and install.sh produces a healthy
stack in a standalone directory.

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 13:22:25 +02:00
1 parent 438ee46016
commit fe28902c4a
5 files changed
+204 -21

No files matched your search

+70 -16
View File
@@ -6,6 +6,9 @@ on:
pull_request: pull_request:
branches: [master] branches: [master]
permissions:
contents: read
jobs: jobs:
backend-tests: backend-tests:
name: Backend Tests name: Backend Tests
@@ -96,38 +99,83 @@ jobs:
run: go build -v ./cmd/server run: go build -v ./cmd/server
docker-build: docker-build:
name: Docker Build Test name: Docker Build (${{ matrix.app }})
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
app: [backend, auth, frontend]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Build backend image - name: Build ${{ matrix.app }} image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: apps/backend/Dockerfile file: apps/${{ matrix.app }}/Dockerfile
push: false push: false
tags: primora-backend:test load: true
tags: primora-${{ matrix.app }}:test
cache-from: type=gha,scope=${{ matrix.app }}
cache-to: type=gha,mode=max,scope=${{ matrix.app }}
- name: Build auth image - name: Smoke check ${{ matrix.app }} image contents
uses: docker/build-push-action@v6 run: |
with: case "${{ matrix.app }}" in
context: . backend) docker run --rm --entrypoint sh primora-backend:test -c 'test -x /usr/local/bin/primora-backend && test -x /usr/local/bin/dbx-mcp' ;;
file: apps/auth/Dockerfile auth) docker run --rm --entrypoint sh primora-auth:test -c 'test -f /workspace/apps/auth/dist/index.js' ;;
push: false frontend) docker run --rm --entrypoint sh primora-frontend:test -c 'test -f /usr/share/nginx/html/index.html' ;;
tags: primora-auth:test esac
- name: Build frontend image - name: Scan ${{ matrix.app }} image
uses: aquasecurity/[email protected]
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 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: apps/frontend/Dockerfile file: apps/${{ matrix.app }}/Dockerfile
push: false push: true
tags: primora-frontend:test 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: migrations:
name: Migration Up/Down name: Migration Up/Down
@@ -186,6 +234,12 @@ jobs:
- name: Prepare env - name: Prepare env
run: cp .env.example .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 - name: Start stack
run: >- run: >-
docker compose docker compose
+5 -2
View File
@@ -39,6 +39,9 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3 - uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-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 - uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
@@ -52,8 +55,8 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
ghcr.io/${{ github.repository_owner }}/primora-${{ matrix.app }}:${{ env.VERSION }} ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:${{ env.VERSION }}
ghcr.io/${{ github.repository_owner }}/primora-${{ matrix.app }}:latest ghcr.io/${{ env.GHCR_OWNER }}/primora-${{ matrix.app }}:latest
cache-from: type=gha,scope=${{ matrix.app }} cache-from: type=gha,scope=${{ matrix.app }}
cache-to: type=gha,mode=max,scope=${{ matrix.app }} cache-to: type=gha,mode=max,scope=${{ matrix.app }}
+40 -3
View File
@@ -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. - **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. - **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. Prerequisites: Docker with the Compose plugin.
### One-liner (no clone)
```bash ```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 ./scripts/setup.sh # creates .env, generates secrets, starts the stack
``` ```
@@ -33,13 +52,31 @@ or manually:
```bash ```bash
cp .env.example .env # fill JWT_SECRET and BETTER_AUTH_SECRET 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. 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 ## Verification
| Check | Endpoint | | Check | Endpoint |
+3
View File
@@ -29,6 +29,7 @@ services:
restart: unless-stopped restart: unless-stopped
backend: backend:
image: ghcr.io/dvorinka/primora-backend:latest
build: build:
context: . context: .
dockerfile: apps/backend/Dockerfile dockerfile: apps/backend/Dockerfile
@@ -56,6 +57,7 @@ services:
restart: unless-stopped restart: unless-stopped
auth: auth:
image: ghcr.io/dvorinka/primora-auth:latest
build: build:
context: . context: .
dockerfile: apps/auth/Dockerfile dockerfile: apps/auth/Dockerfile
@@ -81,6 +83,7 @@ services:
restart: unless-stopped restart: unless-stopped
frontend: frontend:
image: ghcr.io/dvorinka/primora-frontend:latest
build: build:
context: . context: .
dockerfile: apps/frontend/Dockerfile dockerfile: apps/frontend/Dockerfile
Executable
+86
View File
@@ -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"