Best practice for promoting container images from MR builds to production on main?

Hi everyone,

I’d like some advice on structuring build vs. promotion in GitLab CI/CD.

  • Current idea:

    • On Merge Request (MR) pipelines, I build and push a test container image.

    • The image is tagged with the MR branch name (e.g., feature/foo).

    • This gives me a preview/test image for reviewers.

  • Promotion question:

    • When the MR is merged to main, I’d like to promote the same container image (built during the MR pipeline) into my production repository, instead of rebuilding.

    • However, once on main, I’m not sure what’s the best way to reliably know which branch was merged, so I can pick the right test image to promote.

  • Current setup:

    • I use merge-commit with semi-linear history to avoid race conditions and ensure the promoted image includes all changes.

    • To check the branch that was merged, I currently run:

      git log --merges -n 1 --pretty=format:"%s" | awk -F"'" '/Merge branch/{print $2}'
      
      

Questions:

  1. Is tagging MR images with the branch name a good approach for this?

  2. How do others solve the “promotion” problem when moving from MR → main?

  3. Do you usually rebuild on main, or reuse the MR-built image?

  4. Is there a recommended way in GitLab to track the branch/tag that got merged, so I can promote the correct image?

I’d love to hear how others are handling this.

Thanks!


Example of pipeline:

variables:
  IMAGE_REGISTRY: registry.example.com
  IMAGE_NAME: ${IMAGE_REGISTRY}/my-project/app
  IMAGE_NAME_TEST: ${IMAGE_NAME}-test
  IMAGE_TAG: ${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME}

stages:
  - build-and-push
  - test
  - promote

# Rules
.rules:mr:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
.rules:main:
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

# Jobs
build-and-push:
  stage: build-and-push
  image: quay.io/buildah/stable:v1.41.4
  extends: [.rules:mr]
  script:
    - echo "Building and pushing test image..."
    - buildah bud -t ${IMAGE_NAME_TEST}:${IMAGE_TAG} .
    - buildah push ${IMAGE_NAME_TEST}:${IMAGE_TAG}

test:
  stage: test
  extends: [.rules:mr]
  image: ${IMAGE_NAME_TEST}:${IMAGE_TAG}
  needs: [build-and-push]
  script:
    - echo "Run integration tests here"
    - echo "Tests passed!"

promote:
  stage: promote
  image: registry.redhat.io/ubi9/skopeo:9.6
  extends: [.rules:main]
  variables:
    IMAGE_TAG: latest
  script:
    - echo "Promoting image with skopeo..."
    # Extract merged branch tag
    - BRANCH_TAG=$(git log --merges -n 1 --pretty=format:"%s" | awk -F"'" '/Merge branch/{print $2}')
    # Copy from test repo to prod repo
    - skopeo copy docker://${IMAGE_NAME_TEST}:${BRANCH_TAG} docker://${IMAGE_NAME}:${IMAGE_TAG}
    - echo "Promoted ${IMAGE_NAME_TEST}:${BRANCH_TAG} -> ${IMAGE_NAME}:${IMAGE_TAG}"
    - skopeo delete docker://${IMAGE_NAME_TEST}:${BRANCH_TAG}