Skip to main content

Manage Files in Dynamically Registered Git Repositories

· 6 min read
Jake Klein
Engineer @ Syntasso

Application repositories contain much more than application code. Platform teams often need to manage agent instructions, linter configuration, CI workflows, security policies, dependency rules, or service catalogue metadata.

Making the first commit is the easy part. The real challenge comes later, when the organisation changes its standards. Someone then needs to find every repository, update the right files, correct manual drift, and remove files that are no longer required.

A Kratix Promise can own that complete lifecycle. The user identifies their Git repository and describes what they need. The Promise generates the desired files, and Kratix schedules them to that repository. When the platform team updates the Promise, Kratix rolls the change out to the repositories it manages. It also corrects drift and removes the managed files when their Resource requests are deleted.

The files become managed platform resources, not the forgotten output of a one-off scaffolding task.

Worked example: managing a security baseline

To demonstrate the pattern, we built a SecureRepo Promise. It manages a small security baseline in a repository, customised from the user's input.

A Resource request starts a Promise workflow, which registers a Git repository as a Kratix Destination and schedules managed files to it. Promise and Resource updates continue through the same lifecycle.

A Promise dynamically registers a Git repository and manages its files over time

The Promise API

The API captures the repository and the inputs used to generate its security configuration:

promise.yaml (API excerpt)
spec:
api:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: securerepos.example.kratix.io
spec:
group: example.kratix.io
names:
kind: SecureRepo
plural: securerepos
singular: securerepo
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
repo:
type: string
language:
type: string
securityContact:
type: string
scanSchedule:
type: string
required:
- repo
- language
- securityContact

The example request onboards repo-alpha:

apiVersion: example.kratix.io/v1alpha1
kind: SecureRepo
metadata:
name: repo-alpha
namespace: default
spec:
repo: repo-alpha
language: go
securityContact: security@example.com
scanSchedule: weekly

This example combines spec.repo with a platform-configured Gitea URL; the API could accept a complete repository URL instead.

The Promise workflow

The Promise workflow performs five steps:

  1. Combines the platform-configured Gitea URL with spec.repo.
  2. Creates a GitStateStore for the repository through the Kubernetes API.
  3. Creates a Destination that references the GitStateStore and labels it with the repository name.
  4. Writes the managed files to /kratix/output.
  5. Writes a dynamic Destination selector matching the repository label.

The credentials remain a platform concern. The generated GitStateStore references an existing Secret, so users do not include credentials in their requests.

This abridged version of the workflow shows the scheduling mechanics. The complete Promise includes the generated file contents and schedule selection.

promise.yaml (workflow excerpt)
workflows:
resource:
configure:
- apiVersion: platform.kratix.io/v1alpha1
kind: Pipeline
metadata:
name: configure
spec:
rbac:
permissions:
- resourceNamespace: "*"
apiGroups: ["platform.kratix.io"]
resources: ["gitstatestores", "destinations"]
verbs: ["get", "create", "update", "patch"]
containers:
- name: configure
image: ghcr.io/syntasso/kratix-pipeline-utility:v0.0.4
envFrom:
- secretRef:
name: gitea-credentials
command:
- python3
- -c
- |
import subprocess
from textwrap import dedent
import kratix_sdk as ks

sdk = ks.KratixSDK()
spec = sdk.read_resource_input()
repo = spec.get_value("spec.repo")

resources = dedent(f"""
apiVersion: platform.kratix.io/v1alpha1
kind: GitStateStore
metadata:
name: secure-{repo}
spec:
url: <GIT_BASE_URL>/{repo}
branch: main
secretRef:
name: gitea-credentials
namespace: default
---
apiVersion: platform.kratix.io/v1alpha1
kind: Destination
metadata:
name: secure-{repo}
labels:
repo: {repo}
spec:
path: "."
filepath: {{ mode: none }}
strictMatchLabels: true
initWorkloads: {{ enabled: false }}
stateStoreRef:
name: secure-{repo}
kind: GitStateStore
""")
subprocess.run(
["kubectl", "apply", "-f", "-"],
check=True,
input=resources.encode(),
)

# File contents are generated from the Resource request.
sdk.write_output("SECURITY.md", security_md.encode())
sdk.write_output(
".github/workflows/security.yml",
scan_workflow.encode(),
)
sdk.write_destination_selectors([
ks.DestinationSelector(match_labels={"repo": repo})
])
Prefer GitOps for a long-term implementation

This example uses kubectl apply to keep creation of the GitStateStore and Destination easy to follow. A stronger long-term approach is to write those resources to a separate directory under /kratix/output, then use Kratix destination selectors to schedule them to the Platform Destination. This uses the same GitOps scheduling mechanism and keeps their lifecycle declarative.

The omitted generation logic writes securityContact into SECURITY.md and uses scanSchedule to select a daily or weekly cron expression for .github/workflows/security.yml. The language can select an appropriate scanner and configuration.

When the workflow finishes, Kratix schedules the output to the new Destination. If that Destination is not ready yet, the Work remains unscheduled until it becomes available. Kratix then commits the files to the repository. The workflow does not need to clone the repository, create commits, or poll for readiness.

Next, we apply the example request to see it in action:

kubectl apply --filename request-repo-alpha.yaml
The repo-alpha repository containing SECURITY.md, a GitHub workflows directory, and Kratix ownership metadata

Kratix commits the security baseline to the user-provided repository

The Destination uses filepath.mode: none, so the files retain their paths from /kratix/output. Kratix also writes a state file under .kratix, recording which files belong to the WorkPlacement.

A Gitea commit showing the generated security workflow, Kratix ownership metadata, and SECURITY.md

The commit contains the managed files and Kratix ownership metadata

Managed beyond the first commit

The first commit begins the management lifecycle. If the organisation adopts a new scanner or changes its reporting policy, the platform team updates the Promise once. Kratix reruns the workflow for existing requests and rolls the updated files out to their repositories. If a managed file drifts, Kratix restores the desired content.

When the Resource request is deleted, Kratix removes the files associated with its Work. Only the files owned by that request are removed. The rest of the repository remains under the application team's control.

The same pattern works for agent instructions, linters, CI workflows, catalogue metadata, and many other repository files. The Promise defines the desired state, and Kratix looks after it for as long as the request exists.

Want to explore more patterns? Browse the Kratix Guides to learn about workflow controls, waits in Pipelines, Compound Promises, and other ways Kratix can make platform teams' lives easier.