Skip to content

feat: Pipeline-as-Code (GitOps)#34

Merged
TerrifiedBug merged 14 commits intomainfrom
feat/gitops
Mar 7, 2026
Merged

feat: Pipeline-as-Code (GitOps)#34
TerrifiedBug merged 14 commits intomainfrom
feat/gitops

Conversation

@TerrifiedBug
Copy link
Copy Markdown
Owner

Summary

  • Add gitOpsMode and gitWebhookSecret fields to the Environment model with database migration
  • Create POST /api/webhooks/git endpoint that receives GitHub push events, verifies HMAC signatures, fetches changed YAML files, and imports pipeline configurations
  • Extend environment.update router to accept gitOpsMode; auto-generates webhook secret when switching to bidirectional mode
  • Add GitOps Mode selector (Off / Push Only / Bi-directional) to the environment Git Integration settings, with webhook URL and secret display
  • Show an info banner in the pipeline editor when the environment uses bidirectional GitOps
  • Add docs/public/operations/gitops.md guide and update environments docs and SUMMARY.md

Test plan

  • Run npx prisma migrate dev and verify the migration applies cleanly
  • Open an environment detail page and verify GitOps Mode selector appears in Git Integration
  • Set mode to Bi-directional, save, verify webhook URL and secret are displayed
  • Set mode back to Off, save, verify webhook secret is cleared
  • Send a test POST to /api/webhooks/git with valid HMAC and a push payload containing YAML changes; verify pipeline is created/updated
  • Send a request with invalid/missing signature; verify 401 response
  • Open the pipeline editor for a pipeline in a bidirectional environment; verify the GitOps banner appears
  • Open the pipeline editor for a pipeline in an "off" environment; verify no banner
  • Verify npx tsc --noEmit passes with no errors

@github-actions github-actions bot added documentation Improvements or additions to documentation feature and removed documentation Improvements or additions to documentation labels Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR introduces Pipeline-as-Code (GitOps) support with three sync modes (Off / Push Only / Bi-directional), a POST /api/webhooks/git endpoint for GitHub push events, and UI integration for webhook configuration and GitOps status display.

All previously flagged security and validation issues have been successfully addressed:

  • Webhook secret is encrypted at rest and decrypted before HMAC comparison
  • JSON payload parsing is guarded with proper error handling
  • Pipeline names are validated against the tRPC schema regex before creation
  • File filtering is scoped to the environment's directory prefix (prevents cross-environment contamination)
  • environment.get is gated with withTeamAccess("VIEWER") and returns only a boolean presence flag for the webhook secret
  • Webhook secret is only returned when freshly generated during mode switches, not on unrelated updates
  • Audit logging is implemented for all pipeline imports with error isolation (failures don't mask successful imports)
  • Token/URL parsing is hoisted outside the per-file loop for efficiency

One correctness issue remains: a malformed YAML file in a push event creates a permanently empty pipeline record in the database when the pipeline is new, because importVectorConfig is called after the initial pipeline creation transaction. This only affects net-new pipelines (updates to existing ones rewrite the graph regardless). A developer pushing a work-in-progress or misconfigured YAML file will leave an empty pipeline shell in the database that appears in the UI but has no nodes or edges.

Confidence Score: 4/5

  • Safe to merge after the empty-pipeline-on-parse-failure bug is fixed via parsing before the transaction.
  • All critical security issues from the initial review have been thoroughly addressed: encryption, validation, authorization, audit logging, and cross-environment contamination prevention. One important correctness bug remains that manifests under normal usage (pushing malformed YAML creates permanently empty pipeline shells for new pipelines), but is straightforward to fix by reordering the parse and transaction steps.
  • src/app/api/webhooks/git/route.ts — specifically lines 178-190, the ordering of importVectorConfig vs. the pipeline find-or-create transaction.

Sequence Diagram

sequenceDiagram
    participant GH as GitHub
    participant WH as POST /api/webhooks/git
    participant DB as PostgreSQL
    participant GHAPI as GitHub Contents API

    GH->>WH: Push event (x-hub-signature-256)
    WH->>DB: findMany bidirectional envs
    DB-->>WH: environments[]
    WH->>WH: HMAC verify each secret (timingSafeEqual)
    alt No match
        WH-->>GH: 401 Invalid signature
    end
    WH->>WH: Parse JSON, validate branch, build envSlug
    WH->>WH: Filter changed files by envSlug/ prefix
    loop each changed YAML file
        WH->>GHAPI: GET /repos/{owner}/{repo}/contents/{file}
        GHAPI-->>WH: file content
        WH->>WH: importVectorConfig(content, "yaml") ⚠️ before transaction
        WH->>DB: $transaction(findFirst or create pipeline) [Serializable]
        DB-->>WH: pipeline record
        WH->>DB: $transaction(deleteMany nodes/edges, createMany nodes/edges)
        WH->>DB: writeAuditLog(gitops.pipeline.imported) with try/catch
    end
    WH-->>GH: 200 { processed, results }
Loading

Comments Outside Diff (1)

  1. src/app/api/webhooks/git/route.ts, line 178-190 (link)

    Empty pipeline persists when YAML parse fails

    The first $transaction (lines 178–186) creates an empty pipeline record and commits before importVectorConfig is called on line 190. If importVectorConfig throws because the repository file is syntactically invalid YAML or has an unrecognized structure, the outer catch at line 278 records an error — but the empty pipeline that was just created is now durably stored in the database with no nodes or edges.

    For an existing pipeline this is harmless (the second transaction, which would have wiped its nodes, is never reached). For a new pipeline it leaves a permanently empty, user-invisible record that will appear in the UI.

    Example trigger: a developer pushes a new my-pipeline.yaml that is malformed YAML or lacks required fields. VectorFlow creates the my-pipeline record in the DB, fails to parse it, reports an error, and the empty pipeline survives.

    Fix: Parse the YAML content before any database writes:

    // Parse and validate content first — bail out early on invalid YAML before any DB writes
    let parsedConfig: ImportResult;
    try {
      parsedConfig = importVectorConfig(content, "yaml");
    } catch (parseErr) {
      results.push({
        file,
        status: "skipped",
        error: `Invalid YAML: ${String(parseErr)}`,
      });
      continue;
    }
    const { nodes, edges, globalConfig } = parsedConfig;
    
    // Only now create/find the pipeline (content already validated)
    const pipeline = await prisma.$transaction(async (tx) => {
      const existing = await tx.pipeline.findFirst({
        where: { environmentId: matchedEnv.id, name: pipelineName },
      });
      if (existing) return existing;
      return tx.pipeline.create({
        data: { name: pipelineName, environmentId: matchedEnv.id },
      });
    }, { isolationLevel: Prisma.TransactionIsolationLevel.Serializable });
    
    // Now update the graph...

Last reviewed commit: 2e608bc

- POST /api/webhooks/git receives GitHub push events, verifies HMAC
  signature, fetches changed YAML files, and imports pipeline configs
- environment.update now accepts gitOpsMode; auto-generates webhook
  secret when switching to bidirectional mode
- GitOps Mode selector (Off / Push Only / Bi-directional)
- Webhook URL and secret display with copy-to-clipboard
- Instructions for configuring GitHub webhook
…ments

Warns users that manual changes may be overwritten by the next git push
when the environment uses bidirectional GitOps mode.
- New operations/gitops.md with full setup guide for push-only and
  bi-directional modes, GitHub webhook setup instructions
- Updated environments.md with GitOps Mode section
- Added GitOps page to SUMMARY.md navigation
- Encrypt gitWebhookSecret via encrypt() before storing, decrypt on read
- Add writeAuditLog call for gitops.pipeline.imported events
- Remove unreachable TOML format branch (only YAML files are collected)
Ensures names match the same schema used by the tRPC pipeline.create
procedure, preventing invalid characters from entering the database.
- Guard environment.get with withTeamAccess("VIEWER") to prevent
  unauthenticated access to environment details
- Stop returning decrypted webhook secret from the get query; return
  hasWebhookSecret boolean instead (secret only available via the
  EDITOR-gated update mutation response)
- Hoist repo URL parsing and git token decryption out of the per-file
  loop in the webhook handler to avoid redundant work
- Update GitSyncSection frontend to track webhook secret from mutation
  response state rather than the query
Stop decrypting and returning the stored webhook HMAC secret on every
environment.update call. The plaintext secret is now only included in
the response when freshly generated (switching to bidirectional mode
for the first time). Unrelated updates (rename, token rotation, etc.)
no longer leak the credential.

Also updates the UI hint to explain the secret is shown only once.
@TerrifiedBug TerrifiedBug merged commit 8c59f05 into main Mar 7, 2026
12 checks passed
@TerrifiedBug TerrifiedBug deleted the feat/gitops branch March 7, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants