From 58425411b65c5ef83ebde2cd01317e2b7f7fee0c Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 16:52:36 +0100 Subject: [PATCH 1/9] Add Helm chart planning bundle --- .../2026-06-17_helm-chart/overview.md | 67 ++++++++++ .../2026-06-17_helm-chart/plan.yaml | 122 ++++++++++++++++++ .../2026-06-17_helm-chart/research.md | 65 ++++++++++ 3 files changed, 254 insertions(+) create mode 100644 .project_planning/2026-06-17_helm-chart/overview.md create mode 100644 .project_planning/2026-06-17_helm-chart/plan.yaml create mode 100644 .project_planning/2026-06-17_helm-chart/research.md diff --git a/.project_planning/2026-06-17_helm-chart/overview.md b/.project_planning/2026-06-17_helm-chart/overview.md new file mode 100644 index 0000000..59e7a05 --- /dev/null +++ b/.project_planning/2026-06-17_helm-chart/overview.md @@ -0,0 +1,67 @@ +## Request + +Plan issue https://github.com/phpdocker-io/readability-js-server/issues/53: add a Helm chart for Readability JS Server, cover deployment/service/optional ingress/optional autoscaling/optional disruption budget, support Artifact Hub discoverability, and add a health endpoint that is used by Docker Compose and Kubernetes probes. + +## Overview + +Add a conventional, production-usable Helm chart under the repository with chart metadata, configurable values, reusable template helpers, and templates for the Kubernetes resources needed to run the existing container. The chart should default to `phpdockerio/readability-js-server` on app version `1.8.0`, expose port `3000`, use environment variables already supported by `src/config.js`, and provide optional ingress, HPA, and PDB resources behind explicit values. + +The chart values should be configurable in the same broad way users expect from established charts: security contexts, resource requests and limits, ingress class names, ingress annotations and TLS, HPA behavior and scaling knobs, PDB settings, pod labels/annotations, service account settings, image pull secrets, node selectors, tolerations, affinity, topology spread constraints, priority class, and probe tuning. Defaults should remain safe and minimal, but the templates should not force users to fork the chart for common Kubernetes policy and scheduling requirements. + +Add `GET /healthz` to the Express app as a lightweight operational endpoint. It must not affect `POST /`, the fixed success response shape, the error envelope, or the existing `GET /` 400 POST guidance. Use `/healthz` in Docker Compose healthchecks and Helm liveness/readiness probes. + +Publish chart packages through a standard Helm repository hosted on GitHub Pages using `helm/chart-releaser-action`. Artifact Hub should reference that repository URL; charts are not uploaded directly to Artifact Hub. + +## Key Decisions + +- Use separate chart and app versions. Start the chart at `0.1.0` and set `appVersion` plus the default image tag to the current `package.json` version, `1.8.0`. This follows common Helm practice and allows chart-only fixes without forcing app releases. +- Use `GET /healthz` for health checks. This keeps `GET /` behavior unchanged while giving Compose and Kubernetes a stable endpoint for operational readiness. +- Publish via GitHub Pages plus `helm/chart-releaser-action`. This is the lowest-friction path for a GitHub-hosted project and gives Artifact Hub a normal Helm repository to index. +- Keep app release publishing tag-driven. Docker image publishing remains tied to `vX.Y.Z` tags; chart publishing can run when chart files change on `master`. +- Add Helm verification commands to repo tooling. The executor should add chart lint/render checks alongside existing `pnpm lint` and `pnpm test` expectations. + +## Tradeoffs + +- Lockstep chart and app versions were rejected because chart-only fixes would require app-version-like releases. Independent chart versions are slightly more documentation-heavy but more maintainable. +- OCI chart publishing was deferred because it adds registry layout and Artifact Hub metadata complexity that is not needed for one public chart. +- GitHub Releases-only chart publishing was rejected because Artifact Hub needs an indexable Helm repository or OCI registry, not standalone release assets. +- Adding a health endpoint increases HTTP surface area, but it is operationally useful and safer than using `GET /` or a parse request as a probe. +- HTTP probes are preferred over TCP-only probes because they verify the Express route stack is serving requests, not just that the socket is open. + +## Scope Boundaries + +In scope: + +- Add `GET /healthz` with tests and documentation. +- Add a Compose healthcheck using `/healthz`. +- Add a Helm chart with Deployment, Service, optional Ingress, optional HPA, optional PDB, configurable image, service account, pod labels/annotations, resources, security contexts, probes, supported environment variables, ingress class/annotations/TLS, image pull secrets, pod scheduling controls, topology spread constraints, priority class, and HPA/PDB tuning knobs. +- Add chart documentation and release/versioning guidance. +- Add CI or Makefile verification for Helm linting/rendering. +- Add chart publishing workflow support and Artifact Hub metadata/documentation. + +Out of scope: + +- Changing `POST /` behavior or response shape. +- Changing `GET /` 400 guidance. +- Adding authentication, caching, persistence, distributed rate limiting, or a metrics endpoint. +- Upgrading Node, pnpm, `@mozilla/readability`, jsdom, DOMPurify, or runtime dependency policy. +- Publishing directly to Artifact Hub, because Artifact Hub indexes an external Helm repository or OCI registry. + +## Verification Strategy + +- `pnpm lint`: cheap/medium. Required by repo instructions for docs, API, config, dependency, and general code changes. +- `pnpm test`: cheap/medium. Required because the health endpoint changes HTTP surface area and the chart work touches deployment docs/config. +- New health endpoint tests: cheap. Should verify `GET /healthz` status/body and that `GET /` still returns the existing 400 guidance. +- Helm lint: cheap/medium. Add or document a command such as `helm lint charts/readability-js-server`. +- Helm template/render check: cheap/medium. Add or document a command such as `helm template readability-js-server charts/readability-js-server` to catch template errors with defaults. +- Optional chart packaging check: cheap/medium. Useful if release workflow packages charts, for example `helm package charts/readability-js-server --destination /tmp`. +- Docker build: medium/expensive. Not required for chart-only template changes, but relevant if the health endpoint change should be validated in the container. +- Memory soak: expensive. Not required; the health endpoint and Helm chart should not affect allocation behavior under article parsing load. + +## Decision Log + +- 2026-06-17: User requested planning for issue 53. +- 2026-06-17: User added a health endpoint to scope and requested it be used by Compose and any other relevant deployment surfaces. +- 2026-06-17: Research confirmed Artifact Hub indexes external Helm repositories or OCI registries; it does not require direct artifact upload. +- 2026-06-17: Research confirmed major charts commonly keep chart `version` separate from `appVersion`. +- 2026-06-17: Planning defaults selected: chart `0.1.0`, app `1.8.0`, health path `/healthz`, GitHub Pages chart repository via chart-releaser. diff --git a/.project_planning/2026-06-17_helm-chart/plan.yaml b/.project_planning/2026-06-17_helm-chart/plan.yaml new file mode 100644 index 0000000..2a891e7 --- /dev/null +++ b/.project_planning/2026-06-17_helm-chart/plan.yaml @@ -0,0 +1,122 @@ +steps: + - id: step-1 + title: Add health endpoint and tests + scope: Add a lightweight `GET /healthz` endpoint to the Express app and cover it with tests while preserving the existing `POST /` API and `GET /` 400 guidance. + files: + - src/app.js + - test/validation.test.js + - README.md + constraints: + - Do not change the success response field set from `src/response.js`. + - Do not change the error envelope or status-code contract. + - Keep `GET /` returning the existing 400 guidance. + - Keep the endpoint lightweight; it must not perform upstream fetches or parse work. + acceptance: + - `GET /healthz` returns a successful JSON response suitable for health probes. + - Existing `GET /` guidance still returns 400. + - Health endpoint behavior is documented. + verification: + - pnpm lint + - pnpm test + + - id: step-2 + title: Add Docker Compose healthcheck + scope: Wire the new health endpoint into the example Compose deployment. + files: + - examples/compose.yaml + - README.md + constraints: + - Use the same health path selected for Kubernetes probes. + - Do not require additional services or persistent state. + - Ensure the command works with the published container image contents. + acceptance: + - Compose example includes a healthcheck for the service. + - Documentation mentions the healthcheck and endpoint. + verification: + - pnpm lint + - pnpm test + + - id: step-3 + title: Create production-usable Helm chart + scope: Add a Helm chart for the service with conventional metadata, values, helpers, and Kubernetes manifests for Deployment, Service, optional Ingress, optional HPA, and optional PDB. + files: + - charts/readability-js-server/Chart.yaml + - charts/readability-js-server/values.yaml + - charts/readability-js-server/templates/_helpers.tpl + - charts/readability-js-server/templates/deployment.yaml + - charts/readability-js-server/templates/service.yaml + - charts/readability-js-server/templates/ingress.yaml + - charts/readability-js-server/templates/hpa.yaml + - charts/readability-js-server/templates/pdb.yaml + - charts/readability-js-server/templates/serviceaccount.yaml + - charts/readability-js-server/templates/NOTES.txt + - charts/readability-js-server/README.md + constraints: + - Start chart `version` at `0.1.0`. + - Set `appVersion` and default image tag to the current package version, `1.8.0`. + - Default image repository must be `phpdockerio/readability-js-server`. + - Default container port must match the service default, `3000`. + - Use `/healthz` for readiness and liveness probes. + - Keep optional resources disabled unless their values enable them. + - Expose mature chart configuration: security contexts, resource requests and limits, ingress class/annotations/TLS, HPA behavior and scaling knobs, PDB settings, pod labels/annotations, service account settings, image pull secrets, node selectors, tolerations, affinity, topology spread constraints, priority class, and probe tuning. + - Support environment variables already accepted by `src/config.js`. + acceptance: + - Chart renders successfully with default values. + - Default manifests deploy one replica behind a ClusterIP Service. + - Ingress, HPA, and PDB templates render only when enabled. + - Users can configure common Kubernetes policy and scheduling requirements without forking the chart. + - Chart README documents installation, values, versioning policy, and Artifact Hub expectations. + verification: + - helm lint charts/readability-js-server + - helm template readability-js-server charts/readability-js-server + - pnpm lint + - pnpm test + + - id: step-4 + title: Add Helm verification tooling + scope: Add Makefile and CI support for chart linting and rendering so Helm regressions are checked consistently. + files: + - Makefile + - .github/workflows/build-publish.yaml + - README.md + - AGENTS.md + constraints: + - Preserve existing Node 24 and pnpm 11.7.0 policy. + - Keep existing `pnpm lint` and `pnpm test` checks. + - Use official or widely adopted Helm setup actions in CI. + - Do not require a Kubernetes cluster for verification. + acceptance: + - Local chart verification commands are documented and available through Makefile. + - CI checks Helm lint/template in addition to existing lint/test. + - Repo instructions mention the new Helm verification commands. + verification: + - make helm-lint + - make helm-template + - pnpm lint + - pnpm test + + - id: step-5 + title: Add chart publishing and Artifact Hub metadata + scope: Add GitHub Pages/chart-releaser publishing support and metadata/documentation needed for Artifact Hub indexing. + files: + - .github/workflows/chart-release.yaml + - charts/readability-js-server/Chart.yaml + - charts/readability-js-server/README.md + - artifacthub-repo.yml + - README.md + constraints: + - Artifact Hub should reference the external Helm repository; do not attempt to upload charts directly to Artifact Hub. + - Keep Docker image publishing tag-driven through the existing release workflow. + - Chart publishing should run from chart changes on `master` or an explicit manual trigger. + - Do not introduce OCI publishing unless the maintainer changes the publishing target. + acceptance: + - Chart release workflow packages changed charts and publishes an indexable Helm repository via GitHub Pages. + - README explains how users add the Helm repo and install the chart. + - Artifact Hub metadata is present enough to support repository registration/ownership. + - Release/versioning documentation explains when to bump chart `version` versus `appVersion`. + verification: + - helm lint charts/readability-js-server + - helm template readability-js-server charts/readability-js-server + - helm package charts/readability-js-server --destination /tmp/readability-js-server-chart + - pnpm lint + - pnpm test diff --git a/.project_planning/2026-06-17_helm-chart/research.md b/.project_planning/2026-06-17_helm-chart/research.md new file mode 100644 index 0000000..80096db --- /dev/null +++ b/.project_planning/2026-06-17_helm-chart/research.md @@ -0,0 +1,65 @@ +## Question + +Planning issue https://github.com/phpdocker-io/readability-js-server/issues/53: add a Helm chart, publish it for Artifact Hub discovery, and include a new health endpoint for Compose/chart probes. + +## Findings + +Helm has two separate version concepts. `Chart.yaml version` is the chart package version, must be SemVer, and is used by Helm packaging/repository tooling. `appVersion` is informational application version metadata and is explicitly unrelated to chart version calculations: https://helm.sh/docs/topics/charts/ + +Established charts mostly treat them as independent, even if both move during app releases: + +- Bitnami nginx: chart `version: 22.1.1`, `appVersion: 1.29.1`: https://github.com/bitnami/charts/blob/main/bitnami/nginx/Chart.yaml +- ingress-nginx: chart `version: 4.15.1`, `appVersion: 1.15.1`: https://github.com/kubernetes/ingress-nginx/blob/main/charts/ingress-nginx/Chart.yaml +- kube-prometheus-stack: chart `version: 86.2.3`, `appVersion: v0.91.0`: https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/Chart.yaml +- Grafana: chart `version: 10.5.14`, `appVersion: 12.3.1`: https://github.com/grafana/helm-charts/blob/main/charts/grafana/Chart.yaml + +Artifact Hub indexes Helm chart repositories and OCI Helm charts, not arbitrary GitHub Releases by themselves. Most metadata comes from `Chart.yaml`, `README`, and `LICENSE`; optional Artifact Hub annotations and `artifacthub-repo.yml` improve presentation, ownership claim, and verified publisher support: https://artifacthub.io/docs/topics/repositories/helm-charts/ and https://artifacthub.io/docs/topics/annotations/helm/ + +Publishing options: + +- GitHub Pages + `helm/chart-releaser-action`: conventional for GitHub-hosted projects; creates chart GitHub Releases, updates `index.yaml`, and serves a Helm repo from Pages. Artifact Hub can index that repo URL. Docs: https://helm.sh/docs/howto/chart_releaser_action/ +- OCI registry: supported by Helm and Artifact Hub. Helm OCI chart refs require chart name as basename and chart SemVer as tag; Artifact Hub requires one OCI repository per chart and special handling for `artifacthub-repo.yml`. Docs: https://helm.sh/docs/topics/registries/ and https://artifacthub.io/docs/topics/repositories/helm-charts/ +- GitHub Releases-only: insufficient for Artifact Hub discoverability unless paired with an `index.yaml` served as a Helm repo. This is what chart-releaser adds. +- Manual static Helm repo: possible, but chart-releaser automates the boring parts and matches GitHub Pages well. + +## Implications + +For this repo, the pragmatic path is to use a normal HTTP Helm repository on GitHub Pages, published by `helm/chart-releaser-action`, and register that repo URL in Artifact Hub. This fits the existing GitHub Release workflow without requiring an OCI registry decision or ORAS metadata flow. + +Use `package.json` as the app source of truth and set chart `appVersion` plus default image tag from that app version. Keep chart `version` as a chart package version, not necessarily identical to app version. + +Recommended versioning policy: + +- Initial chart could be `0.1.0` or `1.8.0`; `0.1.0` is cleaner if treating the chart as a new artifact. +- On every app release where the chart's default image tag changes, bump chart `version` and `appVersion`. +- For chart-only fixes, bump only chart `version`; leave `appVersion` unchanged. +- Avoid lockstep-only chart versioning unless you are comfortable forcing app releases for chart-only fixes. + +The health endpoint should be planned as part of the app contract before probes land. Current rules say `POST /` is the API and `GET /` must keep returning 400 guidance, so a separate endpoint such as `GET /healthz` or `GET /health` is the least disruptive option. It needs tests because it changes HTTP surface area and will be consumed by Docker Compose and Kubernetes probes. + +## Risks and Uncertainties + +Lockstep chart/app versions are simpler for humans but awkward for chart-only fixes. + +Independent chart versions are standard Helm practice but require release documentation so maintainers know when to bump `version` versus `appVersion`. + +OCI publishing is viable, but adds registry naming, auth, Artifact Hub metadata, and one-repo-per-chart considerations. It is probably unnecessary for this small repo unless the maintainer specifically wants registry-only distribution. + +GitHub Pages introduces a new repo setting/branch dependency. Artifact Hub indexing depends on a stable public `index.yaml`. + +## Sources + +- Helm Chart.yaml version and appVersion docs: https://helm.sh/docs/topics/charts/ +- Helm chart repository guide: https://helm.sh/docs/topics/chart_repository/ +- Helm chart-releaser guide: https://helm.sh/docs/howto/chart_releaser_action/ +- Helm OCI registry docs: https://helm.sh/docs/topics/registries/ +- Artifact Hub Helm repository docs: https://artifacthub.io/docs/topics/repositories/helm-charts/ +- Artifact Hub repository ownership/verified publisher docs: https://artifacthub.io/docs/topics/repositories/ +- Artifact Hub Helm annotations: https://artifacthub.io/docs/topics/annotations/helm/ +- Issue #53: https://github.com/phpdocker-io/readability-js-server/issues/53 + +## Open Questions + +- Should the chart be versioned from `0.1.0` as a new artifact, or start at the current app version `1.8.0` for simpler release alignment? +- Preferred health path: `/healthz`, `/health`, or `/readyz`? +- Should chart publishing run only on app tags, or also on chart changes merged to `main`? From 5f9c9e0213b9b5361aa3b482045278d7849a7f35 Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 16:57:34 +0100 Subject: [PATCH 2/9] Add healthz probe endpoint --- README.md | 4 ++-- src/app.js | 6 ++++++ test/validation.test.js | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0fb5b04..e027676 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ At the time of this uplift, `@mozilla/readability@0.6.0` was already the latest - Sanitization: DOMPurify 3 - Deployment image: `node:24-alpine` -The container runs as a non-root user and the service exposes a single `POST /` endpoint. +The container runs as a non-root user and the service exposes `POST /` plus a lightweight `GET /healthz` probe endpoint. ## API @@ -193,7 +193,7 @@ This service is still an untrusted content fetcher. Do not relax the defaults wi ## Limits -- Single endpoint only: `POST /` +- Public API endpoints: `POST /` and `GET /healthz` - No authentication - No cache - No persistence diff --git a/src/app.js b/src/app.js index cbc6c9f..126ea2a 100644 --- a/src/app.js +++ b/src/app.js @@ -436,6 +436,12 @@ function createApp(configInput, loggerInput) { const logger = loggerInput || createLogger(); const app = express(); + app.get("/healthz", (_req, res) => { + res.status(200).json({ + ok: true, + }); + }); + app.use(express.json({ limit: config.requestBodyLimit })); app.use(createConcurrencyGate(config.maxConcurrentRequests)); diff --git a/test/validation.test.js b/test/validation.test.js index 8167402..34b2b7b 100644 --- a/test/validation.test.js +++ b/test/validation.test.js @@ -13,6 +13,23 @@ test("GET / returns guidance for POST JSON", async () => { }); }); +test("GET /healthz returns a lightweight probe response", async (t) => { + const fetchMock = t.mock.method(global, "fetch", async () => { + throw new Error("fetch should not be called for health checks"); + }); + + t.after(() => { + fetchMock.mock.restore(); + }); + + const response = await supertest(createTestApp()).get("/healthz").expect(200); + + assert.deepEqual(response.body, { + ok: true, + }); + assert.equal(fetchMock.mock.callCount(), 0); +}); + test("POST / rejects missing and empty url values", async () => { const app = createTestApp(); From 125044ca9006cfb0dc9b13eead6a6c59451ffd16 Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 16:59:17 +0100 Subject: [PATCH 3/9] Add compose healthcheck for /healthz --- README.md | 4 ++-- examples/compose.yaml | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e027676..f2ae603 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ At the time of this uplift, `@mozilla/readability@0.6.0` was already the latest - Sanitization: DOMPurify 3 - Deployment image: `node:24-alpine` -The container runs as a non-root user and the service exposes `POST /` plus a lightweight `GET /healthz` probe endpoint. +The container runs as a non-root user and the service exposes `POST /` plus a lightweight `GET /healthz` probe endpoint. The Compose example uses that same `/healthz` path for its container healthcheck. ## API @@ -176,7 +176,7 @@ The image is based on `node:24-alpine`, installs production dependencies only, a CI on pull requests and pushes to `master` runs lint and tests only. Container publishing happens from the tag-triggered release workflow. -For Docker Compose setup, see [`examples/compose.yaml`](examples/compose.yaml). +For Docker Compose setup, see [`examples/compose.yaml`](examples/compose.yaml). That example publishes port `3000` and checks `GET /healthz` from inside the container using the `node` runtime that ships in the published image. ## Security posture diff --git a/examples/compose.yaml b/examples/compose.yaml index 7b9a52d..5f7d1cb 100644 --- a/examples/compose.yaml +++ b/examples/compose.yaml @@ -3,6 +3,18 @@ services: image: phpdockerio/readability-js-server:latest ports: - "3000:3000" + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3000/healthz').then((response) => process.exit(response.ok ? 0 : 1)).catch(() => process.exit(1));", + ] + interval: 10s + timeout: 3s + retries: 3 + start_period: 5s # Uncomment and set any of the following environment variables to override defaults: # environment: # PORT: 3000 From 97cbf0b9774d0467bb03661c2b7e136302abc124 Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 17:06:27 +0100 Subject: [PATCH 4/9] Add production Helm chart --- charts/readability-js-server/Chart.yaml | 17 +++ charts/readability-js-server/README.md | 84 +++++++++++++ .../readability-js-server/templates/NOTES.txt | 18 +++ .../templates/_helpers.tpl | 73 +++++++++++ .../templates/deployment.yaml | 102 +++++++++++++++ .../readability-js-server/templates/hpa.yaml | 41 ++++++ .../templates/ingress.yaml | 35 ++++++ .../readability-js-server/templates/pdb.yaml | 25 ++++ .../templates/service.yaml | 22 ++++ .../templates/serviceaccount.yaml | 16 +++ charts/readability-js-server/values.yaml | 117 ++++++++++++++++++ 11 files changed, 550 insertions(+) create mode 100644 charts/readability-js-server/Chart.yaml create mode 100644 charts/readability-js-server/README.md create mode 100644 charts/readability-js-server/templates/NOTES.txt create mode 100644 charts/readability-js-server/templates/_helpers.tpl create mode 100644 charts/readability-js-server/templates/deployment.yaml create mode 100644 charts/readability-js-server/templates/hpa.yaml create mode 100644 charts/readability-js-server/templates/ingress.yaml create mode 100644 charts/readability-js-server/templates/pdb.yaml create mode 100644 charts/readability-js-server/templates/service.yaml create mode 100644 charts/readability-js-server/templates/serviceaccount.yaml create mode 100644 charts/readability-js-server/values.yaml diff --git a/charts/readability-js-server/Chart.yaml b/charts/readability-js-server/Chart.yaml new file mode 100644 index 0000000..eec5fbb --- /dev/null +++ b/charts/readability-js-server/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +name: readability-js-server +description: A production-usable Helm chart for Readability JS Server +type: application +version: 0.1.0 +appVersion: "1.8.0" +home: https://github.com/phpdocker-io/readability-js-server +sources: + - https://github.com/phpdocker-io/readability-js-server +keywords: + - readability + - article-extraction + - express + - nodejs +maintainers: + - name: Luis Pabon + url: https://github.com/luispabon diff --git a/charts/readability-js-server/README.md b/charts/readability-js-server/README.md new file mode 100644 index 0000000..34a8888 --- /dev/null +++ b/charts/readability-js-server/README.md @@ -0,0 +1,84 @@ +# readability-js-server Helm chart + +This chart deploys Readability JS Server as a single-replica `Deployment` behind a `ClusterIP` `Service` by default. Optional `Ingress`, `HorizontalPodAutoscaler`, and `PodDisruptionBudget` resources stay disabled until you opt in. + +## Prerequisites + +- Kubernetes 1.26 or newer +- Helm 3.12 or newer + +## Installation + +Install from the repository checkout: + +```bash +helm install readability-js-server ./charts/readability-js-server +``` + +Override values at install or upgrade time: + +```bash +helm upgrade --install readability-js-server ./charts/readability-js-server \ + --namespace readability \ + --create-namespace \ + --set image.tag=1.8.0 \ + --set ingress.enabled=true \ + --set ingress.hosts[0].host=readability.example.com +``` + +## Defaults + +- Chart version: `0.1.0` +- App version: `1.8.0` +- Image repository: `phpdockerio/readability-js-server` +- Image tag: `1.8.0` +- Container listen port: `3000` +- Service type: `ClusterIP` +- Replica count: `1` +- Liveness and readiness probes: `GET /healthz` + +## Configuration + +The chart exposes the settings most clusters need without requiring a fork: + +| Area | Values | +| --- | --- | +| Image | `image.repository`, `image.tag`, `image.pullPolicy`, `imagePullSecrets` | +| Service account | `serviceAccount.create`, `serviceAccount.name`, `serviceAccount.annotations`, `serviceAccount.labels`, `serviceAccount.automountServiceAccountToken` | +| Pod metadata | `podLabels`, `podAnnotations`, `priorityClassName` | +| Security | `podSecurityContext`, `containerSecurityContext` | +| Scheduling | `nodeSelector`, `tolerations`, `affinity`, `topologySpreadConstraints` | +| Networking | `service.type`, `service.port`, `service.annotations`, `service.labels`, `ingress.*` | +| Capacity | `replicaCount`, `resources`, `autoscaling.*`, `pdb.*`, `terminationGracePeriodSeconds` | +| Probes | `probes.liveness.*`, `probes.readiness.*`, `probes.startup.*` | +| App config | `appConfig.PORT`, `appConfig.REQUEST_BODY_LIMIT`, `appConfig.FETCH_TIMEOUT_MS`, `appConfig.FETCH_MAX_BYTES`, `appConfig.FETCH_MAX_REDIRECTS`, `appConfig.BLOCK_PRIVATE_NETWORKS`, `appConfig.READABILITY_MAX_ELEMS`, `appConfig.MAX_CONCURRENT_REQUESTS`, `appConfig.CONTENT_FORMAT` | + +`appConfig` maps directly to the environment variables already supported by `src/config.js`. `READABILITY_MAX_ELEMS` is unset by default so the service preserves its current runtime behavior until you choose a limit. + +## Optional resources + +- `ingress.enabled=true` renders a Kubernetes `Ingress`. Configure hostnames, annotations, class name, paths, and TLS through `ingress.*`. +- `autoscaling.enabled=true` renders an `autoscaling/v2` `HorizontalPodAutoscaler`. You can use the built-in CPU and memory targets or provide a full `autoscaling.metrics` list plus `autoscaling.behavior`. +- `pdb.enabled=true` renders a `PodDisruptionBudget`. Set exactly one of `pdb.minAvailable` or `pdb.maxUnavailable`. + +## Versioning policy + +Chart and application versions are intentionally separate: + +- `version` tracks chart packaging changes. +- `appVersion` tracks the default Readability JS Server image version. + +When the chart changes without a new application release, only the chart `version` should move. When the default image tag changes, bump both the chart `version` and `appVersion`. + +## Artifact Hub expectations + +This chart is structured for a conventional Helm repository published via GitHub Pages. Once that repository is in place, Artifact Hub should index the repository URL rather than individual GitHub Release assets. Consumers should expect standard Helm metadata, independent chart and app versioning, and release notes attached to chart packages. + +## Verification + +Render and lint the chart locally: + +```bash +helm lint charts/readability-js-server +helm template readability-js-server charts/readability-js-server +``` diff --git a/charts/readability-js-server/templates/NOTES.txt b/charts/readability-js-server/templates/NOTES.txt new file mode 100644 index 0000000..3889909 --- /dev/null +++ b/charts/readability-js-server/templates/NOTES.txt @@ -0,0 +1,18 @@ +1. Check the release status: + kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/instance={{ .Release.Name }}" + +2. Port-forward the service locally: + kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "readability-js-server.fullname" . }} 3000:{{ .Values.service.port }} + +3. Probe the service: + curl -i http://127.0.0.1:3000/healthz +{{- if .Values.ingress.enabled }} + +4. If your ingress controller is ready, test the configured hostnames: +{{- range .Values.ingress.hosts }} +{{- $host := .host }} +{{- range .paths }} + curl -i http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host }}{{ .path }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/readability-js-server/templates/_helpers.tpl b/charts/readability-js-server/templates/_helpers.tpl new file mode 100644 index 0000000..c6e3dac --- /dev/null +++ b/charts/readability-js-server/templates/_helpers.tpl @@ -0,0 +1,73 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "readability-js-server.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "readability-js-server.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 "readability-js-server.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels. +*/}} +{{- define "readability-js-server.labels" -}} +helm.sh/chart: {{ include "readability-js-server.chart" . }} +app.kubernetes.io/name: {{ include "readability-js-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels. +*/}} +{{- define "readability-js-server.selectorLabels" -}} +app.kubernetes.io/name: {{ include "readability-js-server.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use. +*/}} +{{- define "readability-js-server.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} +{{- default (include "readability-js-server.fullname" .) .Values.serviceAccount.name -}} +{{- else -}} +{{- default "default" .Values.serviceAccount.name -}} +{{- end -}} +{{- end -}} + +{{/* +Render a standard HTTP probe. +*/}} +{{- define "readability-js-server.httpProbe" -}} +httpGet: + path: {{ .path | quote }} + port: http +initialDelaySeconds: {{ .initialDelaySeconds }} +periodSeconds: {{ .periodSeconds }} +timeoutSeconds: {{ .timeoutSeconds }} +successThreshold: {{ .successThreshold }} +failureThreshold: {{ .failureThreshold }} +{{- end -}} diff --git a/charts/readability-js-server/templates/deployment.yaml b/charts/readability-js-server/templates/deployment.yaml new file mode 100644 index 0000000..8ffb4e0 --- /dev/null +++ b/charts/readability-js-server/templates/deployment.yaml @@ -0,0 +1,102 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "readability-js-server.fullname" . }} + labels: + {{- include "readability-js-server.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "readability-js-server.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "readability-js-server.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "readability-js-server.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} + terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . | quote }} + {{- end }} + containers: + - name: {{ include "readability-js-server.name" . }} + image: "{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ default .Values.appConfig.PORT .Values.containerPort }} + protocol: TCP + env: + - name: PORT + value: {{ printf "%d" (int64 .Values.appConfig.PORT) | quote }} + - name: REQUEST_BODY_LIMIT + value: {{ .Values.appConfig.REQUEST_BODY_LIMIT | quote }} + - name: FETCH_TIMEOUT_MS + value: {{ printf "%d" (int64 .Values.appConfig.FETCH_TIMEOUT_MS) | quote }} + - name: FETCH_MAX_BYTES + value: {{ printf "%d" (int64 .Values.appConfig.FETCH_MAX_BYTES) | quote }} + - name: FETCH_MAX_REDIRECTS + value: {{ printf "%d" (int64 .Values.appConfig.FETCH_MAX_REDIRECTS) | quote }} + - name: BLOCK_PRIVATE_NETWORKS + value: {{ printf "%v" .Values.appConfig.BLOCK_PRIVATE_NETWORKS | quote }} + {{- if ne .Values.appConfig.READABILITY_MAX_ELEMS nil }} + - name: READABILITY_MAX_ELEMS + value: {{ printf "%d" (int64 .Values.appConfig.READABILITY_MAX_ELEMS) | quote }} + {{- end }} + - name: MAX_CONCURRENT_REQUESTS + value: {{ printf "%d" (int64 .Values.appConfig.MAX_CONCURRENT_REQUESTS) | quote }} + - name: CONTENT_FORMAT + value: {{ .Values.appConfig.CONTENT_FORMAT | quote }} + {{- if .Values.probes.startup.enabled }} + startupProbe: + {{- include "readability-js-server.httpProbe" .Values.probes.startup | nindent 12 }} + {{- end }} + {{- if .Values.probes.liveness.enabled }} + livenessProbe: + {{- include "readability-js-server.httpProbe" .Values.probes.liveness | nindent 12 }} + {{- end }} + {{- if .Values.probes.readiness.enabled }} + readinessProbe: + {{- include "readability-js-server.httpProbe" .Values.probes.readiness | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/readability-js-server/templates/hpa.yaml b/charts/readability-js-server/templates/hpa.yaml new file mode 100644 index 0000000..3c1f9ee --- /dev/null +++ b/charts/readability-js-server/templates/hpa.yaml @@ -0,0 +1,41 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "readability-js-server.fullname" . }} + labels: + {{- include "readability-js-server.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "readability-js-server.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + {{- if .Values.autoscaling.metrics }} + metrics: + {{- toYaml .Values.autoscaling.metrics | nindent 4 }} + {{- else }} + metrics: + {{- if ne .Values.autoscaling.targetCPUUtilizationPercentage nil }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if ne .Values.autoscaling.targetMemoryUtilizationPercentage nil }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} + {{- end }} + {{- with .Values.autoscaling.behavior }} + behavior: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/readability-js-server/templates/ingress.yaml b/charts/readability-js-server/templates/ingress.yaml new file mode 100644 index 0000000..c7a0f0a --- /dev/null +++ b/charts/readability-js-server/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "readability-js-server.fullname" . }} + labels: + {{- include "readability-js-server.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . | quote }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path | quote }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "readability-js-server.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/readability-js-server/templates/pdb.yaml b/charts/readability-js-server/templates/pdb.yaml new file mode 100644 index 0000000..080f1e6 --- /dev/null +++ b/charts/readability-js-server/templates/pdb.yaml @@ -0,0 +1,25 @@ +{{- if .Values.pdb.enabled }} +{{- $hasMin := ne .Values.pdb.minAvailable nil }} +{{- $hasMax := ne .Values.pdb.maxUnavailable nil }} +{{- if and $hasMin $hasMax }} +{{- fail "pdb.minAvailable and pdb.maxUnavailable are mutually exclusive" }} +{{- end }} +{{- if and (not $hasMin) (not $hasMax) }} +{{- fail "set pdb.minAvailable or pdb.maxUnavailable when pdb.enabled=true" }} +{{- end }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "readability-js-server.fullname" . }} + labels: + {{- include "readability-js-server.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "readability-js-server.selectorLabels" . | nindent 6 }} + {{- if $hasMin }} + minAvailable: {{ .Values.pdb.minAvailable }} + {{- else }} + maxUnavailable: {{ .Values.pdb.maxUnavailable }} + {{- end }} +{{- end }} diff --git a/charts/readability-js-server/templates/service.yaml b/charts/readability-js-server/templates/service.yaml new file mode 100644 index 0000000..d7c778a --- /dev/null +++ b/charts/readability-js-server/templates/service.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "readability-js-server.fullname" . }} + labels: + {{- include "readability-js-server.labels" . | nindent 4 }} + {{- with .Values.service.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.service.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "readability-js-server.selectorLabels" . | nindent 4 }} diff --git a/charts/readability-js-server/templates/serviceaccount.yaml b/charts/readability-js-server/templates/serviceaccount.yaml new file mode 100644 index 0000000..481d219 --- /dev/null +++ b/charts/readability-js-server/templates/serviceaccount.yaml @@ -0,0 +1,16 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "readability-js-server.serviceAccountName" . }} + labels: + {{- include "readability-js-server.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/readability-js-server/values.yaml b/charts/readability-js-server/values.yaml new file mode 100644 index 0000000..3184261 --- /dev/null +++ b/charts/readability-js-server/values.yaml @@ -0,0 +1,117 @@ +replicaCount: 1 + +nameOverride: "" +fullnameOverride: "" + +image: + repository: phpdockerio/readability-js-server + pullPolicy: IfNotPresent + tag: "1.8.0" + +imagePullSecrets: [] + +serviceAccount: + create: true + name: "" + annotations: {} + labels: {} + automountServiceAccountToken: true + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +containerSecurityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + +priorityClassName: "" +nodeSelector: {} +tolerations: [] +affinity: {} +topologySpreadConstraints: [] +terminationGracePeriodSeconds: 30 + +containerPort: null + +service: + type: ClusterIP + port: 3000 + annotations: {} + labels: {} + +resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +appConfig: + PORT: 3000 + REQUEST_BODY_LIMIT: 16kb + FETCH_TIMEOUT_MS: 10000 + FETCH_MAX_BYTES: 5242880 + FETCH_MAX_REDIRECTS: 5 + BLOCK_PRIVATE_NETWORKS: true + READABILITY_MAX_ELEMS: null + MAX_CONCURRENT_REQUESTS: 10 + CONTENT_FORMAT: markdown + +probes: + liveness: + enabled: true + path: /healthz + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 2 + successThreshold: 1 + failureThreshold: 3 + readiness: + enabled: true + path: /healthz + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + successThreshold: 1 + failureThreshold: 3 + startup: + enabled: false + path: /healthz + initialDelaySeconds: 0 + periodSeconds: 10 + timeoutSeconds: 2 + successThreshold: 1 + failureThreshold: 30 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: readability-js-server.local + paths: + - path: / + pathType: Prefix + tls: [] + +autoscaling: + enabled: false + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 80 + targetMemoryUtilizationPercentage: null + metrics: [] + behavior: {} + +pdb: + enabled: false + minAvailable: 1 + maxUnavailable: null From 246686313a17840c612618e7c72eef35072a6f33 Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 17:10:55 +0100 Subject: [PATCH 5/9] Add Helm verification workflow --- .github/workflows/build-publish.yaml | 11 +++++++++++ AGENTS.md | 6 ++++-- Makefile | 11 +++++++++++ README.md | 10 +++++++++- 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-publish.yaml b/.github/workflows/build-publish.yaml index 7d54f81..eb5ddb0 100644 --- a/.github/workflows/build-publish.yaml +++ b/.github/workflows/build-publish.yaml @@ -28,6 +28,11 @@ jobs: cache: pnpm cache-dependency-path: pnpm-lock.yaml + - name: Set up Helm + uses: azure/setup-helm@v4 + with: + version: v3.16.3 + - name: Install dependencies run: pnpm install --frozen-lockfile @@ -36,3 +41,9 @@ jobs: - name: Test run: pnpm test + + - name: Helm lint + run: make helm-lint + + - name: Helm template + run: make helm-template diff --git a/AGENTS.md b/AGENTS.md index 9e6676f..95d6a93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,18 +13,20 @@ - Start: `pnpm start` - Lint: `pnpm lint` - Test: `pnpm test` +- Helm lint: `make helm-lint` or `helm lint charts/readability-js-server` +- Helm template: `make helm-template` or `helm template readability-js-server charts/readability-js-server` - Memory soak: `make soak` or `node scripts/memory-soak.js --requests 100 --concurrency 2 --sample-every 10` - Docker build: `docker build -t readability-js .` - Docker run: `docker run --rm -p 3000:3000 readability-js` - Release tag: `make release-tag VERSION=1.8.0` -The Makefile mirrors those workflows with `make install`, `make start`, `make lint`, `make lint-fix`, `make build-container`, `make run-container`, `make release-tag`, and `make example-request`. +The Makefile mirrors those workflows with `make install`, `make start`, `make lint`, `make lint-fix`, `make helm-lint`, `make helm-template`, `make helm-verify`, `make build-container`, `make run-container`, `make release-tag`, and `make example-request`. `package.json` is the single source of truth for the service version. Release publishing is tag-driven: bump `package.json`'s `version`, commit it, create a matching `vX.Y.Z` tag, and push the tag to trigger Docker publish plus GitHub Release creation. ## Testing expectations -- Run `pnpm lint` and `pnpm test` for any docs, API, config, or dependency change. +- Run `pnpm lint`, `pnpm test`, and the Helm verification targets for any docs, API, config, chart, or dependency change. - Add or update tests when a change touches response shape, error normalization, URL validation, sanitization, redirect handling, concurrency gating, or config parsing. - Use the memory soak script when a change could affect allocation behavior or long-run stability. diff --git a/Makefile b/Makefile index 06a9028..29beabb 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,17 @@ lint: lint-fix: pnpm lint:fix +HELM_CHART ?= charts/readability-js-server +HELM_RELEASE_NAME ?= readability-js-server + +helm-lint: + helm lint $(HELM_CHART) + +helm-template: + helm template $(HELM_RELEASE_NAME) $(HELM_CHART) + +helm-verify: helm-lint helm-template + build-container: docker build -t readability-js . diff --git a/README.md b/README.md index f2ae603..a2c9fbc 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,13 @@ pnpm lint pnpm test ``` +Run the Helm chart checks with the Makefile: + +```bash +make helm-lint +make helm-template +``` + The repo also exposes a memory soak harness: ```bash @@ -155,6 +162,7 @@ The Makefile provides the same checks: ```bash make lint make lint-fix +make helm-verify ``` For release tagging there is also: @@ -174,7 +182,7 @@ docker run --rm -p 3000:3000 readability-js The image is based on `node:24-alpine`, installs production dependencies only, and runs the service as a non-root user. -CI on pull requests and pushes to `master` runs lint and tests only. Container publishing happens from the tag-triggered release workflow. +CI on pull requests and pushes to `master` runs lint, tests, and Helm chart verification. Container publishing happens from the tag-triggered release workflow. For Docker Compose setup, see [`examples/compose.yaml`](examples/compose.yaml). That example publishes port `3000` and checks `GET /healthz` from inside the container using the `node` runtime that ships in the published image. From 43334609d4bbdb084d48051081176fecf5079d22 Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 17:19:47 +0100 Subject: [PATCH 6/9] Add Helm chart publishing and Artifact Hub metadata --- .github/workflows/chart-release.yaml | 70 +++++++++++++++++++++++++ README.md | 28 ++++++++++ artifacthub-repo.yml | 11 ++++ charts/readability-js-server/Chart.yaml | 20 ++++++- charts/readability-js-server/README.md | 33 +++++++++--- 5 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/chart-release.yaml create mode 100644 artifacthub-repo.yml diff --git a/.github/workflows/chart-release.yaml b/.github/workflows/chart-release.yaml new file mode 100644 index 0000000..323b7f3 --- /dev/null +++ b/.github/workflows/chart-release.yaml @@ -0,0 +1,70 @@ +name: Chart Release + +on: + push: + branches: [ master ] + paths: + - .github/workflows/chart-release.yaml + - artifacthub-repo.yml + - charts/** + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.7.0 + with: + charts_dir: charts + pages_branch: gh-pages + packages_with_index: true + skip_existing: true + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + - name: Publish Artifact Hub repository metadata + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + if ! git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1; then + echo "gh-pages branch does not exist yet; skipping artifacthub-repo.yml publication" + exit 0 + fi + + tmp_dir="$(mktemp -d)" + trap 'rm -rf "${tmp_dir}"' EXIT + + git clone --branch gh-pages --single-branch \ + "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ + "${tmp_dir}" + + install -m 0644 artifacthub-repo.yml "${tmp_dir}/artifacthub-repo.yml" + + cd "${tmp_dir}" + if git diff --quiet -- artifacthub-repo.yml; then + echo "artifacthub-repo.yml already up to date on gh-pages" + exit 0 + fi + + git config user.name "${GITHUB_ACTOR}" + git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" + git add artifacthub-repo.yml + git commit -m "Publish Artifact Hub repository metadata" + git push origin gh-pages diff --git a/README.md b/README.md index a2c9fbc..23728ac 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,34 @@ make start Release versions come from [`package.json`](package.json). To publish a release, bump `version`, commit the change, create a `vX.Y.Z` tag, and push that tag. The release workflow publishes Docker images for `X.Y.Z`, `X.Y`, `X`, and `latest`, and creates the matching GitHub Release with generated notes. +## Helm chart + +The Kubernetes chart is published as a conventional Helm repository on GitHub Pages: + +```bash +helm repo add phpdocker-io https://phpdocker-io.github.io/readability-js-server +helm repo update +helm install readability-js-server phpdocker-io/readability-js-server \ + --namespace readability \ + --create-namespace +``` + +For local chart development, install from the checkout with `helm install readability-js-server ./charts/readability-js-server`. + +Artifact Hub should reference the external Helm repository URL `https://phpdocker-io.github.io/readability-js-server`. It should not be configured to ingest GitHub release assets directly. + +## Release and versioning + +Docker image publishing remains tag-driven. Bump [`package.json`](package.json), commit it, create the matching `vX.Y.Z` tag, and push the tag to publish the container image and GitHub Release. + +Helm chart publishing is separate and runs from chart changes on `master` or an explicit manual trigger. It packages changed charts from `charts/`, updates the GitHub Pages repository on `gh-pages`, and publishes `artifacthub-repo.yml` next to `index.yaml` for Artifact Hub. + +For the chart itself: + +- Bump `charts/readability-js-server/Chart.yaml` `version` for any chart package change, including templates, defaults, README content, metadata, or publishing annotations. +- Bump `appVersion` only when the chart's default application image tag changes. +- When the default application image tag changes, bump both `version` and `appVersion`. + ## Testing Run the lint and test suites with pnpm: diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml new file mode 100644 index 0000000..2c8118b --- /dev/null +++ b/artifacthub-repo.yml @@ -0,0 +1,11 @@ +# Artifact Hub repository metadata for the published Helm repository. +# This file must be served from the same path as index.yaml: +# https://phpdocker-io.github.io/readability-js-server/artifacthub-repo.yml +# +# Add repositoryID after the repository has been created in Artifact Hub to +# enable verified publisher checks. +# repositoryID: 00000000-0000-0000-0000-000000000000 + +owners: + - name: Luis Pabon + email: luis.pabon@auronconsulting.co.uk diff --git a/charts/readability-js-server/Chart.yaml b/charts/readability-js-server/Chart.yaml index eec5fbb..15cb95e 100644 --- a/charts/readability-js-server/Chart.yaml +++ b/charts/readability-js-server/Chart.yaml @@ -2,9 +2,26 @@ apiVersion: v2 name: readability-js-server description: A production-usable Helm chart for Readability JS Server type: application -version: 0.1.0 +version: 0.1.1 appVersion: "1.8.0" home: https://github.com/phpdocker-io/readability-js-server +annotations: + artifacthub.io/license: Apache-2.0 + artifacthub.io/links: | + - name: source + url: https://github.com/phpdocker-io/readability-js-server + - name: support + url: https://github.com/phpdocker-io/readability-js-server/issues + - name: chart repository + url: https://phpdocker-io.github.io/readability-js-server + artifacthub.io/maintainers: | + - name: Luis Pabon + email: luis.pabon@auronconsulting.co.uk + artifacthub.io/changes: | + - kind: added + description: Publish chart packages and index.yaml to the GitHub Pages Helm repository. + - kind: added + description: Add Artifact Hub repository metadata and installation guidance for the hosted chart repository. sources: - https://github.com/phpdocker-io/readability-js-server keywords: @@ -14,4 +31,5 @@ keywords: - nodejs maintainers: - name: Luis Pabon + email: luis.pabon@auronconsulting.co.uk url: https://github.com/luispabon diff --git a/charts/readability-js-server/README.md b/charts/readability-js-server/README.md index 34a8888..bcb5389 100644 --- a/charts/readability-js-server/README.md +++ b/charts/readability-js-server/README.md @@ -9,7 +9,17 @@ This chart deploys Readability JS Server as a single-replica `Deployment` behind ## Installation -Install from the repository checkout: +Add the published Helm repository and install the chart: + +```bash +helm repo add phpdocker-io https://phpdocker-io.github.io/readability-js-server +helm repo update +helm install readability-js-server phpdocker-io/readability-js-server \ + --namespace readability \ + --create-namespace +``` + +For local chart development, install directly from the repository checkout: ```bash helm install readability-js-server ./charts/readability-js-server @@ -28,7 +38,7 @@ helm upgrade --install readability-js-server ./charts/readability-js-server \ ## Defaults -- Chart version: `0.1.0` +- Chart version: `0.1.1` - App version: `1.8.0` - Image repository: `phpdockerio/readability-js-server` - Image tag: `1.8.0` @@ -65,14 +75,24 @@ The chart exposes the settings most clusters need without requiring a fork: Chart and application versions are intentionally separate: -- `version` tracks chart packaging changes. -- `appVersion` tracks the default Readability JS Server image version. +- `version` tracks chart package changes. Bump it for any chart content change, including templates, values, `Chart.yaml` metadata, README updates, or Artifact Hub annotations. +- `appVersion` tracks the default Readability JS Server image version used by the chart. When the chart changes without a new application release, only the chart `version` should move. When the default image tag changes, bump both the chart `version` and `appVersion`. -## Artifact Hub expectations +Docker image publishing remains tag-driven from the root release workflow and follows `package.json`. Helm chart publishing is separate: pushes to `master` that change `charts/**`, plus explicit manual workflow runs, publish the chart repository to GitHub Pages. + +## Hosted repository and Artifact Hub + +The published Helm repository URL is: + +```text +https://phpdocker-io.github.io/readability-js-server +``` + +Artifact Hub should register that external Helm repository URL. It should not be pointed at GitHub Releases or at the source repository itself. -This chart is structured for a conventional Helm repository published via GitHub Pages. Once that repository is in place, Artifact Hub should index the repository URL rather than individual GitHub Release assets. Consumers should expect standard Helm metadata, independent chart and app versioning, and release notes attached to chart packages. +Repository-level Artifact Hub metadata lives in `artifacthub-repo.yml`. The chart release workflow copies that file onto `gh-pages` so it is served next to `index.yaml`, which is the layout Artifact Hub expects for ownership claims and verified publisher metadata. ## Verification @@ -81,4 +101,5 @@ Render and lint the chart locally: ```bash helm lint charts/readability-js-server helm template readability-js-server charts/readability-js-server +helm package charts/readability-js-server ``` From 8bcea3cc62629cf8541215e85e345aa3be41f44e Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 17:21:21 +0100 Subject: [PATCH 7/9] Keep initial chart version at 0.1.0 --- charts/readability-js-server/Chart.yaml | 2 +- charts/readability-js-server/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/readability-js-server/Chart.yaml b/charts/readability-js-server/Chart.yaml index 15cb95e..8a59de3 100644 --- a/charts/readability-js-server/Chart.yaml +++ b/charts/readability-js-server/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: readability-js-server description: A production-usable Helm chart for Readability JS Server type: application -version: 0.1.1 +version: 0.1.0 appVersion: "1.8.0" home: https://github.com/phpdocker-io/readability-js-server annotations: diff --git a/charts/readability-js-server/README.md b/charts/readability-js-server/README.md index bcb5389..b43bd0e 100644 --- a/charts/readability-js-server/README.md +++ b/charts/readability-js-server/README.md @@ -38,7 +38,7 @@ helm upgrade --install readability-js-server ./charts/readability-js-server \ ## Defaults -- Chart version: `0.1.1` +- Chart version: `0.1.0` - App version: `1.8.0` - Image repository: `phpdockerio/readability-js-server` - Image tag: `1.8.0` From 9878ae3f89c5fe0ad2d30b81b6db9c41cac2e6ad Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 17:23:04 +0100 Subject: [PATCH 8/9] Record helm chart execution state --- .../2026-06-17_helm-chart/execution.md | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .project_planning/2026-06-17_helm-chart/execution.md diff --git a/.project_planning/2026-06-17_helm-chart/execution.md b/.project_planning/2026-06-17_helm-chart/execution.md new file mode 100644 index 0000000..2313ea9 --- /dev/null +++ b/.project_planning/2026-06-17_helm-chart/execution.md @@ -0,0 +1,44 @@ +## Execution State + +- Active branch: `cl/2026-06-17_helm-chart` +- Planning artifacts version-controlled: `yes` +- Verification strategy: + - Required baseline: `pnpm lint`, `pnpm test` + - Helm verification: `helm lint charts/readability-js-server`, `helm template readability-js-server charts/readability-js-server` + - Packaging check for chart release step: `helm package charts/readability-js-server --destination /tmp/readability-js-server-chart` + +## Steps + +- `step-1` Add health endpoint and tests: `complete` +- `step-2` Add Docker Compose healthcheck: `complete` +- `step-3` Create production-usable Helm chart: `complete` +- `step-4` Add Helm verification tooling: `complete` +- `step-5` Add chart publishing and Artifact Hub metadata: `complete` + +## Sub-agents + +- `step-1`: worker `Hilbert` on `gpt-5.4-mini` (cheaper tier than parent runtime); planner `delegate_profile` absent, executor chose cheapest safe option for scoped API/test/docs change. Commit `66c2d99331b894c051f634b4ce05b4d18a9cf31b`, merged as `fa9df4d`. +- `step-2`: worker `Gauss` on `gpt-5.4-mini` (cheaper tier than parent runtime); planner `delegate_profile` absent, executor chose cheapest safe option for scoped Compose/docs change. Commit `24d6615df74e4553b88b890ab8f5ac56e4c0b265`, merged as `7c11baa`. +- `step-3`: worker `Laplace` on `gpt-5.4` (more capable tier than earlier mini workers); planner `delegate_profile` absent, executor escalated for multi-file Helm chart design and templating. Commit `63891955686d5aceb78fc832d1e6a4b907df3ed0`, merged as `25aaa5f`. +- `step-4`: worker `Goodall` on `gpt-5.4-mini` (cheaper tier than parent runtime); planner `delegate_profile` absent, executor chose cheapest safe option for Makefile/CI/docs updates. Commit `4e8048d0c7a1c28f411f9a67537a7594960703ed`, merged as `7fe3d2a`. +- `step-5`: worker `Leibniz` on `gpt-5.4` (more capable tier than earlier mini workers); planner `delegate_profile` absent, executor escalated for GitHub Pages/chart-releaser and Artifact Hub metadata workflow integration. Commit `b525e72766a29e77bf527c5bfd7fef8e6593201d`, fix-pass commit `efcef26e4f1b1c1ef5521548b7fb11200875d9ba`, merged as `1051886` and `6cf1b1c`. + +## Verification + +- `step-3` worker reported: `helm lint charts/readability-js-server`, `helm template readability-js-server charts/readability-js-server`, optional render with ingress/HPA/PDB enabled, `pnpm lint`, `pnpm test` all passed in a Node 24 container because host Node 22 broke the pinned pnpm toolchain. +- `step-4` worker reported: `make helm-verify` passed locally; `pnpm lint` and `pnpm test` passed in a Node 24 container. +- `step-5` worker reported: `make helm-lint`, `make helm-template`, `helm package charts/readability-js-server --destination /tmp/readability-js-server-chart`, `pnpm lint`, and `pnpm test` passed. +- Executor verification on feature branch passed: + - `make helm-lint` + - `make helm-template` + - `helm package charts/readability-js-server --destination /tmp/readability-js-server-chart` + - `docker run --rm -v /home/luis/Projects/readability-js-server:/workspace -w /workspace -e PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 node:24-bullseye bash -lc 'corepack enable && pnpm install --frozen-lockfile && pnpm lint && pnpm test'` + +## Deviations / Blockers + +- `step-4` cleanup left a stale `.worktrees/step-4` directory with root-owned files from containerized verification; executor removed the branch and cleaned the residual directory separately. +- `step-5` initial implementation incorrectly bumped the first chart release from `0.1.0` to `0.1.1`; executor rejected that, ran a scoped fix pass, and merged the corrected `0.1.0` state. + +## Reviewer Handoff + +- Ready. Executor state committed as `906f72d`; feature branch working tree is clean. From 91a4d36f4dc45bbb1a5b031b5b07a72777ec4e3f Mon Sep 17 00:00:00 2001 From: Luis Pabon Date: Wed, 17 Jun 2026 17:56:36 +0100 Subject: [PATCH 9/9] chore: remove planning artifacts for helm chart --- .../2026-06-17_helm-chart/execution.md | 44 ------- .../2026-06-17_helm-chart/overview.md | 67 ---------- .../2026-06-17_helm-chart/plan.yaml | 122 ------------------ .../2026-06-17_helm-chart/research.md | 65 ---------- 4 files changed, 298 deletions(-) delete mode 100644 .project_planning/2026-06-17_helm-chart/execution.md delete mode 100644 .project_planning/2026-06-17_helm-chart/overview.md delete mode 100644 .project_planning/2026-06-17_helm-chart/plan.yaml delete mode 100644 .project_planning/2026-06-17_helm-chart/research.md diff --git a/.project_planning/2026-06-17_helm-chart/execution.md b/.project_planning/2026-06-17_helm-chart/execution.md deleted file mode 100644 index 2313ea9..0000000 --- a/.project_planning/2026-06-17_helm-chart/execution.md +++ /dev/null @@ -1,44 +0,0 @@ -## Execution State - -- Active branch: `cl/2026-06-17_helm-chart` -- Planning artifacts version-controlled: `yes` -- Verification strategy: - - Required baseline: `pnpm lint`, `pnpm test` - - Helm verification: `helm lint charts/readability-js-server`, `helm template readability-js-server charts/readability-js-server` - - Packaging check for chart release step: `helm package charts/readability-js-server --destination /tmp/readability-js-server-chart` - -## Steps - -- `step-1` Add health endpoint and tests: `complete` -- `step-2` Add Docker Compose healthcheck: `complete` -- `step-3` Create production-usable Helm chart: `complete` -- `step-4` Add Helm verification tooling: `complete` -- `step-5` Add chart publishing and Artifact Hub metadata: `complete` - -## Sub-agents - -- `step-1`: worker `Hilbert` on `gpt-5.4-mini` (cheaper tier than parent runtime); planner `delegate_profile` absent, executor chose cheapest safe option for scoped API/test/docs change. Commit `66c2d99331b894c051f634b4ce05b4d18a9cf31b`, merged as `fa9df4d`. -- `step-2`: worker `Gauss` on `gpt-5.4-mini` (cheaper tier than parent runtime); planner `delegate_profile` absent, executor chose cheapest safe option for scoped Compose/docs change. Commit `24d6615df74e4553b88b890ab8f5ac56e4c0b265`, merged as `7c11baa`. -- `step-3`: worker `Laplace` on `gpt-5.4` (more capable tier than earlier mini workers); planner `delegate_profile` absent, executor escalated for multi-file Helm chart design and templating. Commit `63891955686d5aceb78fc832d1e6a4b907df3ed0`, merged as `25aaa5f`. -- `step-4`: worker `Goodall` on `gpt-5.4-mini` (cheaper tier than parent runtime); planner `delegate_profile` absent, executor chose cheapest safe option for Makefile/CI/docs updates. Commit `4e8048d0c7a1c28f411f9a67537a7594960703ed`, merged as `7fe3d2a`. -- `step-5`: worker `Leibniz` on `gpt-5.4` (more capable tier than earlier mini workers); planner `delegate_profile` absent, executor escalated for GitHub Pages/chart-releaser and Artifact Hub metadata workflow integration. Commit `b525e72766a29e77bf527c5bfd7fef8e6593201d`, fix-pass commit `efcef26e4f1b1c1ef5521548b7fb11200875d9ba`, merged as `1051886` and `6cf1b1c`. - -## Verification - -- `step-3` worker reported: `helm lint charts/readability-js-server`, `helm template readability-js-server charts/readability-js-server`, optional render with ingress/HPA/PDB enabled, `pnpm lint`, `pnpm test` all passed in a Node 24 container because host Node 22 broke the pinned pnpm toolchain. -- `step-4` worker reported: `make helm-verify` passed locally; `pnpm lint` and `pnpm test` passed in a Node 24 container. -- `step-5` worker reported: `make helm-lint`, `make helm-template`, `helm package charts/readability-js-server --destination /tmp/readability-js-server-chart`, `pnpm lint`, and `pnpm test` passed. -- Executor verification on feature branch passed: - - `make helm-lint` - - `make helm-template` - - `helm package charts/readability-js-server --destination /tmp/readability-js-server-chart` - - `docker run --rm -v /home/luis/Projects/readability-js-server:/workspace -w /workspace -e PNPM_CONFIG_MINIMUM_RELEASE_AGE=0 node:24-bullseye bash -lc 'corepack enable && pnpm install --frozen-lockfile && pnpm lint && pnpm test'` - -## Deviations / Blockers - -- `step-4` cleanup left a stale `.worktrees/step-4` directory with root-owned files from containerized verification; executor removed the branch and cleaned the residual directory separately. -- `step-5` initial implementation incorrectly bumped the first chart release from `0.1.0` to `0.1.1`; executor rejected that, ran a scoped fix pass, and merged the corrected `0.1.0` state. - -## Reviewer Handoff - -- Ready. Executor state committed as `906f72d`; feature branch working tree is clean. diff --git a/.project_planning/2026-06-17_helm-chart/overview.md b/.project_planning/2026-06-17_helm-chart/overview.md deleted file mode 100644 index 59e7a05..0000000 --- a/.project_planning/2026-06-17_helm-chart/overview.md +++ /dev/null @@ -1,67 +0,0 @@ -## Request - -Plan issue https://github.com/phpdocker-io/readability-js-server/issues/53: add a Helm chart for Readability JS Server, cover deployment/service/optional ingress/optional autoscaling/optional disruption budget, support Artifact Hub discoverability, and add a health endpoint that is used by Docker Compose and Kubernetes probes. - -## Overview - -Add a conventional, production-usable Helm chart under the repository with chart metadata, configurable values, reusable template helpers, and templates for the Kubernetes resources needed to run the existing container. The chart should default to `phpdockerio/readability-js-server` on app version `1.8.0`, expose port `3000`, use environment variables already supported by `src/config.js`, and provide optional ingress, HPA, and PDB resources behind explicit values. - -The chart values should be configurable in the same broad way users expect from established charts: security contexts, resource requests and limits, ingress class names, ingress annotations and TLS, HPA behavior and scaling knobs, PDB settings, pod labels/annotations, service account settings, image pull secrets, node selectors, tolerations, affinity, topology spread constraints, priority class, and probe tuning. Defaults should remain safe and minimal, but the templates should not force users to fork the chart for common Kubernetes policy and scheduling requirements. - -Add `GET /healthz` to the Express app as a lightweight operational endpoint. It must not affect `POST /`, the fixed success response shape, the error envelope, or the existing `GET /` 400 POST guidance. Use `/healthz` in Docker Compose healthchecks and Helm liveness/readiness probes. - -Publish chart packages through a standard Helm repository hosted on GitHub Pages using `helm/chart-releaser-action`. Artifact Hub should reference that repository URL; charts are not uploaded directly to Artifact Hub. - -## Key Decisions - -- Use separate chart and app versions. Start the chart at `0.1.0` and set `appVersion` plus the default image tag to the current `package.json` version, `1.8.0`. This follows common Helm practice and allows chart-only fixes without forcing app releases. -- Use `GET /healthz` for health checks. This keeps `GET /` behavior unchanged while giving Compose and Kubernetes a stable endpoint for operational readiness. -- Publish via GitHub Pages plus `helm/chart-releaser-action`. This is the lowest-friction path for a GitHub-hosted project and gives Artifact Hub a normal Helm repository to index. -- Keep app release publishing tag-driven. Docker image publishing remains tied to `vX.Y.Z` tags; chart publishing can run when chart files change on `master`. -- Add Helm verification commands to repo tooling. The executor should add chart lint/render checks alongside existing `pnpm lint` and `pnpm test` expectations. - -## Tradeoffs - -- Lockstep chart and app versions were rejected because chart-only fixes would require app-version-like releases. Independent chart versions are slightly more documentation-heavy but more maintainable. -- OCI chart publishing was deferred because it adds registry layout and Artifact Hub metadata complexity that is not needed for one public chart. -- GitHub Releases-only chart publishing was rejected because Artifact Hub needs an indexable Helm repository or OCI registry, not standalone release assets. -- Adding a health endpoint increases HTTP surface area, but it is operationally useful and safer than using `GET /` or a parse request as a probe. -- HTTP probes are preferred over TCP-only probes because they verify the Express route stack is serving requests, not just that the socket is open. - -## Scope Boundaries - -In scope: - -- Add `GET /healthz` with tests and documentation. -- Add a Compose healthcheck using `/healthz`. -- Add a Helm chart with Deployment, Service, optional Ingress, optional HPA, optional PDB, configurable image, service account, pod labels/annotations, resources, security contexts, probes, supported environment variables, ingress class/annotations/TLS, image pull secrets, pod scheduling controls, topology spread constraints, priority class, and HPA/PDB tuning knobs. -- Add chart documentation and release/versioning guidance. -- Add CI or Makefile verification for Helm linting/rendering. -- Add chart publishing workflow support and Artifact Hub metadata/documentation. - -Out of scope: - -- Changing `POST /` behavior or response shape. -- Changing `GET /` 400 guidance. -- Adding authentication, caching, persistence, distributed rate limiting, or a metrics endpoint. -- Upgrading Node, pnpm, `@mozilla/readability`, jsdom, DOMPurify, or runtime dependency policy. -- Publishing directly to Artifact Hub, because Artifact Hub indexes an external Helm repository or OCI registry. - -## Verification Strategy - -- `pnpm lint`: cheap/medium. Required by repo instructions for docs, API, config, dependency, and general code changes. -- `pnpm test`: cheap/medium. Required because the health endpoint changes HTTP surface area and the chart work touches deployment docs/config. -- New health endpoint tests: cheap. Should verify `GET /healthz` status/body and that `GET /` still returns the existing 400 guidance. -- Helm lint: cheap/medium. Add or document a command such as `helm lint charts/readability-js-server`. -- Helm template/render check: cheap/medium. Add or document a command such as `helm template readability-js-server charts/readability-js-server` to catch template errors with defaults. -- Optional chart packaging check: cheap/medium. Useful if release workflow packages charts, for example `helm package charts/readability-js-server --destination /tmp`. -- Docker build: medium/expensive. Not required for chart-only template changes, but relevant if the health endpoint change should be validated in the container. -- Memory soak: expensive. Not required; the health endpoint and Helm chart should not affect allocation behavior under article parsing load. - -## Decision Log - -- 2026-06-17: User requested planning for issue 53. -- 2026-06-17: User added a health endpoint to scope and requested it be used by Compose and any other relevant deployment surfaces. -- 2026-06-17: Research confirmed Artifact Hub indexes external Helm repositories or OCI registries; it does not require direct artifact upload. -- 2026-06-17: Research confirmed major charts commonly keep chart `version` separate from `appVersion`. -- 2026-06-17: Planning defaults selected: chart `0.1.0`, app `1.8.0`, health path `/healthz`, GitHub Pages chart repository via chart-releaser. diff --git a/.project_planning/2026-06-17_helm-chart/plan.yaml b/.project_planning/2026-06-17_helm-chart/plan.yaml deleted file mode 100644 index 2a891e7..0000000 --- a/.project_planning/2026-06-17_helm-chart/plan.yaml +++ /dev/null @@ -1,122 +0,0 @@ -steps: - - id: step-1 - title: Add health endpoint and tests - scope: Add a lightweight `GET /healthz` endpoint to the Express app and cover it with tests while preserving the existing `POST /` API and `GET /` 400 guidance. - files: - - src/app.js - - test/validation.test.js - - README.md - constraints: - - Do not change the success response field set from `src/response.js`. - - Do not change the error envelope or status-code contract. - - Keep `GET /` returning the existing 400 guidance. - - Keep the endpoint lightweight; it must not perform upstream fetches or parse work. - acceptance: - - `GET /healthz` returns a successful JSON response suitable for health probes. - - Existing `GET /` guidance still returns 400. - - Health endpoint behavior is documented. - verification: - - pnpm lint - - pnpm test - - - id: step-2 - title: Add Docker Compose healthcheck - scope: Wire the new health endpoint into the example Compose deployment. - files: - - examples/compose.yaml - - README.md - constraints: - - Use the same health path selected for Kubernetes probes. - - Do not require additional services or persistent state. - - Ensure the command works with the published container image contents. - acceptance: - - Compose example includes a healthcheck for the service. - - Documentation mentions the healthcheck and endpoint. - verification: - - pnpm lint - - pnpm test - - - id: step-3 - title: Create production-usable Helm chart - scope: Add a Helm chart for the service with conventional metadata, values, helpers, and Kubernetes manifests for Deployment, Service, optional Ingress, optional HPA, and optional PDB. - files: - - charts/readability-js-server/Chart.yaml - - charts/readability-js-server/values.yaml - - charts/readability-js-server/templates/_helpers.tpl - - charts/readability-js-server/templates/deployment.yaml - - charts/readability-js-server/templates/service.yaml - - charts/readability-js-server/templates/ingress.yaml - - charts/readability-js-server/templates/hpa.yaml - - charts/readability-js-server/templates/pdb.yaml - - charts/readability-js-server/templates/serviceaccount.yaml - - charts/readability-js-server/templates/NOTES.txt - - charts/readability-js-server/README.md - constraints: - - Start chart `version` at `0.1.0`. - - Set `appVersion` and default image tag to the current package version, `1.8.0`. - - Default image repository must be `phpdockerio/readability-js-server`. - - Default container port must match the service default, `3000`. - - Use `/healthz` for readiness and liveness probes. - - Keep optional resources disabled unless their values enable them. - - Expose mature chart configuration: security contexts, resource requests and limits, ingress class/annotations/TLS, HPA behavior and scaling knobs, PDB settings, pod labels/annotations, service account settings, image pull secrets, node selectors, tolerations, affinity, topology spread constraints, priority class, and probe tuning. - - Support environment variables already accepted by `src/config.js`. - acceptance: - - Chart renders successfully with default values. - - Default manifests deploy one replica behind a ClusterIP Service. - - Ingress, HPA, and PDB templates render only when enabled. - - Users can configure common Kubernetes policy and scheduling requirements without forking the chart. - - Chart README documents installation, values, versioning policy, and Artifact Hub expectations. - verification: - - helm lint charts/readability-js-server - - helm template readability-js-server charts/readability-js-server - - pnpm lint - - pnpm test - - - id: step-4 - title: Add Helm verification tooling - scope: Add Makefile and CI support for chart linting and rendering so Helm regressions are checked consistently. - files: - - Makefile - - .github/workflows/build-publish.yaml - - README.md - - AGENTS.md - constraints: - - Preserve existing Node 24 and pnpm 11.7.0 policy. - - Keep existing `pnpm lint` and `pnpm test` checks. - - Use official or widely adopted Helm setup actions in CI. - - Do not require a Kubernetes cluster for verification. - acceptance: - - Local chart verification commands are documented and available through Makefile. - - CI checks Helm lint/template in addition to existing lint/test. - - Repo instructions mention the new Helm verification commands. - verification: - - make helm-lint - - make helm-template - - pnpm lint - - pnpm test - - - id: step-5 - title: Add chart publishing and Artifact Hub metadata - scope: Add GitHub Pages/chart-releaser publishing support and metadata/documentation needed for Artifact Hub indexing. - files: - - .github/workflows/chart-release.yaml - - charts/readability-js-server/Chart.yaml - - charts/readability-js-server/README.md - - artifacthub-repo.yml - - README.md - constraints: - - Artifact Hub should reference the external Helm repository; do not attempt to upload charts directly to Artifact Hub. - - Keep Docker image publishing tag-driven through the existing release workflow. - - Chart publishing should run from chart changes on `master` or an explicit manual trigger. - - Do not introduce OCI publishing unless the maintainer changes the publishing target. - acceptance: - - Chart release workflow packages changed charts and publishes an indexable Helm repository via GitHub Pages. - - README explains how users add the Helm repo and install the chart. - - Artifact Hub metadata is present enough to support repository registration/ownership. - - Release/versioning documentation explains when to bump chart `version` versus `appVersion`. - verification: - - helm lint charts/readability-js-server - - helm template readability-js-server charts/readability-js-server - - helm package charts/readability-js-server --destination /tmp/readability-js-server-chart - - pnpm lint - - pnpm test diff --git a/.project_planning/2026-06-17_helm-chart/research.md b/.project_planning/2026-06-17_helm-chart/research.md deleted file mode 100644 index 80096db..0000000 --- a/.project_planning/2026-06-17_helm-chart/research.md +++ /dev/null @@ -1,65 +0,0 @@ -## Question - -Planning issue https://github.com/phpdocker-io/readability-js-server/issues/53: add a Helm chart, publish it for Artifact Hub discovery, and include a new health endpoint for Compose/chart probes. - -## Findings - -Helm has two separate version concepts. `Chart.yaml version` is the chart package version, must be SemVer, and is used by Helm packaging/repository tooling. `appVersion` is informational application version metadata and is explicitly unrelated to chart version calculations: https://helm.sh/docs/topics/charts/ - -Established charts mostly treat them as independent, even if both move during app releases: - -- Bitnami nginx: chart `version: 22.1.1`, `appVersion: 1.29.1`: https://github.com/bitnami/charts/blob/main/bitnami/nginx/Chart.yaml -- ingress-nginx: chart `version: 4.15.1`, `appVersion: 1.15.1`: https://github.com/kubernetes/ingress-nginx/blob/main/charts/ingress-nginx/Chart.yaml -- kube-prometheus-stack: chart `version: 86.2.3`, `appVersion: v0.91.0`: https://github.com/prometheus-community/helm-charts/blob/main/charts/kube-prometheus-stack/Chart.yaml -- Grafana: chart `version: 10.5.14`, `appVersion: 12.3.1`: https://github.com/grafana/helm-charts/blob/main/charts/grafana/Chart.yaml - -Artifact Hub indexes Helm chart repositories and OCI Helm charts, not arbitrary GitHub Releases by themselves. Most metadata comes from `Chart.yaml`, `README`, and `LICENSE`; optional Artifact Hub annotations and `artifacthub-repo.yml` improve presentation, ownership claim, and verified publisher support: https://artifacthub.io/docs/topics/repositories/helm-charts/ and https://artifacthub.io/docs/topics/annotations/helm/ - -Publishing options: - -- GitHub Pages + `helm/chart-releaser-action`: conventional for GitHub-hosted projects; creates chart GitHub Releases, updates `index.yaml`, and serves a Helm repo from Pages. Artifact Hub can index that repo URL. Docs: https://helm.sh/docs/howto/chart_releaser_action/ -- OCI registry: supported by Helm and Artifact Hub. Helm OCI chart refs require chart name as basename and chart SemVer as tag; Artifact Hub requires one OCI repository per chart and special handling for `artifacthub-repo.yml`. Docs: https://helm.sh/docs/topics/registries/ and https://artifacthub.io/docs/topics/repositories/helm-charts/ -- GitHub Releases-only: insufficient for Artifact Hub discoverability unless paired with an `index.yaml` served as a Helm repo. This is what chart-releaser adds. -- Manual static Helm repo: possible, but chart-releaser automates the boring parts and matches GitHub Pages well. - -## Implications - -For this repo, the pragmatic path is to use a normal HTTP Helm repository on GitHub Pages, published by `helm/chart-releaser-action`, and register that repo URL in Artifact Hub. This fits the existing GitHub Release workflow without requiring an OCI registry decision or ORAS metadata flow. - -Use `package.json` as the app source of truth and set chart `appVersion` plus default image tag from that app version. Keep chart `version` as a chart package version, not necessarily identical to app version. - -Recommended versioning policy: - -- Initial chart could be `0.1.0` or `1.8.0`; `0.1.0` is cleaner if treating the chart as a new artifact. -- On every app release where the chart's default image tag changes, bump chart `version` and `appVersion`. -- For chart-only fixes, bump only chart `version`; leave `appVersion` unchanged. -- Avoid lockstep-only chart versioning unless you are comfortable forcing app releases for chart-only fixes. - -The health endpoint should be planned as part of the app contract before probes land. Current rules say `POST /` is the API and `GET /` must keep returning 400 guidance, so a separate endpoint such as `GET /healthz` or `GET /health` is the least disruptive option. It needs tests because it changes HTTP surface area and will be consumed by Docker Compose and Kubernetes probes. - -## Risks and Uncertainties - -Lockstep chart/app versions are simpler for humans but awkward for chart-only fixes. - -Independent chart versions are standard Helm practice but require release documentation so maintainers know when to bump `version` versus `appVersion`. - -OCI publishing is viable, but adds registry naming, auth, Artifact Hub metadata, and one-repo-per-chart considerations. It is probably unnecessary for this small repo unless the maintainer specifically wants registry-only distribution. - -GitHub Pages introduces a new repo setting/branch dependency. Artifact Hub indexing depends on a stable public `index.yaml`. - -## Sources - -- Helm Chart.yaml version and appVersion docs: https://helm.sh/docs/topics/charts/ -- Helm chart repository guide: https://helm.sh/docs/topics/chart_repository/ -- Helm chart-releaser guide: https://helm.sh/docs/howto/chart_releaser_action/ -- Helm OCI registry docs: https://helm.sh/docs/topics/registries/ -- Artifact Hub Helm repository docs: https://artifacthub.io/docs/topics/repositories/helm-charts/ -- Artifact Hub repository ownership/verified publisher docs: https://artifacthub.io/docs/topics/repositories/ -- Artifact Hub Helm annotations: https://artifacthub.io/docs/topics/annotations/helm/ -- Issue #53: https://github.com/phpdocker-io/readability-js-server/issues/53 - -## Open Questions - -- Should the chart be versioned from `0.1.0` as a new artifact, or start at the current app version `1.8.0` for simpler release alignment? -- Preferred health path: `/healthz`, `/health`, or `/readyz`? -- Should chart publishing run only on app tags, or also on chart changes merged to `main`?