From 23d39a2c3e8abef922eb1f0d701adf2bfbff0d6d Mon Sep 17 00:00:00 2001 From: Gunju Kim Date: Fri, 26 Jun 2026 06:46:13 +0900 Subject: [PATCH] Add image publishing and Kubernetes sample --- .dockerignore | 10 ++++ .github/workflows/ci.yaml | 11 ++++ .github/workflows/release.yaml | 36 ++++++++++++++ Dockerfile | 30 +++++++++++ Makefile | 10 ++++ README.md | 18 +++++++ examples/kubernetes.yaml | 91 ++++++++++++++++++++++++++++++++++ 7 files changed, 206 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/release.yaml create mode 100644 Dockerfile create mode 100644 examples/kubernetes.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6914a82 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.github +.claude +CLAUDE.local.md + +bin +cover.out +*.jsonl + +.DS_Store diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0d48a25..57609ac 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,6 +25,17 @@ jobs: - name: Build run: make build + image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build image + run: make image IMAGE=agora:ci + verify: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..a854267 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,36 @@ +name: Release + +on: + push: + branches: [main] + +concurrency: + group: release + cancel-in-progress: false + +jobs: + build-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - 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: Build and push image + run: >- + make image + REGISTRY=ghcr.io/${{ github.repository_owner }} + IMAGE_NAME=${{ github.event.repository.name }} + VERSION=main + PUSH=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..568d25e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.26-alpine AS build +WORKDIR /src + +COPY go.* ./ +RUN go mod download + +COPY cmd ./cmd +COPY internal ./internal + +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/agora ./cmd/agora + +FROM alpine:3 + +RUN addgroup -S agora \ + && adduser -S -G agora agora \ + && mkdir -p /data \ + && chown agora:agora /data + +ENV AGORA_ADDR=0.0.0.0:8080 +ENV AGORA_DATA=/data/agora.jsonl + +EXPOSE 8080 +VOLUME ["/data"] + +COPY --from=build /out/agora /usr/local/bin/agora + +USER agora +ENTRYPOINT ["/usr/local/bin/agora"] diff --git a/Makefile b/Makefile index 15fc81f..782ab7d 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,12 @@ SHELL = /usr/bin/env bash -o pipefail BINARY ?= agora BINDIR ?= bin +REGISTRY ?= ghcr.io/kelos-dev +IMAGE_NAME ?= agora +VERSION ?= latest +IMAGE ?= $(REGISTRY)/$(IMAGE_NAME):$(VERSION) +CONTAINER_TOOL ?= docker +PUSH ?= false GO_FILES := $(shell find . -name '*.go' -not -path './$(BINDIR)/*') .PHONY: all @@ -39,6 +45,10 @@ build: ## Build the Agora binary. mkdir -p $(BINDIR) CGO_ENABLED=0 go build -o $(BINDIR)/$(BINARY) ./cmd/agora +.PHONY: image +image: ## Build the Agora server container image. + $(CONTAINER_TOOL) buildx build $(if $(filter true,$(PUSH)),--push,--load) --tag $(IMAGE) . + .PHONY: run run: ## Run the Agora server. go run ./cmd/agora diff --git a/README.md b/README.md index 3b6c061..17b46dd 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,24 @@ AGORA_DATA=agora.jsonl AGORA_TOKEN= ``` +## Kubernetes + +A sample manifest is available at `examples/kubernetes.yaml`: + +```bash +kubectl apply -f examples/kubernetes.yaml +kubectl -n agora port-forward svc/agora 8080:80 +``` + +Then open: + +```text +http://127.0.0.1:8080 +``` + +The sample runs the published `ghcr.io/kelos-dev/agora:main` image with a +persistent volume mounted at `/data`. + ## Agent Skill The installable Codex skill lives in `skills/agora-reporting`. diff --git a/examples/kubernetes.yaml b/examples/kubernetes.yaml new file mode 100644 index 0000000..b6aa969 --- /dev/null +++ b/examples/kubernetes.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: agora +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: agora-data + namespace: agora +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: agora + namespace: agora + labels: + app.kubernetes.io/name: agora +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: agora + template: + metadata: + labels: + app.kubernetes.io/name: agora + spec: + securityContext: + runAsNonRoot: true + runAsUser: 65532 + runAsGroup: 65532 + fsGroup: 65532 + fsGroupChangePolicy: OnRootMismatch + containers: + - name: agora + image: ghcr.io/kelos-dev/agora:main + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + - name: AGORA_ADDR + value: 0.0.0.0:8080 + - name: AGORA_DATA + value: /data/agora.jsonl + readinessProbe: + httpGet: + path: /api/healthz + port: http + livenessProbe: + httpGet: + path: /api/healthz + port: http + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: data + mountPath: /data + volumes: + - name: data + persistentVolumeClaim: + claimName: agora-data +--- +apiVersion: v1 +kind: Service +metadata: + name: agora + namespace: agora + labels: + app.kubernetes.io/name: agora +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: agora + ports: + - name: http + port: 80 + targetPort: http