Skip to main content

Merge and Tag

The merge_and_tag_job is used to automate merge request merging and create realignment branches. It merges a merge request, creates a new branch for realignment, and opens a new merge request to align branches.

Overview

This job:

  • Extracts version number from the source branch name
  • Checks and merges a merge request via GitLab API
  • Creates a new realign/{VERSION} branch from master
  • Creates a new merge request from the realign branch to develop
  • Exports version and tag information as environment variables for other jobs

Variables

The following variables can be configured:

VariableDescriptionDefaultRequired
MERGE_AND_TAG_IMAGEDocker image to use'node:latest'No

GitLab CI/CD Variables

The job uses the following built-in GitLab CI/CD variables:

  • CI_MERGE_REQUEST_SOURCE_BRANCH_NAME: Source branch name of the merge request
  • CI_MERGE_REQUEST_IID: Internal ID of the merge request
  • CI_PROJECT_ID: Project ID
  • CI_API_V4_URL: GitLab API v4 URL
  • CI_PROJECT_ID: Project identifier

Required Secrets

The following secrets must be configured in GitLab CI/CD variables:

  • GITLAB_TOKEN: GitLab personal access token or project access token with API permissions

Usage

Basic Usage

variables:
MERGE_AND_TAG_IMAGE: 'node:latest'

.merge_and_tag_job:
image: $MERGE_AND_TAG_IMAGE
before_script:
- |
echo "Installing jq..."
apt update && apt install -y jq
VERSION=$(echo ${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} | awk -F "/" '{print $2}')
echo "EXTRA_DESCRIPTION=RELEASE=v${VERSION} | DATE=$(date +%Y-%m-%d)" >> variables.env
echo "TAG=v${VERSION}" >> variables.env
echo "VERSION=${VERSION}" >> variables.env
script:
- |
# ... (see full script in job definition)
artifacts:
reports:
dotenv: variables.env

Job Details

Before Script

The before_script section performs the following setup:

  1. Install jq:

    apt update && apt install -y jq
    • Installs jq for JSON parsing of GitLab API responses
  2. Extract version from branch name:

    VERSION=$(echo ${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} | awk -F "/" '{print $2}')
    • Extracts version from branch name (e.g., release/1.0.01.0.0)
    • Assumes branch format: `{prefix}/{version}`
  3. Generate environment variables:

    echo "EXTRA_DESCRIPTION=RELEASE=v${VERSION} | DATE=$(date +%Y-%m-%d)" >> variables.env
    echo "TAG=v${VERSION}" >> variables.env
    echo "VERSION=${VERSION}" >> variables.env
    • Creates variables.env file with:
      • EXTRA_DESCRIPTION: Release description with version and date
      • TAG: Version tag (e.g., v1.0.0)
      • VERSION: Version number (e.g., 1.0.0)

Script

The main script performs the following steps:

  1. Check Merge Request State:

    MR_STATE=$(curl --silent --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}" | \
    jq -r '.state')
    • Queries GitLab API to get current MR state
    • Uses jq to extract the state field
  2. Validate and Merge:

    if [ "$MR_STATE" == "merged" ]; then
    echo "ERROR - MR already merged"
    exit 1
    else
    # Merge the MR
    curl --silent --request PUT \
    --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/merge"

    # Verify merge succeeded
    NEW_MR_STATE=$(curl --silent --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
    "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}" | \
    jq -r '.state')

    if [ "$NEW_MR_STATE" != "merged" ]; then
    echo "ERROR - FAIL to merge request; make sure it's ready to be merged before run!!!!"
    exit 1
    fi
    fi
    • Checks if MR is already merged (exits if so)
    • Merges the MR via GitLab API
    • Verifies merge was successful
  3. Create Realign Branch:

    NEW_BRANCH_NAME="realign/${VERSION}"
    SOURCE_BRANCH="master"

    curl --request POST "${CI_API_V4_URL}/projects/${PROJECT_ID}/repository/branches" \
    --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
    --header "Content-Type: application/json" \
    --data "{
    \"branch\": \"${NEW_BRANCH_NAME}\",
    \"ref\": \"${SOURCE_BRANCH}\"
    }"
    • Creates a new branch realign/{VERSION} from master
    • Example: realign/1.0.0
  4. Create Merge Request to Develop:

    SOURCE_BRANCH="realign/${VERSION}"
    TARGET_BRANCH="develop"
    MR_TITLE="Merge $SOURCE_BRANCH into $TARGET_BRANCH"
    MR_DESCRIPTION="This merge request was created automatically by the pipeline."

    curl --request POST "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests" \
    --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
    --header "Content-Type: application/json" \
    --data "{
    \"id\": ${PROJECT_ID},
    \"source_branch\": \"${SOURCE_BRANCH}\",
    \"target_branch\": \"${TARGET_BRANCH}\",
    \"title\": \"${MR_TITLE}\",
    \"description\": \"${MR_DESCRIPTION}\"
    }"
    • Creates a new merge request from realign/{VERSION} to develop
    • This aligns the develop branch with the released version

Artifacts

The job creates a variables.env file that is exposed as a dotenv artifact:

artifacts:
reports:
dotenv: variables.env

This makes the variables available to subsequent jobs:

  • VERSION: Version number
  • TAG: Version tag (v{VERSION})
  • EXTRA_DESCRIPTION: Release description with date

Workflow

The job follows this workflow:

  1. Extract Version: From branch name (e.g., release/1.0.01.0.0)
  2. Check MR State: Verify merge request is not already merged
  3. Merge MR: Merge the current merge request
  4. Create Realign Branch: Create realign/1.0.0 from master
  5. Create Alignment MR: Create MR from realign/1.0.0 to develop
  6. Export Variables: Make version/tag available to other jobs

Configuration Examples

Release Workflow

variables:
MERGE_AND_TAG_IMAGE: 'node:latest'

stages:
- release

.merge_and_tag_job:
image: $MERGE_AND_TAG_IMAGE
before_script:
- |
echo "Installing jq..."
apt update && apt install -y jq
VERSION=$(echo ${CI_MERGE_REQUEST_SOURCE_BRANCH_NAME} | awk -F "/" '{print $2}')
echo "EXTRA_DESCRIPTION=RELEASE=v${VERSION} | DATE=$(date +%Y-%m-%d)" >> variables.env
echo "TAG=v${VERSION}" >> variables.env
echo "VERSION=${VERSION}" >> variables.env
script:
- |
echo "Checking Merge Request State..."
MR_STATE=$(curl --silent --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}" | jq -r '.state')
echo "state MR $CI_MERGE_REQUEST_IID is $MR_STATE"

if [ "$MR_STATE" == "merged" ]; then
echo "ERROR - MR already merged"
exit 1
else
echo "merging MR $CI_MERGE_REQUEST_IID"
curl --silent --request PUT --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/merge"
NEW_MR_STATE=$(curl --silent --header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}" | jq -r '.state')
echo "new state MR $CI_MERGE_REQUEST_IID is $NEW_MR_STATE"

if [ "$NEW_MR_STATE" != "merged" ]; then
echo "ERROR - FAIL to merge request; make sure it's ready to be merged before run!!!!"
exit 1
fi

PROJECT_ID=$CI_PROJECT_ID
NEW_BRANCH_NAME="realign/${VERSION}"
SOURCE_BRANCH="master"

curl --request POST "${CI_API_V4_URL}/projects/${PROJECT_ID}/repository/branches" \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
--header "Content-Type: application/json" \
--data "{
\"branch\": \"${NEW_BRANCH_NAME}\",
\"ref\": \"${SOURCE_BRANCH}\"
}"

sleep 5

PROJECT_ID=$CI_PROJECT_ID
SOURCE_BRANCH="realign/${VERSION}"
TARGET_BRANCH="develop"
MR_TITLE="Merge $SOURCE_BRANCH into $TARGET_BRANCH"
MR_DESCRIPTION="This merge request was created automatically by the pipeline."

curl --request POST "https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests" \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
--header "Content-Type: application/json" \
--data "{
\"id\": ${PROJECT_ID},
\"source_branch\": \"${SOURCE_BRANCH}\",
\"target_branch\": \"${TARGET_BRANCH}\",
\"title\": \"${MR_TITLE}\",
\"description\": \"${MR_DESCRIPTION}\"
}"
fi
artifacts:
reports:
dotenv: variables.env
only:
- merge_requests

merge-and-tag:
extends: .merge_and_tag_job
stage: release
when: manual

Branch Naming Convention

The job expects merge requests from branches following this pattern:

{prefix}/{version}

Examples:

  • release/1.0.0 → Version: 1.0.0
  • release/2.1.3 → Version: 2.1.3
  • hotfix/1.0.1 → Version: 1.0.1

The version is extracted by taking the second part after splitting by /.

Using Exported Variables

Subsequent jobs can use the exported variables:

create-git-tag:
stage: release
script:
- echo "Creating tag: ${TAG}"
- echo "Version: ${VERSION}"
- git tag -a "${TAG}" -m "${EXTRA_DESCRIPTION}"
- git push origin "${TAG}"
dependencies:
- merge-and-tag

The variables are automatically available because of the dotenv artifact report.

Prerequisites

  • GitLab Token: Personal access token or project access token with API permissions:
    • api scope (read/write access)
    • Merge request permissions
    • Repository write permissions
  • Merge Request: Must exist and be in a mergeable state
  • Branch Format: Source branch must follow `{prefix}/{version}` format
  • Target Branch: The MR should target master or main
  • Master Branch: The master branch must exist (used as source for realign branch)

Security Considerations

  • GitLab Token: Store GITLAB_TOKEN as a masked and protected CI/CD variable
  • Token Permissions: Use a token with minimal required permissions
  • Manual Execution: Consider using when: manual to require approval before execution
  • Branch Protection: Ensure branch protection rules allow API-based merges

Notes

  • The job uses GitLab API v4 for all operations
  • Version extraction assumes branch name format: `{prefix}/{version}`
  • The job creates a 5-second delay after creating the branch before creating the MR
  • The realign branch is created from master, not from the merged branch
  • The alignment MR is created automatically to help keep develop in sync
  • Variables are exported via dotenv artifact for use in subsequent jobs
  • The job fails if the MR is already merged or if merge fails
  • The job should typically run on merge request pipelines with manual approval
  • The EXTRA_DESCRIPTION includes both version and date for release tracking