Mirroring Git With Jenkins

Introduction

Some projects need a second Git remote for resilience, migration, or operational independence. Git already provides the primitives for this; CI can provide the schedule and credentials.

This article started as a 2019 Jenkins note. The mirroring idea still holds, but one detail in the original example did not age well: it disabled SSH host-key checking to make automation easier. That trades away an important part of SSH’s server-authentication model and should not be copied into production CI.

If you are also storing repository metadata outside the normal commit graph, see Git Notes Storage.

Git Mirroring

Git has mirroring built into git clone --mirror and git push --mirror.

A mirror includes refs that an ordinary clone may not copy, which makes it useful for repository replication. It also means git push --mirror is intentionally powerful: destination refs can be created, updated, and deleted to match the source. Use a destination dedicated to the mirror rather than a repository with independent work.

A minimal manual flow looks like:

git clone --mirror [email protected]:team/project.git project.git
cd project.git
git push --mirror [email protected]:team/project.git

Jenkins Pattern

The Jenkins design I used had two levels:

  1. a reusable mirror job that accepts a source and one or more destinations
  2. a scheduled orchestration job that invokes it for each repository

The exact parameter plugin is less important than keeping the inputs bounded. Do not let an untrusted caller supply arbitrary shell fragments or arbitrary remote URLs to a privileged mirroring credential.

SSH trust belongs in the runner image or credential setup

The original pipeline contained this:

StrictHostKeyChecking=no

That suppresses the check that tells SSH whether it is talking to the expected server. OpenSSH’s known_hosts mechanism exists specifically to detect changed or unexpected host identities.

For unattended CI, provision the expected host keys ahead of time instead:

  • bake a reviewed known_hosts file into the runner image, or
  • provide it through the CI credential/configuration system, or
  • manage host certificates with a trusted SSH certificate authority.

The important property is that the expected host identity comes from a trusted configuration channel, not from the same network connection you are trying to verify.

A simplified Jenkins stage can then rely on normal SSH verification:

node {
  stage("Mirror") {
    deleteDir()

    sshagent(credentials: ['git-mirror']) {
      sh '''
        set -eu
        git clone --mirror "$SOURCE/$PROJECT_PATH" repo.git
        cd repo.git
        git push --mirror "$DESTINATION/$PROJECT_PATH"
      '''
    }
  }
}

The runner is responsible for having the correct host keys installed before that stage executes.

Scheduling Multiple Repositories

A separate scheduled job can keep the repository list explicit:

def mirrors = [
  [source: 'ssh://[email protected]',
   destination: 'ssh://[email protected]',
   path: 'ryjen/blog'],
  [source: 'ssh://[email protected]',
   destination: 'ssh://[email protected]',
   path: 'ryjen/dotfiles'],
]

for (def mirror : mirrors) {
  build job: 'git_mirror', parameters: [
    string(name: 'SOURCE', value: mirror.source),
    string(name: 'DESTINATION', value: mirror.destination),
    string(name: 'PROJECT_PATH', value: mirror.path),
  ]
}

In a larger system I would also add bounded retries, per-repository status, metrics, and explicit failure reporting rather than treating the schedule itself as proof that backups are healthy.

Conclusion

The durable part of the old experiment is that Git mirroring is small enough to compose from native Git operations and CI scheduling. The security boundary is just as important as the automation: protect the mirroring credential, verify the SSH server, and remember that a successful scheduled job is not the same as a tested restore.

References