Skip to content

feat: git sync — webhook, prune, manual flow (Plan C of issue #16)#140

Merged
michaelmcnees merged 16 commits into
mainfrom
feat/git-sync-complete
May 30, 2026
Merged

feat: git sync — webhook, prune, manual flow (Plan C of issue #16)#140
michaelmcnees merged 16 commits into
mainfrom
feat/git-sync-complete

Conversation

@michaelmcnees

Copy link
Copy Markdown
Collaborator

Summary

Plan C (final) for issue #16 — completes the core GitOps loop. Plans A (#133) and B (#134) are already merged.

  • Prune — migration 020 adds workflow_definitions.disabled_at and a git_repo_workflows join table. The sync engine records each seen workflow per repo, then disables any that disappeared when prune: true. Reappearing files clear disabled_at on the next sync. History preserved.
  • Webhook receiverPOST /hooks/git/<repo-id> verifies HMAC-SHA256 via X-Hub-Signature-256, emits git.push.received, and fires SyncRepo in a goroutine so the provider gets its 202 fast. Body size capped at 1 MiB. Constant-time HMAC comparison.
  • Manual flowmantle repos plan <name> previews pending changes without writing; mantle repos apply <name> syncs on demand for auto_apply: false repos.
  • Delete cleanuprepo.Store.Delete now removes the cloned working tree under <artifact>/git/<repo-id>/ when a repo is unregistered. DB ON DELETE CASCADE handles the join-table rows automatically.
  • URL sanitizationsanitizeURL strips user:password@ from URLs in audit metadata and last_sync_error, so go-git errors echoing a clone URL can't leak inline credentials.
  • mantle repos update — wires the existing Store.Update into a CLI subcommand for in-place edits.
  • --webhook-secret flag on repos add and repos update so operators can arm HMAC verification without raw SQL.
  • Audit eventsgit.sync.pruned, git.push.received.

What operators can do with A + B + C

  • Register repos (CLI or git_sync.repos: in mantle.yaml)
  • Run mantle serve — reconciler materializes config, poller syncs each enabled auto_apply: true repo at poll_interval
  • Set up push webhooks (POST /hooks/git/<id> + X-Hub-Signature-256) for immediate syncs
  • Preview (plan), manually apply (apply), or force-sync (sync) on any repo
  • Delete a repo — DB row + cloned working tree + join-table rows all clean up
  • Pruned workflows survive as disabled versions in history, ready to be re-enabled if the file reappears

Closes the core GitOps loop of #16.

Test plan

  • go test ./... passes in CI
  • go test ./internal/repo/sync/ — prune, URL sanitization, PlanRepo
  • go test ./internal/repo/ — GetByID, Delete cleanup, webhook_secret persistence
  • go test ./internal/server/ — webhook handler (202 / 403 / 404)
  • go test ./internal/cli/ — repos plan, apply, update, webhook-secret

🤖 Generated with Claude Code

michaelmcnees and others added 14 commits May 29, 2026 23:05
Adds disabled_at column to workflow_definitions and git_repo_workflows
join table to support prune behavior for GitOps sync (issue #16).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Pruned field to Report, call RecordSeen+Reenable per file, and
drive Disable+audit+RemoveSeenRecords for stale workflows when Prune
is set. Use DB clock for syncStart to avoid Go/Postgres time skew.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Registers POST /hooks/git/<repo-id>: HMAC-SHA256 verifies against
webhook_secret, emits git.push.received audit event, fires SyncRepo
in a goroutine for immediate 202 response. Adds Store.GetByID
(no team_id filter) and wires RepoStore/GitDriver into Server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a cloneBasePath parameter to Store.Delete so the CLI removes the
on-disk clone directory (filepath.Join(cloneBasePath, repoID)) after the
DB transaction commits. Filesystem failures are logged via slog.Warn but
never fail the call — an orphan directory is preferable to an orphan row.
The CLI resolves the path from cfg.Storage.Path (same fallback as serve).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds PlanRepo (dry-run: pull + discover + classify without any DB writes
or audit events), and exposes it via `mantle repos plan <name>`. Also adds
`mantle repos apply <name>` as an explicit manual-sync alias for operators
using auto_apply:false repos. Refactors newReposSyncCommand to share
buildSyncDriver so all three commands use identical driver-selection logic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add --webhook-secret flag to repos add and repos update (blocker)
- Remove stray fmt.Println from webhook test
- Clarify plan/apply/update help text, mention commands in repos Long
- Sanitize last_sync_error for pull/discover failure paths

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gosec G118 (CWE-400) flags goroutines that use context.Background()
when a request-scoped context is available. Two fixes:

- emitGitAudit: called synchronously while the request is still in
  flight, so r.Context() is correct and preserves trace/deadline info.
- background SyncRepo goroutine: context.WithoutCancel(r.Context())
  propagates request values (e.g. trace IDs) without tying the
  goroutine's lifetime to the HTTP request, so it survives after the
  202 response is sent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add WebhookSecret field to Repo struct (was dropped during rebase)
- Restore reposCtx to return (context.Context, *config.Config); Plan C
  tests call store.Get with a real context — the 1-return form was wrong
- Update govulncheck exclusions to include go-git transitive dep vulns
  (matching feat/git-sync-engine) and refactor with KNOWN variable

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@michaelmcnees, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 14 minutes and 11 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5be6a4c7-deb4-443c-8367-057bff43b8cb

📥 Commits

Reviewing files that changed from the base of the PR and between 7d76ee8 and 117540f.

📒 Files selected for processing (21)
  • CLAUDE.md
  • packages/engine/internal/audit/audit.go
  • packages/engine/internal/cli/repos.go
  • packages/engine/internal/cli/repos_test.go
  • packages/engine/internal/cli/serve.go
  • packages/engine/internal/db/migrations/020_git_sync_prune.sql
  • packages/engine/internal/db/migrations/021_git_repo_webhook_secret.sql
  • packages/engine/internal/repo/store.go
  • packages/engine/internal/repo/store_test.go
  • packages/engine/internal/repo/sync/engine.go
  • packages/engine/internal/repo/sync/engine_test.go
  • packages/engine/internal/repo/sync/prune.go
  • packages/engine/internal/repo/sync/prune_test.go
  • packages/engine/internal/repo/sync/sanitize.go
  • packages/engine/internal/repo/sync/sanitize_test.go
  • packages/engine/internal/repo/types.go
  • packages/engine/internal/server/git_webhook.go
  • packages/engine/internal/server/git_webhook_test.go
  • packages/engine/internal/server/server.go
  • packages/engine/internal/workflow/store.go
  • packages/engine/internal/workflow/store_test.go
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/git-sync-complete

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f07f8df758

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 88 to +92
`INSERT INTO git_repos
(team_id, name, url, branch, path, poll_interval, credential, auto_apply, prune)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
(team_id, name, url, branch, path, poll_interval, credential, auto_apply, prune, webhook_secret)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING id, name, url, branch, path, poll_interval, credential,
auto_apply, prune, enabled, created_at, updated_at`,
auto_apply, prune, enabled, webhook_secret, created_at, updated_at`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the webhook_secret migration before using it

This insert now references git_repos.webhook_secret, but the only migrations in this commit still leave that column absent (019_git_repos.sql explicitly says it is not created, and 020_git_sync_prune.sql only adds prune metadata). On a freshly migrated database, mantle repos add and the new webhook-secret tests fail with column "webhook_secret" does not exist, so repo registration is broken until a migration adds the column.

Useful? React with 👍 / 👎.

Comment on lines +66 to +69
Metadata: map[string]string{"name": repoRow.Name},
})

// Fire sync in the background so the provider gets its 202 fast.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the repo team when syncing from webhooks

For webhooks on repos owned by any non-default team, this goroutine calls SyncRepo with the unauthenticated HTTP request context after GetByID deliberately bypasses team scoping. SyncRepo/workflow.Save derive team_id from auth.TeamIDFromContext, which falls back to the default team without an authenticated user, so a push to another team's repo writes or prunes workflow definitions in the default team instead of the repo's team.

Useful? React with 👍 / 👎.

Michael McNees and others added 2 commits May 29, 2026 23:14
Migration 019 intentionally deferred this column to Plan C. The column
is required by store.Create and store.Update which pass webhook_secret
as parameter $10 in the INSERT/UPDATE statements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
store.Get selected 15 columns but did not include webhook_secret, so
the field always read back as empty string. Added it to the SELECT and
Scan alongside the other nullable fields, matching store.GetByID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@michaelmcnees michaelmcnees merged commit 1bd8cbf into main May 30, 2026
7 checks passed
@michaelmcnees michaelmcnees deleted the feat/git-sync-complete branch May 30, 2026 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant