From 2c535080d2f03eac027d51c741f7447396feaaf0 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 9 Sep 2026 12:44:55 +0100 Subject: [PATCH] build(confidence-engine): add manual Jenkins deployment --- Jenkinsfile | 111 ++++++++++++++++++++ deploy.env.example | 12 +++ docs/current-handoff.md | 12 +++ docs/current-project-state.md | 14 +++ scripts/deploy-production.sh | 189 ++++++++++++++++++++++++++++++++++ 5 files changed, 338 insertions(+) create mode 100644 Jenkinsfile create mode 100644 deploy.env.example create mode 100755 scripts/deploy-production.sh diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..d751362 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,111 @@ +// Confidence Engine — Manual Jenkins Deployment Pipeline +// +// Trigger: manually, via "Build with Parameters" +// Parameter: GIT_REF (string) — the Git ref to deploy +// +// Pre-requisites in Jenkins: +// 1. SSH credential of type "SSH Username with private key" +// named 'confidence-engine-deploy-ssh' that can reach +// CT 112 (confidence-engine / 192.168.68.73). +// The username from the credential is used for SSH login. +// 2. The Gitea repository configured in the job SCM section. + +pipeline { + agent none + + parameters { + string( + name: 'GIT_REF', + defaultValue: '', + description: 'Git ref to deploy (branch name, tag, or full commit SHA). Leave blank to fail.' + ) + } + + environment { + TARGET_HOST = 'confidence-engine' + DEPLOY_DIR = '/opt/confidence-engine' + HEALTH_URL = 'http://127.0.0.1:3000/api/health' + } + + stages { + + stage('Resolve') { + steps { + script { + def ref = params.GIT_REF.trim() + if (!ref || ref.isEmpty()) { + error 'GIT_REF parameter is blank or empty. Provide a Git ref to deploy.' + } + + echo "Requested ref: ${ref}" + + // Resolve the ref to an exact SHA via Gitea remote. + // If ref is already a 40-char hex SHA, use it directly. + def shaPattern = ~/^[0-9a-fA-F]{40}$/ + def resolvedSha + if (ref ==~ shaPattern) { + resolvedSha = ref + echo "Provided ref is a full commit SHA: ${resolvedSha}" + } else { + // For branches/tags, look up on origin + resolvedSha = sh( + script: "git ls-remote origin refs/heads/${ref} refs/tags/${ref} 2>/dev/null | awk '/^[0-9a-f]/{print \$1; exit}'", + returnStdout: true + ).trim() + + if (!resolvedSha || resolvedSha.length() != 40) { + // Broader fallback — might match partial SHA or ref prefix + resolvedSha = sh( + script: "git ls-remote origin ${ref} 2>/dev/null | awk '/^[0-9a-f]/{print \$1; exit}'", + returnStdout: true + ).trim() + + if (!resolvedSha || resolvedSha.length() != 40) { + error "Cannot resolve '${ref}' to a commit SHA on origin. Check the ref and repository configuration." + } + } + } + + echo "Resolved SHA: ${resolvedSha}" + env.DEPLOY_SHA = resolvedSha + } + } + } + + stage('Deploy') { + steps { + script { + // Run the deployment script on CT 112 via SSH + withCredentials([sshUserPrivateKey(credentialsId: 'confidence-engine-deploy-ssh')]) { + sh """ + ssh -o StrictHostKeyChecking=no \\ + root@${TARGET_HOST} \\ + "bash -s" \\ + < ${WORKSPACE}/scripts/deploy-production.sh \\ + "${DEPLOY_SHA}" \\ + "${DEPLOY_DIR}" \\ + "${HEALTH_URL}" + """ + } + } + } + } + + stage('Verify/result') { + steps { + script { + echo "Deployment stages completed. Check the Deploy stage output above for success/failure." + } + } + } + } + + post { + failure { + echo 'DEPLOYMENT FAILED — check the Deploy stage logs for details.' + } + success { + echo "DEPLOYMENT SUCCEEDED — deployed SHA: ${env.DEPLOY_SHA}" + } + } +} diff --git a/deploy.env.example b/deploy.env.example new file mode 100644 index 0000000..4f9bf3d --- /dev/null +++ b/deploy.env.example @@ -0,0 +1,12 @@ +# Confidence Engine — Deploy Environment (example) +# +# Copy to /opt/confidence-engine/deploy.env on CT 112. +# This file is NOT tracked by Git. It is owned/administered on the host. +# +# Build-time (baked into Docker image via --build-arg): +NEXT_PUBLIC_SUPABASE_URL=https://supabase.rdbcloud.co.uk +NEXT_PUBLIC_SUPABASE_ANON_KEY=replace-with-supabase-anon-key + +# Runtime (passed to container at start): +OLLAMA_BASE_URL=http://192.168.x.x:11434 +OLLAMA_MODEL=replace-with-model-name diff --git a/docs/current-handoff.md b/docs/current-handoff.md index ae870f7..a328bd9 100644 --- a/docs/current-handoff.md +++ b/docs/current-handoff.md @@ -75,6 +75,18 @@ Does the complete investigation process leave real people materially clearer abo - The full apply-proposal owner suite remains known-red in independent pre-existing 60B.43 tests, so the changed Done-for-now boundary was verified through exact isolated owner tests. Zero live calls occurred during closeout. Next boundary: reuse the existing investigation and click Done for now once, observing cases/update and subsequent synthesis. - Live UI proved Done for now briefly clarified the question, then server graph replacement reopened it: the client sends `preDoneGraph`, and the server previously had no deterministic closure owner. Episode-mode update now adds the selected `targetNodeId` to `resolvedNodeIds` only after successful semantic application; semantic no-ops and meaningful mutations remain valid, while failed episodes do not resolve the target and ordinary updates are unchanged. Zero live calls occurred during implementation. Next boundary: one live Done-for-now check on the existing investigation. +## Deployment automation (new) + +- Manual Jenkins deployment pipeline established and version-controlled. +- `Jenkinsfile` in repository root — Declarative Pipeline, three stages: Resolve → Deploy → Verify/result. +- `scripts/deploy-production.sh` — executes on CT 112; validates SHA, fetches Git ref, checks out exact commit, builds Docker image tagged by SHA, replaces container, polls `/api/health`, one-step rollback to prior image on failure. +- `deploy.env.example` — example of required runtime/build environment file (NOT tracked). +- Production env owned by `/opt/confidence-engine/deploy.env` on CT 112 (not in Git). +- `GIT_REF` is runtime-selectable (Jenkins parameter); resolves to immutable SHA before deployment. +- No Docker registry introduced. SHA-tagged images retained for rollback support. +- Jenkins job remains manually triggered — no automatic webhook deployment. +- No product behaviour changed. + ## Repository checkpoint - **Branch:** `feature/product-platform-foundation-v0.62` diff --git a/docs/current-project-state.md b/docs/current-project-state.md index 224c62f..de3c624 100644 --- a/docs/current-project-state.md +++ b/docs/current-project-state.md @@ -188,6 +188,20 @@ First document to read: **`docs/current-handoff.md`** (methodology continuity + Implementation status last checked against source: Experiment 43 + v0.61 apparatus (tsx helper, reconstruction-only seam, focused-deconstruction schema fix). Multi-investigation architecture verified at v0.60g2+ and structurally complete. The current-state document was verified as accurate by focused code inspection of API routes, orchestrator imports/calls, and cross-module traces for all passive classifiers. No corrections were required. +## Deployment Automation + +A manual Jenkins deployment pipeline has been established and is now repository-owned. + +- **Jenkinsfile** (root) — Declarative Pipeline with three stages: `Resolve` → `Deploy` → `Verify/result`. +- **Parameter:** `GIT_REF` (string) — user-supplied Git ref (branch, tag, or SHA). Blank value fails clearly. +- **Resolution:** Jenkins resolves the ref to an exact commit SHA via `git ls-remote origin` before deployment. The SHA is deployed immutably. +- **Target host:** CT 112 (`confidence-engine`, 192.168.68.73) at `/opt/confidence-engine`. +- **Docker image:** tagged `confidence-engine:`, built on CT 112, no registry required. +- **Environment:** production values in `/opt/confidence-engine/deploy.env` on CT 112 (not in Git). `deploy.env.example` provided as reference. +- **Health check:** polls `http://127.0.0.1:3000/api/health`; requires `{"healthy":true}` within 60s. +- **Rollback:** one-step rollback to the previous container image on failure (if available). +- **Not configured in this increment:** Jenkins job, deploy.env values, SSH credential, first deployment run. + ## 10. Post-v0.8 Methodology Learning ### Durable methodology principles (2026-08-19) diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh new file mode 100755 index 0000000..9c74bd6 --- /dev/null +++ b/scripts/deploy-production.sh @@ -0,0 +1,189 @@ +#!/usr/bin/env bash +# Confidence Engine — Production Deployment Script +# +# Executes on CT 112 at /opt/confidence-engine. +# Input: $1 = exact commit SHA (validated) +# +# Environment: +# DEPLOY_DIR - target repository path (defaults /opt/confidence-engine) +# HEALTH_URL - health check endpoint (defaults http://127.0.0.1:3000/api/health) +# +# Reads deploy.env from $DEPLOY_DIR/deploy.env for runtime values. + +set -euo pipefail + +# ── Parameters ────────────────────────────────────────────────────── + +DEPLOY_SHA="${1:?Error: missing commit SHA argument}" +DEPLOY_DIR="${DEPLOY_DIR:-/opt/confidence-engine}" +HEALTH_URL="${HEALTH_URL:-http://127.0.0.1:3000/api/health}" + +# ── Validation ────────────────────────────────────────────────────── + +if [[ ${#DEPLOY_SHA} -lt 7 ]]; then + echo "ERROR: SHA must be at least 7 hex characters." + exit 1 +fi + +if ! echo "$DEPLOY_SHA" | grep -qE '^[0-9a-fA-F]+$'; then + echo "ERROR: SHA contains non-hex characters." + exit 1 +fi + +echo "Deploying SHA: $DEPLOY_SHA" + +cd "$DEPLOY_DIR" || { echo "ERROR: Cannot cd to $DEPLOY_DIR"; exit 1; } + +# ── Safety checks ─────────────────────────────────────────────────── + +# deploy.env must exist on the host (not tracked by Git) +if [[ ! -f deploy.env ]]; then + echo "DEPLOYMENT BLOCKED - deploy.env not found at deploy.env" + exit 1 +fi + +# Check for uncommitted changes that would block safe checkout +if ! git diff-index --quiet HEAD -- 2>/dev/null; then + echo "DEPLOYMENT BLOCKED - target working tree has uncommitted changes" + exit 1 +fi + +if [[ -n "$(git ls-files --others --exclude-standard)" ]]; then + echo "DEPLOYMENT BLOCKED - target working tree has untracked files" + exit 1 +fi + +# ── Fetch and verify SHA ──────────────────────────────────────────── + +echo "Fetching latest refs from origin..." +git fetch origin >/dev/null 2>&1 || { + echo "ERROR: git fetch origin failed." + exit 1 +} + +if ! git rev-parse --verify "$DEPLOY_SHA" >/dev/null 2>&1; then + echo "ERROR: SHA $DEPLOY_SHA not found in repository after fetch." + exit 1 +fi + +# ── Detached checkout (never modifies working tree or remote) ─────── + +echo "Checking out exactly $DEPLOY_SHA..." +git checkout --detach "$DEPLOY_SHA" >/dev/null 2>&1 || { + echo "ERROR: Could not checkout SHA $DEPLOY_SHA." + exit 1 +} + +# Verify the detached HEAD matches our deploy SHA +CURRENT_SHA="$(git rev-parse HEAD)" +if [[ "${CURRENT_SHA%"${CURRENT_SHA#?}"}" != "${DEPLOY_SHA%"${DEPLOY_SHA#?}"}" ]] || \ + [[ "$CURRENT_SHA" != "$DEPLOY_SHA" && "${CURRENT_SHA:0:7}" != "${DEPLOY_SHA:0:7}" ]]; then + echo "ERROR: Checked out SHA $CURRENT_SHA does not match requested $DEPLOY_SHA." + exit 1 +fi + +# ── Load runtime environment ──────────────────────────────────────── + +if [[ -f deploy.env ]]; then + set -a + # shellcheck disable=SC1091 + source deploy.env + set +a +else + echo "ERROR: deploy.env not found at $DEPLOY_DIR/deploy.env" + exit 1 +fi + +# ── Build Docker image ────────────────────────────────────────────── + +IMAGE_TAG="confidence-engine:${DEPLOY_SHA}" + +echo "Building Docker image ${IMAGE_TAG}..." +docker build \ + --build-arg NEXT_PUBLIC_SUPABASE_URL="${NEXT_PUBLIC_SUPABASE_URL}" \ + --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY}" \ + -t "${IMAGE_TAG}" \ + . + +echo "Image ${IMAGE_TAG} built successfully." + +# ── Container replacement ─────────────────────────────────────────── + +CONTAINER_NAME="confidence-engine" + +# Record previous image before replacing container +PREV_IMAGE="" +if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then + PREV_IMAGE="$(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo "")" +fi + +echo "Stopping existing container (if running)..." +docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true +docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + +echo "Starting new container from ${IMAGE_TAG}..." +docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p 3000:3000 \ + -e OLLAMA_BASE_URL="${OLLAMA_BASE_URL}" \ + -e OLLAMA_MODEL="${OLLAMA_MODEL}" \ + "${IMAGE_TAG}" + +echo "Container $CONTAINER_NAME started." + +# ── Health check polling ──────────────────────────────────────────── + +HEALTH_CHECK_TIMEOUT=60 +HEALTH_CHECK_INTERVAL=3 +ELAPSED=0 + +echo "Waiting for health check at ${HEALTH_URL}..." + +while [[ $ELAPSED -lt $HEALTH_CHECK_TIMEOUT ]]; do + if curl -sf "$HEALTH_URL" | grep -q '"healthy"[[:space:]]*:[[:space:]]*true'; then + echo "" + echo "=============================================" + echo "DEPLOYMENT SUCCEEDED" + echo "Deployed SHA: $DEPLOY_SHA" + echo "Current image: $(docker inspect --format='{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || echo 'unknown')" + echo "=============================================" + exit 0 + fi + + sleep "$HEALTH_CHECK_INTERVAL" + ELAPSED=$((ELAPSED + HEALTH_CHECK_INTERVAL)) +done + +echo "" +echo "=============================================" +echo "DEPLOYMENT FAILED - Health check timed out after ${HEALTH_CHECK_TIMEOUT}s" +echo "=============================================" + +# ── Rollback (one attempt) ────────────────────────────────────────── + +if [[ -n "$PREV_IMAGE" ]]; then + echo "" + echo "Attempting rollback to previous image: $PREV_IMAGE" + docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + + docker run -d \ + --name "$CONTAINER_NAME" \ + --restart unless-stopped \ + -p 3000:3000 \ + -e OLLAMA_BASE_URL="${OLLAMA_BASE_URL}" \ + -e OLLAMA_MODEL="${OLLAMA_MODEL}" \ + "$PREV_IMAGE" + + if [[ $? -eq 0 ]]; then + echo "DEPLOYMENT FAILED - ROLLED BACK to $PREV_IMAGE" + else + echo "DEPLOYMENT FAILED - ROLLBACK ALSO FAILED" + fi +else + echo "No previous image available for rollback." +fi + +echo "DEPLOYMENT FAILED - MANUAL RECOVERY REQUIRED" +exit 1