Skip to content

Commit 330275a

Browse files
tijderclaude
andcommitted
feat: ship the admin docs as a zip on every release
doc/admin/{en,nl}/01-installation-helm.md describe how the chart works, laid out like the player repo's doc/. ci/build-docs.sh fills the empty VALUES:BEGIN/END markers with a table generated from values.yaml and packages ister-chart-docs-<version>.zip, which release.yml now attaches to the GitHub Release; ci.yml builds it on every PR so a broken marker fails there instead. doc/ is .helmignore'd and doc/** now triggers a release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9565faf commit 330275a

9 files changed

Lines changed: 422 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ jobs:
6767
with:
6868
args: -strict -summary -ignore-missing-schemas kubeconform-manifests.yaml
6969

70+
# Catches a broken VALUES marker or values.yaml drift on the PR instead of
71+
# letting it fail the release build.
72+
- name: Build the docs zip
73+
run: ./ci/build-docs.sh 0.0.0-ci
74+
7075
e2e:
7176
name: End-to-end on kind
7277
runs-on: ubuntu-latest

.github/workflows/release.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ on:
1212
- values.yaml
1313
- values.schema.json
1414
- templates/**
15+
# The docs ship as a zip on every release, so a doc change must release too.
16+
- doc/**
1517
workflow_dispatch:
1618
inputs:
1719
bump:
@@ -104,6 +106,11 @@ jobs:
104106
- name: Generate release notes
105107
run: ./ci/release-notes.sh "${{ steps.version.outputs.version }}" "${{ steps.version.outputs.prev }}"
106108

109+
# doc/ with the values reference filled in from values.yaml; attached to the
110+
# release below, mirroring the player repo's docs zip.
111+
- name: Build the docs zip
112+
run: ./ci/build-docs.sh "${{ steps.version.outputs.version }}"
113+
107114
- name: Package
108115
run: |
109116
helm dependency build
@@ -133,6 +140,6 @@ jobs:
133140
git tag -a "v${VERSION}" -m "v${VERSION}"
134141
git push origin HEAD:main --follow-tags
135142
136-
gh release create "v${VERSION}" "ister-${VERSION}.tgz" \
143+
gh release create "v${VERSION}" "ister-${VERSION}.tgz" "ister-chart-docs-${VERSION}.zip" \
137144
--title "v${VERSION}" \
138145
--notes-file RELEASE_NOTES.md

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,8 @@ charts/
1313
*.tgz
1414
RELEASE_NOTES.md
1515

16+
# Built by ci/build-docs.sh; attached to the GitHub Release, never committed.
17+
ister-chart-docs-*.zip
18+
1619
# Rendered manifests the CI kubeconform step feeds to the container.
1720
kubeconform-manifests.yaml

.helmignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ values-production.yaml
1616
# CI scaffolding is not part of the chart.
1717
.github/
1818
ci/
19+
20+
# Documentation ships as a release-asset zip (ci/build-docs.sh), not inside the chart.
21+
doc/
22+
ister-chart-docs-*.zip

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ helm template ister . -f ci/values-ci.yaml # values.schema.json is e
1919
helm package .
2020

2121
ci/release-notes.sh 0.3.0 v0.2.0 && cat RELEASE_NOTES.md # release notes, runs locally
22+
ci/build-docs.sh 0.0.0-local # the docs zip, runs locally
2223
```
2324

2425
There are four value profiles and CI renders all of them; a change to `values.yaml` or a template
@@ -96,6 +97,12 @@ Automatic, and the details matter before you touch `Chart.yaml`:
9697
of truth. `ci/release-notes.sh` mirrors that fallback so the migrations row is never blank.
9798
- Renovate (`renovate.json`), not Dependabot — Dependabot's docker manager cannot tell two images
9899
in one `values.yaml` apart when they share a tag string (dependabot-core#6891).
100+
- Every release also ships `ister-chart-docs-<version>.zip`, built by `ci/build-docs.sh` from
101+
`doc/admin/{en,nl}/` (the player repo's `doc/` layout). The values reference in each chapter is
102+
an **empty `VALUES:BEGIN`/`VALUES:END` marker pair in git** that the script fills from
103+
`values.yaml` at build time — never commit content between the markers, and never spell the
104+
literal markers out anywhere else in `doc/` (the script fills every pair it finds). `doc/` is
105+
`.helmignore`d, so it never ends up in the chart tgz; a `doc/**` change does trigger a release.
99106

100107
`server` and `player` publish semver tags: `values.yaml` pins both at `1.0.0` and Renovate bumps
101108
them from there. `migrations` has no `tag` of its own — see the appVersion note above.

ci/build-docs.sh

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#!/usr/bin/env bash
2+
# Build the docs zip for a chart release.
3+
#
4+
# ci/build-docs.sh [<chart-version>]
5+
#
6+
# Copies doc/ into a build dir, fills the VALUES:BEGIN/VALUES:END markers with a
7+
# reference table generated from values.yaml, and packages the result as
8+
# ister-chart-docs-<version>.zip in the repo root. The committed chapters keep the
9+
# markers empty — the table exists only inside the zip, so it can never go stale
10+
# against values.yaml. Runs fine outside CI.
11+
set -euo pipefail
12+
13+
cd "$(dirname "$0")/.."
14+
root="$PWD"
15+
16+
version="${1:-$(grep -oP '^version:\s*\K\S+' Chart.yaml)}"
17+
version="${version#v}"
18+
zip_name="ister-chart-docs-${version}.zip"
19+
20+
build_dir="$(mktemp -d)"
21+
trap 'rm -rf "$build_dir"' EXIT
22+
cp -r doc "$build_dir/doc"
23+
24+
echo "=== generating the values reference from values.yaml"
25+
# python3 rather than yq, for the same reason as release-notes.sh: this must run on
26+
# any dev box. Leaves are dict-less values; lists count as leaves so the example-heavy
27+
# ones (libraries, mediaVolumes, extraEnv) show up as one row, not an explosion.
28+
python3 - values.yaml > "$build_dir/values-table.md" <<'PY'
29+
import json, sys, yaml
30+
31+
values = yaml.safe_load(open(sys.argv[1]))
32+
33+
def rows(node, path):
34+
for key, value in node.items():
35+
here = path + [key]
36+
if isinstance(value, dict) and value:
37+
yield from rows(value, here)
38+
else:
39+
yield ".".join(here), json.dumps(value)
40+
41+
print("| Key | Default |")
42+
print("|---|---|")
43+
for key, default in rows(values, []):
44+
print(f"| `{key}` | `{default}` |")
45+
PY
46+
47+
echo "=== filling the markers"
48+
python3 - "$build_dir/doc" "$build_dir/values-table.md" <<'PY'
49+
import pathlib, sys
50+
51+
doc_dir = pathlib.Path(sys.argv[1])
52+
table = pathlib.Path(sys.argv[2]).read_text().rstrip("\n")
53+
BEGIN, END = "<!-- VALUES:BEGIN", "<!-- VALUES:END -->"
54+
55+
filled = 0
56+
for chapter in sorted(doc_dir.rglob("*.md")):
57+
text = chapter.read_text()
58+
if BEGIN not in text and END not in text:
59+
continue
60+
# One well-formed pair per chapter, or the build fails rather than shipping a
61+
# half-substituted document.
62+
if text.count(BEGIN) != 1 or text.count(END) != 1:
63+
sys.exit(f"{chapter}: expected exactly one VALUES:BEGIN/VALUES:END pair")
64+
head, rest = text.split(BEGIN, 1)
65+
begin_line, rest = rest.split("\n", 1)
66+
_, tail = rest.split(END, 1)
67+
chapter.write_text(f"{head}{BEGIN}{begin_line}\n{table}\n{END}{tail}")
68+
filled += 1
69+
70+
if filled == 0:
71+
sys.exit("no chapter carries the VALUES markers — nothing to fill")
72+
print(f"filled the values table into {filled} chapter(s)", file=sys.stderr)
73+
PY
74+
75+
echo "=== packaging $zip_name"
76+
rm -f "$root/$zip_name"
77+
(cd "$build_dir" && zip -qr "$root/$zip_name" doc -x '*/.gitkeep')
78+
unzip -l "$root/$zip_name"

doc/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Ister chart documentation
2+
3+
This directory holds the administrator documentation for the ister Helm chart, laid out
4+
the same way as the player repo's `doc/`.
5+
6+
## Contents
7+
8+
- `admin/en/` — the administrator guide in English (installation, architecture, values)
9+
- `admin/nl/` — de beheerdershandleiding in het Nederlands (same chapters, translated)
10+
11+
## The generated values reference
12+
13+
Each chapter that ends in a values reference carries an empty pair of HTML-comment
14+
markers, `VALUES:BEGIN` and `VALUES:END` (spelled out only in the chapters themselves —
15+
reproducing them here would get this README a table too).
16+
17+
The markers stay empty in git. `ci/build-docs.sh` fills them with a table generated from
18+
`values.yaml` when it builds the zip, so the shipped reference always matches the
19+
release it accompanies. Never hand-edit between the markers.
20+
21+
## Building the docs zip locally
22+
23+
```sh
24+
ci/build-docs.sh # version taken from Chart.yaml
25+
ci/build-docs.sh 0.0.0-local
26+
```
27+
28+
This writes `ister-chart-docs-<version>.zip` in the repo root (git-ignored) and leaves
29+
the committed chapters untouched. The release workflow runs the same script and attaches
30+
the zip to every GitHub Release.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Installing ister with Helm
2+
3+
This chapter explains how the ister Helm chart works and how to install it. The chart
4+
deploys the full ister media server: the API server, the web player, and — unless you
5+
bring your own — PostgreSQL, RabbitMQ and Typesense.
6+
7+
## What the chart deploys
8+
9+
| Component | What it is | Bundled by default? |
10+
|---|---|---|
11+
| server | The Spring Boot backend (`ghcr.io/ister-app/server`) | always |
12+
| website | The web player (`ghcr.io/ister-app/player`) | always |
13+
| PostgreSQL | The database | yes (`database.mode: internal`) |
14+
| RabbitMQ | The message broker (Bitnami subchart) | yes (`rabbitmq.enabled: true`) |
15+
| Typesense | The search engine | yes (`typesense.enabled: true`) |
16+
| Flyway | Database migrations (`ghcr.io/ister-app/migrations`) | runs as an init container |
17+
18+
The defaults deploy a self-contained ister with no ingress and no media volumes — enough
19+
to try it out, not a production setup.
20+
21+
## Prerequisites
22+
23+
- A Kubernetes cluster and Helm 3.
24+
- A [TMDB API key](https://www.themoviedb.org/settings/api) for movie/show metadata.
25+
- An OIDC issuer (for example Keycloak) — ister does not manage users itself.
26+
- For `database.mode: cnpg`: the [CloudNativePG](https://cloudnative-pg.io/) operator.
27+
- For ingress with TLS: an ingress controller and, optionally, cert-manager.
28+
29+
## Installing
30+
31+
The chart is published as an OCI artifact:
32+
33+
```sh
34+
helm install ister oci://ghcr.io/ister-app/charts/ister \
35+
--namespace ister --create-namespace \
36+
--set server.tmdbApiKey=<your-key> \
37+
--set server.oidc.url=https://keycloak.example.com/realms/auth
38+
```
39+
40+
For anything beyond a first try, start from `values-production.example.yaml` in the
41+
[chart repository](https://github.com/ister-app/chart) and install with `-f`. Pin a chart
42+
version with `--version`; a GitHub Release exists for every chart version with release
43+
notes listing the exact image versions it deploys.
44+
45+
## Bundled or external datastores
46+
47+
Everything backing the server can either be deployed by the chart or pointed at an
48+
existing service. Whatever you choose, the templates downstream never notice the
49+
difference — that is the chart's central design rule.
50+
51+
### PostgreSQL — `database.mode`
52+
53+
- `internal` — a single postgres Deployment + PVC. Development only: one instance, no
54+
backups.
55+
- `cnpg` — a CloudNativePG `Cluster` (3 instances by default, anti-affinity, optional
56+
Barman Cloud backups to S3 under `database.cnpg.backup`). Requires the CNPG operator.
57+
- `external` — an existing PostgreSQL, configured under `database.external`.
58+
59+
All three modes converge on **one Secret with the keys `host`, `port`, `dbname`, `user`,
60+
`password`** — the shape CNPG generates for its `<cluster>-app` Secret, which the other
61+
two modes hand-write to match. If you bring your own Secret (`existingSecret`), it must
62+
have exactly those keys; for a CNPG cluster managed outside this chart, its `<cluster>-app`
63+
Secret already does.
64+
65+
### RabbitMQ — `rabbitmq.enabled`
66+
67+
`true` deploys the Bitnami RabbitMQ subchart (everything under `rabbitmq:` is passed
68+
straight to it). `false` uses the `externalRabbitmq` block instead — its `existingSecret`
69+
needs the key `rabbitmq-password`.
70+
71+
### Typesense — `typesense.enabled`
72+
73+
`true` deploys Typesense with its own PVC. `false` uses `typesense.external` — its
74+
`existingSecret` needs the key `api-key`.
75+
76+
## Secrets and passwords
77+
78+
- **Nothing secret belongs in a values file.** Every credential has an `existingSecret`
79+
escape hatch; use it (or a secret manager) anywhere that matters.
80+
- **Generated passwords survive upgrades.** When you let the chart generate a password,
81+
it looks up the live Secret on upgrade and keeps the existing value — otherwise every
82+
`helm upgrade` would lock the server out of its own database. This is also why
83+
`helm template` output differs from what `helm install` actually applies.
84+
- **Rotating a Secret restarts the server.** All credentials are hashed into a
85+
`checksum/secrets` pod annotation, so a changed Secret rolls the Deployment.
86+
87+
## Database migrations
88+
89+
Flyway runs as an **init container on the server pod** (preceded by a `wait-for-db`
90+
container), so the server can never start against an unmigrated schema. It is not a Helm
91+
hook on purpose: with a bundled database, a pre-install hook would wait for a database
92+
that Helm has not created yet. The migrations image publishes the same version as the
93+
server, so `flyway.image.tag` stays empty and follows the server version automatically.
94+
95+
## Media libraries and volumes
96+
97+
Two value lists connect the server to your media:
98+
99+
- `server.libraries` — the libraries ister scans; each has a `name` and a `type`
100+
(`SHOW`, `MOVIE`, `MUSIC`, `BOOK`, `PODCAST`, `COMIC`).
101+
- `server.mediaVolumes` — the volumes mounted into the server pod, each backed by exactly
102+
one of `hostPath`, `existingClaim` or `nfs`, and linked to a library via `library`
103+
(omit it for a mount-only volume). **No trailing slash on `mountPath`** — the scanner
104+
skips the tree if there is one.
105+
106+
Two more storage knobs matter:
107+
108+
- `cache` — a PVC for the server's cache directory (transcodes, images). It is
109+
`ReadWriteOnce` by default, which is why `server.replicaCount` must stay at 1 — the
110+
chart refuses to render more replicas unless you switch to `ReadWriteMany`.
111+
- `server.tmp` — scratch space for transcoding. Disable it when `mountPath` falls inside
112+
one of your media volumes, or the dedicated volume shadows that path.
113+
114+
## Ingress
115+
116+
`ingress.enabled: true` publishes the player at `/` and the API at `server.contextPath`
117+
(default `/api`) on `ingress.host`, with TLS via cert-manager when
118+
`ingress.tls.certIssuer` is set. `ingress.wellKnown.enabled` additionally serves
119+
`/.well-known/ister` for client discovery — implemented as an ingress-nginx
120+
server-snippet, which modern ingress-nginx disables by default
121+
(`allow-snippet-annotations`); enable that on the controller first.
122+
123+
## Operations
124+
125+
- **Upgrades**: `helm upgrade ister oci://ghcr.io/ister-app/charts/ister --version <v>`
126+
with your values file. Generated passwords are preserved (see above).
127+
- **Smoke test**: `helm test ister -n ister --logs` runs the shipped connectivity test.
128+
- **Monitoring**: `monitoring.enabled` renders a Prometheus Operator ServiceMonitor for
129+
the server's actuator; in `cnpg` mode, `database.cnpg.podMonitor` covers the database.
130+
- **Uninstall**: the database, cache and Typesense PVCs are kept by default
131+
(`retain: true`) so an uninstall is not destructive; set `retain: false` where you
132+
want them purged with the release.
133+
134+
## Values reference
135+
136+
Every value the chart accepts, with its default. This table is generated from the
137+
`values.yaml` of the release this documentation shipped with; `values.schema.json`
138+
enforces the same contract on every render.
139+
140+
<!-- VALUES:BEGIN (generated by ci/build-docs.sh — do not edit between the markers) -->
141+
<!-- VALUES:END -->

0 commit comments

Comments
 (0)