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:
| Variable | Description | Default | Required |
|---|---|---|---|
MERGE_AND_TAG_IMAGE | Docker 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 requestCI_MERGE_REQUEST_IID: Internal ID of the merge requestCI_PROJECT_ID: Project IDCI_API_V4_URL: GitLab API v4 URLCI_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:
-
Install jq:
apt update && apt install -y jq- Installs
jqfor JSON parsing of GitLab API responses
- Installs
-
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.0→1.0.0) - Assumes branch format:
`{prefix}/{version}`
- Extracts version from branch name (e.g.,
-
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.envfile with:EXTRA_DESCRIPTION: Release description with version and dateTAG: Version tag (e.g.,v1.0.0)VERSION: Version number (e.g.,1.0.0)
- Creates
Script
The main script performs the following steps:
-
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
jqto extract thestatefield
-
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
-
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}frommaster - Example:
realign/1.0.0
- Creates a new branch
-
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}todevelop - This aligns the develop branch with the released version
- Creates a new merge request from
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 numberTAG: Version tag (v{VERSION})EXTRA_DESCRIPTION: Release description with date
Workflow
The job follows this workflow:
- Extract Version: From branch name (e.g.,
release/1.0.0→1.0.0) - Check MR State: Verify merge request is not already merged
- Merge MR: Merge the current merge request
- Create Realign Branch: Create
realign/1.0.0frommaster - Create Alignment MR: Create MR from
realign/1.0.0todevelop - 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.0release/2.1.3→ Version:2.1.3hotfix/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:
apiscope (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
masterormain - Master Branch: The
masterbranch must exist (used as source for realign branch)
Security Considerations
- GitLab Token: Store
GITLAB_TOKENas a masked and protected CI/CD variable - Token Permissions: Use a token with minimal required permissions
- Manual Execution: Consider using
when: manualto 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
developin 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_DESCRIPTIONincludes both version and date for release tracking