feat: add Docker deployment and Gitea Actions CI/CD
Add multi-stage Dockerfile, docker-compose with persistent volumes, and Gitea Actions workflows for CI (lint/test/build) and releases (tag, Docker image push, changelog). Support DATABASE_PATH env var for configurable SQLite location to enable proper volume mounting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
gearbox.db*
|
||||||
|
uploads/*
|
||||||
|
!uploads/.gitkeep
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.claude
|
||||||
|
.gitea
|
||||||
|
.planning
|
||||||
28
.gitea/workflows/ci.yml
Normal file
28
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: oven/bun:1
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: bun run lint
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: bun test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: bun run build
|
||||||
108
.gitea/workflows/release.yml
Normal file
108
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bump:
|
||||||
|
description: "Version bump type"
|
||||||
|
required: true
|
||||||
|
default: "patch"
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- patch
|
||||||
|
- minor
|
||||||
|
- major
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: docker
|
||||||
|
container:
|
||||||
|
image: oven/bun:1
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: bun run lint
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: bun test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
release:
|
||||||
|
needs: ci
|
||||||
|
runs-on: dind
|
||||||
|
steps:
|
||||||
|
- name: Clone repository
|
||||||
|
run: |
|
||||||
|
apk add --no-cache git curl jq
|
||||||
|
git clone https://${{ secrets.GITEA_TOKEN }}@gitea.jeanlucmakiola.de/${{ gitea.repository }}.git repo
|
||||||
|
cd repo
|
||||||
|
git checkout ${{ gitea.ref_name }}
|
||||||
|
|
||||||
|
- name: Compute version
|
||||||
|
working-directory: repo
|
||||||
|
run: |
|
||||||
|
LATEST_TAG=$(git tag -l 'v*' --sort=-v:refname | head -n1)
|
||||||
|
if [ -z "$LATEST_TAG" ]; then
|
||||||
|
LATEST_TAG="v0.0.0"
|
||||||
|
fi
|
||||||
|
MAJOR=$(echo "$LATEST_TAG" | sed 's/v//' | cut -d. -f1)
|
||||||
|
MINOR=$(echo "$LATEST_TAG" | sed 's/v//' | cut -d. -f2)
|
||||||
|
PATCH=$(echo "$LATEST_TAG" | sed 's/v//' | cut -d. -f3)
|
||||||
|
case "${{ gitea.event.inputs.bump }}" in
|
||||||
|
major) MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0 ;;
|
||||||
|
minor) MINOR=$((MINOR+1)); PATCH=0 ;;
|
||||||
|
patch) PATCH=$((PATCH+1)) ;;
|
||||||
|
esac
|
||||||
|
NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}"
|
||||||
|
echo "VERSION=$NEW_VERSION" >> "$GITHUB_ENV"
|
||||||
|
echo "PREV_TAG=$LATEST_TAG" >> "$GITHUB_ENV"
|
||||||
|
echo "New version: $NEW_VERSION"
|
||||||
|
|
||||||
|
- name: Generate changelog
|
||||||
|
working-directory: repo
|
||||||
|
run: |
|
||||||
|
if [ "$PREV_TAG" = "v0.0.0" ]; then
|
||||||
|
CHANGELOG=$(git log --pretty=format:"- %s" HEAD)
|
||||||
|
else
|
||||||
|
CHANGELOG=$(git log --pretty=format:"- %s" "${PREV_TAG}..HEAD")
|
||||||
|
fi
|
||||||
|
echo "CHANGELOG<<CHANGELOG_EOF" >> "$GITHUB_ENV"
|
||||||
|
echo "$CHANGELOG" >> "$GITHUB_ENV"
|
||||||
|
echo "CHANGELOG_EOF" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Create and push tag
|
||||||
|
working-directory: repo
|
||||||
|
run: |
|
||||||
|
git config user.name "Gitea Actions"
|
||||||
|
git config user.email "actions@gitea.jeanlucmakiola.de"
|
||||||
|
git tag -a "$VERSION" -m "Release $VERSION"
|
||||||
|
git push origin "$VERSION"
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
working-directory: repo
|
||||||
|
run: |
|
||||||
|
REGISTRY="gitea.jeanlucmakiola.de"
|
||||||
|
IMAGE="${REGISTRY}/${{ gitea.repository_owner }}/gearbox"
|
||||||
|
docker build -t "${IMAGE}:${VERSION}" -t "${IMAGE}:latest" .
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ gitea.repository_owner }}" --password-stdin
|
||||||
|
docker push "${IMAGE}:${VERSION}"
|
||||||
|
docker push "${IMAGE}:latest"
|
||||||
|
|
||||||
|
- name: Create Gitea release
|
||||||
|
run: |
|
||||||
|
API_URL="${GITHUB_SERVER_URL}/api/v1/repos/${{ gitea.repository }}/releases"
|
||||||
|
curl -s -X POST "$API_URL" \
|
||||||
|
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(jq -n \
|
||||||
|
--arg tag "$VERSION" \
|
||||||
|
--arg name "$VERSION" \
|
||||||
|
--arg body "$CHANGELOG" \
|
||||||
|
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')"
|
||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM oven/bun:1 AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
COPY . .
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
FROM oven/bun:1-slim AS production
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/dist/client ./dist/client
|
||||||
|
COPY src/server ./src/server
|
||||||
|
COPY src/db ./src/db
|
||||||
|
COPY src/shared ./src/shared
|
||||||
|
COPY drizzle.config.ts package.json ./
|
||||||
|
COPY drizzle ./drizzle
|
||||||
|
COPY entrypoint.sh ./
|
||||||
|
RUN chmod +x entrypoint.sh && mkdir -p data uploads
|
||||||
|
EXPOSE 3000
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD bun -e "fetch('http://localhost:3000/api/health').then(r=>r.ok?process.exit(0):process.exit(1)).catch(()=>process.exit(1))"
|
||||||
|
ENTRYPOINT ["./entrypoint.sh"]
|
||||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
services:
|
||||||
|
gearbox:
|
||||||
|
build: .
|
||||||
|
container_name: gearbox
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- DATABASE_PATH=./data/gearbox.db
|
||||||
|
volumes:
|
||||||
|
- gearbox-data:/app/data
|
||||||
|
- gearbox-uploads:/app/uploads
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
gearbox-data:
|
||||||
|
gearbox-uploads:
|
||||||
@@ -5,6 +5,6 @@ export default defineConfig({
|
|||||||
schema: "./src/db/schema.ts",
|
schema: "./src/db/schema.ts",
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: "gearbox.db",
|
url: process.env.DATABASE_PATH || "gearbox.db",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
4
entrypoint.sh
Executable file
4
entrypoint.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
bun run db:push
|
||||||
|
exec bun run src/server/index.ts
|
||||||
@@ -2,7 +2,7 @@ import { Database } from "bun:sqlite";
|
|||||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
import * as schema from "./schema.ts";
|
import * as schema from "./schema.ts";
|
||||||
|
|
||||||
const sqlite = new Database("gearbox.db");
|
const sqlite = new Database(process.env.DATABASE_PATH || "gearbox.db");
|
||||||
sqlite.run("PRAGMA journal_mode = WAL");
|
sqlite.run("PRAGMA journal_mode = WAL");
|
||||||
sqlite.run("PRAGMA foreign_keys = ON");
|
sqlite.run("PRAGMA foreign_keys = ON");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user