Publish paperless-ngx-helm chart for upstream v3.3.2
Validate, publish and promote Paperless-ngx / chart (push) Successful in 35s

This commit is contained in:
feedc0de committed 2026-10-19 21:22:12 +02:00
commit fb5db18f00
13 files changed
+361

No files matched your search

+71
View File
@@ -0,0 +1,71 @@
name: Validate, publish and promote Paperless-ngx
on:
push:
pull_request:
jobs:
chart:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: azure/setup-helm@v4
with:
version: v4.3.0
- name: Validate chart
run: |
python3 -m venv "${RUNNER_TEMP}/chart-test"
"${RUNNER_TEMP}/chart-test/bin/pip" install PyYAML
PATH="${RUNNER_TEMP}/chart-test/bin:${PATH}" ./test.sh
- name: Select immutable chart version
if: gitea.ref == 'refs/heads/main'
run: printf 'CHART_VERSION=%s\n' "$(./scripts/chart-version.sh)" >> "${GITHUB_ENV}"
- name: Package and publish chart
if: gitea.ref == 'refs/heads/main'
env:
PACKAGE_USERNAME: ${{ secrets.PACKAGE_USERNAME }}
PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
run: |
set -euo pipefail
test -n "${PACKAGE_USERNAME}" && test -n "${PACKAGE_TOKEN}"
mkdir -p .chart-packages
helm package chart/paperless-ngx --destination .chart-packages --version "${CHART_VERSION}"
curl --fail --silent --show-error --user "${PACKAGE_USERNAME}:${PACKAGE_TOKEN}" \
--request POST --upload-file ".chart-packages/paperless-ngx-${CHART_VERSION}.tgz" \
https://code.brunner.ninja/api/packages/feedc0de/helm/api/charts
- name: Check current main
id: current-main
if: gitea.ref == 'refs/heads/main'
env:
COMMIT_SHA: ${{ gitea.sha }}
run: |
git fetch --no-tags origin main
if test "$(git rev-parse FETCH_HEAD)" = "${COMMIT_SHA}"; then
echo 'promote=true' >> "${GITEA_OUTPUT}"
else
echo 'promote=false' >> "${GITEA_OUTPUT}"
fi
- uses: actions/checkout@v4
if: steps.current-main.outputs.promote == 'true'
with:
repository: feedc0de/homelab-gitops
ref: main
token: ${{ secrets.GITOPS_TOKEN }}
path: homelab-gitops
- name: Promote chart
if: steps.current-main.outputs.promote == 'true'
working-directory: homelab-gitops
run: |
set -euo pipefail
./scripts/update-chart-version.sh paperless "${CHART_VERSION}"
git config user.name feedc0de
git config user.email feedc0de@users.noreply.code.brunner.ninja
git add bootstrap/paperless.yaml
if git diff --cached --quiet; then exit 0; fi
git commit -m "Deploy paperless chart ${CHART_VERSION}"
for attempt in 1 2 3; do
git push origin HEAD:main && exit 0
test "${attempt}" = 3 && exit 1
git fetch origin main
git rebase origin/main
done
+27
View File
@@ -0,0 +1,27 @@
# Paperless-ngx Helm chart
This independent chart packages the upstream Paperless-ngx image without bundling
PostgreSQL or Valkey. It declares the Paperless Deployment, Service, Ingress,
probes, Secret references, and optional PVCs. The chart Git tag follows the
upstream application release (`v3.3.2`); later chart-only commits append Git
describe's count and hash. The default image tag comes from `appVersion`.
Supply existing Secret names for the Paperless key and PostgreSQL password.
The chart never renders credentials. Set `persistence.data.existingClaim` and
`persistence.runtime.existingClaim` to retain existing claims. The shared data
claim may also be mounted by a separate scanner release. Scheduling affinity,
tolerations, OCR worker count, and cluster hostname are values, not chart
constants. The homelab deployment is in
[homelab-gitops/apps/paperless](https://code.brunner.ninja/feedc0de/homelab-gitops/src/branch/main/apps/paperless).
```sh
helm repo add brunner.ninja https://brunner.ninja/charts
helm repo update brunner.ninja
helm upgrade --install paperless brunner.ninja/paperless-ngx \
--namespace paperless-ngx --create-namespace -f values.yaml --wait --timeout 15m
```
For local testing run `./test.sh`; `./install.sh values.yaml` uses the current
Kubernetes context and the chart in this checkout. CI publishes the chart,
then promotes only its chart version to GitOps. Required Actions secrets are
`PACKAGE_USERNAME`, `PACKAGE_TOKEN`, and `GITOPS_TOKEN`.
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v2
name: paperless-ngx
description: Paperless-ngx application without a bundled database or cache
type: application
version: 0.0.0
appVersion: 3.3.2
@@ -0,0 +1,91 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: paperless
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: Recreate
selector:
matchLabels: {app: paperless}
template:
metadata:
labels: {app: paperless}
spec:
automountServiceAccountToken: false
enableServiceLinks: false
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
containers:
- name: paperless
image: {{ printf "%s:%s" .Values.image.repository (.Values.image.tag | default .Chart.AppVersion) | quote }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: [{name: http, containerPort: 8000}]
env:
- {name: PAPERLESS_URL, value: {{ .Values.url | quote }}}
- {name: PAPERLESS_REDIS, value: {{ .Values.cache.url | quote }}}
- {name: PAPERLESS_DBENGINE, value: postgresql}
- {name: PAPERLESS_DBHOST, value: {{ .Values.database.host | quote }}}
- {name: PAPERLESS_DBNAME, value: {{ .Values.database.name | quote }}}
- {name: PAPERLESS_DBUSER, value: {{ .Values.database.user | quote }}}
- name: PAPERLESS_DBPASS
valueFrom:
secretKeyRef: {name: {{ .Values.database.passwordSecretName }}, key: {{ .Values.database.passwordSecretKey }}}
- {name: PAPERLESS_TIME_ZONE, value: {{ .Values.timeZone | quote }}}
- {name: PAPERLESS_OCR_LANGUAGE, value: {{ .Values.ocrLanguage | quote }}}
- {name: PAPERLESS_TASK_WORKERS, value: {{ .Values.taskWorkers | quote }}}
- {name: PAPERLESS_THREADS_PER_WORKER, value: {{ .Values.threadsPerWorker | quote }}}
- {name: PAPERLESS_CONSUMER_POLLING_INTERVAL, value: {{ .Values.consumerPollingInterval | quote }}}
{{- if .Values.oidc.enabled }}
- {name: PAPERLESS_APPS, value: allauth.socialaccount.providers.openid_connect}
- {name: PAPERLESS_SOCIAL_AUTO_SIGNUP, value: "true"}
- {name: PAPERLESS_SOCIAL_ACCOUNT_SYNC_GROUPS, value: "true"}
- {name: PAPERLESS_SOCIAL_ACCOUNT_SYNC_SUPERUSER_GROUP, value: {{ .Values.oidc.adminGroup | quote }}}
- name: PAPERLESS_SOCIALACCOUNT_PROVIDERS
valueFrom:
secretKeyRef: {name: {{ .Values.oidc.providersSecretName }}, key: {{ .Values.oidc.providersSecretKey }}}
{{- end }}
- name: PAPERLESS_SECRET_KEY
valueFrom:
secretKeyRef: {name: {{ .Values.secretKey.secretName }}, key: {{ .Values.secretKey.secretKey }}}
volumeMounts:
- {name: runtime, mountPath: /usr/src/paperless/data}
- {name: data, mountPath: /usr/src/paperless/media, subPath: media}
- {name: data, mountPath: /usr/src/paperless/consume, subPath: consume}
- {name: data, mountPath: /usr/src/paperless/export, subPath: export}
startupProbe:
httpGet: {path: /, port: http}
periodSeconds: 10
failureThreshold: 60
timeoutSeconds: 10
readinessProbe:
httpGet: {path: /, port: http}
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 6
livenessProbe:
exec:
command:
- sh
- -ec
- |
for service in svc-worker svc-webserver svc-consumer svc-scheduler; do
pid="$(s6-svstat -o pid "/run/service/$service")"
[ "$pid" -gt 0 ]
done
periodSeconds: 60
timeoutSeconds: 30
failureThreshold: 30
resources:
{{ toYaml .Values.resources | indent 12 }}
volumes:
- name: runtime
persistentVolumeClaim: {claimName: {{ .Values.persistence.runtime.existingClaim | default "paperless-runtime" }}}
- name: data
persistentVolumeClaim: {claimName: {{ .Values.persistence.data.existingClaim | default "paperless-data" }}}
@@ -0,0 +1,24 @@
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: paperless
{{- with .Values.ingress.annotations }}
annotations:
{{ toYaml . | indent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: paperless
port: {name: http}
{{- end }}
+33
View File
@@ -0,0 +1,33 @@
{{- if not .Values.persistence.data.existingClaim }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-data
annotations:
helm.sh/resource-policy: keep
spec:
accessModes: [ReadWriteMany]
{{- with .Values.persistence.data.storageClassName }}
storageClassName: {{ . }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.data.size }}
---
{{- end }}
{{- if not .Values.persistence.runtime.existingClaim }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: paperless-runtime
annotations:
helm.sh/resource-policy: keep
spec:
accessModes: [ReadWriteOnce]
{{- with .Values.persistence.runtime.storageClassName }}
storageClassName: {{ . }}
{{- end }}
resources:
requests:
storage: {{ .Values.persistence.runtime.size }}
{{- end }}
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Service
metadata:
name: paperless
spec:
selector: {app: paperless}
ports: [{name: http, port: 8000, targetPort: http}]
+13
View File
@@ -0,0 +1,13 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["database", "secretKey"],
"properties": {
"database": {"type": "object", "required": ["passwordSecretName"], "properties": {"passwordSecretName": {"type": "string", "minLength": 1}}},
"secretKey": {"type": "object", "required": ["secretName"], "properties": {"secretName": {"type": "string", "minLength": 1}}},
"persistence": {"type": "object", "properties": {
"data": {"type": "object", "properties": {"existingClaim": {"type": "string"}}},
"runtime": {"type": "object", "properties": {"existingClaim": {"type": "string"}}}
}}
}
}
+44
View File
@@ -0,0 +1,44 @@
image:
repository: ghcr.io/paperless-ngx/paperless-ngx
tag: ""
pullPolicy: IfNotPresent
replicaCount: 1
url: ""
timeZone: UTC
ocrLanguage: eng
taskWorkers: 2
threadsPerWorker: 1
consumerPollingInterval: 5
database:
host: postgresql
name: paperless
user: paperless
passwordSecretName: ""
passwordSecretKey: USERDB_PASSWORD
cache:
url: redis://valkey:6379
secretKey:
secretName: ""
secretKey: secret-key
oidc:
enabled: false
providersSecretName: ""
providersSecretKey: providers
adminGroup: Paperless Admins
persistence:
data:
existingClaim: ""
size: 20Gi
storageClassName: ""
runtime:
existingClaim: ""
size: 4Gi
storageClassName: ""
ingress:
enabled: false
className: ""
host: ""
annotations: {}
affinity: {}
tolerations: []
resources: {}
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")"
values=${1:?usage: ./install.sh VALUES_FILE}
./test.sh
helm upgrade --install paperless chart/paperless-ngx --namespace paperless-ngx --create-namespace --values "$values" --wait --timeout 15m
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
description=$(git describe --tags --long --match 'v[0-9]*' --dirty)
case "$description" in *-dirty) echo 'Refusing to package a dirty tree' >&2; exit 1 ;; esac
version=${description#v}
case "$version" in *-0-g*) version=${version%%-0-g*} ;; esac
printf '%s\n' "$version"
Executable
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
set -eu
cd "$(dirname "$0")"
helm lint --strict chart/paperless-ngx -f values.example.yaml
helm template paperless chart/paperless-ngx -n example -f values.example.yaml | python3 -c '
import sys, yaml
objects=[x for x in yaml.safe_load_all(sys.stdin) if x]
one=lambda kind,name: next(x for x in objects if x["kind"]==kind and x["metadata"]["name"]==name)
d=one("Deployment","paperless")
s=one("Service","paperless")
assert d["spec"]["selector"]["matchLabels"]==s["spec"]["selector"]
c=d["spec"]["template"]["spec"]["containers"][0]
assert all(k in c for k in ("startupProbe","readinessProbe","livenessProbe"))
assert {v["name"] for v in d["spec"]["template"]["spec"]["volumes"]}=={"data","runtime"}
assert any(e["name"]=="PAPERLESS_DBPASS" and "secretKeyRef" in e["valueFrom"] for e in c["env"])
assert not any(x["kind"]=="Secret" for x in objects)
assert one("PersistentVolumeClaim","paperless-data") and one("PersistentVolumeClaim","paperless-runtime")
'
+14
View File
@@ -0,0 +1,14 @@
url: https://paper.example.com
database:
passwordSecretName: paperless-postgres
secretKey:
secretName: paperless-secret-key
persistence:
data:
size: 20Gi
runtime:
size: 4Gi
ingress:
enabled: true
className: traefik
host: paper.example.com