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
22 changes: 22 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Exclude everything from the Docker context by default.
# The backend image is built from the repository root because uv keeps the
# workspace lockfile at the root level.
*

# Workspace dependency files
!pyproject.toml
!uv.lock

# Backend source and config
!backend/
!backend/pyproject.toml
!backend/alembic.ini
!backend/alembic/
!backend/alembic/**
!backend/app/
!backend/app/**
!backend/entrypoint.sh

# Do not ship test code or the CLI in the image
backend/tests/
cli/
14 changes: 14 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Backend settings
DATABASE_URL=sqlite:///./data/telemetry.db
LOG_LEVEL=INFO # DEBUG | INFO | WARNING | ERROR | CRITICAL
RATE_LIMIT_PER_MINUTE=60 # max telemetry requests per IP per minute
LOG_IP_MASK_OCTETS=1 # trailing IPv4 octets masked in logs (1-4)
ENABLE_DOCS=true # expose /api/docs (Swagger UI) + /api/openapi.json; set false in production
PRUNE_AFTER_DAYS=365 # installations not seen for this long are moved to the pruned table (all-time totals kept)
PRUNE_INTERVAL_HOURS=24 # how often the background retention job runs
CORS_ORIGINS=["http://localhost:8001"]

# TLS / reverse proxy settings
# Forwarded headers (X-Forwarded-For/Proto) are trusted only from matching IPs.
# "*" trusts all proxies. Set to your proxy IP or CIDR in production.
FORWARDED_ALLOW_IPS=*
170 changes: 170 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Docker Build & Publish

on:
push:
branches: [develop]
release:
types: [published]
workflow_dispatch:
inputs:
branch:
description: "Branch to build from"
required: false
default: develop

env:
REGISTRY: ghcr.io

jobs:
prepare-tags-and-names:
runs-on: ubuntu-latest

permissions:
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.inputs.branch || github.ref }}

- name: Derive version
id: version
run: |
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
else
VERSION="$(git describe --tags --always 2>/dev/null || echo 'v0.0.0-dev')"
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "sha_short=$(echo '${{ github.sha }}' | cut -c1-7)" >> "$GITHUB_OUTPUT"

- name: Sanitize version for Docker tag
id: sanitize
run: |
VERSION="${{ steps.version.outputs.version }}"
SANITIZED="$(echo "$VERSION" | sed 's/[^a-zA-Z0-9_.-]/-/g')"
SANITIZED="$(echo "$SANITIZED" | sed 's/^[^a-zA-Z0-9_]\+//')"
echo "sanitized_tag=${SANITIZED:0:128}" >> "$GITHUB_OUTPUT"

- name: Normalize repository name
id: repo
uses: actions/github-script@v7
with:
result-encoding: string
script: return `${context.repo.owner}/${context.repo.repo}`.toLowerCase()

outputs:
version: ${{ steps.version.outputs.version }}
sha_short: ${{ steps.version.outputs.sha_short }}
sanitized_tag: ${{ steps.sanitize.outputs.sanitized_tag }}
repository: ${{ steps.repo.outputs.result }}

build-and-push:
runs-on: ${{ matrix.runner }}
needs: prepare-tags-and-names
strategy:
fail-fast: false
matrix:
service:
- name: backend
image: librislog-telemetry-api
dockerfile: ./backend/Dockerfile
context: .

arch: [amd64, arm64]

include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm

permissions:
contents: read
packages: write

steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.inputs.branch || github.ref }}

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

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Generate image tags
id: tags
run: |
IMAGE="${{ env.REGISTRY }}/${{ needs.prepare-tags-and-names.outputs.repository }}/${{ matrix.service.image }}"
SHA_SHORT="${{ needs.prepare-tags-and-names.outputs.sha_short }}"

if [ "${{ github.event_name }}" = "release" ]; then
SANITIZED="${{ needs.prepare-tags-and-names.outputs.sanitized_tag }}"
TAGS="${IMAGE}:${SANITIZED}-${{ matrix.arch }},${IMAGE}:latest-${{ matrix.arch }}"
else
TAGS="${IMAGE}:develop-${{ matrix.arch }},${IMAGE}:${SHA_SHORT}-${{ matrix.arch }}"
fi

echo "tags=${TAGS}" >> "$GITHUB_OUTPUT"

- name: Build and push
uses: docker/build-push-action@v7
with:
context: ${{ matrix.service.context }}
file: ${{ matrix.service.dockerfile }}
push: true
platforms: linux/${{ matrix.arch }}
tags: ${{ steps.tags.outputs.tags }}
build-args: |
APP_VERSION=${{ needs.prepare-tags-and-names.outputs.version }}
GIT_SHA=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

create-manifest:
needs: [prepare-tags-and-names, build-and-push]
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
service:
- name: backend
image: librislog-telemetry-api

permissions:
packages: write

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

- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create multi-arch manifest
run: |
IMAGE="${{ env.REGISTRY }}/${{ needs.prepare-tags-and-names.outputs.repository }}/${{ matrix.service.image }}"
SHA_SHORT="${{ needs.prepare-tags-and-names.outputs.sha_short }}"

if [ "${{ github.event_name }}" = "release" ]; then
SANITIZED="${{ needs.prepare-tags-and-names.outputs.sanitized_tag }}"
docker buildx imagetools create -t "${IMAGE}:${SANITIZED}" "${IMAGE}:${SANITIZED}-amd64" "${IMAGE}:${SANITIZED}-arm64"
docker buildx imagetools create -t "${IMAGE}:latest" "${IMAGE}:latest-amd64" "${IMAGE}:latest-arm64"
else
docker buildx imagetools create -t "${IMAGE}:develop" "${IMAGE}:develop-amd64" "${IMAGE}:develop-arm64"
docker buildx imagetools create -t "${IMAGE}:${SHA_SHORT}" "${IMAGE}:${SHA_SHORT}-amd64" "${IMAGE}:${SHA_SHORT}-arm64"
fi
158 changes: 158 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: Tests

on:
push:
branches: [main, develop]
pull_request:
branches: [develop, main]
workflow_dispatch:

env:
PYTHON_VERSION: "3.14"

jobs:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: uv sync --frozen

- name: Run tests
run: uv run pytest --junitxml=report.xml --cov=app --cov-report=term-missing

- name: Upload report
if: always()
uses: actions/upload-artifact@v7
with:
name: test-report-backend
path: backend/report.xml

cli:
runs-on: ubuntu-latest
defaults:
run:
working-directory: cli
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install dependencies
run: uv sync --frozen

- name: Run tests
run: uv run pytest --junitxml=report.xml

- name: Upload report
if: always()
uses: actions/upload-artifact@v7
with:
name: test-report-cli
path: cli/report.xml

report:
needs: [backend, cli]
if: always()
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
issues: write
steps:
- uses: actions/download-artifact@v8
with:
path: artifacts
pattern: "*report*"

- name: Initialize git for test reporter
run: |
git init
git config user.email "ci@github.com"
git config user.name "CI"
git add -A
git commit --allow-empty -m "ci"

- name: Publish backend results
id: backend
uses: dorny/test-reporter@v3
continue-on-error: true
with:
name: Backend Tests (pytest)
path: artifacts/test-report-backend/report.xml
reporter: java-junit
fail-on-error: "false"

- name: Publish CLI results
id: cli
uses: dorny/test-reporter@v3
continue-on-error: true
with:
name: CLI Tests (pytest)
path: artifacts/test-report-cli/report.xml
reporter: java-junit
fail-on-error: "false"

- name: Post PR comment
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@v9
env:
backend_passed: ${{ steps.backend.outputs.passed }}
backend_failed: ${{ steps.backend.outputs.failed }}
backend_skipped: ${{ steps.backend.outputs.skipped }}
backend_conclusion: ${{ steps.backend.outputs.conclusion }}
cli_passed: ${{ steps.cli.outputs.passed }}
cli_failed: ${{ steps.cli.outputs.failed }}
cli_skipped: ${{ steps.cli.outputs.skipped }}
cli_conclusion: ${{ steps.cli.outputs.conclusion }}
with:
script: |
function suite(label, prefix) {
const passed = parseInt(process.env[`${prefix}_passed`] || '0');
const failed = parseInt(process.env[`${prefix}_failed`] || '0');
const skipped = parseInt(process.env[`${prefix}_skipped`] || '0');
const conclusion = process.env[`${prefix}_conclusion`] || 'skipped';
const total = passed + failed + skipped;
const emoji = conclusion === 'success' ? '✅' : conclusion === 'failure' ? '❌' : '⏭️';
return { passed, failed, skipped, total, line: `${emoji} **${label}** — ${passed} passed, ${failed} failed, ${skipped} skipped (${total} total)` };
}

const results = [
suite('Backend (pytest)', 'backend'),
suite('CLI (pytest)', 'cli'),
];

const sha = context.sha;
const shaLink = `[\`${sha.slice(0, 7)}\`](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/commit/${sha})`;
const totalPassed = results.reduce((s, r) => s + r.passed, 0);
const totalFailed = results.reduce((s, r) => s + r.failed, 0);
const totalSkipped = results.reduce((s, r) => s + r.skipped, 0);
const total = totalPassed + totalFailed + totalSkipped;

const summary = totalFailed === 0 && totalSkipped === 0
? `**Summary:** ✅ All ${total} tests passed`
: `**Summary:** ${totalPassed} ✅ passed` + (totalFailed > 0 ? `, ${totalFailed} ❌ failed` : '') + (totalSkipped > 0 ? `, ${totalSkipped} ⏭️ skipped` : '') + ` (${total} total)`;

const body = `## 🧪 Test Results — ${shaLink}\n\n`
+ results.map(r => r.line).join('\n')
+ `\n\n${summary}`;

await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
22 changes: 22 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,25 @@ __marimo__/

# Streamlit
.streamlit/secrets.toml

/ideas.txt
/backend/data/
/data/
/data-e2e/
/backend/data/
!frontend/src/lib
cookies.txt
profile-snapshot

node_modules/
/*.png
*-snapshot.md

uvicorn.log

# AI tools
/.memsearch
/.playwright-mcp
/.sverklo
.plan/
/.opencode
Loading
Loading