import { GlobalConfig } from '../../../config/global.ts'; import { logger } from '../../../logger/index.ts'; import type { BranchStatus } from '../../../types/index.ts'; import { coerceArray } from '../../../util/array.ts'; import * as git from '../../../util/git/index.ts'; import { getBaseUrl, setBaseUrl } from '../../../util/http/scm-manager.ts'; import { sanitize } from '../../../util/sanitize.ts'; import { ensureTrailingSlash } from '../../../util/url.ts'; import type { BranchStatusConfig, CreatePRConfig, EnsureCommentConfig, EnsureCommentRemovalConfigByContent, EnsureCommentRemovalConfigByTopic, EnsureIssueConfig, FindPRConfig, Issue, MergePRConfig, PlatformParams, PlatformResult, Pr, RepoParams, RepoResult, UpdatePrConfig, } from '../types.ts'; import { repoFingerprint } from '../util.ts'; import { smartTruncate } from '../utils/pr-body.ts'; import { mapPrFromScmToRenovate } from './mapper.ts'; import { createScmPr, getAllRepoPrs, getAllRepos, getCurrentUser, getDefaultBranch, getRepo, getRepoPr, updateScmPr, } from './scm-manager-helper.ts'; import { getRepoUrl, mapPrState, matchPrState, smartLinks } from './utils.ts'; interface SCMMRepoConfig { repository: string; prList: Pr[] | null; defaultBranch: string; ignorePrAuthor: boolean; } export const id = 'scm-manager'; export const experimental = true; let config: SCMMRepoConfig = {} as any; export async function initPlatform({ endpoint, token, }: PlatformParams): Promise { if (!endpoint) { throw new Error('Init: SCM-Manager endpoint not configured'); } if (!token) { throw new Error('Init: SCM-Manager API token not configured'); } const baseUrl = `${ensureTrailingSlash(endpoint)}api/v2`; setBaseUrl(baseUrl); try { const me = await getCurrentUser(token); const gitAuthor = `${me.displayName} <${me.mail}>`; const result = { endpoint: baseUrl, gitAuthor }; logger.debug({ result }, 'Platform result'); return result; } catch (err) { logger.debug( { err }, 'Init: Error authenticating with SCM-Manager. Check your token', ); throw new Error('Init: Authentication failure'); } } export async function initRepo({ repository, gitUrl, }: RepoParams): Promise { const repo = await getRepo(repository); const defaultBranch = await getDefaultBranch(repo); const url = getRepoUrl(repo, gitUrl, getBaseUrl()); config = {} as any; config.repository = repository; config.defaultBranch = defaultBranch; config.ignorePrAuthor = GlobalConfig.get('ignorePrAuthor'); await git.initRepo({ ...config, url, }); // Reset cached resources invalidatePrCache(); const result = { defaultBranch: config.defaultBranch, isFork: false, repoFingerprint: repoFingerprint(config.repository, getBaseUrl()), }; logger.debug({ result }, `Repo initialized`); return result; } export async function getRepos(): Promise { const repos = (await getAllRepos()).filter((repo) => repo.type === 'git'); return repos.map((repo) => `${repo.namespace}/${repo.name}`); } export async function getBranchPr(branchName: string): Promise { return await findPr({ branchName, state: 'open' }); } export async function findPr({ branchName, prTitle, state = 'all', }: FindPRConfig): Promise { const inProgressPrs = await getPrList(); const result = inProgressPrs.find( (pr) => branchName === pr.sourceBranch && (!prTitle || prTitle === pr.title) && matchPrState(pr, state), ); if (result) { logger.debug({ result }, `Found PR`); return result; } logger.debug( `Could not find PR with source branch ${branchName} and title ${ prTitle ?? '' } and state ${state}`, ); return null; } export async function getPr(number: number): Promise { const inProgressPrs = await getPrList(); const cachedPr = inProgressPrs.find((pr) => pr.number === number); if (cachedPr) { logger.debug('Returning from cached PRs'); return cachedPr; } try { const result = await getRepoPr(config.repository, number); logger.debug('Returning PR from API'); return mapPrFromScmToRenovate(result); } catch (err) { logger.error({ err, number }, 'Can not find a PR with the given id'); return null; } } export async function getPrList(): Promise { if (config.prList === null) { try { config.prList = ( await getAllRepoPrs(config.repository, config.ignorePrAuthor) ).map((pr) => mapPrFromScmToRenovate(pr)); } catch (error) { logger.error(error); } } return coerceArray(config.prList); } export async function createPr({ sourceBranch, targetBranch, prTitle, prBody, draftPR, }: CreatePRConfig): Promise { const createdPr = await createScmPr(config.repository, { source: sourceBranch, target: targetBranch, title: prTitle, description: sanitize(prBody), status: draftPR ? 'DRAFT' : 'OPEN', }); logger.debug( `PR created with title '${createdPr.title}' from source '${createdPr.source}' to target '${createdPr.target}'`, ); return mapPrFromScmToRenovate(createdPr); } export async function updatePr({ number, prTitle, prBody, state, targetBranch, }: UpdatePrConfig): Promise { await updateScmPr(config.repository, number, { title: prTitle, description: sanitize(prBody) ?? undefined, target: targetBranch, status: mapPrState(state), }); logger.debug(`Updated PR #${number} with title ${prTitle}`); } export function mergePr(_config: MergePRConfig): Promise { logger.debug('Not implemented mergePr'); return Promise.resolve(false); } export function getBranchStatus( _branchName: string, _internalChecksAsSuccess: boolean, ): Promise { logger.debug('Not implemented getBranchStatus'); return Promise.resolve('red'); } export function setBranchStatus( _branchStatusConfig: BranchStatusConfig, ): Promise { logger.debug('Not implemented setBranchStatus'); return Promise.resolve(); } export function getBranchStatusCheck( _branchName: string, _context: string | null | undefined, ): Promise { logger.debug('Not implemented setBranchStatus'); return Promise.resolve(null); } export function addReviewers( _number: number, _reviewers: string[], ): Promise { logger.debug('Not implemented addReviewers'); return Promise.resolve(); } export function addAssignees( _number: number, _assignees: string[], ): Promise { logger.debug('Not implemented addAssignees'); return Promise.resolve(); } export function deleteLabel(_number: number, _label: string): Promise { logger.debug('Not implemented deleteLabel'); return Promise.resolve(); } export function getIssueList(): Promise { logger.debug('Not implemented getIssueList'); return Promise.resolve([]); } export function findIssue(_title: string): Promise { logger.debug('Not implemented findIssue'); return Promise.resolve(null); } export function ensureIssue( _config: EnsureIssueConfig, ): Promise<'updated' | 'created' | null> { logger.debug('Not implemented ensureIssue'); return Promise.resolve(null); } export function ensureIssueClosing(_title: string): Promise { logger.debug('Not implemented ensureIssueClosing'); return Promise.resolve(); } export function ensureComment(_config: EnsureCommentConfig): Promise { logger.debug('Not implemented ensureComment'); return Promise.resolve(false); } export function ensureCommentRemoval( _ensureCommentRemoval: EnsureCommentRemovalConfigByTopic | EnsureCommentRemovalConfigByContent, ): Promise { logger.debug('Not implemented ensureCommentRemoval'); return Promise.resolve(); } export function massageMarkdown(prBody: string): string { return smartTruncate(smartLinks(prBody), maxBodyLength()); } export function getRepoForceRebase(): Promise { return Promise.resolve(false); } export function getRawFile( _fileName: string, _repoName?: string, _branchOrTag?: string, ): Promise { logger.debug('Not implemented getRawFile'); return Promise.resolve(null); } export function getJsonFile( _fileName: string, _repoName?: string, _branchOrTag?: string, ): Promise { logger.debug('Not implemented getJsonFile'); return Promise.resolve(null); } export function maxBodyLength(): number { return 200000; } export function invalidatePrCache(): void { config.prList = null; }