Azure Pipelines with multiple GitHub orgs
I had to connect one Azure DevOps organization to two different GitHub organizations. I wanted to build source code from one GitHub org, and use the shared pipeline templates from a second GitHub org.
However, each time I tried to make a GitHub service connection with the OAuth flow, the popup opened, used an existing GitHub session, and closed again. I did not get a chance to select the organization that I needed.
The scenario
Let’s establish a few reference variables:
- Azure DevOps organization:
<ADO_ORG_NAME> - Azure DevOps project:
<ADO_PROJECT_NAME> - GitHub org that holds the source code:
<GITHUB_SOURCE_ORG> - GitHub org that holds the shared templates:
<GITHUB_TEMPLATES_ORG>
The Azure Pipelines app was already installed and operational for <GITHUB_TEMPLATES_ORG>, because our pipelines and templates were in that org. The goal was to let pipelines in <ADO_PROJECT_NAME> get source code from <GITHUB_SOURCE_ORG>, and continue to get templates from <GITHUB_TEMPLATES_ORG>.
Why the popup closed each time
The confusing part was that nothing looked broken. I was authenticated, GitHub knew who I was, but Azure DevOps did not let me select the second organization.
The cause is that the Azure DevOps “create service connection” UI mixes two different mechanisms into what looks like one sign-in flow:
- GitHub OAuth (user-level): Authenticates you as a person and gives read access to the repositories that you can see. This is what the AzurePipelines OAuth configuration does. If the popup closes immediately, GitHub silently uses your existing OAuth session again. You do not get to select an org, because GitHub resolves the prompt before you can interact with it.
- GitHub App installation (org-level): Gives Azure Pipelines access to repositories in one specific GitHub organization. Until you install the app on an org, Azure DevOps has no record of it.
The UI only shows an org picker when the app is not installed yet. Because the app was already installed on my first org, the popup short-circuited to that existing installation.
So although my OAuth login was correct, Azure DevOps had no record of an app installation for the second GitHub org. The popup was not broken. It correctly showed that, from the point of view of Azure DevOps, there was nothing new to authorize.
Making the second org visible to Azure DevOps
The solution was to do the GitHub App installation and the Azure DevOps registration manually, and not let the wizard do it.
First, I installed the Azure Pipelines GitHub App directly on the second organization. To do this, I added the numeric org ID as a target_id parameter on the install URL. This skips the unclear “which account do you mean” step, and makes GitHub install the app on the organization that I specified:
https://github.com/apps/azure-pipelines/installations/new?target_id=<GITHUB_TARGET_ORG_ID>

After the app installation, I could find the installation in the settings of the organization on GitHub. This page is also useful when you must examine or change the repository scope of an existing installation:
https://github.com/organizations/<GITHUB_ORG_NAME>/settings/installations/<GITHUB_INSTALLATION_ID>
The last step was to tell Azure DevOps that this installation exists. Usually this occurs automatically in the redirect after the app installation. But you can also start it directly. To do this, go to the Azure DevOps signup callback with the correct installation ID:
https://aex.dev.azure.com/signup/github?acquisitionId=<AZURE_PIPELINES_ACQUISITION_ID>&installation_id=<GITHUB_INSTALLATION_ID>&setup_action=install

When this registration step was complete, the usual OAuth service connection flow in Azure DevOps operated as I expected. I could then make a service connection with a scope of the second GitHub org.
A note on the identifiers, because it is easy to confuse them. The target_id is the numeric ID of the GitHub organization on which you install the app. The installation_id identifies one specific installation of the Azure Pipelines app on one specific org, so you get a different value for each organization. The acquisitionId is different again. It identifies the Azure Pipelines integration product in Azure DevOps. In my tests it stayed the same for each organization and tenant.
Putting it together in the pipeline
With the two GitHub App installations in place, I had two separate service connections in the Azure DevOps project. One connection was for the source repositories, and one was for the shared templates. The separation made the access scope easy to understand, because each connection only needs permission for the repositories that it uses.
In the pipeline, you declare both as repository resources and point each at the correct endpoint:
resources:
repositories:
- repository: templates
type: github
name: <GITHUB_TEMPLATES_ORG>/<TEMPLATES_REPO>
endpoint: <SC_TEMPLATES>
ref: refs/tags/<TEMPLATE_VERSION_TAG>
- repository: sharedsrc
type: github
name: <GITHUB_SOURCE_ORG>/<SOURCE_REPO>
endpoint: <SC_SOURCE>
I also pinned the templates reference to a tag, not a branch. With a tag, a change in the templates repo cannot break a pipeline in the other organization without a deliberate version bump.
What I took away from this
The lesson was not really about permissions in the usual sense. The lesson was that GitHub OAuth and the GitHub App installation are two different layers. Azure DevOps only uses the second layer when it decides which organizations and repositories a service connection can see. When both app installations were correctly registered, the full setup operated exactly as I expected from the start.
Useful links
- Make the Azure Pipelines app install on a specific org:
https://github.com/apps/azure-pipelines/installations/new?target_id=<GITHUB_TARGET_ORG_ID> - Go directly to the target selection step for an existing installation:
https://github.com/apps/azure-pipelines/installations/select_target?target_id=<GITHUB_TARGET_ORG_ID> - Examine or change the repository access of an installation on GitHub:
https://github.com/organizations/<GITHUB_ORG_NAME>/settings/installations/<GITHUB_INSTALLATION_ID> - Manually start the Azure DevOps registration handshake for an installation:
https://aex.dev.azure.com/signup/github?acquisitionId=<AZURE_PIPELINES_ACQUISITION_ID>&installation_id=<GITHUB_INSTALLATION_ID>&setup_action=install