Skip to content
Open
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
30 changes: 30 additions & 0 deletions .github/workflows/docker-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Clean up PR images

on:
schedule:
- cron: "0 4 * * *" # daily at 04:00 UTC
workflow_dispatch:

permissions:
packages: write

jobs:
cleanup:
name: Delete stale PR images
runs-on: ubuntu-latest

strategy:
matrix:
image: [pinchy, pinchy-openclaw]

steps:
- name: Delete PR images older than 15 days
uses: snok/container-retention-policy@v3
with:
account: ${{ github.repository_owner }}
token: ${{ secrets.GITHUB_TOKEN }}
image-names: ${{ matrix.image }}
tag-selection: "tagged"
tag-regex: "^feat[-/]|^fix[-/]|^chore[-/]|^docs[-/]"
cut-off: 15 days ago UTC
timestamp-to-use: "created_at"
57 changes: 57 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Publish Docker images

on:
push:
tags: ["v*"]
branches: ["**", "!main"]

permissions:
contents: read
packages: write

jobs:
build-and-push:
name: Build & push images
runs-on: ubuntu-latest

strategy:
matrix:
include:
- dockerfile: Dockerfile.pinchy
image: pinchy
- dockerfile: Dockerfile.openclaw
image: pinchy-openclaw

steps:
- uses: actions/checkout@v4

- uses: docker/setup-buildx-action@v3

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

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/${{ matrix.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=ref,event=branch
type=ref,event=branch,suffix=-{{sha}}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,6 @@ coverage/
# Playwright
packages/web/test-results/
packages/web/playwright-report/

# Helm local overrides
deploy/helm/*/lab-values.yaml
9 changes: 9 additions & 0 deletions deploy/helm/pinchy/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
apiVersion: v2
name: pinchy
description: Self-hosted AI assistant with OpenClaw gateway
type: application
version: 0.1.0
appVersion: "0.1.0"
maintainers:
- name: Tight Line Software
url: https://www.tightlinesoftware.com
19 changes: 19 additions & 0 deletions deploy/helm/pinchy/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Pinchy has been deployed!

{{- if .Values.ingress.enabled }}

Access Pinchy at: https://{{ .Values.ingress.host }}
{{- else }}

To access Pinchy, forward the service port:

kubectl port-forward svc/{{ .Release.Name }}-pinchy 7777:{{ .Values.pinchy.service.port }}

Then open http://localhost:7777
{{- end }}

On first launch, complete the setup wizard to configure your AI provider.

To check pod status:

kubectl get pods -l app.kubernetes.io/instance={{ .Release.Name }}
53 changes: 53 additions & 0 deletions deploy/helm/pinchy/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{{/*
Validate required values and fail early if missing.
*/}}
{{- define "pinchy.validateRequired" -}}
{{- if not .Values.postgresql.install -}}
{{- if not .Values.postgresql.host -}}
{{- fail "postgresql.host is required when postgresql.install is false." -}}
{{- end -}}
{{- end -}}
{{- if .Values.ingress.enabled -}}
{{- if not .Values.ingress.host -}}
{{- fail "ingress.host is required when ingress is enabled." -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Name of the auto-managed secrets resource.
*/}}
{{- define "pinchy.secretName" -}}
{{- printf "%s-pinchy-secrets" .Release.Name -}}
{{- end -}}

{{/*
PostgreSQL host: internal service name when installed, else user-provided.
*/}}
{{- define "pinchy.pgHost" -}}
{{- if .Values.postgresql.install -}}
{{- printf "%s-postgresql" .Release.Name -}}
{{- else -}}
{{- .Values.postgresql.host -}}
{{- end -}}
{{- end -}}

{{/*
Secret name and key for the PostgreSQL password.
When postgresql.auth.existingSecret is set, use that; otherwise use our managed secret.
*/}}
{{- define "pinchy.pgSecretName" -}}
{{- if .Values.postgresql.auth.existingSecret -}}
{{- .Values.postgresql.auth.existingSecret -}}
{{- else -}}
{{- include "pinchy.secretName" . -}}
{{- end -}}
{{- end -}}

{{- define "pinchy.pgSecretKey" -}}
{{- if .Values.postgresql.auth.existingSecret -}}
{{- .Values.postgresql.auth.secretKey -}}
{{- else -}}
postgresql-password
{{- end -}}
{{- end -}}
35 changes: 35 additions & 0 deletions deploy/helm/pinchy/templates/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Release.Name }}-pinchy
labels:
app.kubernetes.io/name: pinchy
app.kubernetes.io/instance: {{ .Release.Name }}
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.org/websocket-services: {{ .Release.Name }}-pinchy
{{- with .Values.ingress.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- toYaml .Values.ingress.tls | nindent 4 }}
{{- end }}
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Release.Name }}-pinchy
port:
number: {{ .Values.pinchy.service.port }}
{{- end }}
18 changes: 18 additions & 0 deletions deploy/helm/pinchy/templates/openclaw-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-openclaw
labels:
app.kubernetes.io/name: pinchy-openclaw
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: gateway
spec:
type: ClusterIP
ports:
- port: {{ .Values.openclaw.service.port }}
targetPort: gateway
name: gateway
selector:
app.kubernetes.io/name: pinchy-openclaw
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: gateway
110 changes: 110 additions & 0 deletions deploy/helm/pinchy/templates/openclaw-statefulset.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{{- $secretName := include "pinchy.secretName" . -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-openclaw
labels:
app.kubernetes.io/name: pinchy-openclaw
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: gateway
spec:
serviceName: {{ .Release.Name }}-openclaw
replicas: 1
updateStrategy:
type: OnDelete
selector:
matchLabels:
app.kubernetes.io/name: pinchy-openclaw
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: gateway
template:
metadata:
labels:
app.kubernetes.io/name: pinchy-openclaw
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: gateway
spec:
automountServiceAccountToken: false
initContainers:
- name: init-config
image: {{ .Values.openclaw.image.repository }}:{{ .Values.openclaw.image.tag | default .Chart.AppVersion }}
imagePullPolicy: {{ .Values.openclaw.image.pullPolicy }}
command: ["sh", "-c"]
args:
- |
# Seed openclaw.json with the gateway token from the Secret.
# ensure-gateway-token.js will skip generation since token already exists.
TOKEN=$(cat /secret/gateway-token)
mkdir -p /root/.openclaw
cat > /root/.openclaw/openclaw.json <<EOJSON
{
"gateway": {
"mode": "local",
"bind": "lan",
"auth": {
"mode": "token",
"token": "${TOKEN}"
}
}
}
EOJSON
volumeMounts:
- name: secret
mountPath: /secret
readOnly: true
- name: openclaw-data
mountPath: /root
- name: copy-plugins
image: {{ .Values.pinchy.image.repository }}:{{ .Values.pinchy.image.tag | default .Chart.AppVersion }}
imagePullPolicy: {{ .Values.pinchy.image.pullPolicy }}
command: ["sh", "-c"]
args:
- cp -r /openclaw-extensions/* /target/
volumeMounts:
- name: plugins
mountPath: /target
containers:
- name: openclaw
image: {{ .Values.openclaw.image.repository }}:{{ .Values.openclaw.image.tag | default .Chart.AppVersion }}
imagePullPolicy: {{ .Values.openclaw.image.pullPolicy }}
ports:
- name: gateway
containerPort: 18789
volumeMounts:
- name: openclaw-data
mountPath: /root
- name: plugins
mountPath: /root/.openclaw/extensions
readOnly: true
livenessProbe:
tcpSocket:
port: gateway
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
tcpSocket:
port: gateway
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.openclaw.resources | nindent 12 }}
volumes:
- name: secret
secret:
secretName: {{ $secretName }}
items:
- key: gateway-token
path: gateway-token
- name: plugins
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: openclaw-data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.openclaw.storage.storageClass }}
storageClassName: {{ .Values.openclaw.storage.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.openclaw.storage.size }}
Loading
Loading