{"id":50634642,"url":"https://github.com/home-operations/konflate","last_synced_at":"2026-06-11T05:00:54.047Z","repository":{"id":362686104,"uuid":"1259878971","full_name":"home-operations/konflate","owner":"home-operations","description":"A read-only pull request review tool for Flux using Flate for rendering.","archived":false,"fork":false,"pushed_at":"2026-06-10T02:09:53.000Z","size":896,"stargazers_count":17,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-10T04:22:01.302Z","etag":null,"topics":["0ver","flate","fluxcd","helmrelease","kubernetes","kustomize"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/home-operations.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-05T00:33:40.000Z","updated_at":"2026-06-10T03:25:15.000Z","dependencies_parsed_at":null,"dependency_job_id":"84abda4c-1ca6-42fc-8751-aecc91051b7e","html_url":"https://github.com/home-operations/konflate","commit_stats":null,"previous_names":["home-operations/konflate"],"tags_count":34,"template":false,"template_full_name":null,"purl":"pkg:github/home-operations/konflate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/home-operations%2Fkonflate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/home-operations%2Fkonflate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/home-operations%2Fkonflate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/home-operations%2Fkonflate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/home-operations","download_url":"https://codeload.github.com/home-operations/konflate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/home-operations%2Fkonflate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34183109,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-11T02:00:06.485Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["0ver","flate","fluxcd","helmrelease","kubernetes","kustomize"],"created_at":"2026-06-07T01:01:42.037Z","updated_at":"2026-06-11T05:00:54.000Z","avatar_url":"https://github.com/home-operations.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# konflate\n\n**Review your GitOps pull requests as _rendered_ Flux diffs — not raw file diffs.**\n\n[![Tests](https://img.shields.io/github/actions/workflow/status/home-operations/konflate/tests.yaml?branch=main\u0026label=tests)](https://github.com/home-operations/konflate/actions/workflows/tests.yaml)\n[![Lint](https://img.shields.io/github/actions/workflow/status/home-operations/konflate/lint.yaml?branch=main\u0026label=lint)](https://github.com/home-operations/konflate/actions/workflows/lint.yaml)\n[![Release](https://img.shields.io/github/actions/workflow/status/home-operations/konflate/release.yaml?branch=main\u0026label=release)](https://github.com/home-operations/konflate/actions/workflows/release.yaml)\n[![License](https://img.shields.io/github/license/home-operations/konflate)](https://github.com/home-operations/konflate/blob/main/LICENSE)\n\n\u003c/div\u003e\n\nA one-line bump to a Flux resource — a `HelmRelease` chart version, an\n`OCIRepository` tag, a `Kustomization` edit — can add, remove, or mutate dozens\nof rendered Kubernetes resources. The git diff shows the line; it doesn't show\nthat. konflate does: it renders the Flux cluster at the PR's **merge-base** and\nat its **head** using [flate](https://github.com/home-operations/flate), diffs\nthe two, and presents the result as a GitHub-style review UI with the blast\nradius, image changes, render failures, and heuristic danger flags surfaced up\nfront.\n\n## How it works\n\n1. konflate lists the open pull requests for one repository from its forge\n   (GitHub / GitLab / Forgejo, cloud or self-hosted) using the native Go SDK.\n2. For each PR it clones the repo, computes `merge-base(head, target)`, and\n   extracts both trees (so changes that landed on the base branch _after_ the PR\n   opened don't pollute the diff — exactly how GitHub computes a PR diff).\n3. It renders the Flux cluster at both trees with flate (two orchestrators\n   sharing one source cache) and pairs the outputs into resource-level changes.\n4. It produces a `DiffResult`: per-resource YAML diffs with server-side syntax\n   highlighting (with word-level intra-line highlighting and expandable folded\n   context), a navigation tree (`HelmRelease`/`Kustomization` → kind →\n   resource), plus the review signals — **impact** (blast radius), **image\n   changes**, **render failures**, and **danger lint** (data-loss, privilege,\n   RBAC, availability).\n\n    The **image changes** signal lists the `container` and `initContainer` image\n    references that changed across _every rendered workload_ — so it captures\n    whatever the charts and kustomizations actually deploy: app images, sidecars,\n    and controller images pulled in by OCI Helm charts alike, each keyed to the\n    workloads that reference it. (A chart's own OCI **artifact** version bump\n    shows up as a changed `HelmRelease`/`OCIRepository` resource in the diff; its\n    effect on the running images surfaces here.)\n\n5. The three-panel web UI renders it — PRs on the left, changed resources in the\n   middle, the diff on the right — and updates live over a websocket as renders\n   complete. Diff rendering runs in a bounded, per-PR-coalescing job queue.\n\nkonflate is **read-only toward your forge**: it never writes comments,\nstatuses, or checks. PRs refresh automatically — each open PR re-renders on a\nconfigurable interval (the missed-webhook backstop), and an authenticated CI\npush or a verified inbound webhook updates one immediately. There is no manual\nrefresh trigger, so a public instance exposes no unauthenticated way to make it\ndo work.\n\n## Quick start\n\n```bash\ndocker run --rm -p 8080:8080 \\\n  -e KONFLATE_REPO='github://onedr0p/home-ops' \\\n  -e KONFLATE_TOKEN=\"$GITHUB_TOKEN\" \\\n  ghcr.io/home-operations/konflate:rolling\n```\n\nOpen \u003chttp://localhost:8080\u003e; konflate lists the open PRs and renders them. The\ntoken is **optional** — without one it works against public repositories, just\nwith the forge's lower unauthenticated API rate limit (see\n[Authentication](#authentication)).\n\n## Helm (Kubernetes)\n\nkonflate publishes an **OCI** Helm chart to `oci://ghcr.io/home-operations/charts/konflate`:\n\n```bash\nhelm install konflate oci://ghcr.io/home-operations/charts/konflate \\\n  --namespace konflate --create-namespace \\\n  --set config.repo='github://onedr0p/home-ops' \\\n  --set secret.token=\"$GITHUB_TOKEN\"\n```\n\nNotable values (see [`charts/konflate/values.yaml`](charts/konflate/values.yaml)):\n\n| Value                                            | Purpose                                                                      |\n| ------------------------------------------------ | ---------------------------------------------------------------------------- |\n| `config.repo` _(required)_                       | the [forge URI](#the-forge-uri) to review                                    |\n| `config.refreshInterval`                         | per-PR auto-refresh / re-list interval (default `30m`)                       |\n| `secret.token` / `.webhookSecret` / `.pushToken` | sensitive env, written to a Secret (or use `secret.existingSecret`)          |\n| `persistence.enabled`                            | keep the flate source cache across restarts (PVC)                            |\n| `ingress.enabled`                                | expose the UI via an Ingress                                                 |\n| `httpRoute.enabled`                              | expose the UI via a Gateway API `HTTPRoute` (set `parentRefs` + `hostnames`) |\n| `monitoring.serviceMonitor.enabled`              | scrape `/metrics` (Prometheus Operator)                                      |\n\nThe pod runs read-only-rootfs as nonroot (65532) with the cache + clone dirs on\nmounted volumes.\n\n## Configuration\n\nAll configuration is via environment variables.\n\n| Variable                    | Default       | Description                                                                                                                                                                                                                                                      |\n| --------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `KONFLATE_REPO`             | _(required)_  | The repository, as a [forge URI](#the-forge-uri), e.g. `github://owner/repo`.                                                                                                                                                                                    |\n| `KONFLATE_TOKEN`            | _(none)_      | Forge API token. **Optional** — read-only auth that raises the API rate limit and unlocks private repos. Gates no feature (see [Authentication](#authentication)).                                                                                               |\n| `KONFLATE_CLUSTER_PATH`     | _(repo root)_ | Directory flate renders from (the GitRepository root that Flux `spec.path` resolves against). Empty = repo root — correct for the standard `./kubernetes/...` layout.                                                                                            |\n| `KONFLATE_WEBHOOK_SECRET`   | _(none)_      | Secret for verifying inbound webhooks. Set it to enable `POST /hooks`; unset ⇒ `501`.                                                                                                                                                                            |\n| `KONFLATE_PUSH_TOKEN`       | _(none)_      | Bearer token for the CI push endpoint. Set it to enable `POST /api/prs/{n}/refresh`; unset ⇒ `501`.                                                                                                                                                              |\n| `KONFLATE_PORT`             | `8080`        | Main HTTP port (UI, API, websocket, webhook).                                                                                                                                                                                                                    |\n| `KONFLATE_METRICS_ADDR`     | `:9090`       | Listen address for the **separate** metrics server. Bind to loopback to keep it private.                                                                                                                                                                         |\n| `KONFLATE_LOG_LEVEL`        | `info`        | `debug`, `info`, `warn`, or `error`.                                                                                                                                                                                                                             |\n| `KONFLATE_LOG_FORMAT`       | `json`        | `json` or `text`.                                                                                                                                                                                                                                                |\n| `KONFLATE_CACHE_DIR`        | XDG cache     | flate source cache (Helm charts, OCI layers, git). Persist it across restarts.                                                                                                                                                                                   |\n| `KONFLATE_CLONE_DIR`        | `$TMPDIR`     | Base directory for ephemeral per-diff clones (cleaned up after each render).                                                                                                                                                                                     |\n| `KONFLATE_MAX_DIFF_CONC`    | _(auto)_      | Max concurrent diff renders. Unset/`0` auto-derives from the CPU budget (GOMAXPROCS, capped at 4); higher = more throughput, more memory.                                                                                                                        |\n| `KONFLATE_REFRESH_INTERVAL` | `30m`         | Go duration. Each open PR re-renders if no webhook refreshed it within this window, and the open-PR list is reconciled this often. The missed-webhook backstop.                                                                                                  |\n| `KONFLATE_CLOSED_PR_MAX`    | `25`          | Max merged PRs kept on the \"recently merged\" shelf below the open list (most-recent win). `0` disables the count cap. This count — not the age — is what bounds memory.                                                                                          |\n| `KONFLATE_CLOSED_PR_TTL`    | `336h`        | How long a merged PR stays on the shelf before pruning (Go duration, e.g. `720h` = 30d). `0` disables the age cap. In-memory: a restart clears the shelf regardless.                                                                                             |\n| `KONFLATE_MERGE_COMMAND`    | _(per forge)_ | Go `text/template` for the **Copy to merge** command shown on the review screen and PR list (konflate never runs it — you paste it into your own shell). Empty = the forge default (`gh`/`glab`/`tea`). Only `.Number` and `.Repo` are exposed, both shell-safe. |\n\nMerged PRs move to a collapsed **Recently merged** group below the open list (their diff is frozen at merge time); abandoned (closed-unmerged) PRs are dropped immediately.\n\n## The forge URI\n\n`KONFLATE_REPO` encodes the forge type, the (optional) self-hosted host, and the\nrepository path in one unambiguous value:\n\n```\nscheme://[host]/path\n```\n\n- **scheme** — `github`, `gitlab`, or `forgejo`.\n- **host** — a self-hosted instance (`host` or `host:port`). Omit entirely for\n  the cloud SaaS (github.com / gitlab.com / codeberg.org).\n- **path** — `owner/repo`, or `group[/subgroup]/repo` for GitLab.\n\n| Forge URI                               | Resolves to                  |\n| --------------------------------------- | ---------------------------- |\n| `github://onedr0p/home-ops`             | GitHub cloud                 |\n| `github://ghe.example.com/team/cluster` | GitHub Enterprise Server     |\n| `gitlab://group/subgroup/cluster`       | GitLab cloud (gitlab.com)    |\n| `gitlab://gl.example.com/group/cluster` | self-hosted GitLab           |\n| `forgejo://me/home-ops`                 | Forgejo cloud (codeberg.org) |\n| `forgejo://git.example.com/me/home-ops` | self-hosted Forgejo          |\n\n## Authentication\n\nThe forge token (`KONFLATE_TOKEN`) is **optional** and used only for forge read\nauth — it raises the API rate limit and unlocks private repositories. It gates\nno behaviour: konflate works the same with or without it.\n\nThe inbound endpoints are gated solely by **their own secret**, independent of\nthe token:\n\n| Endpoint                    | Enabled when…                 | Otherwise |\n| --------------------------- | ----------------------------- | --------- |\n| `POST /hooks`               | `KONFLATE_WEBHOOK_SECRET` set | `501`     |\n| `POST /api/prs/{n}/refresh` | `KONFLATE_PUSH_TOKEN` set     | `501`     |\n\nSo a public, secret-less instance — even one pointed at a repo you don't own —\nexposes no way to make it do work: there is no manual-refresh endpoint, and the\nwebhook/push endpoints return `501` until you set their secret. PRs still stay\ncurrent via the per-PR auto-refresh (see [Triggering\nre-renders](#triggering-re-renders)).\n\n## HTTP endpoints\n\nMain server (`KONFLATE_PORT`):\n\n| Method \u0026 path                 | Purpose                                                                                    |\n| ----------------------------- | ------------------------------------------------------------------------------------------ |\n| `GET /`                       | The web UI.                                                                                |\n| `GET /api/prs`                | Tracked PRs and each one's diff-job status.                                                |\n| `GET /api/prs/{n}/diff`       | A PR's rendered diff (`200` ready/error, `202` still rendering).                           |\n| `POST /api/prs/{n}/refresh`   | **Auth** (bearer `KONFLATE_PUSH_TOKEN`) — re-render one PR. `501` unless the token is set. |\n| `POST /hooks`                 | Verified forge webhook — re-renders the affected PR. `501` unless the secret is set.       |\n| `GET /ws`                     | Websocket stream of diff-job status events.                                                |\n| `GET /healthz`, `GET /readyz` | Liveness / readiness.                                                                      |\n\nOperational server (`KONFLATE_METRICS_ADDR`): `GET /metrics`.\n\n## Triggering re-renders\n\nkonflate lists and renders PRs at startup; after that it keeps them current\nitself, with two optional triggers for immediacy:\n\n**Automatically (always on)** — every open PR re-renders once its last render is\nolder than `KONFLATE_REFRESH_INTERVAL` (default 30m), and the open-PR list is\nreconciled on the same interval to pick up newly opened and merged PRs. This is\nthe missed-webhook backstop and needs no configuration. (Merged PRs are frozen\nand never auto-refresh.) A webhook or push refreshing a PR resets its clock, so\na busy PR isn't needlessly re-rendered and load staggers across PRs.\n\n**From a CI workflow** (`KONFLATE_PUSH_TOKEN` set) — re-render a PR immediately\nafter you push to it:\n\n```bash\ncurl -fsS -X POST \\\n  -H \"Authorization: Bearer ${KONFLATE_PUSH_TOKEN}\" \\\n  https://konflate.example.com/api/prs/${PR_NUMBER}/refresh\n```\n\n**Native webhooks** (authenticated mode, `KONFLATE_WEBHOOK_SECRET` set) — point a\nforge webhook at `https://konflate.example.com/hooks` with the shared secret.\nkonflate verifies the signature with the per-forge scheme automatically:\n\n| Forge   | Header                | Verification                        |\n| ------- | --------------------- | ----------------------------------- |\n| GitHub  | `X-Hub-Signature-256` | HMAC-SHA256, `sha256=` + hex        |\n| Forgejo | `X-Gitea-Signature`   | HMAC-SHA256, bare hex               |\n| GitLab  | `X-Gitlab-Token`      | constant-time compare of the secret |\n\nRate limiting is intentionally **not** built in — put konflate behind your\nreverse proxy / ingress and rate-limit there.\n\n## Metrics\n\nServed on the separate operational port (keep it off your public ingress):\n\n| Metric                           | Type      | Meaning                                |\n| -------------------------------- | --------- | -------------------------------------- |\n| `konflate_diff_jobs_total`       | counter   | Completed renders, by `result`.        |\n| `konflate_diff_duration_seconds` | histogram | Render wall-clock (clone + 2 renders). |\n| `konflate_diff_queue_depth`      | gauge     | PRs queued or rendering.               |\n| `konflate_pull_requests`         | gauge     | Open PRs tracked.                      |\n| `konflate_http_requests_total`   | counter   | Main-server requests, by status class. |\n\nPlus the standard Go runtime and process collectors.\n\n## Development\n\n[mise](https://mise.jdx.dev) is the single source of truth for the toolchain —\nboth the `go` and `node` versions are pinned in `.mise/config.toml`, shared with\n`go.mod` and the container build, and grouped (non-automerged) in Renovate — and\nit is the task runner. The UI is [Svelte 5](https://svelte.dev) + Vite +\nTailwind v4 (all latest), built into `internal/web/dist` and embedded via\n`go:embed`. All UI dependencies are declared in `internal/web/package.json`.\n\n```bash\nmise run ui-install     # install UI deps (npm ci)\nmise run ui-typecheck   # svelte-check\nmise run ui-build       # build the UI bundles into internal/web/dist\nmise run ui-test        # Playwright headless-Chromium UI tests\nmise run build          # go build ./...\nmise run test           # unit + server tests (race-enabled in CI)\nmise run lint           # golangci-lint\nmise run dev            # run konflate locally (set KONFLATE_REPO first)\n```\n\nTests come in three tiers:\n\n- **Unit** — pure logic (config, diff render/lint/impact, engine pairing,\n  webhook crypto, provider mapping) plus the HTTP server and the websocket hub\n  driven over real sockets with a fake engine. Run by `mise run test`.\n- **UI** (`mise run ui-test`) — Playwright drives the real built UI in headless\n  Chromium with the API and websocket stubbed by a fixture, asserting the\n  3-panel render, filtering, and split view. Runs in CI.\n- **Integration** (`-tags integration`, env-gated) — renders a real PR with the\n  real engine; skips unless `KONFLATE_REPO` + `KONFLATE_INTEGRATION_PR` are set:\n\n    ```bash\n    KONFLATE_REPO=github://owner/repo KONFLATE_INTEGRATION_PR=123 \\\n      mise run test-integration\n    ```\n\n## Security\n\nkonflate is designed to be safe to expose internally, and to leak nothing even\nif it were public:\n\n- **Read-only toward forges.** It never writes comments, statuses, checks, or\n  any other forge state.\n- **No secret leakage.** Renders run with flate's missing-secrets allowance, so\n  Kubernetes `Secret` values are never materialized; no API type or log line\n  carries the forge token.\n- **XSS-safe rendering.** Only chroma-produced, HTML-escaped token spans are\n  inserted as markup; every other value is set as text. A strict\n  `Content-Security-Policy` (`script-src 'self'`) blocks injected inline scripts\n  as a backstop.\n- **No unauthenticated trigger surface.** There is no manual-refresh endpoint,\n  and the webhook/push endpoints return `501` until their secret is set. See\n  [Authentication](#authentication).\n- **Constant-time** comparison for the push token and the GitLab webhook token.\n\n## License\n\nSee [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhome-operations%2Fkonflate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhome-operations%2Fkonflate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhome-operations%2Fkonflate/lists"}