GitLab CI/CD for GitHub and Bitbucket repositories, with your own runners

GitLab can run CI/CD for code that lives on GitHub, Bitbucket Cloud or any other Git server. You create a GitLab project with Run CI/CD for external repository; GitLab pull-mirrors the repository, runs the .gitlab-ci.yml from it, and reports pipeline status back as commit statuses. The feature needs GitLab Premium or Ultimate. You attach your own runners to the mirror project the same way you would for any GitLab project.

The moving parts of CI/CD for external repositories

You keep your code, issues and pull requests on GitHub or Bitbucket. GitLab creates a lightweight project that holds a pull mirror of the repository and runs pipelines on each mirror update. Three pieces make it work:

  • Pull mirroring. GitLab fetches branches and tags from the remote. Without a webhook, it pulls again 30 minutes after the previous pull, with a minimum interval of 5 minutes.
  • A webhook on the remote that calls GitLab's /mirror/pull API on each push, so pipelines start within seconds.
  • Status reporting. For GitHub, the GitHub integration posts pipeline results as commit statuses. For Bitbucket, you add a small job that calls the Bitbucket API.

The .gitlab-ci.yml file lives in the GitHub or Bitbucket repository and arrives through the mirror. You do not push to the GitLab project.

Tiers and prerequisites

CI/CD for external repositories, pull mirroring and the GitHub integration all require Premium or Ultimate on GitLab.com, GitLab Self-Managed or GitLab Dedicated. Free-tier namespaces do not see the option. On a self-managed instance, an administrator must also enable GitHub (or "Repository by URL") as an import source and allow project mirroring; if the Run CI/CD for external repository tab is missing, check those settings first.

You also need:

  • For GitHub: a GitHub personal access token with the repo and admin:repo_hook scopes, from an account with owner-level access to the repository. GitHub OAuth does not work for this flow.
  • For Bitbucket Cloud: an Atlassian API token with repository read and write access, and permission to add webhooks.
  • A GitLab personal access token with the api scope, for webhook URLs you set up by hand.

Connect a GitHub repository

The automatic path does most of the work:

  1. In GitLab, select New project > Run CI/CD for external repository > GitHub.
  2. Paste the GitHub personal access token so GitLab can list your repositories.
  3. Pick the repositories to connect. GitLab creates one project per repository, sets up pull mirroring, configures the GitHub integration and registers webhooks for push and pull request events.

For GitHub Enterprise Server, or if you want to control each piece, connect by hand:

  1. Create the project with Run CI/CD for external repository > Repository by URL, using the GitHub HTTPS URL and a token with repo scope as the password.
  2. In the new project, open Settings > Integrations > GitHub. Enter a GitHub token with repo:status scope and the repository URL. Tick Enable static status check names if you plan to mark the GitLab check as required in GitHub branch protection; static names stay the same across branches.
  3. In GitHub, add a webhook for Pushes and Pull requests that points to:
https://gitlab.com/api/v4/projects/<NAMESPACE>%2F<PROJECT>/mirror/pull?private_token=<GITLAB_PERSONAL_ACCESS_TOKEN>

Use a dedicated GitLab bot account for that token, because the token sits in a URL in GitHub's webhook settings. Add .gitlab-ci.yml to the GitHub repository and push. The pipeline result appears on the GitHub commit and on pull requests.

Pipelines for GitHub pull requests

With the automatic GitHub connection, GitLab also creates pipelines for external pull requests. Opening or updating a pull request produces a second pipeline with CI_PIPELINE_SOURCE set to external_pull_request_event. Use rules to avoid running everything twice:

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "external_pull_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

test:
  tags: [linux, docker]
  script:
    - echo "PR !$CI_EXTERNAL_PULL_REQUEST_IID from $CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME"
    - make test

GitLab exposes the pull request details in variables prefixed CI_EXTERNAL_PULL_REQUEST_. Two limits apply. Pull requests from forks do not trigger pipelines, since the fork's branches are not in the mirror. Manual GitHub Enterprise connections do not get external pull request pipelines at all. After a pull request closes, further pushes to its branch run only branch pipelines.

Connect a Bitbucket Cloud repository

Bitbucket has no dedicated integration, so you combine "Repository by URL" with a webhook and a status job.

  1. In GitLab, select New project > Run CI/CD for external repository > Repository by URL. Enter the Bitbucket HTTPS clone URL without the username@ part, and the credentials for the token.
  2. In Bitbucket, open Repository settings > Webhooks and add a webhook with the Repository push trigger pointing to https://gitlab.com/api/v4/projects/<project_id>/mirror/pull?private_token=<token>.
  3. In the GitLab project, add masked CI/CD variables BITBUCKET_ACCESS_TOKEN and BITBUCKET_USERNAME, plus BITBUCKET_NAMESPACE and BITBUCKET_REPOSITORY if the names differ from the GitLab project.
  4. Add jobs that post a build status to Bitbucket after the pipeline passes or fails.
GitLab's Bitbucket page still describes Bitbucket app passwords. Atlassian stopped new app passwords in September 2025 and set June 2026 for disabling existing ones. Use an Atlassian API token: for Git over HTTPS the username is x-bitbucket-api-token-auth, and for REST calls you authenticate with your Atlassian account email and the token.
stages: [test, ci_status]

unit-tests:
  stage: test
  tags: [linux, docker]
  script: make test

.bitbucket-status:
  stage: ci_status
  image: alpine:latest
  tags: [linux, docker]
  script:
    - apk add --no-cache curl
    - |
      curl --fail --user "$BITBUCKET_USERNAME:$BITBUCKET_ACCESS_TOKEN" \
        -H "Content-Type: application/json" \
        -d "{\"state\":\"$BUILD_STATE\",\"key\":\"gitlab-ci\",\"url\":\"$CI_PIPELINE_URL\",\"description\":\"GitLab pipeline\"}" \
        "https://api.bitbucket.org/2.0/repositories/$BITBUCKET_NAMESPACE/$BITBUCKET_REPOSITORY/commit/$CI_COMMIT_SHA/statuses/build"

report-success:
  extends: .bitbucket-status
  variables: { BUILD_STATE: SUCCESSFUL }
  when: on_success

report-failure:
  extends: .bitbucket-status
  variables: { BUILD_STATE: FAILED }
  when: on_failure

Here BITBUCKET_USERNAME holds the Atlassian account email. Set BITBUCKET_NAMESPACE and BITBUCKET_REPOSITORY as project variables, or derive them from CI_PROJECT_NAMESPACE and CI_PROJECT_NAME if they match. Bitbucket pull requests do not get their own pipeline type; GitLab runs branch pipelines for the source branch, and Bitbucket shows the commit status on the pull request.

Attach your own runners

The mirror project is an ordinary GitLab project, so runners attach to it as usual:

  1. Open the mirror project's Settings > CI/CD > Runners and select Create project runner. Give it tags such as linux and docker. For many mirrored repositories, put the projects in one group and create a group runner under Build > Runners in place of one runner per project.
  2. Copy the glrt- token and register the machine:
sudo gitlab-runner register \
  --non-interactive \
  --url "https://gitlab.com/" \
  --token "$RUNNER_TOKEN" \
  --executor "docker" \
  --docker-image alpine:latest \
  --description "external-ci-01"

Reference the runner's tags in the .gitlab-ci.yml in your GitHub or Bitbucket repository, and turn off instance runners on the project if every job should land on your hardware. Jobs on your own runners do not use GitLab.com compute minutes. The GitLab custom runner guide covers executors, concurrent, autoscaling and Kubernetes. If you deploy a static site from the mirror, GitLab Pages on a custom runner shows the Pages job.

Security considerations

  • Mirror pipelines run as the mirror's creator. GitLab triggers them with that user's permissions. Create the project from a bot account with the narrowest group membership that works.
  • Anyone with push access on GitHub or Bitbucket controls your pipeline. Protect the default branch on the remote and require review for changes to .gitlab-ci.yml, for example with a CODEOWNERS entry.
  • Protect deploy secrets in GitLab. Mark deploy variables as protected, protect the default branch in the mirror project, and use a protected runner for deploy jobs.
  • Treat webhook URLs as secrets. They carry a GitLab token. Rotate it if someone with admin access to the remote repository leaves.

Troubleshooting

  • "Run CI/CD for external repository" is missing. Your namespace is on the Free tier, or a self-managed admin has not enabled the import source and mirroring.
  • Pipelines start up to 30 minutes late. The webhook is missing or failing. Check delivery logs in GitHub or Bitbucket for 401 or 404 responses, which point to a revoked token or wrong project path.
  • Mirroring stopped. After 14 consecutive failures GitLab stops retrying. Fix the credentials, then select Update now in Settings > Repository > Mirroring repositories.
  • No status on GitHub. The GitHub integration token lacks repo:status or expired.
  • Jobs stuck in pending. The tags in .gitlab-ci.yml do not match an online runner assigned to the mirror project.

FAQ

Can I use GitLab CI with a GitHub repository for free?

Not through the built-in feature: CI/CD for external repositories and pull mirroring need Premium or Ultimate. On Free, you can push to a GitLab project yourself from GitHub Actions or a Git hook and run pipelines there, without automatic status reporting.

Do pull requests from forks run GitLab pipelines?

No. External pull request pipelines cover branches in the same repository only.

Can I use GitLab CI with Bitbucket Server or Gitea?

Yes, with Repository by URL. Any Git server GitLab can reach works for mirroring. You write the status reporting job yourself.

Does .gitlab-ci.yml go in the GitHub repo or the GitLab project?

Commit it to the root of the GitHub repository. GitLab reads it from the mirror, and the GitLab project holds no code of its own.

Managed runners with cirunner.dev

cirunner.dev can supply the runners for your mirror projects. It provisions a fresh VM per job, registers it with GitLab using a runner token you provide, scales with the queue and destroys the machine after the job. You choose CPU, RAM, x86_64 or arm64, region, base image and an optional GPU, and pay per compute minute.

The service is in early access. GitLab CI and GitHub Actions launch first; Bitbucket Pipelines is on the roadmap, and you can vote for it on the early access form.

Skip the runner fleet

cirunner.dev boots a fresh VM for every GitLab CI/CD job, registers it, and destroys it when the job ends. GitHub Actions and GitLab CI are our launch platforms.

Join early access