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
32 changes: 32 additions & 0 deletions docs/public/user-guide/environments.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,38 @@ You can copy a pipeline from one environment to another using the **Promote to..
Secrets and certificates are stripped during promotion. After promoting a pipeline, configure the appropriate secrets in the target environment before deploying.
{% endhint %}

## Deploy approval

Environments can require **admin approval** before pipelines are deployed. This is useful for production environments where you want a second pair of eyes on every configuration change.

### Enabling approval

{% stepper %}
{% step %}
### Open environment settings
Navigate to **Environments**, click the environment name, and click **Edit**.
{% endstep %}
{% step %}
### Enable the toggle
Turn on **Require approval for deploys**.
{% endstep %}
{% step %}
### Save
Click **Save** to apply the change.
{% endstep %}
{% endstepper %}

When enabled:
- Users with the **Editor** role will see a **Request Deploy** button instead of **Publish to Agents** in the deploy dialog. Their deploy requests are queued for review.
- Users with the **Admin** role can deploy directly (no approval needed) and can review, approve, or reject pending requests from other users.
- A **Pending Approval** badge appears on the pipeline list and in the pipeline editor toolbar while a request is outstanding.

{% hint style="info" %}
An admin cannot approve their own deploy request. This ensures a genuine four-eyes review process.
{% endhint %}

For more details on how the approval workflow operates, see [Pipelines -- Deploy approval workflows](pipelines.md#deploy-approval-workflows).

## Editing and deleting environments

- **Edit** -- Click the **Edit** button on the environment detail page to rename the environment or change its secret backend configuration.
Expand Down
26 changes: 26 additions & 0 deletions docs/public/user-guide/pipelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ A pipeline moves through several states during its lifecycle:
- **Stopped** -- The pipeline is deployed but all agent nodes have stopped processing it.
- **Crashed** -- One or more agent nodes report that the pipeline has crashed. Check the pipeline logs for details.
- **Pending deploy** -- Shown as an additional badge when the saved configuration differs from what is currently deployed. Deploy the pipeline to push the latest changes.
- **Pending Approval** -- Shown when an editor has submitted a deploy request that is waiting for admin approval. See [Deploy approval workflows](#deploy-approval-workflows).

## Creating a pipeline

Expand Down Expand Up @@ -178,6 +179,31 @@ Tags are color-coded in the pipeline list for quick visual identification:
Tags are metadata labels only -- they do not enforce any access controls or data-handling policies. Use them as a visual aid for compliance awareness and pipeline organization.
{% endhint %}


## Deploy approval workflows

Environments can optionally require **admin approval** before a pipeline is deployed. When enabled, editors who click **Deploy** will submit a deploy request instead of deploying directly. Admins can then review, approve, or reject the request.

### How it works

1. An admin enables **Require approval for deploys** on the environment settings page (see [Environments](environments.md#deploy-approval)).
2. When an editor clicks **Deploy** in the pipeline editor, the deploy dialog shows a **Request Deploy** button instead of **Publish to Agents**.
3. The editor submits a deploy request with a changelog entry. The pipeline list and pipeline editor toolbar show a **Pending Approval** badge.
4. An admin opens the deploy dialog for the pipeline and sees the request in **review mode** -- displaying the requester, changelog, and a config diff.
5. The admin can **Approve & Deploy** (which immediately deploys the pipeline) or **Reject** (with an optional note).

{% hint style="info" %}
Admins can always deploy directly, even when approval is required. The approval gate only applies to users with the Editor role.
{% endhint %}

### Cancelling a request

The editor who submitted a pending deploy request can cancel it from the pipeline editor toolbar by clicking the **X** button next to the **Pending Approval** badge.

### Pipeline list indicators

Pipelines with pending deploy requests show a **Pending Approval** badge in the status column on the Pipelines page, so admins can quickly identify which pipelines need attention.

## Filtering by environment

Pipelines are scoped to the currently selected **environment** (shown in the sidebar). Switch environments to view pipelines in a different environment. Each environment maintains its own independent set of pipelines, agent nodes, and secrets.
38 changes: 38 additions & 0 deletions prisma/migrations/20260307100000_add_deploy_requests/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-- AlterTable
ALTER TABLE "Environment" ADD COLUMN "requireDeployApproval" BOOLEAN NOT NULL DEFAULT false;

-- CreateTable
CREATE TABLE "DeployRequest" (
"id" TEXT NOT NULL,
"pipelineId" TEXT NOT NULL,
"environmentId" TEXT NOT NULL,
"requestedById" TEXT NOT NULL,
"configYaml" TEXT NOT NULL,
"changelog" TEXT NOT NULL,
"nodeSelector" JSONB,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"reviewedById" TEXT,
"reviewNote" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"reviewedAt" TIMESTAMP(3),

CONSTRAINT "DeployRequest_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "DeployRequest_pipelineId_status_idx" ON "DeployRequest"("pipelineId", "status");

-- CreateIndex
CREATE INDEX "DeployRequest_environmentId_status_idx" ON "DeployRequest"("environmentId", "status");

-- AddForeignKey
ALTER TABLE "DeployRequest" ADD CONSTRAINT "DeployRequest_pipelineId_fkey" FOREIGN KEY ("pipelineId") REFERENCES "Pipeline"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DeployRequest" ADD CONSTRAINT "DeployRequest_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DeployRequest" ADD CONSTRAINT "DeployRequest_requestedById_fkey" FOREIGN KEY ("requestedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "DeployRequest" ADD CONSTRAINT "DeployRequest_reviewedById_fkey" FOREIGN KEY ("reviewedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
43 changes: 35 additions & 8 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ model User {
totpSecret String?
totpEnabled Boolean @default(false)
totpBackupCodes String?
scimExternalId String? @unique
auditLogs AuditLog[]
pipelinesUpdated Pipeline[] @relation("PipelineUpdatedBy")
pipelinesCreated Pipeline[] @relation("PipelineCreatedBy")
vrlSnippets VrlSnippet[]
dashboardViews DashboardView[]
serviceAccounts ServiceAccount[]
createdAt DateTime @default(now())
scimExternalId String? @unique
auditLogs AuditLog[]
pipelinesUpdated Pipeline[] @relation("PipelineUpdatedBy")
pipelinesCreated Pipeline[] @relation("PipelineCreatedBy")
vrlSnippets VrlSnippet[]
dashboardViews DashboardView[]
serviceAccounts ServiceAccount[]
deployRequestsMade DeployRequest[] @relation("deployRequester")
deployRequestsReviewed DeployRequest[] @relation("deployReviewer")
createdAt DateTime @default(now())
}

enum AuthMethod {
Expand Down Expand Up @@ -88,10 +90,12 @@ model Environment {
gitToken String? // Stored encrypted via crypto.ts
gitOpsMode String @default("off") // "off" | "push" | "bidirectional"
gitWebhookSecret String? // HMAC secret for validating incoming git webhooks
requireDeployApproval Boolean @default(false)
alertRules AlertRule[]
alertWebhooks AlertWebhook[]
notificationChannels NotificationChannel[]
serviceAccounts ServiceAccount[]
deployRequests DeployRequest[]
createdAt DateTime @default(now())
}

Expand Down Expand Up @@ -225,6 +229,7 @@ model Pipeline {
eventSamples EventSample[]
slis PipelineSli[]
tags Json? @default("[]") // string[] of classification tags like ["PII", "PCI-DSS"]
deployRequests DeployRequest[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Expand Down Expand Up @@ -499,6 +504,28 @@ model VrlSnippet {
@@index([teamId])
}

model DeployRequest {
id String @id @default(cuid())
pipelineId String
pipeline Pipeline @relation(fields: [pipelineId], references: [id], onDelete: Cascade)
environmentId String
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
requestedById String
requestedBy User @relation("deployRequester", fields: [requestedById], references: [id])
configYaml String
changelog String
nodeSelector Json?
status String @default("PENDING") // PENDING | APPROVED | REJECTED | CANCELLED
reviewedById String?
reviewedBy User? @relation("deployReviewer", fields: [reviewedById], references: [id])
reviewNote String?
createdAt DateTime @default(now())
reviewedAt DateTime?

@@index([pipelineId, status])
@@index([environmentId, status])
}

enum AlertMetric {
node_unreachable
cpu_usage
Expand Down
22 changes: 22 additions & 0 deletions src/app/(dashboard)/environments/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";
import { Skeleton } from "@/components/ui/skeleton";
import { SecretsSection } from "@/components/environment/secrets-section";
import { CertificatesSection } from "@/components/environment/certificates-section";
Expand Down Expand Up @@ -75,6 +76,7 @@ export default function EnvironmentDetailPage({
mountPath: "secret/data/vectorflow",
role: "",
});
const [editRequireApproval, setEditRequireApproval] = useState(false);
const [enrollmentToken, setEnrollmentToken] = useState<string | null>(null);

const updateMutation = useMutation(
Expand Down Expand Up @@ -121,6 +123,7 @@ export default function EnvironmentDetailPage({
function startEditing() {
if (!env) return;
setEditName(env.name);
setEditRequireApproval(env.requireDeployApproval ?? false);
setEditSecretBackend(env.secretBackend ?? "BUILTIN");
const vaultCfg = (env.secretBackendConfig as Record<string, string>) ?? {};
setEditVaultConfig({
Expand All @@ -136,6 +139,7 @@ export default function EnvironmentDetailPage({
updateMutation.mutate({
id,
name: editName,
requireDeployApproval: editRequireApproval,
secretBackend: editSecretBackend,
...(editSecretBackend === "VAULT" ? {
secretBackendConfig: editVaultConfig,
Expand Down Expand Up @@ -234,6 +238,19 @@ export default function EnvironmentDetailPage({
onChange={(e) => setEditName(e.target.value)}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label htmlFor="require-approval">Require approval for deploys</Label>
<p className="text-xs text-muted-foreground">
When enabled, editors must request admin approval before deploying pipelines.
</p>
</div>
<Switch
id="require-approval"
checked={editRequireApproval}
onCheckedChange={setEditRequireApproval}
/>
</div>
<div className="flex gap-2">
<Button onClick={handleSave} disabled={updateMutation.isPending}>
{updateMutation.isPending ? "Saving..." : "Save"}
Expand All @@ -257,6 +274,11 @@ export default function EnvironmentDetailPage({
<p className="text-xs text-muted-foreground">
{env.hasEnrollmentToken ? "Enrollment token configured" : "No enrollment token"}
</p>
{env.requireDeployApproval && (
<p className="text-xs text-amber-600 dark:text-amber-400 font-medium mt-1">
Deploy approval required
</p>
)}
</CardContent>
</Card>
<Card>
Expand Down
21 changes: 20 additions & 1 deletion src/app/(dashboard)/pipelines/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useTRPC } from "@/trpc/client";
import { toast } from "sonner";
import { Plus, MoreHorizontal, Copy, Trash2, BarChart3, ArrowUpRight } from "lucide-react";
import { Plus, MoreHorizontal, Copy, Trash2, BarChart3, ArrowUpRight, Clock } from "lucide-react";
import { useEnvironmentStore } from "@/stores/environment-store";
import { useTeamStore } from "@/stores/team-store";

Expand Down Expand Up @@ -168,6 +168,19 @@ export default function PipelinesPage() {
);
const liveRates = liveRatesQuery.data?.rates ?? {};

// Fetch pending deploy requests for the current environment
const pendingRequestsQuery = useQuery(
trpc.deploy.listPendingRequests.queryOptions(
{ environmentId: effectiveEnvId },
{ enabled: !!effectiveEnvId }
)
);
const pendingRequests = pendingRequestsQuery.data ?? [];
const pendingByPipeline = new Map<string, number>();
for (const req of pendingRequests) {
pendingByPipeline.set(req.pipelineId, (pendingByPipeline.get(req.pipelineId) ?? 0) + 1);
}

const router = useRouter();
const queryClient = useQueryClient();

Expand Down Expand Up @@ -280,6 +293,12 @@ export default function PipelinesPage() {
Pending deploy
</Badge>
)}
{pendingByPipeline.has(pipeline.id) && (
<Badge variant="outline" className="bg-amber-500/15 text-amber-700 dark:text-amber-400 border-amber-500/30 gap-1">
<Clock className="h-3 w-3" />
Pending Approval
</Badge>
)}
</div>
</TableCell>
{/* Health */}
Expand Down
Loading
Loading