{"id":46456533,"url":"https://github.com/deviavir/thule","last_synced_at":"2026-06-12T10:00:13.743Z","repository":{"id":337795632,"uuid":"1155245015","full_name":"DeviaVir/thule","owner":"DeviaVir","description":"Plan and diff your kubernetes changes","archived":false,"fork":false,"pushed_at":"2026-04-22T11:20:09.000Z","size":192,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-22T12:33:06.111Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DeviaVir.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-02-11T09:42:11.000Z","updated_at":"2026-04-22T11:20:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/DeviaVir/thule","commit_stats":null,"previous_names":["deviavir/thule"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/DeviaVir/thule","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeviaVir%2Fthule","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeviaVir%2Fthule/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeviaVir%2Fthule/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeviaVir%2Fthule/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DeviaVir","download_url":"https://codeload.github.com/DeviaVir/thule/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DeviaVir%2Fthule/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34238714,"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-12T02:00:06.859Z","response_time":109,"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":[],"created_at":"2026-03-06T01:59:43.800Z","updated_at":"2026-06-12T10:00:13.701Z","avatar_url":"https://github.com/DeviaVir.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Thule\n\n**Thule is a read-only merge request planner for Kubernetes GitOps repositories** — think *Atlantis, but for Flux/kustomize/Helm repos*.\n\nWhen someone opens or updates a merge request against your GitOps repo, Thule renders the Kubernetes resources that MR would produce, diffs them against what is actually running in the target cluster, and posts the result as an MR comment — before anyone merges. It **never applies anything**; Flux (or whatever reconciles your repo) remains the only write path to the cluster.\n\n```\nGitLab MR webhook ──► thule-api ──► Redis queue ──► thule-worker\n                                                       │\n                                          clone repo, discover projects,\n                                          render manifests, diff vs live\n                                          cluster state (read-only)\n                                                       │\n                                    ◄── plan comment + commit status ───┘\n```\n\n## Why\n\nGitOps closes the loop *after* merge: a bad change is discovered when Flux applies it. Thule moves that discovery to review time:\n\n- **Plan comments** — a `terraform plan`-style summary (CREATE / PATCH / DELETE / no-op) per project, with rendered resource detail, posted on the MR and superseded on every push.\n- **Commit statuses** — `thule/plan` reflects plan success/failure, so you can make it a required check.\n- **Policy findings** — built-in checks (e.g. Secret changes, cluster-wide RBAC changes) surface in the plan comment.\n- **Repository guards** — MR-wide rules over the shape of the change set (e.g. \"don't roll the same app in two regions in one MR\"), configured per repo in `.thule.yaml`.\n- **Locking** — Atlantis-style per-project locks prevent conflicting parallel plans on the same paths.\n\n## How it works\n\n1. **`thule-api`** receives GitLab MR webhooks (secret-validated), deduplicates deliveries, and enqueues jobs to Redis.\n2. **`thule-worker`** dequeues, syncs a local clone of the repo, and discovers affected *projects*: for every changed file it walks up the directory tree looking for a `thule.conf`.\n3. For each project it renders the desired state (`yaml`, `kustomize`, `helm`, or `flux` mode), lists the corresponding live resources from the target cluster (read-only; `get`/`list`/`watch` is all the RBAC it needs), and computes a diff.\n4. The aggregated plan is posted as an MR comment (previous plans are marked superseded) and a commit status is set.\n\nStale runs are abandoned when a newer SHA arrives; run records and artifacts are kept for debugging.\n\n## Quick start\n\nPrerequisites: Go 1.22+, and Redis if you run api/worker (unit tests need neither).\n\n```bash\n# run the test suite\ngo test ./...\n\n# render + diff + policy locally, printing the plan comment body\ngo run ./cmd/thule plan --project ./apps/payments --sha local\n```\n\nRun the service pair:\n\n```bash\n# API: receives webhooks, enqueues\nTHULE_API_ADDR=:8080 THULE_WEBHOOK_SECRET=supersecret go run ./cmd/thule-api\n\n# worker: plans\nTHULE_REPO_ROOT=$(pwd) go run ./cmd/thule-worker\n```\n\nTo post real MR comments and commit statuses, give the worker a GitLab token:\n\n```bash\nTHULE_GITLAB_TOKEN=\u003ctoken\u003e \\\nTHULE_GITLAB_PROJECT_PATH=group/your-gitops-repo \\\nTHULE_GITLAB_BASE_URL=https://gitlab.example.com/api/v4 \\\ngo run ./cmd/thule-worker\n```\n\nSee [docs/gitlab-setup.md](docs/gitlab-setup.md) for webhook configuration, `/thule plan` comment command routing, and lock behavior.\n\n## Configuring a repository\n\n### Per-project: `thule.conf`\n\nDrop a `thule.conf` at the root of each *project* (any directory tree that plans as one unit — typically one per cluster or per app). Changed files map to the nearest `thule.conf` above them.\n\n```yaml\nversion: v1\nproject: payments            # name shown in the plan comment\nclusterRef: prod-eu-1        # which cluster credentials to use\nnamespace: payments\nrender:\n  mode: flux                 # yaml | kustomize | helm | flux\n  path: manifests\n  flux:\n    includeKinds:\n      - HelmRelease\n      - Kustomization\ndiff:\n  prune: false               # also report would-be deletions\n  ignoreFields:\n    - metadata.annotations\npolicy:\n  profile: baseline          # baseline | strict\ncomment:\n  maxResourceDetails: 100    # cap rendered detail per comment\n```\n\n`clusterRef` resolves through a credential catalog — see\n[docs/cluster-access-examples.md](docs/cluster-access-examples.md) for GKE and bare-metal examples.\n\n### Repo-wide: `.thule.yaml`\n\nOptional, at the repository root. Where `thule.conf` configures one project, `.thule.yaml` configures MR-wide behavior: guards over the shape of the whole change set, and a follow-up comment posted after each plan.\n\n```yaml\nguards:\n  # Fail when one MR modifies the same app under two or more groups of a\n  # guarded tree laid out as \u003cprefix\u003e/\u003cgroup\u003e/\u003capp\u003e/... -- a \"group\" is\n  # whatever failure domain the path encodes (region, site, shard).\n  - name: prod-regions\n    description: region-redundant apps must roll one region at a time\n    type: same-app-across-groups\n    prefix: clusters/prod\n    exempt:\n      - flux-system\n\nfollowUp:\n  # Posted as a standalone comment after each plan comment (never\n  # superseded). Useful to trigger downstream bots once the plan exists.\n  # Placeholders: {sha}, {summary}.\n  comment: '/review --extra=\"Thule planned {summary} at {sha}\"'\n```\n\nGuard results surface in three places: a banner on top of the plan comment, a `thule/guards` commit status (failed on violation, success once the MR is split; absent when no guarded tree is touched), and loud failure of the status when `.thule.yaml` itself is invalid. Make `thule/guards` a required check to enforce guards rather than just surface them.\n\n## Deployment\n\nBoth binaries ship in one container image (see [Dockerfile](Dockerfile)). A typical production layout:\n\n- `thule-api` Deployment behind an internal ingress, receiving GitLab webhooks\n- `thule-worker` Deployment with: an SSH key for cloning the GitOps repo, a GitLab API token for comments/statuses, kubeconfigs (or workload identity) for the clusters it diffs against\n- Redis as the queue between them (no persistence required; jobs are re-derivable from webhooks)\n\nKeep the worker's cluster credentials **read-only** — Thule plans, it never applies.\n\n## Architecture notes\n\n- Roadmap: [docs/thule-architecture-roadmap.md](docs/thule-architecture-roadmap.md)\n- Implementation phases: [docs/phase0-implementation.md](docs/phase0-implementation.md) … [docs/phase4-implementation.md](docs/phase4-implementation.md)\n- CI: unit + integration workflows with a 90% unit-coverage gate ([docs/ci-testing.md](docs/ci-testing.md))\n\n## Status\n\nFunctional and in production use against GitLab. In-memory adapters exist for every external dependency (queue, run store, comments, statuses, cluster reads), so the full planning pipeline is testable without infrastructure.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeviavir%2Fthule","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeviavir%2Fthule","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeviavir%2Fthule/lists"}