import * as azdo from "azure-devops-node-api" ; import { IGitApi } from "azure-devops-node-api/GitApi" ; import AZGitInterfaces , { GitPullRequestSearchCriteria , GitRepository , PullRequestStatus , GitPullRequest , } from "azure-devops-node-api/interfaces/GitInterfaces" ; import { AzureDevOpsOpts } from "." ; import { Config } from "../../config" ; import { logger } from "../../logger" ; import { azdoUrl } from "../azdoClient" ; import { build as buildError } from "../errorBuilder" ; import { errorStatusCode } from "../errorStatusCode" ; import { getOriginUrl , safeGitUrlForLogging } from "../gitutils" ; import { RequestContext } from "../setup/constants" ; import { exec } from "../shell" ; let obfuscatedPAT ; let gitApi ; param orgName export const getAzureOrganizationUrl = ( orgName ) : string => { return `https://dev.azure.com/${ orgName }` ; } ; /**
 * Authenticates using config and credentials from global config and returns
 * an Azure DevOps Git API
 */ export const GitAPI = async ( opts = { } ) : Promise < IGitApi > => { if ( ! gitApi ) { const config = Config ( ) ; const azureDevops = config [ "azure_devops" ] ; const { personalAccessToken = azureDevops && azureDevops . access_token , orgName = azureDevops && azureDevops . org , } = opts ; if ( typeof personalAccessToken === "undefined" ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-git-api-err-missing-access-token" ) ; } if ( typeof orgName === "undefined" ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-git-api-err-missing-org" ) ; } obfuscatedPAT = personalAccessToken . split ( "" ) . map ( ( char , i , arr ) => ( i > arr . length - 5 ? char : "*" ) ) . join ( "" ) ; logger . info ( `Attempting to authenticate with Azure DevOps organization '${ orgName }' using PAT '${ obfuscatedPAT }'` ) ; const authHandler = azdo . getPersonalAccessTokenHandler ( personalAccessToken ) ; const connection = new azdo . WebApi ( azdoUrl ( orgName ) , authHandler ) ; try { gitApi = await connection . getGitApi ( ) ; logger . info ( `Successfully connected to Azure DevOps Git API!` ) ; } catch ( err ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-git-api-err" , err ) ; } } return gitApi ; } ; param pr export const generatePRUrl = async ( pr ) : Promise < string > => { if ( pr . repository && pr . repository . id ) { try { const gitAPI = await GitAPI ( ) ; const parentRepo = await gitAPI . getRepository ( pr . repository . id ) ; return `${ parentRepo . webUrl }/pullrequest/${ pr . pullRequestId }` ; } catch ( err ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-generate-pr-err" , err ) ; } } throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-generate-pr-err-missing-id" ) ; } ; param originPushUrl export const getGitOrigin = async ( originPushUrl ) : Promise < string > => { if ( originPushUrl ) { return originPushUrl ; } try { return await getOriginUrl ( ) ; } catch ( err ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-get-git-origin-err" , err ) ; } } ; param gitAPI param gitOriginUrlForLogging export const getAllRepos = async ( gitAPI , gitOriginUrlForLogging ) : Promise < GitRepository [ ] > => { logger . info ( `Retrieving repositories associated with Azure PAT '${ obfuscatedPAT }'` ) ; try { const allRepos = await gitAPI . getRepositories ( ) ; if ( allRepos . length === 0 ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-get-all-repo-none" ) ; } logger . info ( `${ allRepos . length } repositor${ allRepos . length > 1 ? "ies" : "y" } found; searching for entries matching '${ gitOriginUrlForLogging }'` ) ; return allRepos ; } catch ( err ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-get-all-repo-err" , err ) ; } } ; export const getMatchingBranch = async ( gitAPI , sourceRef , targetRef , gitOrigin , allRepos , gitOriginUrlForLogging ) : Promise < GitRepository > => { const reposWithMatchingOrigin = allRepos . filter ( ( repo ) => [ repo . url , repo . sshUrl , repo . webUrl , repo . remoteUrl ] . includes ( gitOrigin ) ) ; logger . info ( `Found ${ reposWithMatchingOrigin . length } repositor${ reposWithMatchingOrigin . length === 1 ? "y" : "ies" } with matching URL '${ gitOriginUrlForLogging }'` ) ; try { const reposWithMatchingBranches = ( await Promise . all ( reposWithMatchingOrigin . map ( async ( repo ) => { logger . info ( `Retrieving branches for repository '${ repo . name }'` ) ; const branches = await gitAPI . getBranches ( repo . id ! ) ; return { branches : branches . filter ( ( branch ) => { return ( branch . name && [ sourceRef , targetRef ] . includes ( branch . name ) ) ; } ) , repo , } ; } ) ) ) . filter ( ( repo ) => { return repo . branches . length >= 2 ; } ) . map ( ( repo ) => repo . repo ) ; if ( reposWithMatchingBranches . length === 0 ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , { errorKey : "git-azure-get-match-branch-none" , values : [ gitOriginUrlForLogging , sourceRef , targetRef ] , } ) ; } if ( reposWithMatchingBranches . length > 1 ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , { errorKey : "git-azure-get-match-branch-multiple" , values : [ gitOriginUrlForLogging , sourceRef , targetRef ] , } ) ; } return reposWithMatchingBranches [ 0 ] ; } catch ( err ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-get-match-branch-err" , err ) ; } } ; const handleErrorForCreatePullRequest = async ( gitAPI , sourceRef , targetRef , repoToPRAgainst , err ) : Promise < never > => { if ( err instanceof Error && err . message . match ( /TF401179/ ) ) { logger . warn ( `Existing pull requests found in repository '${ repoToPRAgainst . name }' matching target refs; Searching for matching pull requests` ) ; const searchCriteria = { sourceRefName : `refs/heads/${ sourceRef }` , targetRefName : `refs/heads/${ targetRef }` , } ; const existingPRs = await gitAPI . getPullRequests ( repoToPRAgainst . id ! , searchCriteria ) ; for ( const pr of existingPRs ) { logger . info ( `Existing pull request found targeting source '${ sourceRef }' and target '${ targetRef }': '${ await generatePRUrl ( pr ) }'` ) ; } } throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-create-pull-request-err" , err ) ; } ; param title param sourceRef param targetRef param options export const createPullRequest = async ( title , sourceRef , targetRef , options ) : Promise < AZGitInterfaces . GitPullRequest | undefined > => { const { description = "Automated PR generated by Bedrock" , originPushUrl = "" , } = options || { } ; const prConfig = { description , sourceRefName : `refs/heads/${ sourceRef }` , targetRefName : `refs/heads/${ targetRef }` , title , } ; const gitOrigin = await getGitOrigin ( originPushUrl ) ; const gitOriginUrlForLogging = safeGitUrlForLogging ( originPushUrl ) ; const gitAPI = await GitAPI ( options ) ; const allRepos = await getAllRepos ( gitAPI , gitOriginUrlForLogging ) ; const repoToPRAgainst = await getMatchingBranch ( gitAPI , sourceRef , targetRef , gitOrigin , allRepos , gitOriginUrlForLogging ) ; logger . info ( `Creating pull request in repository '${ repoToPRAgainst . name }' to merge branch '${ sourceRef }' into '${ targetRef }'` ) ; try { const createdPR = await gitAPI . createPullRequest ( prConfig , repoToPRAgainst . id ! ) ; logger . info ( `Successfully created pull request '${ await generatePRUrl ( createdPR ) }'` ) ; return createdPR ; } catch ( err ) { await handleErrorForCreatePullRequest ( gitAPI , sourceRef , targetRef , repoToPRAgainst , err ) ; } } ; param fileName param branch param repoName param accessOpts export const repositoryHasFile = async ( fileName , branch , repoName , accessOpts ) : Promise < void > => { const gitApi = await GitAPI ( accessOpts ) ; const versionDescriptor = { version : branch } ; const gitItem = await gitApi . getItem ( repoName , fileName , accessOpts . project , "" , undefined , undefined , undefined , undefined , versionDescriptor ) ; if ( gitItem === null ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , { errorKey : "git-azure-file-no-exist-in-repo" , values : [ fileName , branch , repoName ] , } ) ; } } ; param project param fileName param branch param repoName param accessOpts export const validateRepository = async ( project , fileName , branch , repoName , accessOpts ) : Promise < void > => { const gitApi = await GitAPI ( accessOpts ) ; const repo = await gitApi . getRepository ( repoName , project ) ; if ( ! repo ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , { errorKey : "git-azure-repo-no-exist" , values : [ repoName , project ] , } ) ; } await repositoryHasFile ( fileName , branch , repoName , accessOpts ) ; } ; param gitAPI param repoName param projectName param targetRef export const getActivePullRequests = async ( gitAPI , repoName , projectName , targetRef = "master" ) : Promise < GitPullRequest [ ] > => { return await gitAPI . getPullRequests ( repoName , { targetRefName : `refs/heads/${ targetRef }` , status : PullRequestStatus . Active , } , projectName ) ; } ; param pullRequest param rc export const completePullRequest = async ( pullRequest , rc ) : Promise < void > => { logger . info ( `Approving pull request ${ pullRequest . pullRequestId }` ) ; try { const login = [ "login" , "--service-principal" , "--username" , rc . servicePrincipalId ! , "--password" , rc . servicePrincipalPassword ! , "--tenant" , rc . servicePrincipalTenantId ! , ] ; const autoComplete = [ "repos" , "pr" , "update" , "--id" , ( pullRequest . pullRequestId || "" ) . toString ( ) , "--auto-complete" , "true" , "--organization" , getAzureOrganizationUrl ( rc . orgName ) , "--output" , "json" , ] ; await exec ( "az" , login ) ; await exec ( "az" , autoComplete ) ; logger . info ( `Approved pull request ${ pullRequest . pullRequestId }` ) ; } catch ( err ) { throw buildError ( errorStatusCode . GIT_OPS_ERR , "git-azure-approve-pull-request-err" , err ) ; } } ;