-
Notifications
You must be signed in to change notification settings - Fork 0
feat(jenkins): add 5-stage Jenkinsfile, assert-reconciliation.sh, and… #14
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
base: main
Are you sure you want to change the base?
Changes from all commits
e8978b5
5a562c7
c6c21d3
8d0cec9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,176 @@ | ||||||
| // Agentrax — Declarative Jenkins CI/CD Pipeline | ||||||
| // | ||||||
| // Stages: | ||||||
| // 1. Lint — golangci-lint + Helm chart lint (parallel) | ||||||
| // 2. Test — unit & integration tests via envtest | ||||||
| // 3. Docker Build — build image tagged with short Git SHA, push on main | ||||||
| // 4. Integration Test — deploy to kind test namespace, assert reconciliation | ||||||
| // 5. Helm Deploy — manual approval gate then helm upgrade --install | ||||||
| // | ||||||
| // Requirements: | ||||||
| // - Jenkins agent with label 'docker' and Docker-in-Docker socket access | ||||||
| // - Credentials: GHCR_USER (string), GHCR_TOKEN (secret text) | ||||||
| // - Jenkins Slack plugin configured for Slack notifications | ||||||
|
|
||||||
| pipeline { | ||||||
| agent { label 'docker' } | ||||||
|
|
||||||
| environment { | ||||||
| // Go workspace inside the Jenkins workspace to avoid polluting $HOME | ||||||
| GOPATH = "${WORKSPACE}/.go" | ||||||
| GOMODCACHE = "${WORKSPACE}/.go/pkg/mod" | ||||||
| // Image tag is the short Git SHA for traceability | ||||||
| IMAGE_TAG = "${env.GIT_COMMIT?.take(8) ?: env.BUILD_NUMBER}" | ||||||
| IMAGE = "ghcr.io/gitcommitankit/agentrax:${IMAGE_TAG}" | ||||||
| // Kubernetes namespace used exclusively for integration testing | ||||||
| TEST_NS = "agentrax-jenkins-test" | ||||||
| // Kind cluster and kubectl context for integration testing | ||||||
| KIND_CLUSTER = "${env.KIND_CLUSTER ?: 'agentrax-dev'}" | ||||||
| KUBE_CONTEXT = "${env.KUBE_CONTEXT ?: 'kind-agentrax-dev'}" | ||||||
|
Comment on lines
+28
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- repository guidance ---'
for f in /tmp/coderabbit-repo-knowledge/gitcommitankit-agentrax-9ba08467/*/*.md; do
[ -f "$f" ] || continue
printf '\n--- %s ---\n' "$f"
head -80 "$f"
done
printf '%s\n' '--- Jenkinsfile lines 1-155 ---'
cat -n Jenkinsfile | sed -n '1,155p'
printf '%s\n' '--- references to cluster/context variables ---'
rg -n -C 3 'KIND_CLUSTER|KUBE_CONTEXT|kind |kubectl|make (deploy|undeploy)|docker' JenkinsfileRepository: gitcommitankit/agentrax Length of output: 42353 Require matching cluster and context overrides. If Jenkins sets only 🤖 Prompt for AI Agents |
||||||
| } | ||||||
|
|
||||||
| options { | ||||||
| // Abort if the full pipeline exceeds 45 minutes | ||||||
| timeout(time: 45, unit: 'MINUTES') | ||||||
| // Keep the last 10 build logs; discard older ones to save disk space | ||||||
| buildDiscarder(logRotator(numToKeepStr: '10')) | ||||||
| // Prevent concurrent builds on the same branch to avoid races on the | ||||||
| // shared kind cluster used in Stage 4 | ||||||
| disableConcurrentBuilds() | ||||||
| ansiColor('xterm') | ||||||
| } | ||||||
|
|
||||||
| stages { | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Stage 1: Lint | ||||||
| // Runs golangci-lint and Helm chart lint in parallel. | ||||||
| // ----------------------------------------------------------------------- | ||||||
| stage('Lint') { | ||||||
| parallel { | ||||||
| stage('Go Lint') { | ||||||
| steps { | ||||||
| sh 'make golangci-lint' | ||||||
| sh 'make lint' | ||||||
| } | ||||||
| } | ||||||
| stage('Helm Lint') { | ||||||
| steps { | ||||||
| sh 'helm lint charts/agentrax/' | ||||||
| sh 'helm template test charts/agentrax/ --debug > /dev/null' | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Stage 2: Test | ||||||
| // Runs the full unit + envtest integration test suite. | ||||||
| // ----------------------------------------------------------------------- | ||||||
| stage('Test') { | ||||||
| steps { | ||||||
| sh 'make envtest' | ||||||
| sh 'make test' | ||||||
| } | ||||||
| post { | ||||||
| always { | ||||||
| archiveArtifacts artifacts: 'cover.out', allowEmptyArchive: true | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Stage 3: Docker Build | ||||||
| // Builds the operator image. Pushes to GHCR only on the main branch. | ||||||
| // Credentials are bound strictly within the main-branch push path. | ||||||
| // ----------------------------------------------------------------------- | ||||||
| stage('Docker Build') { | ||||||
| steps { | ||||||
| sh "make docker-build IMG=${IMAGE}" | ||||||
| script { | ||||||
| if (env.BRANCH_NAME == 'main') { | ||||||
| withCredentials([ | ||||||
| string(credentialsId: 'GHCR_USER', variable: 'GHCR_USER'), | ||||||
| string(credentialsId: 'GHCR_TOKEN', variable: 'GHCR_TOKEN'), | ||||||
| ]) { | ||||||
| sh 'echo "${GHCR_TOKEN}" | docker login ghcr.io -u "${GHCR_USER}" --password-stdin' | ||||||
| sh "make docker-push IMG=${IMAGE}" | ||||||
| } | ||||||
| } else { | ||||||
| echo "Feature branch — image built but not pushed (branch=${env.BRANCH_NAME})." | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Stage 4: Integration Test (Agentrax-specific stage) | ||||||
| // | ||||||
| // 1. Installs cert-manager, Prometheus Operator CRDs, and Gateway API CRDs | ||||||
| // via `make deploy-deps` (idempotent). | ||||||
| // 2. Loads the locally built image into the kind cluster if kind is present. | ||||||
| // 3. Deploys the operator into the cluster with the newly built image. | ||||||
| // 4. Runs hack/assert-reconciliation.sh — polls until a sample | ||||||
| // AgentDeployment reaches status.phase == Running (60 s timeout). | ||||||
| // | ||||||
| // In post.always, the test namespace is deleted FIRST while the operator | ||||||
| // is still running so finalizers (agentrax.io/mcp-deregister) can execute | ||||||
| // cleanly, followed by `make undeploy`. | ||||||
| // ----------------------------------------------------------------------- | ||||||
| stage('Integration Test') { | ||||||
| steps { | ||||||
| sh "make deploy-deps KUBECTL=\"kubectl --context ${KUBE_CONTEXT}\"" | ||||||
| sh "kind load docker-image ${IMAGE} --name ${KIND_CLUSTER}" | ||||||
| sh "make deploy IMG=${IMAGE} KUBECTL=\"kubectl --context ${KUBE_CONTEXT}\"" | ||||||
| sh "TEST_NS=${TEST_NS} KUBECTL=\"kubectl --context ${KUBE_CONTEXT}\" ./hack/assert-reconciliation.sh" | ||||||
| } | ||||||
| post { | ||||||
| always { | ||||||
| sh "kubectl --context ${KUBE_CONTEXT} delete namespace ${TEST_NS} --ignore-not-found=true" | ||||||
| sh "make undeploy KUBECTL=\"kubectl --context ${KUBE_CONTEXT}\" || true" | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Propagate real operator-cleanup failures.
Proposed fix- sh "make undeploy KUBECTL=\"kubectl --context ${KUBE_CONTEXT}\" || true"
+ sh "make undeploy KUBECTL=\"kubectl --context ${KUBE_CONTEXT}\" ignore-not-found=true"📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // ----------------------------------------------------------------------- | ||||||
| // Stage 5: Helm Deploy | ||||||
| // | ||||||
| // Runs only on the main branch. Requires explicit approval from the | ||||||
| // ops-team before mutating the production cluster. --atomic ensures Helm | ||||||
| // rolls back automatically if any post-install hook fails. | ||||||
| // ----------------------------------------------------------------------- | ||||||
| stage('Helm Deploy') { | ||||||
| when { branch 'main' } | ||||||
| input { | ||||||
| message "Deploy agentrax:${IMAGE_TAG} to production cluster?" | ||||||
| ok 'Approve' | ||||||
| submitter 'ops-team' | ||||||
| } | ||||||
| steps { | ||||||
| sh """ | ||||||
| helm upgrade --install agentrax charts/agentrax/ \ | ||||||
| --namespace agentrax-system \ | ||||||
| --create-namespace \ | ||||||
| --set image.tag=${IMAGE_TAG} \ | ||||||
| --atomic \ | ||||||
| --timeout 5m | ||||||
| """ | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| post { | ||||||
| failure { | ||||||
| slackSend( | ||||||
| color: 'danger', | ||||||
| message: "❌ Agentrax build #${BUILD_NUMBER} FAILED on \`${BRANCH_NAME}\`: ${BUILD_URL}", | ||||||
| ) | ||||||
| } | ||||||
| success { | ||||||
| slackSend( | ||||||
| color: 'good', | ||||||
| message: "✅ Agentrax build #${BUILD_NUMBER} passed on \`${BRANCH_NAME}\` (image: \`${IMAGE_TAG}\`)", | ||||||
| ) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| #!/usr/bin/env bash | ||
| # hack/assert-reconciliation.sh | ||
| # | ||
| # Polls until a sample AgentDeployment in the integration-test namespace | ||
| # reaches status.phase == Running, then exits 0. | ||
| # Exits non-zero (and fails the Jenkins stage) if the deadline is exceeded. | ||
| # | ||
| # Environment variables (all optional — defaults shown): | ||
| # TEST_NS Namespace to apply the sample manifest into (default: agentrax-jenkins-test) | ||
| # TIMEOUT_SEC Maximum seconds to wait for Running phase (default: 60) | ||
| # KUBECTL kubectl binary to use (default: kubectl) | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| : "${TEST_NS:=agentrax-jenkins-test}" | ||
| : "${TIMEOUT_SEC:=60}" | ||
| : "${KUBECTL:=kubectl}" | ||
|
|
||
| SAMPLE_MANIFEST="$(dirname "$0")/testdata/sample-agentdeployment.yaml" | ||
| POLL_INTERVAL=3 | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 1. Ensure the test namespace exists | ||
| # --------------------------------------------------------------------------- | ||
| echo "[assert-reconciliation] Ensuring namespace '${TEST_NS}' exists..." | ||
| ${KUBECTL} create namespace "${TEST_NS}" --dry-run=client -o yaml | ${KUBECTL} apply -f - | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 2. Apply the sample AgentDeployment | ||
| # --------------------------------------------------------------------------- | ||
| if [[ ! -f "${SAMPLE_MANIFEST}" ]]; then | ||
| echo "[assert-reconciliation] ERROR: sample manifest not found at ${SAMPLE_MANIFEST}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "[assert-reconciliation] Applying sample AgentDeployment from ${SAMPLE_MANIFEST}..." | ||
| ${KUBECTL} apply -f "${SAMPLE_MANIFEST}" -n "${TEST_NS}" | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # 3. Poll until status.phase == Running | ||
| # --------------------------------------------------------------------------- | ||
| AD_NAME=$(${KUBECTL} get agentdeployment -n "${TEST_NS}" \ | ||
| -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true) | ||
|
|
||
| if [[ -z "${AD_NAME}" ]]; then | ||
| echo "[assert-reconciliation] ERROR: no AgentDeployment found in namespace '${TEST_NS}'" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| echo "[assert-reconciliation] Waiting up to ${TIMEOUT_SEC}s for AgentDeployment '${AD_NAME}' to reach Running..." | ||
|
|
||
| elapsed=0 | ||
| while true; do | ||
| phase=$(${KUBECTL} get agentdeployment "${AD_NAME}" -n "${TEST_NS}" \ | ||
| -o jsonpath='{.status.phase}' 2>/dev/null || echo "Unknown") | ||
|
|
||
| echo "[assert-reconciliation] t=${elapsed}s status.phase=${phase}" | ||
|
|
||
| if [[ "${phase}" == "Running" ]]; then | ||
| echo "[assert-reconciliation] ✅ AgentDeployment '${AD_NAME}' reached Running in ${elapsed}s." | ||
| exit 0 | ||
| fi | ||
|
|
||
| if [[ "${phase}" == "RolloutFailed" || "${phase}" == "Degraded" ]]; then | ||
| echo "[assert-reconciliation] ❌ Terminal failure phase '${phase}' detected." >&2 | ||
| ${KUBECTL} describe agentdeployment "${AD_NAME}" -n "${TEST_NS}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if (( elapsed >= TIMEOUT_SEC )); then | ||
| echo "[assert-reconciliation] ❌ Timeout after ${TIMEOUT_SEC}s — last phase: '${phase}'" >&2 | ||
| echo "[assert-reconciliation] --- describe output ---" >&2 | ||
| ${KUBECTL} describe agentdeployment "${AD_NAME}" -n "${TEST_NS}" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| sleep "${POLL_INTERVAL}" | ||
| elapsed=$(( elapsed + POLL_INTERVAL )) | ||
| done |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| apiVersion: agentrax.io/v1alpha1 | ||
| kind: AgentDeployment | ||
| metadata: | ||
| name: jenkins-smoke-test | ||
| # Namespace is set by assert-reconciliation.sh at apply time (-n ${TEST_NS}) | ||
| labels: | ||
| app.kubernetes.io/managed-by: jenkins-ci | ||
| agentrax.io/agent: "true" | ||
| spec: | ||
| # Minimal agent that exercises the full reconcile loop: | ||
| # Deployment + Service + ServiceMonitor + HPA creation. | ||
| image: "ghcr.io/gitcommitankit/agentrax-agent:latest" | ||
| replicas: 1 | ||
| tenant: jenkins-test | ||
| resources: | ||
| limits: | ||
| cpu: "250m" | ||
| memory: "256Mi" | ||
| requests: | ||
| cpu: "50m" | ||
| memory: "64Mi" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: gitcommitankit/agentrax
Length of output: 9000
🏁 Script executed:
Repository: gitcommitankit/agentrax
Length of output: 13863
🌐 Web query:
Jenkins multibranch Pipeline separate branch jobs disableConcurrentBuilds lockable resources documentation💡 Result:
To manage concurrent builds in a Jenkins Multibranch Pipeline, you can use the built-in disableConcurrentBuilds option or the Lockable Resources plugin for more granular control. Disable Concurrent Builds The standard way to prevent simultaneous execution of a Pipeline is by using the disableConcurrentBuilds option within the options block of your Jenkinsfile [1][2]. - Standard behavior: If a build is already running, new builds are queued until the current one finishes [1][3]. - Abort previous: You can set abortPrevious: true to automatically abort the running build when a new one is triggered [2][3]. Example syntax: options { disableConcurrentBuilds(abortPrevious: true) } For Multibranch Pipelines, you can dynamically control this behavior based on the branch type (e.g., branches vs. pull requests) by using environment variables [4][5]: options { disableConcurrentBuilds(abortPrevious: env.CHANGE_ID!= null) } Lockable Resources Plugin When you need to protect shared resources or implement more complex concurrency logic (such as disabling concurrency only for specific branches), the Lockable Resources plugin is the standard solution [6][7]. - Implementation: Use the lock step within the stages of your Pipeline [7][8]. - Ephemeral resources: If a resource name is not predefined in Jenkins settings, the plugin creates an ephemeral lock that persists only while a build references it [7][9]. - Dynamic locking: Unlike options, the lock step must be placed inside the steps block [10]. It is commonly used to create mutual exclusion zones for specific branches by locking on a resource name derived from the job or branch name [6][11]. Example pattern for branch-specific locking: steps { lock(resource: "${env.BRANCH_NAME}", inversePrecedence: true) { // Only one build per branch will execute here sh '...' } } Note: The lock step consumes an executor while waiting for a resource to become available. If you need to avoid this, consider placing the lock step before any node allocation or using an agent-less stage for the locking logic [10][12][11].
Citations:
Serialize integration jobs that share
agentrax-dev.The default
KIND_CLUSTER,KUBE_CONTEXT, and fixedTEST_NStarget shared resources. In a multibranch Pipeline,disableConcurrentBuilds()does not coordinate different branch jobs. Add a cluster-scopedlockaround the integration stage, or use build-specific clusters and namespaces.🤖 Prompt for AI Agents
Source: MCP tools