13 Commits
Author SHA1 Message Date
haylanandClaude-Bot 3e3a6752af chore(dev): remove broken auto-run PHPUnit hook
The PostToolUse hook called php bin/phpunit directly, which does not
work inside the Docker-based dev setup. Remove it; tests should be
run via docker compose exec graph vendor/bin/phpunit as documented.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:39:39 +02:00
haylanandClaude-Bot 5b07eae672 docs: update installation guide to use pre-built image
Replace the clone-and-build workflow with a docker-compose snippet
that pulls the image directly from the Gitea container registry.
Add semver tag reference and rename steps to match the new flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:39:33 +02:00
haylanandClaude-Bot 235e63bfc0 fix(config): add default empty values for optional env vars
Declare empty-string defaults for all optional env vars as Symfony
parameters so the container compiles without errors when GITHUB_USER,
GITLAB_TOKEN, GITEA_URL, etc. are not set in the environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:39:29 +02:00
haylanandClaude-Bot 92380e534a feat(docker): migrate base image from php-cli to FrankenPHP Alpine
Replace php:8.4-cli-alpine3.21 with dunglas/frankenphp:1-php8.4-alpine.
No Go toolchain required — the official image ships as a pre-compiled
Alpine binary.

- Drop docker-php-ext-install opcache (FrankenPHP enables it by default)
- Both dev and final stages run frankenphp run --config /etc/caddy/Caddyfile
- Dev stage bakes in Caddyfile.dev (no worker mode, Xdebug compatible)
- Final stage bakes in production Caddyfile

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:38:38 +02:00
haylanandClaude-Bot 4654a287a8 feat(app): add FrankenPHP worker script for Symfony
Boot the Symfony kernel once at startup, then loop with
frankenphp_handle_request() to handle every incoming request without
re-bootstrapping the framework. Supports MAX_REQUESTS env var for
graceful worker restarts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:38:27 +02:00
haylanandClaude-Bot 2427c07f4c feat(docker): add FrankenPHP Caddy configuration
Add production and dev Caddyfiles for FrankenPHP:
- Caddyfile: HTTP-only on :8080, auto_https off, worker mode
- Caddyfile.dev: same but without worker directive so Xdebug
  step-debugging remains functional; Caddy debug logging enabled

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 12:38:23 +02:00
haylan 7c1b893e09 chore: update docker image to php 8.4 2026-05-29 23:20:53 +02:00
haylan 4993fdca00 Update .env 2026-05-29 23:04:39 +02:00
haylan c4803ea5cb fix: fixed labeling to make contaienr apper correctly in the repo 2026-05-29 22:43:53 +02:00
haylan 19bfa4cf28 Update README.md 2026-05-29 22:37:25 +02:00
haylan e70d035672 feat: change to use token 2026-05-29 19:42:10 +02:00
haylan d57320cb77 fix: changed pipline to use workflow token instead self made one 2026-05-29 19:32:36 +02:00
haylan 5feae97dd3 feat: fix manuel pipline. to create a release tag or replace exisiting release 2026-05-29 19:20:32 +02:00
10 changed files with 163 additions and 47 deletions
+1 -10
View File
@@ -1,10 +1 @@
{ {}
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"command": "php bin/phpunit 2>&1 | tail -20"
}
]
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
APP_ENV=dev APP_ENV=dev
APP_DEBUG=0 APP_DEBUG=1
APP_SECRET=changeme_replace_with_random_32char_string APP_SECRET=changeme_replace_with_random_32char_string
# Comma-separated list of allowed hostnames. Leave empty to allow all hosts. # Comma-separated list of allowed hostnames. Leave empty to allow all hosts.
+33 -15
View File
@@ -1,10 +1,10 @@
# Builds and pushes a multi-arch Docker image to the Gitea container registry # Builds and pushes a multi-arch Docker image to the Gitea container registry.
# whenever a semver tag (v*.*.*) is pushed. # Triggered manually via workflow_dispatch — enter an existing semver tag (e.g. 1.2.3)
# in the "Release tag" input. The workflow will fail early if the tag does not exist.
# #
# One-time setup required: # Requires a repository secret REGISTRY_TOKEN — a Gitea PAT with write:package scope.
# 1. Create a Gitea token with "package:write" scope. # Create it at: Settings → Applications → Generate Token (scope: write:package)
# 2. Add it as a repository secret named GITEA_TOKEN # Then add it: Repository → Settings → Secrets → Actions → REGISTRY_TOKEN
# (Repository → Settings → Secrets → Actions).
# #
# After a successful run the image is available at: # After a successful run the image is available at:
# <your-gitea-host>/<owner>/<repo>:<version> # <your-gitea-host>/<owner>/<repo>:<version>
@@ -12,17 +12,33 @@
name: Docker Publish name: Docker Publish
on: on:
push: workflow_dispatch:
tags: inputs:
- 'v*.*.*' tag:
description: 'Release tag (semver, e.g. 1.2.3)'
required: true
type: string
jobs: jobs:
build-push: build-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate tag exists
run: |
if ! git rev-parse "refs/tags/${{ inputs.tag }}" >/dev/null 2>&1; then
echo "Error: tag '${{ inputs.tag }}' does not exist in this repository."
exit 1
fi
git checkout "refs/tags/${{ inputs.tag }}"
# Strip the protocol from the server URL to get the registry hostname. # Strip the protocol from the server URL to get the registry hostname.
# e.g. https://gitea.example.com → gitea.example.com # e.g. https://gitea.example.com → gitea.example.com
@@ -30,17 +46,19 @@ jobs:
run: | run: |
echo "REGISTRY=$(echo '${{ gitea.server_url }}' | sed 's|https://||;s|http://||')" >> $GITHUB_ENV echo "REGISTRY=$(echo '${{ gitea.server_url }}' | sed 's|https://||;s|http://||')" >> $GITHUB_ENV
# Generates OCI-compliant tags and labels from the git tag. # Generates OCI-compliant tags and labels from the provided release tag.
# v1.2.3 → image tags: 1.2.3 / 1.2 / 1 # 1.2.3 → image tags: 1.2.3 / 1.2 / 1
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.REGISTRY }}/${{ gitea.repository }} images: ${{ env.REGISTRY }}/${{ gitea.repository }}
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}},value=${{ inputs.tag }}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}},value=${{ inputs.tag }}
type=semver,pattern={{major}} type=semver,pattern={{major}},value=${{ inputs.tag }}
labels: |
org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }}
# QEMU enables emulation of arm64 on the amd64 runner. # QEMU enables emulation of arm64 on the amd64 runner.
- name: Set up QEMU - name: Set up QEMU
@@ -55,7 +73,7 @@ jobs:
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ gitea.actor }} username: ${{ gitea.actor }}
password: ${{ secrets.GITEA_TOKEN }} password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
+18
View File
@@ -117,6 +117,24 @@ Add to `.claude/settings.json` to run PHPUnit automatically after every file edi
} }
``` ```
## Gitea Workflows
Workflow files live in [.gitea/workflows/](.gitea/workflows/). This project uses Gitea Actions (GitHub Actions-compatible syntax).
**When editing workflow files:**
- Use `gitea.*` context variables, not `github.*` — e.g. `${{ gitea.server_url }}`, `${{ gitea.actor }}`, `${{ gitea.repository }}`
- `GITHUB_ENV` and `GITHUB_OUTPUT` are still the correct env file names (Gitea Actions re-uses these)
- Secrets are set under Repository → Settings → Secrets → Actions
- The registry hostname must be derived from `gitea.server_url` (strip the protocol prefix)
- Triggers use standard `on:` syntax; `tags: '*.*.*'` matches semver pushes without a `v` prefix
**Current workflows:**
| File | Trigger | Purpose |
|---|---|---|
| `docker-publish.yml` | Push tag `*.*.*` | Build & push multi-arch image to Gitea container registry |
## Docker ## Docker
### Development ### Development
+6 -5
View File
@@ -1,10 +1,9 @@
FROM php:8.3-cli-alpine AS base FROM dunglas/frankenphp:1-php8.4-alpine AS base
RUN apk add --no-cache \ RUN apk add --no-cache \
curl \ curl \
icu-libs \ icu-libs \
libzip \ libzip
&& docker-php-ext-install opcache
WORKDIR /app WORKDIR /app
@@ -32,11 +31,12 @@ RUN apk add --no-cache ${PHPIZE_DEPS} linux-headers \
&& docker-php-ext-enable xdebug \ && docker-php-ext-enable xdebug \
&& apk del ${PHPIZE_DEPS} && apk del ${PHPIZE_DEPS}
COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/docker-xdebug.ini COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/docker-xdebug.ini
COPY docker/frankenphp/Caddyfile.dev /etc/caddy/Caddyfile
COPY composer.json composer.lock* ./ COPY composer.json composer.lock* ./
RUN composer install --no-scripts --no-interaction --prefer-dist RUN composer install --no-scripts --no-interaction --prefer-dist
EXPOSE 8080 EXPOSE 8080
ENV APP_ENV=dev APP_DEBUG=1 ENV APP_ENV=dev APP_DEBUG=1
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public", "public/index.php"] CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
# ── final (prod) stage — no composer binary ──────────────────────────────────── # ── final (prod) stage — no composer binary ────────────────────────────────────
FROM base AS final FROM base AS final
@@ -45,6 +45,7 @@ RUN addgroup -S app && adduser -S -G app app
COPY --from=build /app/vendor /app/vendor COPY --from=build /app/vendor /app/vendor
COPY . . COPY . .
COPY docker/frankenphp/Caddyfile /etc/caddy/Caddyfile
RUN mkdir -p var/cache var/log \ RUN mkdir -p var/cache var/log \
&& chown -R app:app /app && chown -R app:app /app
@@ -54,4 +55,4 @@ USER app
EXPOSE 8080 EXPOSE 8080
ENV APP_ENV=prod APP_DEBUG=0 ENV APP_ENV=prod APP_DEBUG=0
CMD ["php", "-S", "0.0.0.0:8080", "-t", "public", "public/index.php"] CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
+33 -11
View File
@@ -6,7 +6,7 @@ A self-hosted Symfony service that merges contribution data from **GitHub**, **G
https://your-host/graph.svg?theme=dark https://your-host/graph.svg?theme=dark
``` ```
![Example graph](https://gitgraph.arthurerlich/graph.svg) ![Example graph](https://gitgraph.arthurerlich.de/graph.svg)
--- ---
@@ -27,15 +27,36 @@ https://your-host/graph.svg?theme=dark
- Docker + Docker Compose - Docker + Docker Compose
### 1. Clone and configure ### 1. Create a `docker-compose.yml`
```bash Use the pre-built image — no need to clone the repo:
git clone https://git.arthurerlich.de/haylan/git-contribution-graph.git
cd git-contribution-graph ```yaml
cp .env .env.local services:
graph:
image: git.arthurerlich.de/haylan/git-contribution-graph:latest
ports:
- "8080:8080"
env_file:
- .env.local
volumes:
- cache:/var/www/html/var/cache
volumes:
cache:
``` ```
Edit `.env.local` with your credentials: The image is published to the Gitea container registry. Pull it manually with:
```bash
docker pull git.arthurerlich.de/haylan/git-contribution-graph:latest
```
Semver tags are published (`0`, `0.0`, `0.0.1`) alongside `latest` — see the [container registry](https://git.arthurerlich.de/haylan/-/packages/container/git-contribution-graph/latest) for all available tags.
### 2. Configure
Create a `.env.local` file next to your `docker-compose.yml`:
```dotenv ```dotenv
APP_SECRET=<generate with: openssl rand -hex 16> APP_SECRET=<generate with: openssl rand -hex 16>
@@ -60,7 +81,7 @@ ALLOWED_HOSTS=
Only configure the platforms you use — unused ones are silently skipped. Only configure the platforms you use — unused ones are silently skipped.
### 2. Start ### 3. Start
```bash ```bash
docker compose up -d docker compose up -d
@@ -68,10 +89,11 @@ docker compose up -d
The service listens on **port 8080** by default. Put Traefik or nginx in front of it for HTTPS. The service listens on **port 8080** by default. Put Traefik or nginx in front of it for HTTPS.
### 3. Health check ### 4. Verify
``` ```bash
GET /health → {"status":"ok"} curl http://localhost:8080/health
# {"status":"ok"}
``` ```
--- ---
+12
View File
@@ -1,3 +1,15 @@
parameters:
env(APP_SECRET): ''
env(ALLOWED_HOSTS): ''
env(GITHUB_USER): ''
env(GITHUB_TOKEN): ''
env(GITLAB_USER): ''
env(GITLAB_TOKEN): ''
env(GITLAB_URL): ''
env(GITEA_USER): ''
env(GITEA_TOKEN): ''
env(GITEA_URL): ''
services: services:
_defaults: _defaults:
autowire: true autowire: true
+13
View File
@@ -0,0 +1,13 @@
{
admin off
auto_https off
frankenphp {
worker /app/public/worker.php
}
}
:8080 {
root * /app/public
php_server
}
+12
View File
@@ -0,0 +1,12 @@
{
admin off
auto_https off
debug
frankenphp
}
:8080 {
root * /app/public
php_server
}
+29
View File
@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
use App\Kernel;
use Symfony\Component\HttpFoundation\Request;
require_once dirname(__DIR__).'/vendor/autoload.php';
$kernel = new Kernel($_SERVER['APP_ENV'] ?? 'prod', (bool) ($_SERVER['APP_DEBUG'] ?? false));
$kernel->boot();
$handler = static function () use ($kernel): void {
$request = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
};
$maxRequests = (int) ($_SERVER['MAX_REQUESTS'] ?? 0);
$requestCount = 0;
while (\frankenphp_handle_request($handler)) {
if ($maxRequests > 0 && ++$requestCount >= $maxRequests) {
break;
}
}
$kernel->shutdown();