pipeline { agent any parameters { string( name: 'REPO_URL', defaultValue: 'https://git.crowmate.fr/crowmate/Nest.git', description: 'Gitea repository URL (to retrieve docker-compose.prod.yml)' ) string( name: 'BRANCH', defaultValue: 'main', description: 'Branch' ) string( name: 'STACK_NAME', defaultValue: 'nest', description: 'Stack name in Portainer' ) string( name: 'IMAGE_TAG', defaultValue: 'latest', description: 'Image tag to deploy' ) } environment { GITEA_CREDENTIALS_ID = 'gitea-token' PORTAINER_URL = 'https://docker.devgoblin.fr' PORTAINER_ENV_ID = '3' } stages { stage('Clone') { steps { cleanWs() checkout([ $class: 'GitSCM', branches: [[name: "*/${params.BRANCH}"]], userRemoteConfigs: [[ url: params.REPO_URL, credentialsId: env.GITEA_CREDENTIALS_ID ]] ]) echo "Repository cloned" } } stage('Validation') { steps { script { if (!fileExists('docker-compose.prod.yml')) { error("docker-compose.prod.yml not found at the repository root") } echo "docker-compose.prod.yml found" } } } stage('Prepare compose payload') { steps { sh ''' base64 -w 0 docker-compose.prod.yml > compose-content.b64 echo "Compose payload prepared" ''' } } stage('Fetch Portainer stacks') { steps { withCredentials([string(credentialsId: 'portainer-token', variable: 'PORTAINER_TOKEN')]) { sh ''' curl -s -X GET "''' + env.PORTAINER_URL + '''/api/stacks" \ -H "X-API-Key: $PORTAINER_TOKEN" > portainer-stacks.json echo "Portainer stacks fetched" ''' } } } stage('Resolve target stack') { steps { script { env.STACK_ID = sh( script: "jq -r '.[] | select(.Name == \"${params.STACK_NAME}\") | .Id' portainer-stacks.json | head -n 1", returnStdout: true ).trim() if (env.STACK_ID) { echo "Existing stack found (ID: ${env.STACK_ID})" } else { echo "Stack does not exist and will be created" } } } } stage('Update existing stack') { when { expression { return env.STACK_ID?.trim() } } steps { withCredentials([string(credentialsId: 'portainer-token', variable: 'PORTAINER_TOKEN')]) { sh ''' COMPOSE_CONTENT=$(cat compose-content.b64) curl -s -X PUT "''' + env.PORTAINER_URL + '''/api/stacks/$STACK_ID?endpointId=''' + env.PORTAINER_ENV_ID + '''" \ -H "X-API-Key: $PORTAINER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"stackFileContent\": \"$COMPOSE_CONTENT\", \"prune\": true, \"pullImage\": true}" > portainer-response.json echo "Stack updated" ''' } } } stage('Create new stack') { when { expression { return !env.STACK_ID?.trim() } } steps { withCredentials([string(credentialsId: 'portainer-token', variable: 'PORTAINER_TOKEN')]) { sh ''' COMPOSE_CONTENT=$(cat compose-content.b64) curl -s -X POST "''' + env.PORTAINER_URL + '''/api/stacks/create/standalone/string?endpointId=''' + env.PORTAINER_ENV_ID + '''" \ -H "X-API-Key: $PORTAINER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"name\": \"''' + params.STACK_NAME + '''\", \"stackFileContent\": \"$COMPOSE_CONTENT\"}" > portainer-response.json echo "Stack created" ''' } } } stage('Validate Portainer response') { steps { sh ''' if jq -e '.message' portainer-response.json > /dev/null 2>&1; then echo "Portainer error: $(jq -r '.message' portainer-response.json)" exit 1 fi echo "Stack deployed successfully" ''' } } } post { success { echo "Stack '${params.STACK_NAME}' deployed to Portainer (tag: ${params.IMAGE_TAG})" } failure { echo "Failed to deploy stack '${params.STACK_NAME}'" } } }