How to use git push from you Gitlab CI?

Status

Currently, we use Gitlab CI jobs to check, build, test and deploy our source code.

However, we may want to go farther and push things from a job. For example, if we split release process between push branch (for validation) and push tag (to trigger deployment), we’ll have to trigger the tag from the end of the branch pipelines. We may also have some weird cases with dependencies between multiple projects.

Gitlab CI provides a token under the CI_JOB_TOKEN. However, this token only allows to perform read actions (git pull, git status, and so on).

The official way?

To fix this, there is a work in progress work around. But… This is a work around, open for more than 2 years…

How we work around ourselves?

Hopefully, Gitlab CI offers other way to push data from a git repository.

Using SSH

Here is the full explanation : https://nicolaw.uk/GitLabCiPushingWrites:

  • create a keypair, using the following process: https://docs.gitlab.com/ee/user/ssh.html#generate-an-ssh-key-pair then
  • add the SSH private key as a secret variable through the Settings > CI/CD Pipelines in GitLab > Variables of the project which’ll run the git commands, and
  • the public part of the SSH key is stored as a deployment key for the target project in Settings > Repository > Deploy Keys section (grant it “write permissions” to the key).

NB : the linked article provides a good practice we don’t use: add “rm -rf .ssh” under the “after_script:” step.

This solution has the drawback to store a secret key in the projects settings, but it’s tied to the target project.

The project which’ll run the git command, and the project where to push the changes can be the same. But be careful to set safeguards to make sure to not trigger again the job when it pushes to the repository.

Using Access Token

This solution is suggested by this comment : https://forum.gitlab.com/t/cannot-push-from-a-project-to-another/11038/3

  • For the target project, create the Access Token under Settings > Access Token with the right accesses.
  • Open the project which need to use this key in gitlab console, go to Settings > CI/CD > Secret variables, and create a variable with value the key (generated in profile).
  • We then use git with the https access, and the new token as password value, in the gitlab-ci.yml file.

This solution seems to give a more accurate access control, since we can control the scope of the key in the Access Token settings. It’s also easier to create an access key than an SSH keypair (we don’t need ssh-keygen).

It has otherwise the same pros/cons of the SSH Keypair explored above.

Sample test

Here is a complete example on a .gitlab-ci.yml I crafted for the occasion.

This sample we update a Changelog file on the same repository with both SSH keypair and Access Token (with two dedicated jobs) to see the result.

variables:
    # Provide a branch where to push commit local changes to avoid 'everything up to date' message.
    TMP_BRANCH: gitlab-ci-job
    # Clone rather than fetch so we have a completely clean workspace without
    # any previous temporary_branch left over from past CI runs.
    GIT_STRATEGY: clone

stages:
    - test

update-changelog-using-ssh:
    stage: test
    tags:
        - some-runner
    before_script:
        - yum -y update
        - yum -y install git
        - mkdir ~/.ssh
        - chmod 700 ~/.ssh
        # Should avoid errors related to unknown host.
        - ssh-keyscan -t rsa gitlab.my-personal-host.com > ~/.ssh/known_hosts
        # The private key used to push changes. The target repository knows the public key thanks to the change
        # in Settings / Repository / Deploy keys (added with "grant write permission to this key" checked)
        # on the repository where we push changes.
        - echo "$TEST_SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
        - chmod 600 ~/.ssh/id_rsa
        # The certificate for https.
        - echo "$SOME_PEM_CERT" | tr -d '\r' > $PWD/some_pem_cert.pem
        - git config --global http.sslCAInfo $PWD/some_pem_cert.pem
        # To keep the one who triggered this branch.
        - git config --global user.email $(git --no-pager show -s --format='%ae' HEAD)
        - git config --global user.name $(git --no-pager show -s --format='%an' HEAD)
        # Avoid warning about push strategy (optional).
        - git config --global push.default simple
        # Set the url to git@hostname:path/to/project.git to ensure we use the ssh keypair.
        - git remote set-url --push origin $(sed 's/.*@\([^/]*\)\//git@\1:/g' <<< $CI_REPOSITORY_URL)
    script:
        - git checkout -b ${TMP_BRANCH}
        # This is specific to our test to avoid race condition with access key example below.
        - git pull origin "${CI_COMMIT_REF_NAME}"
        - echo -e "\n$(date) - Changes made from ${CI_COMMIT_BRANCH} using SSH keypair. ${CI_COMMIT_MESSAGE}" >> Changelog.md
        - git diff
        - git add Changelog.md
        - git commit -m "Automatic update change logs with last changes using SSH keypair."
        # Last option instruct git to push local branch to the remote from where the job has been triggered.
        - git push origin "${TMP_BRANCH}:${CI_COMMIT_REF_NAME}"
    after_script:
        # Safeguard to be sure it remains no sensitive data.
        - rm -rf .ssh
    rules:
        # Since we play with a single repository, use a rule as a safeguard.
        - if: '$CI_COMMIT_MESSAGE !~ /^Automatic/'
          when: on_success

update-changelog-using-access-key:
    stage: test
    tags:
        - some-runner
    before_script:
        - yum -y update
        - yum -y install git
        - mkdir ~/.ssh
        - chmod 700 ~/.ssh
        # Should avoid errors related to unknown host.
        - ssh-keyscan -t rsa gitlab.my-personal-host.com > ~/.ssh/known_hosts
        # The certificate for https.
        - echo "$SOME_PEM_CERT" | tr -d '\r' > $PWD/some_pem_cert.pem
        - git config --global http.sslCAInfo $PWD/some_pem_cert.pem
        # To keep the one who triggered this branch.
        - git config --global user.email $(git --no-pager show -s --format='%ae' HEAD)
        - git config --global user.name $(git --no-pager show -s --format='%an' HEAD)
        # Avoid warning about push strategy (optional).
        - git config --global push.default simple
        # Set the alternate access key in the https url to git. We created it in the Project's Settings / Access Token with "write_repository" access.
        - git remote set-url --push origin $(sed "s/gitlab-ci-token:[^@]*@/gitlab-ci-token:${TEST_ACCESS_TOKEN}@/g" <<< $CI_REPOSITORY_URL)
    script:
        - git checkout -b ${TMP_BRANCH}
        # This is specific to our test to avoid race condition with tke SSH keypair test above.
        - git pull origin "${CI_COMMIT_REF_NAME}"
        - echo -e "\n$(date) - Changes made from ${CI_COMMIT_BRANCH} using Access Key. ${CI_COMMIT_MESSAGE}" >> Changelog.md
        - git diff
        - git add Changelog.md
        - git commit -m "Automatic update change logs with last changes using Access Key."
        # Last option instruct git to push local branch to the remote from where the job has been triggered.
        - git push origin "${TMP_BRANCH}:${CI_COMMIT_REF_NAME}"
    after_script:
        # Safeguard to be sure it remains no sensitive data.
        - rm -rf .ssh
    rules:
        # Since we play with a single repository, use a rule as a safeguard.
        - if: '$CI_COMMIT_MESSAGE !~ /^Automatic/'
          when: on_success

Conclusion

As we can see, there are no great differences between both example when we implement them. So the Access Token keep just a little bit of benefits, since it remains easier to generate it.

From a security standing point, they have both the same drawbacks:

  • we store them as “Secret” variables in the project;
  • they stay alive longer than the job (when the default token is trashed at the end of the job).

So both solutions can be used, but carefully.