Add Helm chart scaffold with Chart.yaml, values.yaml, and helpers #132

Merged
groombook-engineer[bot] merged 16 commits from helm-chart-scaffold into main 2026-03-27 18:36:29 +00:00
19 changed files with 794 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
name: Release Helm Chart
on:
push:
branches: [main]
paths:
- 'charts/**'
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout groombook
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout groombook.github.io
uses: actions/checkout@v4
with:
repository: groombook/groombook.github.io
path: gh-pages
token: ${{ secrets.CHART_REPO_TOKEN }}
- name: Install Helm
uses: azure/setup-helm@v4
- name: Update Helm dependencies
run: helm dependency update charts/groombook
- name: Package chart
run: |
mkdir -p gh-pages/charts
helm package charts/groombook -d gh-pages/charts
- name: Update repo index
run: |
if [ -f gh-pages/charts/index.yaml ]; then
helm repo index gh-pages/charts --merge gh-pages/charts/index.yaml --url https://groombook.github.io/charts
else
helm repo index gh-pages/charts --url https://groombook.github.io/charts
fi
- name: Push to groombook.github.io
run: |
cd gh-pages
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add charts/
git diff --staged --quiet && echo 'No chart changes' && exit 0
git commit -m "Update Helm chart repository"
git push
+10
View File
@@ -0,0 +1,10 @@
.DS_Store
*.swp
*.bak
*.tmp
*.orig
*~
.project
.idea/
*.tmproj
.vscode/
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v2
name: groombook
description: Open source pet grooming business management & CRM
type: application
version: 0.1.0
appVersion: "2026.03.19-ea54506"
home: https://groombook.github.io
sources:
- https://github.com/groombook/groombook
maintainers:
- name: GroomBook
url: https://github.com/groombook
keywords:
- groombook
- pet-grooming
- scheduling
- crm
+121
View File
@@ -0,0 +1,121 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "groombook.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "groombook.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "groombook.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "groombook.labels" -}}
helm.sh/chart: {{ include "groombook.chart" . }}
{{ include "groombook.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "groombook.selectorLabels" -}}
app.kubernetes.io/name: {{ include "groombook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Component labels (extends common labels with component name)
*/}}
{{- define "groombook.componentLabels" -}}
{{ include "groombook.labels" . }}
app.kubernetes.io/component: {{ .component }}
{{- end }}
{{/*
Component selector labels
*/}}
{{- define "groombook.componentSelectorLabels" -}}
{{ include "groombook.selectorLabels" . }}
app.kubernetes.io/component: {{ .component }}
{{- end }}
{{/*
Service account name
*/}}
{{- define "groombook.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "groombook.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
API image reference
*/}}
{{- define "groombook.apiImage" -}}
{{- printf "%s:%s" .Values.api.image.repository (default .Chart.AppVersion .Values.api.image.tag) }}
{{- end }}
{{/*
Web image reference
*/}}
{{- define "groombook.webImage" -}}
{{- printf "%s:%s" .Values.web.image.repository (default .Chart.AppVersion .Values.web.image.tag) }}
{{- end }}
{{/*
Migrate image reference
*/}}
{{- define "groombook.migrateImage" -}}
{{- printf "%s:%s" .Values.migrate.image.repository (default .Chart.AppVersion .Values.migrate.image.tag) }}
{{- end }}
{{/*
Database URL differs by postgresql.mode
Integrated: construct from chart-managed PostgreSQL credentials
Operator: read from credentialsSecret
*/}}
{{- define "groombook.databaseSecretName" -}}
{{- if eq .Values.postgresql.mode "operator" }}
{{- required "postgresql.operator.credentialsSecret is required in operator mode" .Values.postgresql.operator.credentialsSecret }}
{{- else }}
{{- include "groombook.fullname" . }}-db-credentials
{{- end }}
{{- end }}
{{/*
Database URL secret key
*/}}
{{- define "groombook.databaseSecretKey" -}}
{{- if eq .Values.postgresql.mode "operator" -}}
uri
{{- else -}}
database-url
{{- end -}}
{{- end }}
@@ -0,0 +1,70 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "groombook.fullname" . }}-api
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: api
spec:
replicas: {{ .Values.api.replicas }}
selector:
matchLabels:
{{- include "groombook.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: api
template:
metadata:
labels:
{{- include "groombook.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: api
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "groombook.serviceAccountName" . }}
containers:
- name: api
image: {{ include "groombook.apiImage" . }}
imagePullPolicy: {{ .Values.api.image.pullPolicy }}
ports:
- containerPort: 3000
name: http
protocol: TCP
env:
- name: PORT
value: {{ .Values.api.env.port | quote }}
- name: NODE_ENV
value: {{ .Values.api.env.nodeEnv | quote }}
- name: AUTH_DISABLED
value: {{ .Values.api.env.authDisabled | quote }}
{{- if .Values.api.env.corsOrigin }}
- name: CORS_ORIGIN
value: {{ .Values.api.env.corsOrigin | quote }}
{{- end }}
{{- if .Values.api.env.oidcIssuer }}
- name: OIDC_ISSUER
value: {{ .Values.api.env.oidcIssuer | quote }}
{{- end }}
{{- if .Values.api.env.oidcAudience }}
- name: OIDC_AUDIENCE
value: {{ .Values.api.env.oidcAudience | quote }}
{{- end }}
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "groombook.databaseSecretName" . }}
key: {{ include "groombook.databaseSecretKey" . }}
resources:
{{- toYaml .Values.api.resources | nindent 12 }}
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 30
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "groombook.fullname" . }}-api
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: api
spec:
type: {{ .Values.api.service.type }}
selector:
{{- include "groombook.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: api
ports:
- port: {{ .Values.api.service.port }}
targetPort: 3000
protocol: TCP
name: http
@@ -0,0 +1,28 @@
{{- if eq .Values.postgresql.mode "operator" }}
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: {{ include "groombook.fullname" . }}-postgres
labels:
{{- include "groombook.labels" . | nindent 4 }}
spec:
instances: {{ .Values.postgresql.operator.instances }}
storage:
size: {{ .Values.postgresql.operator.storage.size }}
{{- if .Values.postgresql.operator.storage.storageClass }}
storageClass: {{ .Values.postgresql.operator.storage.storageClass }}
{{- end }}
bootstrap:
initdb:
database: {{ .Values.postgresql.operator.bootstrap.database }}
owner: {{ .Values.postgresql.operator.bootstrap.owner }}
{{- if .Values.postgresql.operator.credentialsSecret }}
secret:
name: {{ .Values.postgresql.operator.credentialsSecret }}
{{- end }}
{{- if .Values.postgresql.operator.monitoring.enabled }}
monitoring:
enablePodMonitor: true
{{- end }}
{{- end }}
@@ -0,0 +1,13 @@
{{- if eq .Values.postgresql.mode "integrated" }}
{{- $password := default (randAlphaNum 16) .Values.postgresql.integrated.auth.password }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "groombook.fullname" . }}-db-credentials
labels:
{{- include "groombook.labels" . | nindent 4 }}
type: Opaque
stringData:
postgresql-password: {{ $password | quote }}
database-url: {{ printf "postgres://%s:%s@%s-postgresql:5432/%s" .Values.postgresql.integrated.auth.username $password (include "groombook.fullname" .) .Values.postgresql.integrated.auth.database | quote }}
{{- end }}
@@ -0,0 +1,15 @@
{{- if and .Values.dragonfly.enabled (eq .Values.dragonfly.mode "operator") }}
apiVersion: dragonflydb.io/v1alpha1
kind: Dragonfly
metadata:
name: {{ include "groombook.fullname" . }}-dragonfly
labels:
{{- include "groombook.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.dragonfly.operator.replicas }}
{{- with .Values.dragonfly.operator.resources }}
resources:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,20 @@
{{- if and .Values.dragonfly.enabled (eq .Values.dragonfly.mode "integrated") }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "groombook.fullname" . }}-dragonfly
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: dragonfly
spec:
type: {{ .Values.dragonfly.integrated.service.type }}
selector:
{{- include "groombook.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: dragonfly
ports:
- port: {{ .Values.dragonfly.integrated.service.port }}
targetPort: 6379
protocol: TCP
name: redis
{{- end }}
@@ -0,0 +1,46 @@
{{- if and .Values.dragonfly.enabled (eq .Values.dragonfly.mode "integrated") }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "groombook.fullname" . }}-dragonfly
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: dragonfly
spec:
serviceName: {{ include "groombook.fullname" . }}-dragonfly
replicas: {{ .Values.dragonfly.integrated.replicas }}
selector:
matchLabels:
{{- include "groombook.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: dragonfly
template:
metadata:
labels:
{{- include "groombook.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: dragonfly
spec:
containers:
- name: dragonfly
image: {{ printf "%s:%s" .Values.dragonfly.integrated.image.repository .Values.dragonfly.integrated.image.tag }}
imagePullPolicy: {{ .Values.dragonfly.integrated.image.pullPolicy }}
ports:
- containerPort: 6379
name: redis
protocol: TCP
resources:
{{- toYaml .Values.dragonfly.integrated.resources | nindent 12 }}
{{- if .Values.dragonfly.integrated.storage.size }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.dragonfly.integrated.storage.storageClass }}
storageClassName: {{ .Values.dragonfly.integrated.storage.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.dragonfly.integrated.storage.size }}
{{- end }}
{{- end }}
+42
View File
@@ -0,0 +1,42 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "groombook.fullname" . }}
labels:
{{- include "groombook.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
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:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
{{- if eq .service "api" }}
name: {{ include "groombook.fullname" $ }}-api
port:
number: {{ $.Values.api.service.port }}
{{- else }}
name: {{ include "groombook.fullname" $ }}-web
port:
number: {{ $.Values.web.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,38 @@
{{- if .Values.migrate.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "groombook.fullname" . }}-migrate
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: migrate
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
backoffLimit: {{ .Values.migrate.backoffLimit }}
template:
metadata:
labels:
{{- include "groombook.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: migrate
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
restartPolicy: OnFailure
containers:
- name: migrate
image: {{ include "groombook.migrateImage" . }}
imagePullPolicy: {{ .Values.migrate.image.pullPolicy }}
command: ["pnpm", "db:migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "groombook.databaseSecretName" . }}
key: {{ include "groombook.databaseSecretKey" . }}
{{- end }}
@@ -0,0 +1,19 @@
{{- if eq .Values.postgresql.mode "integrated" }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "groombook.fullname" . }}-postgresql
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: postgresql
spec:
type: ClusterIP
selector:
{{- include "groombook.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: postgresql
ports:
- port: 5432
targetPort: 5432
protocol: TCP
name: postgresql
{{- end }}
@@ -0,0 +1,72 @@
{{- if eq .Values.postgresql.mode "integrated" }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "groombook.fullname" . }}-postgresql
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: postgresql
spec:
serviceName: {{ include "groombook.fullname" . }}-postgresql
replicas: 1
selector:
matchLabels:
{{- include "groombook.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: postgresql
template:
metadata:
labels:
{{- include "groombook.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: postgresql
spec:
containers:
- name: postgresql
image: {{ .Values.postgresql.integrated.image }}
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
name: postgresql
protocol: TCP
env:
- name: POSTGRES_DB
value: {{ .Values.postgresql.integrated.auth.database | quote }}
- name: POSTGRES_USER
value: {{ .Values.postgresql.integrated.auth.username | quote }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "groombook.fullname" . }}-db-credentials
key: postgresql-password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command:
- pg_isready
- -U
- {{ .Values.postgresql.integrated.auth.username | quote }}
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
exec:
command:
- pg_isready
- -U
- {{ .Values.postgresql.integrated.auth.username | quote }}
initialDelaySeconds: 30
periodSeconds: 30
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.postgresql.integrated.storage.storageClass }}
storageClassName: {{ .Values.postgresql.integrated.storage.storageClass }}
{{- end }}
resources:
requests:
storage: {{ .Values.postgresql.integrated.storage.size }}
{{- end }}
@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "groombook.serviceAccountName" . }}
labels:
{{- include "groombook.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "groombook.fullname" . }}-web
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: web
spec:
replicas: {{ .Values.web.replicas }}
selector:
matchLabels:
{{- include "groombook.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: web
template:
metadata:
labels:
{{- include "groombook.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: web
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "groombook.serviceAccountName" . }}
containers:
- name: web
image: {{ include "groombook.webImage" . }}
imagePullPolicy: {{ .Values.web.image.pullPolicy }}
ports:
- containerPort: 80
name: http
protocol: TCP
resources:
{{- toYaml .Values.web.resources | nindent 12 }}
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 30
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "groombook.fullname" . }}-web
labels:
{{- include "groombook.labels" . | nindent 4 }}
app.kubernetes.io/component: web
spec:
type: {{ .Values.web.service.type }}
selector:
{{- include "groombook.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: web
ports:
- port: {{ .Values.web.service.port }}
targetPort: 80
protocol: TCP
name: http
+134
View File
@@ -0,0 +1,134 @@
# -- API deployment
api:
image:
repository: ghcr.io/groombook/api
tag: "" # defaults to chart appVersion
pullPolicy: IfNotPresent
replicas: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
env:
nodeEnv: production
authDisabled: false
corsOrigin: ""
oidcIssuer: ""
oidcAudience: groombook
port: "3000"
service:
type: ClusterIP
port: 3000
# -- Web deployment (nginx)
web:
image:
repository: ghcr.io/groombook/web
tag: "" # defaults to chart appVersion
pullPolicy: IfNotPresent
replicas: 1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
service:
type: ClusterIP
port: 80
# -- Database migration job (runs as pre-install/pre-upgrade hook)
migrate:
enabled: true
image:
repository: ghcr.io/groombook/api
tag: "" # same image as api
pullPolicy: IfNotPresent
backoffLimit: 3
# -- PostgreSQL configuration
postgresql:
# Choose deployment mode: 'integrated' deploys a native PostgreSQL StatefulSet, 'operator' creates a CNPG Cluster CR
mode: integrated
integrated:
image: postgres:16
storage:
size: 10Gi
storageClass: ""
auth:
database: groombook
username: groombook
password: "" # auto-generated if empty
existingSecret: ""
operator:
instances: 3
storage:
size: 10Gi
storageClass: ""
bootstrap:
database: groombook
owner: groombook
credentialsSecret: "" # must pre-exist with key 'uri'
monitoring:
enabled: true
# -- DragonflyDB (cache/pub-sub) — optional, disabled by default
dragonfly:
enabled: false
# Choose deployment mode: 'integrated' deploys a StatefulSet, 'operator' creates a Dragonfly CR
mode: integrated
integrated:
image:
repository: docker.dragonflydb.io/dragonflydb/dragonfly
tag: latest
pullPolicy: IfNotPresent
replicas: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
storage:
size: 1Gi
storageClass: ""
service:
type: ClusterIP
port: 6379
operator:
replicas: 1
resources: {}
# -- Ingress configuration
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: groombook.example.com
paths:
- path: /
pathType: Prefix
service: web
- path: /api
pathType: Prefix
service: api
tls: []
# -- Service account
serviceAccount:
create: true
name: ""
annotations: {}
# -- Global image pull secrets
imagePullSecrets: []
# -- Override chart name
nameOverride: ""
fullnameOverride: ""