Skip to content

fix: Exec and Jenkins Job Agent #408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 12, 2025
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
22 changes: 14 additions & 8 deletions apps/event-worker/src/workers/job-dispatch/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,14 +206,20 @@ export const dispatchGithubJob = async (je: Job) => {
});

try {
await octokit.actions.createWorkflowDispatch({
owner: mergedConfig.owner,
repo: mergedConfig.repo,
workflow_id: mergedConfig.workflowId,
ref,
inputs: { job_id: je.id },
headers,
});
await octokit.actions
.createWorkflowDispatch({
owner: mergedConfig.owner,
repo: mergedConfig.repo,
workflow_id: mergedConfig.workflowId,
ref,
inputs: { job_id: je.id },
headers,
})
.then(() =>
Copy link
Contributor

Choose a reason for hiding this comment

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

is this related to the jenkins stuff? we should probably keep the scope of this PR to jsut that and do this as follow up, umelss do we need this for the jenkins to work?

Copy link
Member Author

@zacharyblasczyk zacharyblasczyk Apr 12, 2025

Choose a reason for hiding this comment

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

This is needed to enable jenkins and exec agents to work properly without breaking the gh job agent.

updateJob(db, je.id, {
status: JobStatus.InProgress,
}),
);
} catch (e) {
const error = e instanceof Error ? e.message : String(e);
log.error(`Failed to create workflow dispatch for job ${je.id}`, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Alert, AlertDescription, AlertTitle } from "@ctrlplane/ui/alert";
import { Form, FormField, useForm } from "@ctrlplane/ui/form";
import { JobAgentType } from "@ctrlplane/validators/jobs";

import { JobAgentJenkinsPipelineConfig } from "~/components/form/job-agent/JobAgentJenkinsPipelineConfig";
import { JobAgentKubernetesConfig } from "~/components/form/job-agent/JobAgentKubernetesConfig";
import { JobAgentScriptConfig } from "~/components/form/job-agent/JobAgentScriptConfig";
import { JobAgentSelector } from "~/components/form/job-agent/JobAgentSelector";
Expand Down Expand Up @@ -83,7 +84,7 @@ const JobAgentForm: React.FC<{
disabled={update.isPending}
/>
)}
{selectedJobAgent?.type.startsWith("exec-") && (
{selectedJobAgent?.type.startsWith("exec") && (
<JobAgentScriptConfig
type={
selectedJobAgent.type.startsWith(JobAgentType.ExecWindows)
Expand All @@ -94,6 +95,13 @@ const JobAgentForm: React.FC<{
disabled={update.isPending}
/>
)}
{selectedJobAgent?.type === JobAgentType.Jenkins && (
<JobAgentJenkinsPipelineConfig
{...field}
disabled={update.isPending}
isPending={update.isPending}
/>
)}
</>
)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import type * as SCHEMA from "@ctrlplane/db/schema";
import type { JobAgentType } from "@ctrlplane/validators/jobs";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { IconBolt, IconCheck, IconRocket } from "@tabler/icons-react";
Expand All @@ -19,6 +20,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@ctrlplane/ui/tooltip";
import { JobAgentTypeDisplayNames } from "@ctrlplane/validators/jobs";

import { api } from "~/trpc/react";
import { useJobStats } from "./_hooks/useJobStats";
Expand Down Expand Up @@ -148,7 +150,7 @@ export const AgentCard: React.FC<AgentCardProps> = ({ agent, className }) => {
<CardContent className="flex items-center justify-between p-0">
<div className="flex items-center gap-6 pl-1">
<div className="text-xs font-medium text-muted-foreground">
Github App
{JobAgentTypeDisplayNames[agent.type as JobAgentType]}
</div>

<TooltipProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type * as SCHEMA from "@ctrlplane/db/schema";
import type { DeploymentCondition } from "@ctrlplane/validators/deployments";
import type { EnvironmentCondition } from "@ctrlplane/validators/environments";
import type { ResourceCondition } from "@ctrlplane/validators/resources";
import { useRouter } from "next/navigation";
import { IconPlus, IconTrash } from "@tabler/icons-react";
import { z } from "zod";
Expand Down Expand Up @@ -31,11 +32,7 @@ import { Switch } from "@ctrlplane/ui/switch";
import { Textarea } from "@ctrlplane/ui/textarea";
import { deploymentCondition } from "@ctrlplane/validators/deployments";
import { environmentCondition } from "@ctrlplane/validators/environments";
import type {
ResourceCondition} from "@ctrlplane/validators/resources";
import {
resourceCondition,
} from "@ctrlplane/validators/resources";
import { resourceCondition } from "@ctrlplane/validators/resources";

import { DeploymentConditionRender } from "~/app/[workspaceSlug]/(app)/_components/deployments/condition/DeploymentConditionRender";
import { EnvironmentConditionRender } from "~/app/[workspaceSlug]/(app)/_components/environment/condition/EnvironmentConditionRender";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { Button } from "@ctrlplane/ui/button";
import { FormDescription, FormItem, FormLabel } from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";

interface JobAgentJenkinsPipelineConfigProps {
value: Record<string, any>;
onChange: (value: Record<string, any>) => void;
disabled?: boolean;
isPending?: boolean;
}

export const JobAgentJenkinsPipelineConfig: React.FC<
JobAgentJenkinsPipelineConfigProps
> = ({ value, onChange, disabled, isPending }) => {
const handleJobUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange({
...value,
jobUrl: e.target.value,
});
};

return (
<div className="space-y-4">
<FormItem>
<FormLabel>Jenkins Job URL</FormLabel>
<Input
placeholder="e.g. http://jenkins/job/org/job/repo/job/branch"
value={value.jobUrl ?? ""}
onChange={handleJobUrlChange}
disabled={disabled}
/>
Comment on lines +28 to +33
Copy link
Contributor

@coderabbitai coderabbitai bot Apr 12, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add URL validation for the Jenkins job URL.

The component provides a format description but lacks validation to ensure users enter a properly formatted Jenkins URL.

 <Input
   placeholder="e.g. http://jenkins/job/org/job/repo/job/branch"
   value={value.jobUrl ?? ""}
   onChange={handleJobUrlChange}
   disabled={disabled}
+  pattern="https?:\/\/.*\/job\/.*\/job\/.*\/job\/.*"
+  title="Please enter a valid Jenkins URL format: http://jenkins/job/org/job/repo/job/branch"
 />

Alternatively, you could implement a custom validation function and display error messages when the URL doesn't match the expected format:

const isValidJenkinsUrl = (url: string) => {
  return /https?:\/\/.*\/job\/.*\/job\/.*\/job\/.*/.test(url);
};

// Add this to your component
const [isValid, setIsValid] = useState(true);

const handleJobUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const newValue = e.target.value;
  setIsValid(newValue === '' || isValidJenkinsUrl(newValue));
  onChange({
    ...value,
    jobUrl: newValue,
  });
};

// Then add error indication to your Input or below it

Copy link
Member Author

Choose a reason for hiding this comment

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

This isn't the only pattern that jobs can come it. So I am not worried about validating input now.

If the jenkins server is available, we might have a different form altogether where we just let them pick from a dropdown, but since we are focused on airgapped instances, I am just keeping it simple for now.

I will probably want something more like: DeploymentJobAgentGithubConfig.tsx

@coderabbitai, please create an issue as a feature request describing what I have laid out here and a proposal of how to handle the two cases airgapped and not airgapped.

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@zacharyblasczyk I've created an issue with a feature request for enhancing the Jenkins job configuration to handle both airgapped and non-airgapped environments. The issue includes:

  1. The current implementation for airgapped environments (simple URL input)
  2. A proposed approach for non-airgapped environments with a dropdown selection
  3. Recommendations for a component structure similar to what you suggested

I've assigned the issue to you as requested. Let me know if you'd like any changes to the description or scope of the feature request.


🏁 Script executed:

#!/bin/bash
# Find the DeploymentJobAgentGithubConfig.tsx file
fd "DeploymentJobAgentGithubConfig.tsx" --type f

# If not found, look for similar files that might help understand the pattern
fd "JobAgent.*Config.tsx" --type f

# Let's also look at the GitHub workflow files to understand job configuration patterns
fd "github.*\.tsx" --type f | grep -i config

Length of output: 1078


🏁 Script executed:

#!/bin/bash
# Let's look at all job agent configuration files
fd "JobAgent" --type f | grep -i config

# Look for deployment related GitHub files
fd "Deployment.*Github" --type f

# Let's also check files in the same directory as our Jenkins config
find "$(dirname "apps/webservice/src/components/form/job-agent/JobAgentJenkinsPipelineConfig.tsx")" -type f | sort

Length of output: 1437


🏁 Script executed:

#!/bin/bash
# Check the content of the JobAgentScriptConfig.tsx file
cat apps/webservice/src/components/form/job-agent/JobAgentScriptConfig.tsx

# Look for any GitHub workflow related configuration components
fd "Github.*Config.tsx" --type f
cat $(fd "Github.*Config.tsx" --type f | head -1)

Length of output: 9461


🏁 Script executed:

#!/bin/bash
# Create a GitHub issue for the feature request
gh issue create \
  --title "Feature request: Enhanced Jenkins job configuration for airgapped and non-airgapped environments" \
  --body "## Background

Currently, the Jenkins job agent configuration (`JobAgentJenkinsPipelineConfig.tsx`) uses a simple input field for manually entering Jenkins job URLs, which works well for airgapped environments where direct server connection isn't available.

## Feature request

Enhance the Jenkins job configuration to handle two distinct scenarios:

### 1. Airgapped environments (current implementation)
- Continue supporting manual URL input for environments without direct Jenkins server access
- Minimal validation to accommodate various Jenkins URL patterns

### 2. Non-airgapped environments
- Implement a dropdown selection interface when the Jenkins server is accessible
- Allow users to browse and select from available Jenkins jobs
- Possibly fetch and display additional job metadata

## Implementation approach

Create a component structure similar to `DeploymentJobAgentGithubConfig.tsx` that can:
- Detect whether the Jenkins server is accessible
- Conditionally render the appropriate interface (manual input or dropdown)
- Provide a seamless experience in both scenarios

## Benefits
- Improved user experience for environments with Jenkins server access
- Reduced possibility of input errors
- Consistency with other job agent configuration interfaces

This issue was created based on a code review discussion in PR #408.

/cc @zacharyblasczyk" \
  --assignee zacharyblasczyk

Length of output: 1832


@zacharyblasczyk I've created a feature request issue (#478) describing the two approaches for Jenkins job configuration:

  1. Airgapped environments: Simple form with manual URL input (current implementation)
  2. Non-airgapped environments: Dropdown selection interface when Jenkins server is accessible

The issue includes a proposal for creating a component similar to DeploymentJobAgentGithubConfig.tsx that can detect accessibility and render the appropriate interface. I've assigned the issue to you as requested.

Here's the link to the issue: #478

✏️ Learnings added
Learnt from: zacharyblasczyk
PR: ctrlplanedev/ctrlplane#408
File: apps/webservice/src/components/form/job-agent/JobAgentJenkinsPipelineConfig.tsx:26-31
Timestamp: 2025-04-12T22:08:13.751Z
Learning: For Jenkins job configuration, two approaches are needed: (1) a simple URL input form for airgapped environments (current focus) and (2) a dropdown selection interface for non-airgapped environments where the Jenkins server is accessible. A component similar to DeploymentJobAgentGithubConfig.tsx is preferred.

Learnt from: zacharyblasczyk
PR: ctrlplanedev/ctrlplane#408
File: apps/webservice/src/components/form/job-agent/JobAgentJenkinsPipelineConfig.tsx:26-31
Timestamp: 2025-04-12T22:08:13.751Z
Learning: For Jenkins job configuration, two approaches are needed: (1) a simple URL input form for airgapped environments (current focus) and (2) a dropdown selection interface for non-airgapped environments where the Jenkins server is accessible. A component similar to DeploymentJobAgentGithubConfig.tsx is preferred.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

<FormDescription>
The URL path to the Jenkins job (format: {"{JENKINS_URL}"}
/job/org/job/repo/job/branch)
</FormDescription>
</FormItem>

<div className="flex">
<Button type="submit" disabled={disabled ?? isPending}>
{isPending ? "Saving..." : "Save"}
</Button>
</div>
</div>
);
};
5 changes: 0 additions & 5 deletions packages/job-dispatch/src/job-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,6 @@ class DispatchBuilder {
data: { jobId: wf.id },
})),
);
await Promise.all(
validJobsWithResolvedVariables.map((j) =>
updateJob(this.db, j.id, { status: JobStatus.InProgress }),
),
);
}

await Promise.all(
Expand Down
10 changes: 10 additions & 0 deletions packages/validators/src/jobs/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,14 @@ export enum JobAgentType {
KubernetesJob = "kubernetes-job",
GithubApp = "github-app",
ExecWindows = "exec-windows",
ExecLinux = "exec-linux",
Jenkins = "jenkins",
}

export const JobAgentTypeDisplayNames: Record<JobAgentType, string> = {
[JobAgentType.KubernetesJob]: "Kubernetes Job",
[JobAgentType.GithubApp]: "Github App",
[JobAgentType.ExecWindows]: "PowerShell",
[JobAgentType.ExecLinux]: "Shell",
[JobAgentType.Jenkins]: "Jenkins",
};
Loading