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
162 changes: 162 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: ci

on:
push:
branches: ["develop"]
tags: ["v*"]
pull_request:

permissions:
contents: read

jobs:
go:
name: go (vet, build)
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: "1.23.x"
cache: true

- name: Verify gofmt
run: |
files=$(gofmt -l .)
if [ -n "$files" ]; then
echo "gofmt needed on:"
echo "$files"
exit 1
fi

- name: Tidy check (no diff)
run: |
go mod tidy
git diff --exit-code

- name: Vet
run: go vet ./...

- name: Build
run: go build -trimpath -ldflags="-s -w" -o cacheppuccino .

docker:
name: docker (build and push)
runs-on: ubuntu-latest
needs: [go]
# if: github.event_name != 'pull_request'

permissions:
contents: read
packages: write

outputs:
docker_image_tag: ${{ steps.meta.outputs.version }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=sha,format=short,prefix=0.1.0-
type=ref,event=tag

- 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
helm:
name: helm (package and push)
runs-on: ubuntu-latest
needs: [docker]
# if: github.event_name != 'pull_request'

permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Helm
uses: azure/setup-helm@v4
with:
version: v3.15.4

- name: Determine chart version
id: ver
shell: bash
run: |
set -euo pipefail

if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
# Tag format: v0.1.0 -> 0.1.0
VERSION="${GITHUB_REF_NAME#v}"
else
# Unique semver prerelease for main pushes
SHORT_SHA="$(echo "${GITHUB_SHA}" | cut -c1-7)"
VERSION="0.1.0-${SHORT_SHA}"
fi

echo "version=${VERSION}" >> "$GITHUB_OUTPUT"

- name: Login to GHCR (Helm OCI)
shell: bash
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin

- name: Lint chart
run: helm lint helm

- name: Tag docker image in Helm Chart values.yaml
working-directory: helm
env:
IMAGE_TAG: ${{ needs.docker.outputs.docker_image_tag }}
run: |
# Update values.yaml with latest docker image
sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" ./values.yaml


- name: Package chart
shell: bash
run: |
set -euo pipefail
mkdir -p dist
helm package helm \
--destination dist \
--version "${{ steps.ver.outputs.version }}" \
--app-version "${{ steps.ver.outputs.version }}"

- name: Push chart to GHCR (OCI)
shell: bash
run: |
set -euo pipefail
OWNER_LC="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
CHART_TGZ="$(ls -1 dist/cacheppuccino-*.tgz | head -n 1)"
helm push "${CHART_TGZ}" "oci://ghcr.io/${OWNER_LC}"
13 changes: 10 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
# -------- build stage --------
FROM golang:1.23 AS build

ENV CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64

WORKDIR /src

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o /out/cacheppuccino .
RUN go build -trimpath -ldflags="-s -w" -o /out/cacheppuccino .

# -------- runtime stage --------
FROM gcr.io/distroless/static-debian12:nonroot

FROM gcr.io/distroless/static-debian12

# FROM alpine:3.21
# RUN apk add --no-cache ca-certificates && update-ca-certificates

WORKDIR /
COPY --from=build /out/cacheppuccino /cacheppuccino
Expand Down
2 changes: 1 addition & 1 deletion config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func LoadConfig() Config {
TranslationAPIKey: mustEnv("TRANSLATION_API_KEY"),
HTTPTimeout: envDuration("HTTP_TIMEOUT", 30*time.Second),
PullInterval: envDuration("PULL_INTERVAL", 10*time.Minute),
InitialPullDeadline: envDuration("INITIAL_PULL_DEADLINE", 45*time.Second),
InitialPullDeadline: envDuration("INITIAL_PULL_DEADLINE", 60*time.Second),
LogLevel: env("LOG_LEVEL", "info"),
}
}
Expand Down
74 changes: 73 additions & 1 deletion db.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
Expand All @@ -26,7 +27,10 @@ func OpenDB(path string) (*DB, error) {

sqldb, err := sql.Open(
sqliteshim.ShimName,
"file:"+path+"?mode=rwc&_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL)",
"file:"+path+"?mode=rwc"+
"&_pragma=busy_timeout(5000)"+
"&_pragma=temp_store=2"+
"&_pragma=busy_timeout(5000)",
)

if err != nil {
Expand Down Expand Up @@ -97,6 +101,74 @@ func (db *DB) UpsertString(ctx context.Context, page, key, lang, value string, u
return err
}

// UpsertStringsBatch upserts many rows efficiently.
// Uses a transaction + chunked multi-row INSERT to avoid SQLite variable limits.
func (db *DB) UpsertStringsBatch(ctx context.Context, rows []StringModel) error {
if len(rows) == 0 {
return nil
}

// SQLite default max variables is often 999.
// We insert 5 columns per row: page, key, lang, value, updated_at.
const (
sqliteMaxVars = 999
colsPerRow = 5
safetyHeadroom = 50 // leave some room
)
maxRowsPerStmt := (sqliteMaxVars - safetyHeadroom) / colsPerRow
if maxRowsPerStmt < 1 {
return fmt.Errorf("invalid maxRowsPerStmt=%d", maxRowsPerStmt)
}

// Transaction: makes the whole import much faster and consistent.
tx, err := db.bun.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()

for i := 0; i < len(rows); i += maxRowsPerStmt {
end := min(i+maxRowsPerStmt, len(rows))

chunk := rows[i:end]

_, err := tx.NewInsert().
Model(&chunk).
On("CONFLICT (page, key, lang) DO UPDATE").
Set("value = EXCLUDED.value").
Set("updated_at = EXCLUDED.updated_at").
Exec(ctx)
if err != nil {
return err
}
}

return tx.Commit()
}

// UpsertStringsFromRows is a convenience wrapper if you want to pass parsed rows directly.
func (db *DB) UpsertStringsFromRows(
ctx context.Context,
rows []StringRow, // rename ParsedRow to your actual ParseXLSX row type
) error {
if len(rows) == 0 {
return nil
}

models := make([]StringModel, 0, len(rows))
for _, r := range rows {
models = append(models, StringModel{
Page: r.Page,
Key: r.Key,
Lang: r.Lang,
Value: r.Value,
UpdatedAt: r.UpdatedAt.UTC(),
})
}

return db.UpsertStringsBatch(ctx, models)
}

func (db *DB) GetStringsByPagesLang(ctx context.Context, pages []string, lang string) (map[string]map[string]string, []string, error) {
cleaned := make([]string, 0, len(pages))
seen := make(map[string]struct{}, len(pages))
Expand Down
12 changes: 10 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
services:
cacheppuccino:
build: .
dns:
- 1.1.1.1
- 8.8.8.8
ports:
- "8080:8080"
user: "0:0"
# user: "0:0"
environment:
TRANSLATION_BASE_URL: ${TRANSLATION_BASE_URL}
TRANSLATION_APPLICATION_ID: ${TRANSLATION_APPLICATION_ID}
TRANSLATION_API_KEY: ${TRANSLATION_API_KEY}
SQLITE_PATH: "/data/cacheppuccino.db"
SQLITE_TMP_DIR: "/data/tmp"
PULL_INTERVAL: ${PULL_INTERVAL:-10m}
HTTP_TIMEOUT: ${HTTP_TIMEOUT:-30s}
INITIAL_PULL_DEADLINE: ${INITIAL_PULL_DEADLINE:-45s}
Expand All @@ -20,8 +24,12 @@ services:
healthcheck:
test: ["CMD", "/cacheppuccino", "--healthcheck"]
interval: 30s
timeout: 3s
timeout: 10s
retries: 3
develop:
watch:
- path: .
action: rebuild

volumes:
cacheppuccino_data:
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.23.0

require (
github.com/getkin/kin-openapi v0.130.0
github.com/rs/cors v1.11.1
github.com/uptrace/bun v1.2.15
github.com/uptrace/bun/dialect/sqlitedialect v1.2.15
github.com/uptrace/bun/driver/sqliteshim v1.2.15
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
Expand Down
Loading