Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
88b942c
docs: add jenkins-deploy solution design spec
jeff-schnitter Aug 14, 2026
db0e37c
docs: add jenkins-deploy implementation plan
jeff-schnitter Aug 14, 2026
25452d7
feat: add jenkins-deploy catalog entity and scorecard
jeff-schnitter Aug 14, 2026
26e7746
feat: add Jenkinsfile template with Cortex deploy recording and async…
jeff-schnitter Aug 14, 2026
cdf4b4a
feat: add Cortex workflow template for triggering Jenkins deploys
jeff-schnitter Aug 14, 2026
00c2d52
feat: add Jenkins devcontainer for GitHub Codespaces
jeff-schnitter Aug 14, 2026
1e2b594
feat: add JenkinsDeploySetup class with prompts and Codespace provisi…
jeff-schnitter Aug 14, 2026
bedc73f
feat: add Jenkins job and credential creation helpers to JenkinsDeplo…
jeff-schnitter Aug 14, 2026
2235df3
feat: add Cortex integration steps and orchestration to JenkinsDeploy…
jeff-schnitter Aug 14, 2026
055be14
docs: add jenkins-deploy solution README
jeff-schnitter Aug 14, 2026
b1d7e7c
feat: add Codespace lifecycle management with delete-on-teardown
jeff-schnitter Aug 17, 2026
878d7b1
docs: update jenkins-deploy README to match github-actions/harness fo…
jeff-schnitter Aug 17, 2026
bdaed51
docs: add GitHub Codespaces to jenkins-deploy README diagram
jeff-schnitter Aug 17, 2026
0783770
docs: fix diagram right-padding alignment in jenkins-deploy README
jeff-schnitter Aug 17, 2026
3657bd4
chore: use worktree-jenkins-deploy branch for Codespace (temp, revert…
jeff-schnitter Aug 17, 2026
829c38f
fix: register Codespace port via API before setting visibility; warn …
jeff-schnitter Aug 17, 2026
275a52b
fix: reuse existing Codespace on re-run instead of provisioning a sec…
jeff-schnitter Aug 17, 2026
36f787b
fix: verify Codespace identity before reusing on re-run
jeff-schnitter Aug 17, 2026
c9188af
chore: add debug logging to _expose_jenkins_port
jeff-schnitter Aug 17, 2026
5ffe405
fix: save GitHub PAT between runs (hidden=True instead of secret=True)
jeff-schnitter Aug 17, 2026
1e4215d
fix: add context text before Codespace/existing Jenkins prompt
jeff-schnitter Aug 17, 2026
7e4493c
fix: set port 8080 public via devcontainer.json instead of ports REST…
jeff-schnitter Aug 17, 2026
6aff0dc
fix: replace Codespace identity heuristic with Jenkins connectivity p…
jeff-schnitter Aug 17, 2026
4db07b3
fix: use state file as ownership record; ask user before reusing Code…
jeff-schnitter Aug 17, 2026
4ad5fc9
fix: substitute real Jenkins URL into workflow at import time
jeff-schnitter Aug 17, 2026
f2dd7cf
fix: set Jenkins admin password via Groovy init script; improve setup…
jeff-schnitter Aug 27, 2026
5600b67
fix: replace Script Console with REST API for Jenkins token generation
jeff-schnitter Aug 28, 2026
2f49764
fix: make Jenkins unsecured/no-CSRF for demo; remove auth from workfl…
jeff-schnitter Aug 28, 2026
0d7c60a
fix: resolve Jenkins callback and BUILD_URL issues in deploy pipeline
jeff-schnitter Aug 28, 2026
2ea25e6
fix: add Authorization header to Jenkins callback curl
jeff-schnitter Aug 28, 2026
169315b
fix: always update Jenkins job config and reduce workflow timeout
jeff-schnitter Aug 28, 2026
e74b1bf
fix: remove plugin version attrs from job XML and soften update failure
jeff-schnitter Aug 28, 2026
c0c5d69
fix: patch existing job's script CDATA instead of replacing full conf…
jeff-schnitter Aug 28, 2026
50067ae
fix: remove credentials from entity custom metadata, use Cortex secrets
jeff-schnitter Aug 28, 2026
5576bf0
fix: move buildUrl to top-level url field in deploy payload
jeff-schnitter Aug 28, 2026
5bd7d5c
chore: clarify test prompt to say Cortex Workflow run
jeff-schnitter Aug 28, 2026
f9a704e
chore: remove redundant deploy confirmation check
jeff-schnitter Aug 28, 2026
2b92036
fix: remove & from Cortex secrets template syntax
jeff-schnitter Aug 28, 2026
329e231
docs: show cortex secrets create for Jenkins auth in Next Steps
jeff-schnitter Aug 28, 2026
7f0b37e
feat: add jenkins_auth Cortex secret to workflow and setup
jeff-schnitter Aug 28, 2026
91c321e
docs: drop unnecessary auth caveat — Jenkins always requires auth
jeff-schnitter Aug 28, 2026
77c5546
fix: remove auth header from jenkins trigger workflow for Unsecured demo
jeff-schnitter Aug 28, 2026
75be774
fix: restore auth header in jenkins trigger workflow
jeff-schnitter Aug 28, 2026
b066cd5
fix: handle 400 as conflict when creating jenkins_auth Cortex secret
jeff-schnitter Aug 28, 2026
495ee31
fix: print action-level details when workflow run fails
jeff-schnitter Aug 28, 2026
d6f224e
fix: use default password instead of API token for jenkins_auth secret
jeff-schnitter Aug 28, 2026
7304a17
fix: suppress 'Done! Watch your deploy' message on workflow failure
jeff-schnitter Aug 28, 2026
887aa0e
fix: use triple braces for jenkins_auth secret to avoid HTML escaping
jeff-schnitter Aug 28, 2026
a46bae2
feat: add intermediate UPDATE callbacks to Jenkins pipeline
jeff-schnitter Aug 31, 2026
9752982
fix: broaden <script> regex to match Jenkins config.xml regardless of…
jeff-schnitter Aug 31, 2026
3966cbe
chore: point Codespace ref back to main for release
jeff-schnitter Aug 31, 2026
5516c60
fix: correct exemption status assertion after revoke (REVOKED not REJ…
jeff-schnitter Aug 31, 2026
0c7cdd0
fix: limit gitops-logs get test to single page to avoid fetch-all tim…
jeff-schnitter Aug 31, 2026
a187183
fix: skip gitops-logs get test when API returns 504 instead of failing
jeff-schnitter Aug 31, 2026
df0a96f
fix: skip test_gitops_logs_page_size when API returns 504
jeff-schnitter Aug 31, 2026
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
23 changes: 23 additions & 0 deletions .devcontainer/jenkins/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
FROM jenkins/jenkins:lts-jdk17

USER root
RUN apt-get update && apt-get install -y curl jq && rm -rf /var/lib/apt/lists/*
USER jenkins

# Pre-install plugins: pipeline, credentials, JCasC, Plain Credentials binding
RUN jenkins-plugin-cli --plugins \
workflow-aggregator \
pipeline-model-definition \
configuration-as-code \
plain-credentials \
credentials-binding \
git \
http_request \
build-user-vars-plugin

ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false"
ENV CASC_JENKINS_CONFIG="/var/jenkins_home/casc_configs/jenkins.yaml"

# Groovy init script to set admin password — JCasC password: field does not reliably
# override the Jenkins initial admin password in the Docker image.
COPY init-admin-password.groovy /usr/share/jenkins/ref/init.groovy.d/
16 changes: 16 additions & 0 deletions .devcontainer/jenkins/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "Cortex Jenkins Demo",
"dockerComposeFile": "docker-compose.yml",
"service": "devcontainer",
"workspaceFolder": "/workspace",
"postCreateCommand": "pip install cortexapps-cli && bash -c 'until curl -s -o /dev/null -w \"%{http_code}\" http://jenkins:8080/login | grep -q 200; do echo \"Waiting for Jenkins...\"; sleep 5; done; echo \"Jenkins is ready at port 8080\"'",
"forwardPorts": [8080],
"portsAttributes": {
"8080": {
"label": "Jenkins UI",
"visibility": "public",
"onAutoForward": "notify"
}
},
"remoteUser": "vscode"
}
20 changes: 20 additions & 0 deletions .devcontainer/jenkins/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: "3.8"
services:
devcontainer:
image: mcr.microsoft.com/devcontainers/base:ubuntu-22.04
volumes:
- ../..:/workspace:cached
command: sleep infinity

jenkins:
build:
context: .
dockerfile: Dockerfile
ports:
- "8080:8080"
volumes:
- ./jenkins.yaml:/var/jenkins_home/casc_configs/jenkins.yaml
- jenkins_home:/var/jenkins_home

volumes:
jenkins_home:
30 changes: 30 additions & 0 deletions .devcontainer/jenkins/init-admin-password.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import hudson.model.User
import hudson.security.AuthorizationStrategy
import hudson.security.HudsonPrivateSecurityRealm
import jenkins.model.Jenkins

// Demo Codespace setup.
//
// Security model: Jenkins is fully unsecured (no auth required) with CSRF disabled.
// The Codespace URL (long random string) is the only access control — appropriate
// for a short-lived demo instance. The admin user is still created with a known
// password so the Jenkins UI can be accessed interactively.

def instance = Jenkins.getInstance()

def realm = new HudsonPrivateSecurityRealm(false)
instance.setSecurityRealm(realm)

def user = User.get("admin")
def details = HudsonPrivateSecurityRealm.Details.fromPlainPassword("cortex-demo")
user.addProperty(details)
user.save()

// Unsecured: all requests (including anonymous POST from Cortex) are permitted.
// This bypasses the Codespace proxy stripping Authorization headers on POST.
instance.setAuthorizationStrategy(AuthorizationStrategy.UNSECURED)

// Disable CSRF so POST requests from Cortex don't need a crumb.
instance.setCrumbIssuer(null)

instance.save()
7 changes: 7 additions & 0 deletions .devcontainer/jenkins/jenkins.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
jenkins:
numExecutors: 2
remotingSecurity:
enabled: true
unclassified:
location:
url: ""
129 changes: 129 additions & 0 deletions cortexapps_cli/solutions/jenkins-deploy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
name: Jenkins Deploy Tracking
description: Track deployments from Jenkins pipelines in Cortex, with a deploy health scorecard measuring delivery cadence.
---

# Jenkins Deploy Tracking

Trigger deploys from Cortex, track them as they run in Jenkins, and surface deploy health back in your service catalog.

```
┌─────────────────────────────────┐
│ Cortex Catalog │
│ │
│ jenkins-demo (service) │
│ ├── x-cortex-custom-metadata │
│ │ jenkins: │
│ │ url / job │
│ └── Scorecard: Deploy Health │
│ Bronze / Silver / Gold │
└──────────────┬──────────────────┘
│ Run workflow from entity page
│ (or: cortex workflows run -t
│ jenkins-trigger-deploy
│ --scope ENTITY --entity <tag>)
┌─────────────────────────────────┐
│ Cortex Workflow │
│ Trigger Jenkins Deploy │
│ │
│ 1. Read Jenkins config from │
│ entity custom metadata │
│ 2. POST /buildWithParameters │
│ to Jenkins via HTTP │
│ 3. Pass callback URL as │
│ pipeline parameter │
│ 4. Wait for callback │
└──────────────┬──────────────────┘
│ POST /buildWithParameters (HTTP + Basic auth)
┌─────────────────────────────────────────────────────────┐
│ GitHub Codespaces (optional — provisioned by setup) │
│ port 8080 exposed publicly for demo │
│ │
│ ┌─────────────────────────────────┐ │
│ │ Jenkins Pipeline │ │
│ │ cortex-deploy │ │
│ │ │ │
│ │ stage: Build │ │
│ │ └── run your deploy steps │ │
│ │ │ │
│ │ stage: Record Deploy in Cortex │ │
│ │ └── POST /deploys ◄────┼── registers deploy │
│ │ (entity: jenkins-demo) │ event on entity │
│ │ │ │
│ │ post { always } │ │
│ │ └── POST callbackUrl ───────►│ Cortex marks │
│ │ status: SUCCESS/FAILURE │ workflow done │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
(or point to your own Jenkins instance — Codespaces not required)
```

## What's Included

- **Entity:** `jenkins-demo` service — a sample entity to receive deploy events
- **Scorecard:** Deploy Health — Bronze/Silver/Gold based on deploy frequency
- **Jenkinsfile:** `cortex-deploy` — a two-stage pipeline (Build → Record Deploy) with an async callback to Cortex; drop it into any existing Jenkins job
- **Cortex workflow:** `jenkins-trigger-deploy` — reads Jenkins coordinates from entity custom metadata, triggers the pipeline via HTTP, and waits for the result
- **Setup script:** Interactive wizard that wires everything together end-to-end; optionally provisions Jenkins in GitHub Codespaces for a zero-install demo

## Quick Start

1. Install the solution:

```
cortex solutions install -s jenkins-deploy
```

2. Follow the post-install setup prompts, or run later:

```
cortex solutions post-install -s jenkins-deploy
```

## How It Works

The Cortex workflow reads Jenkins coordinates from `x-cortex-custom-metadata.jenkins` on the entity, then triggers `cortex-deploy` via `buildWithParameters`, passing a `callback_url` as a pipeline parameter. Cortex waits asynchronously for the pipeline to report back.

Jenkins runs the build, then notifies Cortex twice on completion:
- **Deploy registration** (`POST /api/v1/catalog/{tag}/deploys`) — records the deploy event on the entity, feeding the Deploy Health scorecard
- **Workflow callback** — signals the Cortex workflow run as SUCCESS or FAILURE

## After Installing

If you ran the post-install setup, you're already done — it created the Jenkins job, added credentials, wrote Jenkins coordinates to the entity's custom metadata, imported the Cortex workflow, and triggered a test deploy.

To roll the pattern out to your own services:

1. Add the `cortex-deploy` **Jenkinsfile** stages to any existing Jenkins pipeline (needs only `CORTEX_API_KEY` and `CORTEX_BASE_URL` secret-text credentials)

2. Add a `x-cortex-custom-metadata` block to your entity's catalog YAML with your Jenkins coordinates:

```yaml
x-cortex-custom-metadata:
jenkins:
url: "https://jenkins.example.com"
job: "your-pipeline-name"
```

3. Create a **Cortex secret** with your Jenkins credentials:

```bash
cortex secrets create -f - <<EOF
{
"name": "Jenkins Auth",
"tag": "jenkins_auth",
"secret": "$(echo -n 'your-username:your-api-token' | base64)"
}
EOF
```

4. Run the **Solution: Trigger Jenkins Deploy** workflow from the entity page — it reads the Jenkins coordinates from the entity's custom metadata automatically, with no manual inputs required

## Customizing for Production

- Point the workflow at your real entity by replacing `jenkins-demo` with your service tag
- Add `CORTEX_API_KEY` and `CORTEX_BASE_URL` secret-text credentials to your real Jenkins instances
- The Deploy Health scorecard is scoped to `demo-jenkins-deploys` to avoid affecting your existing services. To roll it out broadly, remove the group filter from the scorecard. To opt in individual services, add the `demo-jenkins-deploys` group to them.
82 changes: 82 additions & 0 deletions cortexapps_cli/solutions/jenkins-deploy/_templates/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
pipeline {
agent any
parameters {
string(name: 'callback_url', defaultValue: '', description: 'Cortex async callback URL')
string(name: 'cortex_entity_tag', defaultValue: '', description: 'Cortex entity tag')
}
environment {
CORTEX_API_KEY = credentials('CORTEX_API_KEY')
CORTEX_BASE_URL = credentials('CORTEX_BASE_URL')
}
stages {
stage('Build') {
steps {
echo 'Starting build...'
// Replace this echo with your actual build commands
}
}
stage('Deploy') {
steps {
script {
// Sends intermediate UPDATE callbacks to the Cortex workflow,
// demonstrating that the callback URL can be called multiple times
// before the terminal SUCCESS/FAILURE.
def callbackUrl = params.callback_url
def updates = [
[progress: 33, message: 'Build complete — starting deploy'],
[progress: 66, message: 'Deploy in progress'],
[progress: 100, message: 'Deploy complete — recording in Cortex'],
]
updates.each { u ->
sleep 5
echo "Deploy progress: ${u.progress}%"
if (callbackUrl) {
def payload = """{"status":"UPDATE","message":"${u.message}","response":{"progress":"${u.progress}%"}}"""
sh """
curl -s -X POST '${callbackUrl}' \\
-H 'Content-Type: application/json' \\
-H "Authorization: Bearer \${CORTEX_API_KEY}" \\
-d '${payload}' || true
"""
}
}
// Replace the placeholder loop above with your actual deploy commands
}
}
}
stage('Record Deploy in Cortex') {
steps {
script {
def timestamp = sh(script: 'date -u +%Y-%m-%dT%H:%M:%SZ', returnStdout: true).trim()
def buildUrl = env.BUILD_URL ?: "${env.JENKINS_URL}job/${env.JOB_NAME}/${env.BUILD_NUMBER}/"
def payload = """{"sha":"${env.BUILD_NUMBER}","timestamp":"${timestamp}","environment":"production","type":"DEPLOY","title":"Triggered by Jenkins","url":"${buildUrl}","deployer":{"name":"Jenkins"},"customData":{"buildNumber":"${env.BUILD_NUMBER}","jobName":"${env.JOB_NAME}"}}"""
sh """
curl -s -f -X POST \\
"\${CORTEX_BASE_URL}/api/v1/catalog/${params.cortex_entity_tag}/deploys" \\
-H "Authorization: Bearer \${CORTEX_API_KEY}" \\
-H "Content-Type: application/json" \\
-d '${payload}' || true
"""
}
}
}
}
post {
always {
script {
if (params.callback_url) {
def status = currentBuild.currentResult == 'SUCCESS' ? 'SUCCESS' : 'FAILURE'
def buildUrl = env.BUILD_URL ?: "${env.JENKINS_URL}job/${env.JOB_NAME}/${env.BUILD_NUMBER}/"
def payload = """{"status":"${status}","message":"Jenkins pipeline ${status.toLowerCase()}","response":{"buildUrl":"${buildUrl}","buildNumber":"${env.BUILD_NUMBER}","jobName":"${env.JOB_NAME}"}}"""
def callbackUrl = params.callback_url
sh """
curl -s -f -X POST '${callbackUrl}' \\
-H 'Content-Type: application/json' \\
-H "Authorization: Bearer \${CORTEX_API_KEY}" \\
-d '${payload}'
"""
}
}
}
}
}
Loading