How to configure a GitLab pipeline to automatically update a remote Git repository

This guide explains how to configure a GitLab pipeline to automatically update a remote Git repository and outlines the main best practices to do it safely, repeatably, and observably.

Prerequisites

  • Project hosted on GitLab with available Runners.
  • Write permissions on the target branch.
  • A token with write permissions to the repository: a Project Access Token or Deploy Token with scope write_repository is recommended (alternatively, a limited Personal Access Token).

General Strategy

  1. The pipeline starts on an event (push, tag, merge, schedule, or trigger).
  2. A job prepares the workspace, applies the changes (e.g., version updates or generated content), and commits them.
  3. The job pushes to the remote branch or opens an automatic merge request, depending on the branch protection policy.

Recommended Environment Variables

Set protected variables under Settings > CI/CD > Variables:

  • GIT_PUSH_USER_NAME and GIT_PUSH_USER_EMAIL to sign bot commits.
  • REPO_PUSH_TOKEN with the minimum required scope (write_repository), marked as Protected and Masked.
  • Any credentials for registries or external services.

Basic Pipeline: Structure

stages:
  - test
  - build
  - update

default:
  image: alpine:3.20
  before_script:
    - apk add --no-cache git bash curl
    - git config --global user.name "${GIT_PUSH_USER_NAME:-ci-bot}"
    - git config --global user.email "${GIT_PUSH_USER_EMAIL:-ci-bot@example.local}"
    # Avoid "detected dubious ownership" issues in recent containers
    - git config --global --add safe.directory "$CI_PROJECT_DIR"

variables:
  GIT_STRATEGY: fetch        # faster than full clone in most cases
  GIT_DEPTH: "50"            # shallow fetch for speed
  FF_USE_FASTZIP: "true"     # improves performance of recent cache/artifacts

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .cache/

Example 1: update files and push to the same branch

Use a secure token via HTTPS; avoid SSH in ephemeral environments. Here we assume that the branch is not Protected or that the user/token has push permissions on the branch.

update:push-current-branch:
  stage: update
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  script:
    - |
      # Apply changes (example: bump patch in package.json)
      ./scripts/bump_version.sh  # <-- your script
      # Staging & commit
      git add -A
      if git diff --cached --quiet; then
        echo "No changes: skipping push."
        exit 0
      fi
      git commit -m "chore(ci): automatic updates [skip ci]"
    - |
      # Reset origin with token to avoid logging the token in history
      git remote set-url origin "https://oauth2:${REPO_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
      # Avoid conflicts: quick rebase and push
      git pull --rebase origin "$CI_COMMIT_BRANCH"
      git push origin "HEAD:$CI_COMMIT_BRANCH"
  dependencies: []
  needs: []
  allow_failure: false
  artifacts:
    when: always
    expire_in: 1 week
    reports:
      dotenv: update.env

Example 2: open an automatic merge request

For protected branches, prefer opening an MR instead of pushing directly.

update:open-mr:
  stage: update
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  image: alpine:3.20
  script:
    - apk add --no-cache git bash curl jq
    - base_branch="$CI_DEFAULT_BRANCH"
    - work_branch="ci/update-$(date +%Y%m%d-%H%M%S)"
    - git checkout -b "$work_branch"
    - ./scripts/generate_docs.sh
    - git add -A
    - |
      if git diff --cached --quiet; then
        echo "No changes: exiting."
        exit 0
      fi
    - git commit -m "docs: update documentation (auto)"
    - git remote set-url origin "https://oauth2:${REPO_PUSH_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
    - git push origin "$work_branch"
    - |
      # Create MR via API
      curl --fail -sS --request POST \
        --header "PRIVATE-TOKEN: ${REPO_PUSH_TOKEN}" \
        --data-urlencode "source_branch=${work_branch}" \
        --data-urlencode "target_branch=${base_branch}" \
        --data-urlencode "remove_source_branch=true" \
        --data-urlencode "title=Automatic updates: ${work_branch}" \
        "https://${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/merge_requests" \
        | jq -r '.web_url'

Credential Management and Security

  • Use protected and masked variables for tokens; do not hardcode them in YAML.
  • Limit the scope and expiration of tokens; prefer project tokens over personal ones.
  • Enable update jobs only on specific branches with rules and only/except.
  • Avoid printing the token in logs. Do not echo remotes containing credentials.
  • For protected branches, use MR approvals and Code Owners.

Conflicts and Synchronization

When the pipeline commits, conflicts may occur with new pushes:

  • Run git pull --rebase before pushing to maintain a linear history.
  • If the rebase fails, fail the job and retry in the next run or open an MR.
  • Consider using a dedicated bot branch (e.g., ci/updates) with a recurring MR.

Performance Optimizations

  • GIT_DEPTH to reduce fetched history; increase the value for steps that need tags/versions.
  • Cache for repeatable dependencies and artifacts to pass build output between stages.
  • Runners with consistent tags and lightweight images (Alpine) when possible.

Observability and Audit

  • Use clear and conventional commit messages (e.g., chore(ci): ...).
  • Tag bot commits with [skip ci] when you don’t want to trigger recursive pipelines.
  • Publish synthetic logs as text artifacts (e.g., generated changelog).

Best Practices Summarized

  • Principle of least privilege: tokens with limited scope, short expiration, protected variables.
  • Protected branches and automatic MRs: prefer them for generated changes.
  • Avoid pipeline loops: use [skip ci] in generated commits.
  • Idempotency: scripts must be runnable multiple times without unintended side effects.
  • Traceability: consistent commit messages, log artifacts, and updated changelogs.
  • Conflict management: rebase before pushing, explicit failure if not automatically resolvable.
  • Separation of duties: test/build jobs distinct from update jobs.
  • Speed without sacrificing correctness: GIT_DEPTH and cache where possible, but full fetch when tags or history are needed.

Final Checks

  1. The configured token has the minimum scope and does not expire before the scheduled execution.
  2. Job rules prevent triggers on unexpected branches.
  3. The update scripts produce deterministic and tested changes.
  4. If the branch is protected, MR creation is automatic and reviewers are assigned via project rules.
Back to top