diff --git a/docs/usage/self-hosted-configuration.md b/docs/usage/self-hosted-configuration.md index a46d563be6d49e69b4a4d82f7e8f8bfff1fea548..e0bdd18a359bfd5f724536bb4529b420a5d2b4c2 100644 --- a/docs/usage/self-hosted-configuration.md +++ b/docs/usage/self-hosted-configuration.md @@ -415,9 +415,19 @@ In practice, it is implemented by converting the `force` configuration into a `p This is set to `true` by default, meaning that any settings (such as `schedule`) take maximum priority even against custom settings existing inside individual repositories. It will also override any settings in `packageRules`. -## forkToken +## forkOrg + +This configuration option lets you choose an organization you want repositories forked into when "fork mode" is enabled. +It must be set to a GitHub Organization name and not a GitHub user account. + +This can be used if you're migrating from user-based forks to organization-based forks. -You probably don't need this option - it is an experimental setting developed for the Forking Renovate hosted GitHub App. +If you've set a `forkOrg` then Renovate will: + +1. Check if a fork exists in the preferred organization before checking it exists in the fork user's account +1. If no fork exists: it will be created in the `forkOrg`, not the user account + +## forkToken If this value is configured then Renovate: diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index ce143bdf4c053d6217ac4e798e5310c06c24dd56..061d43406688649b74607155e0093a7a278267a0 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -444,6 +444,16 @@ const options: RenovateOptions[] = [ supportedPlatforms: ['github'], experimental: true, }, + { + name: 'forkOrg', + description: + 'The preferred organization to create or find forked repositories, when in fork mode.', + stage: 'repository', + type: 'string', + globalOnly: true, + supportedPlatforms: ['github'], + experimental: true, + }, { name: 'githubTokenWarn', description: 'Display warnings about GitHub token not being set.', diff --git a/lib/modules/platform/github/__snapshots__/index.spec.ts.snap b/lib/modules/platform/github/__snapshots__/index.spec.ts.snap index 8dbafeab1175df2142851f1ccc61680de5a478eb..9bbc6ed9a5e14e70751318d56f3b45ad2c4a7b4f 100644 --- a/lib/modules/platform/github/__snapshots__/index.spec.ts.snap +++ b/lib/modules/platform/github/__snapshots__/index.spec.ts.snap @@ -111,7 +111,7 @@ exports[`modules/platform/github/index initRepo should squash 1`] = ` } `; -exports[`modules/platform/github/index initRepo should update fork when using forkToken 1`] = ` +exports[`modules/platform/github/index initRepo should update fork when using forkToken and forkOrg 1`] = ` { "defaultBranch": "master", "isFork": false, diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts index fd917c0a98daa33aa82b8fc8bc9e61c25a2603a1..11cda7e46dc6beb30f8e728b09c414f60caebe02 100644 --- a/lib/modules/platform/github/index.spec.ts +++ b/lib/modules/platform/github/index.spec.ts @@ -397,26 +397,24 @@ describe('modules/platform/github/index', () => { scope.get('/user').reply(200, { login: 'forked', }); - // getBranchCommit scope.post(`/repos/${repo}/forks`).reply(500); await expect( github.initRepo({ repository: 'some/repo', forkToken: 'true', + forkOrg: 'forked', }) ).rejects.toThrow(REPOSITORY_CANNOT_FORK); }); - it('should update fork when using forkToken', async () => { + it('should update fork when using forkToken and forkOrg', async () => { const scope = httpMock.scope(githubApiHost); forkInitRepoMock(scope, 'some/repo', true); - scope.get('/user').reply(200, { - login: 'forked', - }); scope.patch('/repos/forked/repo/git/refs/heads/master').reply(200); const config = await github.initRepo({ repository: 'some/repo', forkToken: 'true', + forkOrg: 'forked', }); expect(config).toMatchSnapshot(); }); diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts index d00cc2df6365c34883116a1c4bedd7710f711356..8b477ebbbaebb5417a1a84b14c40180033bc22fa 100644 --- a/lib/modules/platform/github/index.ts +++ b/lib/modules/platform/github/index.ts @@ -257,20 +257,6 @@ export async function getJsonFile( return JSON5.parse(raw); } -export async function getForkOrgs(token: string): Promise<string[]> { - // This function will be adapted later to support configured forkOrgs - if (!config.renovateForkUser) { - try { - logger.debug('Determining fork user from API'); - const userDetails = await getUserDetails(platformConfig.endpoint, token); - config.renovateForkUser = userDetails.username; - } catch (err) { - logger.debug({ err }, 'Error getting username for forkToken'); - } - } - return config.renovateForkUser ? [config.renovateForkUser] : []; -} - export async function listForks( token: string, repository: string @@ -299,28 +285,38 @@ export async function listForks( export async function findFork( token: string, - repository: string + repository: string, + forkOrg?: string ): Promise<GhRestRepo | null> { const forks = await listForks(token, repository); - const orgs = await getForkOrgs(token); - if (!orgs.length) { - throw new Error(REPOSITORY_CANNOT_FORK); + if (forkOrg) { + logger.debug(`Searching for forked repo in forkOrg (${forkOrg})`); + const forkedRepo = forks.find((repo) => repo.owner.login === forkOrg); + if (forkedRepo) { + logger.debug(`Found repo in forkOrg: ${forkedRepo.full_name}`); + return forkedRepo; + } + logger.debug(`No repo found in forkOrg`); } - let forkedRepo: GhRestRepo | undefined; - for (const forkOrg of orgs) { - logger.debug(`Searching for forked repo in ${forkOrg}`); - forkedRepo = forks.find((repo) => repo.owner.login === forkOrg); + logger.debug(`Searching for forked repo in user account`); + try { + const { username } = await getUserDetails(platformConfig.endpoint, token); + const forkedRepo = forks.find((repo) => repo.owner.login === username); if (forkedRepo) { - logger.debug(`Found existing forked repo: ${forkedRepo.full_name}`); - break; + logger.debug(`Found repo in user account: ${forkedRepo.full_name}`); + return forkedRepo; } + } catch (err) { + throw new Error(REPOSITORY_CANNOT_FORK); } - return forkedRepo ?? null; + logger.debug(`No repo found in user account`); + return null; } export async function createFork( token: string, - repository: string + repository: string, + forkOrg?: string ): Promise<GhRestRepo> { let forkedRepo: GhRestRepo | undefined; try { @@ -328,6 +324,7 @@ export async function createFork( await githubApi.postJson<GhRestRepo>(`repos/${repository}/forks`, { token, body: { + organization: forkOrg ? forkOrg : undefined, name: config.parentRepo!.replace('/', '-_-'), default_branch_only: true, // no baseBranches support yet }, @@ -339,7 +336,8 @@ export async function createFork( if (!forkedRepo) { throw new Error(REPOSITORY_CANNOT_FORK); } - logger.debug(`Created forked repo ${forkedRepo.full_name}, now sleeping 30s`); + logger.info({ forkedRepo: forkedRepo.full_name }, 'Created forked repo'); + logger.debug(`Sleeping 30s after creating fork`); await delay(30000); return forkedRepo; } @@ -348,6 +346,7 @@ export async function createFork( export async function initRepo({ endpoint, repository, + forkOrg, forkToken, renovateUsername, cloneSubmodules, @@ -489,7 +488,7 @@ export async function initRepo({ // save parent name then delete config.parentRepo = config.repository; config.repository = null; - let forkedRepo = await findFork(forkToken, repository); + let forkedRepo = await findFork(forkToken, repository, forkOrg); if (forkedRepo) { config.repository = forkedRepo.full_name; const forkDefaultBranch = forkedRepo.default_branch; @@ -567,7 +566,7 @@ export async function initRepo({ } } else { logger.debug('Forked repo is not found - attempting to create it'); - forkedRepo = await createFork(forkToken, repository); + forkedRepo = await createFork(forkToken, repository, forkOrg); config.repository = forkedRepo.full_name; } } diff --git a/lib/modules/platform/github/types.ts b/lib/modules/platform/github/types.ts index ef01406661370ba3fb352567642a5a84a8b13d08..e285ea5a23b036d879c8399cf4a377486338484d 100644 --- a/lib/modules/platform/github/types.ts +++ b/lib/modules/platform/github/types.ts @@ -90,6 +90,7 @@ export interface LocalRepoConfig { prReviewsRequired: boolean; repoForceRebase?: boolean; parentRepo: string | null; + forkOrg?: string; forkToken?: string; prList: GhPr[] | null; issueList: any[] | null; diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 4a0d5a825b9cb28f5c48c8f61b04df79561e2598..d9bc8f84caa40d8e8cebf538972869686e5b3d73 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -37,6 +37,7 @@ export interface RepoParams { repository: string; endpoint?: string; gitUrl?: GitUrlOption; + forkOrg?: string; forkToken?: string; forkProcessing?: 'enabled' | 'disabled'; renovateUsername?: string;