GitLab CI PR Environment Automation

This guide shows how to set up GitLab CI to automatically create and update ephemeral environments for your pull requests using Blueberry IDP.

Overview

The Blueberry IDP webhook system provides a seamless way to:

  1. Create environments automatically when a PR is opened
  2. Update environments when new commits are pushed to the PR branch
  3. Single pod deployment with acceptable downtime during updates
  4. Manual redeploy option for troubleshooting

Prerequisites

  1. API Token: Create an API token in the Blueberry IDP with environments:create scope
  2. Repository Configuration: Your repository must be configured in the Blueberry IDP
  3. Docker Registry Access: Your CI must push images to the configured registry

GitLab CI Configuration

1. Set up CI/CD Variables

In your GitLab project, go to Settings > CI/CD > Variables and add:

# Required
BLUEBERRY_API_URL=https://blueberry.florenciacomuzzi.com
BLUEBERRY_API_TOKEN=your_api_token_here

# Optional (if using custom registry)
DOCKER_REGISTRY=us-docker.pkg.dev/development-454916/blueberry

2. Basic .gitlab-ci.yml Example

# .gitlab-ci.yml
variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: "/certs"
  IMAGE_NAME: "$DOCKER_REGISTRY/$CI_PROJECT_NAME"
  IMAGE_TAG: "$CI_COMMIT_SHORT_SHA"

stages:
  - test
  - build
  - deploy-environment

# Run tests
test:
  stage: test
  image: python:3.11-slim
  script:
    - pip install -r requirements.txt
    - pytest tests/
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Build and push Docker image
build:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  before_script:
    - echo $GCLOUD_SERVICE_KEY | docker login -u _json_key --password-stdin https://us-docker.pkg.dev
  script:
    - docker build -t $IMAGE_NAME:$IMAGE_TAG .
    - docker push $IMAGE_NAME:$IMAGE_TAG
    - echo "Built and pushed $IMAGE_NAME:$IMAGE_TAG"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Create or update PR environment
deploy-pr-environment:
  stage: deploy-environment
  image: alpine:latest
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      # Determine if this is a merge request
      if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
        echo "Deploying PR environment for MR !$CI_MERGE_REQUEST_IID"

        # Call Blueberry webhook to create/update environment
        RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST \
          "$BLUEBERRY_API_URL/api/webhooks/pr-environment" \
          -H "Authorization: Bearer $BLUEBERRY_API_TOKEN" \
          -H "Content-Type: application/json" \
          -d '{
            "repo_url": "'$CI_PROJECT_URL'",
            "repo_name": "'$CI_PROJECT_NAME'",
            "pr_number": '$CI_MERGE_REQUEST_IID',
            "branch": "'$CI_COMMIT_REF_NAME'",
            "target_branch": "'$CI_MERGE_REQUEST_TARGET_BRANCH_NAME'",
            "commit_sha": "'$CI_COMMIT_SHA'",
            "commit_message": "'$CI_COMMIT_MESSAGE'",
            "commit_author": "'$CI_COMMIT_AUTHOR'",
            "ttl_hours": 168
          }')

        # Extract HTTP status and response body
        HTTP_BODY=$(echo $RESPONSE | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
        HTTP_STATUS=$(echo $RESPONSE | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')

        echo "HTTP Status: $HTTP_STATUS"
        echo "Response: $HTTP_BODY"

        # Check if request was successful
        if [ "$HTTP_STATUS" -eq 200 ]; then
          echo "✅ Environment deployment successful!"

          # Extract URLs from response if available
          APP_URL=$(echo "$HTTP_BODY" | jq -r '.urls.app // empty')
          if [ -n "$APP_URL" ] && [ "$APP_URL" != "null" ]; then
            echo "🌐 Environment URL: $APP_URL"

            # Add comment to merge request (optional)
            if command -v gitlab-comment >/dev/null 2>&1; then
              gitlab-comment "🚀 PR Environment deployed: [$APP_URL](/developer-docs/docs/setup/05-cicd/$APP_URL)"
            fi
          fi

          # Extract environment name and action
          ENV_NAME=$(echo "$HTTP_BODY" | jq -r '.environment_name // empty')
          ACTION=$(echo "$HTTP_BODY" | jq -r '.action // empty')

          if [ -n "$ENV_NAME" ]; then
            echo "📝 Environment: $ENV_NAME (action: $ACTION)"
          fi

        else
          echo "❌ Environment deployment failed!"
          echo "Error details: $HTTP_BODY"
          exit 1
        fi
      else
        echo "â„šī¸  Not a merge request, skipping PR environment deployment"
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  dependencies:
    - build

3. Advanced Configuration with Frontend Apps

For applications with multiple frontend components:

deploy-pr-environment-full:
  stage: deploy-environment
  image: alpine:latest
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
        echo "Deploying full-stack PR environment for MR !$CI_MERGE_REQUEST_IID"

        # Call Blueberry webhook with frontend apps
        curl -X POST "$BLUEBERRY_API_URL/api/webhooks/pr-environment" \
          -H "Authorization: Bearer $BLUEBERRY_API_TOKEN" \
          -H "Content-Type: application/json" \
          -d '{
            "repo_url": "'$CI_PROJECT_URL'",
            "repo_name": "'$CI_PROJECT_NAME'",
            "pr_number": '$CI_MERGE_REQUEST_IID',
            "branch": "'$CI_COMMIT_REF_NAME'",
            "target_branch": "'$CI_MERGE_REQUEST_TARGET_BRANCH_NAME'",
            "commit_sha": "'$CI_COMMIT_SHA'",
            "commit_message": "'$CI_COMMIT_MESSAGE'",
            "commit_author": "'$CI_COMMIT_AUTHOR'",
            "ttl_hours": 168,
            "frontend_apps": [
              {
                "repo_id": "frontend-app",
                "commit_sha": "'$FRONTEND_COMMIT_SHA'",
                "branch": "main"
              }
            ]
          }'
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  variables:
    FRONTEND_COMMIT_SHA: "latest"  # Or fetch from another repo

4. Simplified Version

For basic use cases, use the simplified endpoint:

deploy-simple:
  stage: deploy-environment
  image: alpine:latest
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      if [ "$CI_PIPELINE_SOURCE" = "merge_request_event" ]; then
        curl -X POST "$BLUEBERRY_API_URL/api/webhooks/pr-environment/simple" \
          -H "Authorization: Bearer $BLUEBERRY_API_TOKEN" \
          -H "Content-Type: application/json" \
          -d '{
            "repo_name": "'$CI_PROJECT_NAME'",
            "pr_number": '$CI_MERGE_REQUEST_IID',
            "branch": "'$CI_COMMIT_REF_NAME'",
            "commit_sha": "'$CI_COMMIT_SHA'"
          }'
      fi
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

Environment Lifecycle

Automatic Creation

When you open a merge request, the CI pipeline will:
1. Run tests
2. Build Docker image
3. Call Blueberry webhook to create environment

Automatic Updates

When you push new commits to the MR branch:
1. Build new Docker image with updated commit SHA
2. Call Blueberry webhook to update existing environment
3. Environment is updated with new image (downtime acceptable)

Manual Actions

In the Blueberry IDP environment detail page, you can:
- Redeploy: Restart the environment with current configuration
- Extend TTL: Keep the environment alive longer
- Delete: Clean up the environment manually

Environment Naming

The system uses predictable naming:
- PR-based: pr-123 (for MR #123)
- Branch-based: backend-service-feature-branch-abcd1234

Troubleshooting

Common Issues

  1. Image not found: Ensure Docker image is built and pushed before calling webhook
    bash # Check if image exists docker manifest inspect $IMAGE_NAME:$IMAGE_TAG

  2. Authentication failed: Verify your API token has correct scopes
    bash curl -H "Authorization: Bearer $BLUEBERRY_API_TOKEN" \ "$BLUEBERRY_API_URL/api/health"

  3. Environment not updating: Check if environment exists and is not terminated

  4. Environment might have been manually deleted
  5. TTL might have expired

Debugging Commands

# Test webhook manually
curl -X POST "$BLUEBERRY_API_URL/api/webhooks/pr-environment/simple" \
  -H "Authorization: Bearer $BLUEBERRY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "repo_name": "your-repo",
    "pr_number": 123,
    "branch": "feature-branch",
    "commit_sha": "abcd1234"
  }'

# Check environment status
curl -H "Authorization: Bearer $BLUEBERRY_API_TOKEN" \
     "$BLUEBERRY_API_URL/api/environments"

Best Practices

  1. Resource Management: Use reasonable TTL values (default: 7 days)
  2. Image Tagging: Use commit SHAs for reproducible deployments
  3. Error Handling: Make CI fail if environment deployment fails
  4. Notifications: Consider adding Slack/Teams notifications on success/failure
  5. Cleanup: Set up scheduled pipelines to clean up old environments

Security Considerations

  • Store API tokens as protected CI/CD variables
  • Use minimal scopes for API tokens (environments:create only)
  • Consider IP restrictions for webhook endpoints
  • Regularly rotate API tokens

Integration with PR Comments

You can enhance the workflow by adding comments to merge requests:

# Add this to your deploy job
after_script:
  - |
    if [ "$HTTP_STATUS" -eq 200 ] && [ -n "$APP_URL" ]; then
      # Post comment to MR (requires GitLab API token)
      curl -X POST \
        "$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
        -H "Authorization: Bearer $GITLAB_API_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{"body": "🚀 PR Environment deployed: ['$APP_URL'](/developer-docs/docs/setup/05-cicd/'$APP_URL')"}'
    fi

This creates a comprehensive CI/CD pipeline that automatically manages ephemeral environments for your pull requests, with proper error handling and user feedback.

Document ID: setup/05-cicd/gitlab-ci-pr-environments