Files
tiny-forge/docs/gitops.md
T
alexei.dolgolyov 7733e64b08 feat(gitops): config-as-code via .tinyforge.yml for repo-backed workloads
A dockerfile or static workload can opt in to reading its deploy config from a
.tinyforge.yml in its own repo. Tinyforge fetches the file, shows field-level
drift vs the live config, and an admin applies it with an explicit Sync. The
repo becomes the source of truth for the declared fields. Manual-sync only;
no auto-apply on deploy, no multi-workload reconcile, no create/delete in v1.

Scope is deliberately source-aware and source_config-resident: dockerfile
declares port/healthcheck/deploy_strategy, static declares deploy_strategy.
The file never carries repo coords or secrets (those stay in the encrypted
DB), which keeps credentials out of the repo.

Backend:
- internal/gitops: Spec/ParseSpec (KnownFields rejects unknown keys), a
  source-aware ApplyPlan/BuildPlan, MergeAndValidate (omitted-field-preserving
  deep merge + validate-the-merged-result-then-commit — never a partial
  config), declared-only Drift with normalization, and Fetch with
  ok/no_file/fetch_failed/invalid statuses and token-redacted messages.
- staticsite: DownloadFile added to GitProvider + Gitea/GitHub/GitLab impls,
  reusing each provider's SSRF-safe client; 64 KiB cap; ErrFileNotFound.
- store: 4 additive gitops_* columns + setters (disjoint from UpdateWorkload
  so the edit-form save and a sync never clobber each other).
- api: GET /workloads/{id}/gitops (status + raw + live drift + managed_fields),
  PUT /gitops (admin, enable/path, traversal-safe), POST /gitops/sync (admin,
  per-workload locked read->merge->validate->write, audited to event_log).

Frontend:
- GitOpsPanel.svelte: status pill, a purpose-built field-level drift view,
  .tinyforge.yml preview, enable ToggleSwitch, Sync via ConfirmDialog; all five
  statuses handled, admin affordances gated on the real viewer role.
- GitOps-managed badge (list + detail hero) and a read-only edit-form banner.
- api.ts fetchers + types; i18n apps.detail.gitops.* (en + ru parity).

Built phase-by-phase with an adversarial plan review (caught 5 design flaws
pre-implementation) and an independent review per phase (go / security / ts /
final) — all APPROVE, 0 CRITICAL/HIGH. docs/gitops.md documents the schema and
what's intentionally out of v1. Plan: plans/gitops/.
2026-06-21 23:32:02 +03:00

3.9 KiB

GitOps: config-as-code with .tinyforge.yml

A dockerfile or static workload can read part of its deploy config from a .tinyforge.yml file in its own repo. Tinyforge fetches the file, shows you how it differs from the live config (drift), and applies it when you click Sync — so the repo becomes the source of truth for the declared fields.

This is opt-in per workload and manual-sync only in v1: nothing is applied automatically on deploy, and a sync never runs without an explicit admin action.

Enabling it

  1. Open the workload (Apps → your app).
  2. In the GitOps panel, toggle it on. The default file path is .tinyforge.yml at the repo root; change it if your file lives elsewhere (e.g. deploy/.tinyforge.yml).
  3. Add a .tinyforge.yml to the repo (schema below) and push.
  4. The panel shows the parsed file and any drift vs. the live config. Click Sync now to apply the repo's values to the workload.

Only dockerfile and static sources are eligible — they're the git-backed sources. image and compose workloads don't show the panel.

.tinyforge.yml schema (v1)

version: 1            # required, must be 1
deploy:
  # dockerfile only:
  port: 8080          # container port the app listens on
  healthcheck: /healthz   # HTTP path probed before a blue-green cutover ("" to disable)
  # dockerfile + static:
  deploy_strategy: blue-green   # "" | recreate | blue-green

Notes:

  • Only the fields above are honored. Unknown keys are rejected with an error (so a typo surfaces instead of being silently ignored).
  • Fields you omit are left untouched — the file overlays only what it declares; it never clears the rest of your config.
  • The file is source-aware: a static workload only honors deploy_strategy (a static site has no port/healthcheck); port/healthcheck in a static site's file are ignored.
  • deploy_strategy: "" and recreate are equivalent (both are the default for dockerfile and static), so they never show as drift against each other.

What .tinyforge.yml does not contain

  • No repo location (provider / owner / repo / branch) and no access token — those stay in Tinyforge's encrypted database. This is deliberate: it keeps credentials out of your repo. (You need the repo coords to find the file in the first place, so they can't live in it.)

Drift and sync

  • Drift is computed only over the fields the file declares, after normalization (so a defaulted strategy or a YAML-int vs stored-number difference isn't a false positive).
  • Sync fetches the file, merges the declared fields onto a copy of the live config, validates the merged result with the source's own rules, and only persists it if it passes — a bad file is rejected as a whole and never leaves a partial config. The sync is recorded to the workload's activity log (not the deploy ledger — it changes config, it isn't a deploy).
  • While GitOps is enabled, the edit form shows a banner noting which fields the repo manages; editing them in the UI works, but the next Sync overwrites them with the repo's values.

Not in v1 (planned)

These are intentionally out of scope for the first version; the design leaves clean seams for them:

  • env and faces (public subdomains) — they live in separate stores and (for env) would re-introduce a secrets-in-repo risk; deferred to a typed multi-target apply.
  • Auto-apply on deploy — applying the repo config automatically on every push. v1 keeps a human in the loop with the drift view + manual Sync. When added, it will read the file at the exact deployed commit (a source-plugin concern), not at dispatch time.
  • Multi-workload reconcile — one repo declaring/creating/deleting many workloads (the full Flux/Argo model). v1 is per-workload, config-only, with no create/delete.
  • image / compose sources — not git-backed / overlapping config surface.