Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
757e4bea6f | ||
|
|
01f804a43a
|
||
|
|
487eb11b44 | ||
|
|
d18d5710b5 | ||
|
|
bfa72181eb
|
||
|
|
a1e10a3928 |
No files matched your search
@@ -64,52 +64,86 @@ jobs:
|
||||
echo $CHART_VERSION
|
||||
echo "CHART_VERSION=$CHART_VERSION" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ inputs.chart-location }}
|
||||
- name: "Load Helm Registry Credentials"
|
||||
id: load-jk-cr-creds
|
||||
- name: "Load Chart Registry Credentials"
|
||||
id: load-registry-creds
|
||||
uses: 1password/load-secrets-action@v5
|
||||
env:
|
||||
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||
JK_CR_TOKEN: op://GitHub-Actions/jk-registry-access/password
|
||||
JK_CR_USERNAME: op://GitHub-Actions/jk-registry-access/username
|
||||
JK_CR_CHARTS_LOCATION: op://GitHub-Actions/jk-registry-access/charts-location
|
||||
JK_CR_HELM_REGISTRY: op://GitHub-Actions/jk-registry-access/helm-registry
|
||||
# The values are mapped into each step's own env block and read as quoted
|
||||
# shell variables - never interpolated into the command line.
|
||||
REGISTRY_PASSWORD: op://GitHub-Actions/jlab-registry-access/password
|
||||
REGISTRY_USERNAME: op://GitHub-Actions/jlab-registry-access/username
|
||||
REGISTRY_CHARTS_URL: op://GitHub-Actions/jlab-registry-access/charts-url
|
||||
# The target is a classic, index-based Helm registry, not an OCI one, so
|
||||
# there is no "helm registry login" session and no "helm push": the upload
|
||||
# is a single HTTP POST of the packaged .tgz to <charts-url>/api/charts,
|
||||
# authenticated per request. Three properties of this step are load-bearing
|
||||
# and each of them was paid for once already.
|
||||
#
|
||||
# Two separate failures made this necessary. First, load-secrets-action v5
|
||||
# (bumped in #140) no longer exports the loaded values as environment
|
||||
# variables for later steps the way v4 did; only the step outputs carry
|
||||
# them. Every "${{ env.JK_CR_* }}" reference therefore resolved to an empty
|
||||
# string from that bump on, with nothing reporting an error - the loading
|
||||
# step logs "Populating variable" and stays green either way.
|
||||
# 1) The values are mapped into the step's own env block and read as quoted
|
||||
# shell variables - never interpolated into the command line. load-secrets-
|
||||
# action v5 (bumped in #140) no longer exports the loaded values as
|
||||
# environment variables for later steps the way v4 did; only the step
|
||||
# outputs carry them. Every "${{ env.* }}" reference therefore resolved to
|
||||
# an empty string from that bump on, with nothing reporting an error - the
|
||||
# loading step logs "Populating variable" and stays green either way. The
|
||||
# ":?" guards fail loudly and name the op:// field that is missing, and
|
||||
# quoting keeps an empty value from silently shifting arguments.
|
||||
#
|
||||
# Second, an empty value collapsed into a missing argument instead of an
|
||||
# error: with an empty username, "-u" swallowed the following
|
||||
# "--password-stdin", helm stopped reading the password from stdin,
|
||||
# prompted interactively and died on the runner with "inappropriate ioctl
|
||||
# for device" - a terminal error for what was really an unset variable.
|
||||
# 2) The credentials never reach the process list. "curl --user $U:$P"
|
||||
# would put them in argv; "curl --config -" reads its options from stdin
|
||||
# instead, so nothing sensitive appears in any argument. The price of that
|
||||
# config file is its value syntax: an unquoted value ends at the first
|
||||
# space (measured on curl 8.5.0 - "has space" arrived as "has"), so the
|
||||
# step refuses whitespace in the credentials up front rather than letting
|
||||
# a truncated password come back as a puzzling 401. A Gitea access token
|
||||
# is 40 hex characters, so this only ever fires on a mis-entered item.
|
||||
#
|
||||
# The ":?" guards fail loudly and name the value that is missing, quoting
|
||||
# keeps an empty one from shifting arguments, and the token no longer
|
||||
# appears in the process list.
|
||||
- name: "Login to JK-Registry"
|
||||
# 3) "--fail-with-body" is what makes a failed upload fail the job. Unlike
|
||||
# "helm push", curl exits 0 on HTTP 401/403/409 unless asked not to - the
|
||||
# publish would go green while nothing was uploaded. Measured against Gitea
|
||||
# 1.24.7: a 401 without the flag is exit 0, with the flag exit 22, and the
|
||||
# response body is still printed so the log names the cause.
|
||||
#
|
||||
# The file name is built from the chart name and the version read above
|
||||
# instead of a "<chart>-*.tgz" glob: --upload-file takes exactly one file,
|
||||
# and a glob matching two turns the second into curl's URL argument, while
|
||||
# a glob matching none is passed through literally. Both fail obscurely.
|
||||
# "helm package" keeps the "+" of a "1.4.0+up1.9.2" version verbatim in the
|
||||
# file name, so "<name>-<version>.tgz" is an exact match (measured).
|
||||
- name: "Upload Helm Chart to the Chart Registry"
|
||||
env:
|
||||
JK_CR_TOKEN: ${{ steps.load-jk-cr-creds.outputs.JK_CR_TOKEN }}
|
||||
JK_CR_USERNAME: ${{ steps.load-jk-cr-creds.outputs.JK_CR_USERNAME }}
|
||||
JK_CR_HELM_REGISTRY: ${{ steps.load-jk-cr-creds.outputs.JK_CR_HELM_REGISTRY }}
|
||||
REGISTRY_PASSWORD: ${{ steps.load-registry-creds.outputs.REGISTRY_PASSWORD }}
|
||||
REGISTRY_USERNAME: ${{ steps.load-registry-creds.outputs.REGISTRY_USERNAME }}
|
||||
REGISTRY_CHARTS_URL: ${{ steps.load-registry-creds.outputs.REGISTRY_CHARTS_URL }}
|
||||
CHART_NAME: ${{ inputs.chart-name }}
|
||||
CHART_VERSION: ${{ steps.get-chart-version.outputs.CHART_VERSION }}
|
||||
run: |-
|
||||
: "${JK_CR_HELM_REGISTRY:?empty - check op://GitHub-Actions/jk-registry-access/helm-registry}"
|
||||
: "${JK_CR_USERNAME:?empty - check op://GitHub-Actions/jk-registry-access/username}"
|
||||
: "${JK_CR_TOKEN:?empty - check op://GitHub-Actions/jk-registry-access/password}"
|
||||
printf '%s' "$JK_CR_TOKEN" | helm registry login "$JK_CR_HELM_REGISTRY" -u "$JK_CR_USERNAME" --password-stdin
|
||||
- name: "Push Helm Chart to JK-Container Registry"
|
||||
env:
|
||||
JK_CR_CHARTS_LOCATION: ${{ steps.load-jk-cr-creds.outputs.JK_CR_CHARTS_LOCATION }}
|
||||
run: |-
|
||||
: "${JK_CR_CHARTS_LOCATION:?empty - check op://GitHub-Actions/jk-registry-access/charts-location}"
|
||||
helm push "${{ inputs.chart-name }}"-*.tgz "$JK_CR_CHARTS_LOCATION"
|
||||
: "${REGISTRY_CHARTS_URL:?empty - check op://GitHub-Actions/jlab-registry-access/charts-url}"
|
||||
: "${REGISTRY_USERNAME:?empty - check op://GitHub-Actions/jlab-registry-access/username}"
|
||||
: "${REGISTRY_PASSWORD:?empty - check op://GitHub-Actions/jlab-registry-access/password}"
|
||||
: "${CHART_NAME:?empty - the chart-name input did not arrive}"
|
||||
: "${CHART_VERSION:?empty - could not read version: from ${CHART_NAME}/Chart.yaml}"
|
||||
# 1Password masks the full URL, but a curl connection error prints only
|
||||
# "<host>:<port>", which that mask would not cover. Mask the host too.
|
||||
CHARTS_HOST="${REGISTRY_CHARTS_URL#*://}"
|
||||
echo "::add-mask::${CHARTS_HOST%%/*}"
|
||||
case "${REGISTRY_USERNAME}${REGISTRY_PASSWORD}" in
|
||||
*[[:space:]]*)
|
||||
echo "::error::username/password in op://GitHub-Actions/jlab-registry-access must not contain whitespace - curl --config truncates an unquoted value at the first space"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
CHART_PACKAGE="${CHART_NAME}-${CHART_VERSION}.tgz"
|
||||
test -f "$CHART_PACKAGE" || {
|
||||
echo "::error::${CHART_PACKAGE} does not exist - helm package produced: $(ls -1 ./*.tgz 2>&1)"
|
||||
exit 1
|
||||
}
|
||||
echo "uploading ${CHART_PACKAGE}"
|
||||
printf 'user = %s:%s\nurl = %s\n' \
|
||||
"$REGISTRY_USERNAME" "$REGISTRY_PASSWORD" "${REGISTRY_CHARTS_URL%/}/api/charts" |
|
||||
curl --config - \
|
||||
--fail-with-body \
|
||||
--silent --show-error \
|
||||
--request POST \
|
||||
--upload-file "$CHART_PACKAGE" \
|
||||
--write-out '\nHTTP %{http_code}\n'
|
||||
working-directory: ${{ inputs.chart-location }}
|
||||
- name: "Logout from JK-Registry"
|
||||
env:
|
||||
JK_CR_HELM_REGISTRY: ${{ steps.load-jk-cr-creds.outputs.JK_CR_HELM_REGISTRY }}
|
||||
run: helm registry logout "$JK_CR_HELM_REGISTRY"
|
||||
@@ -26,7 +26,7 @@ Additional rules for all commits, PRs, issues, and comments:
|
||||
|
||||
## What this repo is
|
||||
|
||||
A collection of self-made Helm charts, one directory per chart, published to a private OCI registry via GitHub Actions. Validation happens on three levels: locally with Helm, in the PR pipeline (`validate-charts.yml`: strict lint, package, version-bump check), and in the PR install-test stage (k3d, fixtures under `ci/` — see README):
|
||||
A collection of self-made Helm charts, one directory per chart, published to a private, classic (index-based) Helm registry via GitHub Actions — an HTTP `POST` of the packaged `.tgz`, not an OCI `helm push`; the target URL and its credentials live in 1Password and never in this repository. Validation happens on three levels: locally with Helm, in the PR pipeline (`validate-charts.yml`: strict lint, package, version-bump check), and in the PR install-test stage (k3d, fixtures under `ci/` — see README):
|
||||
|
||||
```bash
|
||||
helm lint --strict <chart-name>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# helm-charts
|
||||
|
||||
Collection of some self-made helm-charts. Each top-level directory is a standalone Helm chart that gets packaged and pushed to a private OCI registry (JK-Registry) via GitHub Actions.
|
||||
Collection of some self-made helm-charts. Each top-level directory is a standalone Helm chart that gets packaged and uploaded to a private, classic (index-based) Helm registry via GitHub Actions.
|
||||
|
||||
## Charts
|
||||
|
||||
@@ -15,7 +15,7 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
| `homeassistant` | Home Assistant | StatefulSet; the only chart carrying Helm tests (`templates/tests/`) |
|
||||
| `matrix-synapse` | Matrix Synapse | StatefulSet plus an nginx delegation Deployment/Service |
|
||||
| `minecraft-router` | itzg/mc-router | Deployment + routing ConfigMap + NodePort Service (TCP only, no ingress); `mappings` is a required value |
|
||||
| `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). Published once per Java line — the image tag comes from `.Chart.AppVersion`, so the Java line is chosen by picking the chart version, and `minecraft.version` ships a default matching that line. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: <release>-java8` / `-java17` / `-java21`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart |
|
||||
| `minecraft-server` | itzg/minecraft-server | StatefulSet + PVC + Service + init-script ConfigMap (no ingress); service type is a value (`ClusterIP` by default, routed via `minecraft-router`). Published once per Java line — the image tag comes from `.Chart.AppVersion`, so the Java line is chosen by picking the chart version, and `minecraft.version` ships a default matching that line. Since chart 2.0.0 the tag is an **immutable dated upstream release** (`appVersion: <release>-java8` / `-java17` / `-java21` / `-java25`, e.g. `2026.8.2-java8`) instead of the continuously rebuilt rolling line tag. Pilot for the strict `values.schema.json` (#68), which is now binding for every chart |
|
||||
| `outline` | Outline | StatefulSet, separate DB/Redis secrets |
|
||||
| `paperless-ngx` | Paperless-ngx | Largest chart: consume CronJob, SMB-connector PV/PVC, pre/post-consumption scripts ConfigMap |
|
||||
| `patchmon` | PatchMon | Two Deployments (frontend + backend), OIDC secret |
|
||||
@@ -41,7 +41,12 @@ Collection of some self-made helm-charts. Each top-level directory is a standalo
|
||||
## CI/CD (GitHub Actions)
|
||||
|
||||
- `publish-<chart>.yml` — one thin workflow per chart; triggers on pushes to `main` touching `<chart>/**` (or manually) and calls the reusable workflow below. **A push to a chart directory on `main` publishes that chart**, so the chart version must be bumped in the same change.
|
||||
- `publish-helm-chart.yml` — reusable workflow: `helm package`, then `helm push` to the private JK-Registry. Registry credentials come from 1Password Connect (`OP_CONNECT_TOKEN` secret, items under `op://GitHub-Actions/`). The loaded values are read via **`steps.<id>.outputs.*`**, mapped into each step's own `env:` block and used as quoted shell variables — never interpolated into the command line. `load-secrets-action` **v5 no longer exports them as environment variables** for later steps the way v4 did, so after the v5 bump every `${{ env.JK_CR_* }}` reference resolved to an empty string while the loading step still logged `Populating variable` and stayed green. Each value is additionally guarded with `:?`, because an empty one used to collapse into a missing argument rather than an error: `-u` consumed the following `--password-stdin`, helm fell back to an interactive prompt and the job died with `inappropriate ioctl for device` — a terminal error for what was really an unset variable.
|
||||
- `publish-helm-chart.yml` — reusable workflow: `helm package`, then a single HTTP `POST` of the packaged `.tgz` to the private chart registry. The target is a **classic, index-based** Helm registry (Gitea's `/api/charts` endpoint), not an OCI one: `helm push` speaks only OCI and is not used, there is no `helm registry login` session, and every request carries its own Basic auth. Credentials **and the target URL** come from 1Password Connect (`OP_CONNECT_TOKEN` secret, item `op://GitHub-Actions/jlab-registry-access/` with `username`, `password`, `charts-url`); the workflow appends `/api/charts` to `charts-url`. No registry address ever appears in this repository. The loaded values are read via **`steps.<id>.outputs.*`**, mapped into the step's own `env:` block and used as quoted shell variables — never interpolated into the command line. `load-secrets-action` **v5 no longer exports them as environment variables** for later steps the way v4 did, so after the v5 bump every `${{ env.* }}` reference resolved to an empty string while the loading step still logged `Populating variable` and stayed green. Each value is additionally guarded with `:?`, because an empty one used to collapse into a missing argument rather than an error. Three properties of the upload step are load-bearing:
|
||||
- **Credentials stay out of the process list.** `curl --user "$U:$P"` would put them in `argv`; the step uses `curl --config -` and pipes the options in on stdin instead.
|
||||
- **A failed upload fails the job.** Unlike `helm push`, `curl` exits **0** on HTTP 401/403/409 — a publish would go green with nothing uploaded. `--fail-with-body` turns those into a non-zero exit and still prints the response body, so the log names the cause.
|
||||
- **The file name is built, not globbed.** `<chart>-<version>.tgz` is assembled from the chart name and the version already read from `Chart.yaml`; `--upload-file` takes exactly one file, and a glob matching two silently turns the second into curl's URL argument. `helm package` keeps the `+` of a `1.4.0+up1.9.2` version verbatim in the file name, so the built name is an exact match.
|
||||
|
||||
Note: the registry **accepts a re-upload of an already published version** (HTTP 201) and replaces the stored artifact, so a republish of an existing version is still silent — see #160.
|
||||
- `change-app-version.yml` — manually dispatched app-version bump; the chart must be listed in its `chart-name` choice list.
|
||||
- `ci/helm-dependencies.sh <chart-dir>` — not a workflow but the step they all share: fetches a chart's declared dependencies into `charts/`, and returns silently for the charts that declare none (all but one). It runs in **five** places, because every helm command this repo builds on exits 1 while a declared dependency is missing: before lint and package in `validate-charts.yml`, before package in `publish-helm-chart.yml`, and inside `ci/run-install-test.sh`, `ci/run-quantity-render-test.sh` and `ci/run-trivy-config.sh`. See "Chart dependencies" in the style guidelines for what that costs.
|
||||
- `validate-charts.yml` — PR pipeline: detects which charts a PR touches and, for exactly those, runs `helm lint --strict`, `helm package`, and a chart-version bump check (`template-chart` is exempt from the version check). PRs without chart changes skip the build matrix. After the build stage, an **install-test** job (same changed-charts matrix) installs each changed chart into an ephemeral [k3d](https://k3d.io) cluster on the runner and requires every workload to become Ready — see below. Both matrices follow the changed chart directories, so a PR that only changes the harness under `ci/` exercises nothing by itself; the Trivy scan has a fixed-sample fallback for that case, the install-test has none — bump the affected chart's patch version to pull it into the matrix. The **resource-quantity render test** is the exception to that rule: it runs over every chart on every PR — see below.
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
# minecraft.version is deliberately NOT set here any more. Since chart 1.0.0 the
|
||||
# chart ships a pinned default that belongs to the Java line it publishes (the
|
||||
# image tag comes from .Chart.AppVersion, and Java 8 runs Minecraft only up to
|
||||
# 1.16.5, 1.18-1.20.4 need Java 17, 1.20.5+ need Java 21). Leaving it unset
|
||||
# means the install-test exercises exactly the default a user would get, so a
|
||||
# Java line published with a mismatched default version fails CI instead of
|
||||
# passing on a CI-only override.
|
||||
# 1.16.5, 1.18-1.20.4 need Java 17, 1.20.5-1.21.11 need Java 21 and 26.1+ needs
|
||||
# Java 25). Leaving it unset means the install-test exercises exactly the
|
||||
# default a user would get, so a Java line published with a mismatched default
|
||||
# version fails CI instead of passing on a CI-only override.
|
||||
#
|
||||
# server.service.type is not set either: the default is ClusterIP, which is what
|
||||
# all three real deployments run (routing is done by the static mappings of the
|
||||
|
||||
@@ -6,15 +6,16 @@ icon: https://avatars.githubusercontent.com/u/988985
|
||||
type: application
|
||||
|
||||
# The Java line of this chart version is chosen through appVersion, which is
|
||||
# the image tag: the chart is published once per line (java8 / java17 / java21)
|
||||
# and minecraft.version in values.yaml must match the line published here.
|
||||
# the image tag: the chart is published once per line (java8 / java17 / java21 /
|
||||
# java25) and minecraft.version in values.yaml must match the line published
|
||||
# here.
|
||||
# Since chart 2.0.0 the tag is a DATED upstream release ("<release>-java<N>")
|
||||
# instead of the rolling line tag ("java<N>"): the rolling tags are rebuilt
|
||||
# continuously, so a pod restart could pull a different image than the last one
|
||||
# without anything being recorded anywhere. The dated tags are immutable, so the
|
||||
# image is now pinned by the chart version like every other chart in this repo.
|
||||
version: 4.1.0+up2026.8.2.java21
|
||||
appVersion: "2026.8.2-java21"
|
||||
version: 4.1.0+up2026.8.2.java25
|
||||
appVersion: "2026.8.2-java25"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
@@ -81,11 +81,13 @@ THIS CHART IS PUBLISHED ONCE PER JAVA LINE — THIS IS {{ $line | default "an un
|
||||
({{ .Chart.AppVersion }}), not from a value. This build runs
|
||||
minecraft.version {{ .Values.minecraft.version }}.
|
||||
|
||||
Java 8 runs Minecraft up to 1.16.5, 1.18-1.20.4 need Java 17 and 1.20.5+ need
|
||||
Java 21. Nothing in this chart checks that minecraft.version fits the line:
|
||||
a mismatch renders and installs without a word, and only the container fails
|
||||
afterwards — CrashLoopBackOff once the startup budget{{ if $budget }} ({{ $budget }}){{ end }} is spent, with
|
||||
the reason in the log and nowhere else:
|
||||
Java 8 runs Minecraft up to 1.16.5, 1.18-1.20.4 need Java 17, 1.20.5-1.21.11
|
||||
need Java 21 and 26.1 and later need Java 25 (read off Mojang's version
|
||||
manifest, javaVersion.majorVersion per release). Nothing in this chart checks
|
||||
that minecraft.version fits the line: a mismatch renders and installs without
|
||||
a word, and only the container fails afterwards — CrashLoopBackOff once the
|
||||
startup budget{{ if $budget }} ({{ $budget }}){{ end }} is spent, with the reason in the log and
|
||||
nowhere else:
|
||||
|
||||
kubectl -n {{ .Release.Namespace }} logs {{ $pod }}
|
||||
|
||||
|
||||
@@ -225,9 +225,13 @@ minecraft:
|
||||
# drift to a new release on every restart.
|
||||
# This value must match the Java line of the chart version: the image tag
|
||||
# comes from .Chart.AppVersion, so the JVM is fixed per published chart
|
||||
# version. Java 8 runs Minecraft up to 1.16.5, 1.18-1.20.4 need Java 17 and
|
||||
# 1.20.5+ need Java 21. This chart publishes appVersion "2026.8.2-java21".
|
||||
version: "1.21.1"
|
||||
# version. Java 8 runs Minecraft up to 1.16.5, 1.18-1.20.4 need Java 17,
|
||||
# 1.20.5-1.21.11 need Java 21 and 26.1 and later need Java 25. The two upper
|
||||
# boundaries are read off Mojang's version manifest (piston-meta, field
|
||||
# javaVersion.majorVersion per release), not estimated: 1.21.11 (2025-12-09)
|
||||
# is the last release asking for 21, and every release from 26.1 (2026-03-24)
|
||||
# on asks for 25. This chart publishes appVersion "2026.8.2-java25".
|
||||
version: "26.2"
|
||||
|
||||
# curseforge, ftb and curseforgeServerpack are mutually exclusive - enabling
|
||||
# more than one is rejected by the chart (before 1.0.0 it silently rendered a
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ type: application
|
||||
# (2026.8.22-9fea41204) and "latest" moves daily. appVersion is therefore the
|
||||
# exact tag, and the +up suffix normalizes the dash to a dot as the versioning
|
||||
# scheme requires - the same shape rss-bridge and minecraft-server use.
|
||||
version: 1.0.1+up2026.8.22.9fea41204
|
||||
appVersion: "2026.8.22-9fea41204"
|
||||
version: 1.0.2+up2026.8.29.d226b78bc
|
||||
appVersion: "2026.8.29-d226b78bc"
|
||||
|
||||
maintainers:
|
||||
- name: jklein
|
||||
Reference in new issue
Block a user