Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,17 @@ prerequisite (Longhorn); the rest are convenience + robustness.
is an OIDC IdP; point `auth.oidc` at it (OIDC backend shipped #110).

### I. GitOps
- [ ] **I1 — plan-on-PR (optional).** Surface a dry-run diff on a pull request
before merge, for a PR-gated Driftless workflow (openctl reconciles on
merge/webhook today — B1–B3; PR-preview was deferred to Argo). Enhancement.
- [x] **I1 — plan-on-PR.** A reusable GitHub Actions workflow template
(`examples/gitops-pr-plan/`, kept out of `.github/workflows/` so it doesn't
self-activate on openctl's own repo) that runs `openctl plan` (K7) on every
PR touching manifests and posts the apply order + `$ref` dependency graph
as a **sticky** PR comment — the PR-gated front half of the DriftlessAF
loop (openctl reconciles on merge/webhook via B1–B3). Offline (no
controller), self-contained (only `gh` + `pull-requests: write`), and a
dependency **cycle** makes `openctl plan` exit non-zero → the check fails
and blocks merge. README covers adoption. Per-resource dry-run **diffs**
stay server-only (K7) and are intentionally out of scope for this offline
preview.

### J. App catalog / examples
- [x] **J1 — example manifests** under `examples/homelab/` — the full stack
Expand Down
64 changes: 64 additions & 0 deletions examples/gitops-pr-plan/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# plan-on-PR (I1)

A reusable GitHub Actions workflow that runs [`openctl plan`](../../internal/cli/plan.go)
on every pull request touching your manifests and posts the apply order + `$ref`
dependency graph as a **sticky PR comment** — so a reviewer sees *what will
happen, in what order, and what waits on what* before merge.

This is the PR-gated front half of the DriftlessAF loop: the PR shows the plan;
the controller reconciles on merge (via `manifests.gitops` pull/webhook — see
[../../docs/gitops-modes.md](../../docs/gitops-modes.md)).

## Adopt it

1. Copy [`plan.yml`](plan.yml) into your **infra** repo at
`.github/workflows/openctl-plan.yml`.
2. Set `MANIFESTS` (in the workflow `env`) to where your manifests live
(default `manifests`). It accepts a directory (walked recursively) or
space-separated `-f` paths.
3. Adjust the `on.pull_request.paths` globs to match your layout.
4. Optionally pin `go install …/openctl@vX.Y.Z` to a release for reproducible
checks.

That's it — no controller connection, no secrets. The workflow needs only
`pull-requests: write` (already declared) to post the comment.

## What the comment shows

```
### openctl plan — ✅ plan computed

Plan: 11 resource(s), 2 wave(s)

Apply order:
wave 1:
Cluster/home
Tunnel/home
wave 2:
DNSRecord/chat ← Tunnel/home
HelmRelease/ollama ← Cluster/home
Platform/home ← Cluster/home
...

External references (must already exist — not in this set):
HelmRelease/ollama → Cluster/home
```

- **Waves** are topological levels — resources in one wave have no
inter-dependency and apply concurrently.
- **`← Kind/Name`** is what a resource waits on (a `$ref` into another resource
in the set).
- **External references** are `$ref` targets *not* in the changed set — they
must already exist in the cluster.

## Gating

`openctl plan` exits non-zero on a **dependency cycle**, which fails the check
and blocks the merge (the comment shows the offending resources).

## Scope

This is the **offline** preview: ordering + the dependency graph. Per-resource
dry-run **diffs** (spec drift vs the live cluster) are server-only — they need a
running controller and its `DryRunApply` — and are intentionally not part of
this workflow. See the K7 notes in [../../ROADMAP.md](../../ROADMAP.md).
87 changes: 87 additions & 0 deletions examples/gitops-pr-plan/plan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# openctl plan-on-PR — a reusable GitHub Actions workflow TEMPLATE.
#
# Copy this into YOUR infra repo at .github/workflows/openctl-plan.yml. It runs
# `openctl plan` on every pull request that touches your manifests and posts the
# apply order + $ref dependency graph as a sticky PR comment, so a reviewer sees
# "what will happen, in what order, and what waits on what" before merge — the
# PR-gated half of the DriftlessAF loop (openctl reconciles on merge/webhook;
# see manifests.gitops in the controller config).
#
# It is offline: `openctl plan` needs no controller connection. Per-resource
# dry-run diffs are server-only and are NOT included (see docs/gitops-modes.md
# and the K7 plan-preview notes). A dependency cycle makes `openctl plan` exit
# non-zero, which fails the check and blocks the merge.
#
# It lives under examples/ in the openctl repo on purpose — it must NOT run on
# openctl's own repo; it's a template for yours.
name: openctl plan

on:
pull_request:
# Adjust to where your manifests live.
paths:
- "manifests/**"
- "**/*.cue"
- "**/*.yaml"
- "**/*.yml"

permissions:
contents: read
pull-requests: write # to post/update the sticky comment

jobs:
plan:
runs-on: ubuntu-latest
env:
# Directory (or space-separated -f args) to plan. Override as needed.
MANIFESTS: manifests
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.25"

- name: Install openctl
# Pin @vX.Y.Z for reproducible checks once you track a release.
run: go install github.com/openctl/openctl/cmd/openctl@latest

- name: Run openctl plan
id: plan
run: |
set +e
out="$(openctl plan -f "$MANIFESTS" 2>&1)"
code=$?
set -e
echo "$out"
# Stash the output for the comment step (multiline-safe).
{
echo 'output<<PLAN_EOF'
echo "$out"
echo 'PLAN_EOF'
} >> "$GITHUB_OUTPUT"
echo "code=$code" >> "$GITHUB_OUTPUT"

- name: Post sticky PR comment
if: always()
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
OUT: ${{ steps.plan.outputs.output }}
CODE: ${{ steps.plan.outputs.code }}
run: |
marker='<!-- openctl-plan -->'
if [ "$CODE" = "0" ]; then status='✅ plan computed'; else status='❌ plan failed (see below — a dependency cycle blocks merge)'; fi
body="$(printf '%s\n### openctl plan — %s\n\n```\n%s\n```\n' "$marker" "$status" "$OUT")"
# Sticky: update an existing openctl-plan comment if present, else create.
cid="$(gh api "repos/$REPO/issues/$PR/comments" --jq ".[] | select(.body | contains(\"$marker\")) | .id" | head -n1)"
if [ -n "$cid" ]; then
gh api -X PATCH "repos/$REPO/issues/comments/$cid" -f body="$body" >/dev/null
else
gh api -X POST "repos/$REPO/issues/$PR/comments" -f body="$body" >/dev/null
fi

- name: Fail the check on a plan error
if: always()
run: exit ${{ steps.plan.outputs.code }}
Loading