From e8a5437cd3bc9a937948e09c5b1357d88c040af4 Mon Sep 17 00:00:00 2001 From: Eric Durand-Tremblay <eric.durand-tremblay@equisoft.com> Date: Fri, 13 Jan 2023 08:33:49 -0500 Subject: [PATCH] feat(manager/composer): support git-tags hostRules for github.com when updating artifacts (#18004) Co-authored-by: Rhys Arkins <rhys@arkins.net> Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> fixes undefined --- .../usage/getting-started/private-packages.md | 19 ++ .../manager/composer/artifacts.spec.ts | 124 ++++++++++- lib/modules/manager/composer/artifacts.ts | 19 +- lib/modules/manager/composer/utils.spec.ts | 199 ++++++++++++++++++ lib/modules/manager/composer/utils.ts | 64 ++++++ 5 files changed, 420 insertions(+), 5 deletions(-) diff --git a/docs/usage/getting-started/private-packages.md b/docs/usage/getting-started/private-packages.md index 61343ad5f8..b0dbd6db7d 100644 --- a/docs/usage/getting-started/private-packages.md +++ b/docs/usage/getting-started/private-packages.md @@ -179,6 +179,25 @@ The following details the most common/popular manager artifacts updating and how Any `hostRules` token for `github.com` or `gitlab.com` are found and written out to `COMPOSER_AUTH` in env for Composer to parse. Any `hostRules` with `hostType=packagist` are also included. +For dependencies on `github.com` without a Packagist server: use a Personal Access Token for `hostRule` with `hostType=git-tags`, do not use an application token. +Avoid adding a `hostRule` with `hostType=github` because: + +- it overrides the default Renovate application token for everything else +- it causes unwanted side effects + +The repository in `composer.json` should have the `vcs` type with a `https` URL. +For example: + +```json +{ + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/organization/private-repository" + } + ] +} +``` ### gomod diff --git a/lib/modules/manager/composer/artifacts.spec.ts b/lib/modules/manager/composer/artifacts.spec.ts index 603a26a9ba..2191ab49e9 100644 --- a/lib/modules/manager/composer/artifacts.spec.ts +++ b/lib/modules/manager/composer/artifacts.spec.ts @@ -7,6 +7,7 @@ import * as docker from '../../../util/exec/docker'; import type { StatusResult } from '../../../util/git/types'; import * as hostRules from '../../../util/host-rules'; import * as _datasource from '../../datasource'; +import { GitTagsDatasource } from '../../datasource/git-tags'; import { PackagistDatasource } from '../../datasource/packagist'; import type { UpdateArtifactsConfig } from '../types'; import * as composer from '.'; @@ -112,7 +113,13 @@ describe('modules/manager/composer/artifacts', () => { hostRules.add({ hostType: 'github', matchHost: 'api.github.com', - token: 'github-token', + token: 'ghp_github-token', + }); + // This rule should not affect the result the Github rule has priority to avoid breaking changes. + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_git-tags-token', }); hostRules.add({ hostType: 'gitlab', @@ -164,7 +171,14 @@ describe('modules/manager/composer/artifacts', () => { cwd: '/tmp/github/some/repo', env: { COMPOSER_AUTH: - '{"github-oauth":{"github.com":"github-token"},"gitlab-token":{"gitlab.com":"gitlab-token"},"gitlab-domains":["gitlab.com"],"http-basic":{"packagist.renovatebot.com":{"username":"some-username","password":"some-password"},"artifactory.yyyyyyy.com":{"username":"some-other-username","password":"some-other-password"}},"bearer":{"packages-bearer.example.com":"abcdef0123456789"}}', + '{"github-oauth":{"github.com":"ghp_git-tags-token"},' + + '"gitlab-token":{"gitlab.com":"gitlab-token"},' + + '"gitlab-domains":["gitlab.com"],' + + '"http-basic":{' + + '"packagist.renovatebot.com":{"username":"some-username","password":"some-password"},' + + '"artifactory.yyyyyyy.com":{"username":"some-other-username","password":"some-other-password"}' + + '},' + + '"bearer":{"packages-bearer.example.com":"abcdef0123456789"}}', COMPOSER_CACHE_DIR: '/tmp/renovate/cache/others/composer', }, }, @@ -172,6 +186,112 @@ describe('modules/manager/composer/artifacts', () => { ]); }); + it('git-tags hostRule for github.com set github-token in COMPOSER_AUTH', async () => { + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}', + }, + }, + }, + ]); + }); + + it('Skip github application access token hostRules in COMPOSER_AUTH', async () => { + hostRules.add({ + hostType: 'github', + matchHost: 'api.github.com', + token: 'ghs_token', + }); + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: 'ghp_token', + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}', + }, + }, + }, + ]); + }); + + it('github hostRule for github.com with x-access-token set github-token in COMPOSER_AUTH', async () => { + hostRules.add({ + hostType: 'github', + matchHost: 'https://api.github.com/', + token: 'x-access-token:ghp_token', + }); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const execSnapshots = mockExecAll(); + fs.readLocalFile.mockResolvedValueOnce('{}'); + const authConfig = { + ...config, + registryUrls: ['https://packagist.renovatebot.com'], + }; + git.getRepoStatus.mockResolvedValueOnce(repoStatus); + expect( + await composer.updateArtifacts({ + packageFileName: 'composer.json', + updatedDeps: [], + newPackageFileContent: '{}', + config: authConfig, + }) + ).toBeNull(); + + expect(execSnapshots).toMatchObject([ + { + options: { + env: { + COMPOSER_AUTH: '{"github-oauth":{"github.com":"ghp_token"}}', + }, + }, + }, + ]); + }); + it('returns updated composer.lock', async () => { fs.readLocalFile.mockResolvedValueOnce('{}'); const execSnapshots = mockExecAll(); diff --git a/lib/modules/manager/composer/artifacts.ts b/lib/modules/manager/composer/artifacts.ts index 32328b39d2..0eae7fefde 100644 --- a/lib/modules/manager/composer/artifacts.ts +++ b/lib/modules/manager/composer/artifacts.ts @@ -18,26 +18,39 @@ import { import { getRepoStatus } from '../../../util/git'; import * as hostRules from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; +import { GitTagsDatasource } from '../../datasource/git-tags'; import { PackagistDatasource } from '../../datasource/packagist'; import type { UpdateArtifact, UpdateArtifactsResult } from '../types'; import type { AuthJson, ComposerLock } from './types'; import { extractConstraints, + findGithubToken, getComposerArguments, getPhpConstraint, requireComposerDependencyInstallation, + takePersonalAccessTokenIfPossible, } from './utils'; function getAuthJson(): string | null { const authJson: AuthJson = {}; - const githubCredentials = hostRules.find({ + const githubToken = findGithubToken({ hostType: 'github', url: 'https://api.github.com/', }); - if (githubCredentials?.token) { + + const gitTagsGithubToken = findGithubToken({ + hostType: GitTagsDatasource.id, + url: 'https://github.com', + }); + + const selectedGithubToken = takePersonalAccessTokenIfPossible( + githubToken, + gitTagsGithubToken + ); + if (selectedGithubToken) { authJson['github-oauth'] = { - 'github.com': githubCredentials.token.replace('x-access-token:', ''), + 'github.com': selectedGithubToken, }; } diff --git a/lib/modules/manager/composer/utils.spec.ts b/lib/modules/manager/composer/utils.spec.ts index 4f2e0fd5ff..c591825be7 100644 --- a/lib/modules/manager/composer/utils.spec.ts +++ b/lib/modules/manager/composer/utils.spec.ts @@ -1,13 +1,24 @@ import { GlobalConfig } from '../../../config/global'; +import * as hostRules from '../../../util/host-rules'; +import { GitTagsDatasource } from '../../datasource/git-tags'; import { extractConstraints, + findGithubToken, getComposerArguments, + isGithubFineGrainedPersonalAccessToken, + isGithubPersonalAccessToken, + isGithubServerToServerToken, requireComposerDependencyInstallation, + takePersonalAccessTokenIfPossible, } from './utils'; jest.mock('../../datasource'); describe('modules/manager/composer/utils', () => { + beforeEach(() => { + hostRules.clear(); + }); + describe('extractConstraints', () => { it('returns from require', () => { expect( @@ -288,4 +299,192 @@ describe('modules/manager/composer/utils', () => { ).toBeFalse(); }); }); + + describe('findGithubToken', () => { + it('returns the token string when hostRule match search with a valid personal access token', () => { + const TOKEN_STRING = 'ghp_TOKEN'; + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: TOKEN_STRING, + }); + expect( + findGithubToken({ + hostType: GitTagsDatasource.id, + url: 'https://github.com', + }) + ).toEqual(TOKEN_STRING); + }); + + it('returns undefined when no hostRule match search', () => { + expect( + findGithubToken({ + hostType: GitTagsDatasource.id, + url: 'https://github.com', + }) + ).toBeUndefined(); + }); + + it('remove x-access-token token prefix', () => { + const TOKEN_STRING_WITH_PREFIX = 'x-access-token:ghp_TOKEN'; + const TOKEN_STRING = 'ghp_TOKEN'; + hostRules.add({ + hostType: GitTagsDatasource.id, + matchHost: 'github.com', + token: TOKEN_STRING_WITH_PREFIX, + }); + expect( + findGithubToken({ + hostType: GitTagsDatasource.id, + url: 'https://github.com', + }) + ).toEqual(TOKEN_STRING); + }); + }); + + describe('isGithubPersonalAccessToken', () => { + it('returns true when string is a github personnal access token', () => { + expect(isGithubPersonalAccessToken('ghp_XXXXXX')).toBeTrue(); + }); + + it('returns false when string is a github application token', () => { + expect(isGithubPersonalAccessToken('ghs_XXXXXX')).toBeFalse(); + }); + + it('returns false when string is a github fine grained personal access token', () => { + expect(isGithubPersonalAccessToken('github_pat_XXXXXX')).toBeFalse(); + }); + + it('returns false when string is not a token at all', () => { + expect(isGithubPersonalAccessToken('XXXXXX')).toBeFalse(); + }); + }); + + describe('isGithubServerToServerToken', () => { + it('returns true when string is a github server to server token', () => { + expect(isGithubServerToServerToken('ghs_XXXXXX')).toBeTrue(); + }); + + it('returns false when string is a github personal access token token', () => { + expect(isGithubServerToServerToken('ghp_XXXXXX')).toBeFalse(); + }); + + it('returns false when string is a github fine grained personal access token', () => { + expect(isGithubPersonalAccessToken('github_pat_XXXXXX')).toBeFalse(); + }); + + it('returns false when string is not a token at all', () => { + expect(isGithubServerToServerToken('XXXXXX')).toBeFalse(); + }); + }); + + describe('isGithubFineGrainedPersonalAccessToken', () => { + it('returns true when string is a github fine grained personal access token', () => { + expect( + isGithubFineGrainedPersonalAccessToken('github_pat_XXXXXX') + ).toBeTrue(); + }); + + it('returns false when string is a github personnal access token', () => { + expect(isGithubFineGrainedPersonalAccessToken('ghp_XXXXXX')).toBeFalse(); + }); + + it('returns false when string is a github application token', () => { + expect(isGithubFineGrainedPersonalAccessToken('ghs_XXXXXX')).toBeFalse(); + }); + + it('returns false when string is not a token at all', () => { + expect(isGithubFineGrainedPersonalAccessToken('XXXXXX')).toBeFalse(); + }); + }); + + describe('takePersonalAccessTokenIfPossible', () => { + it('returns undefined when both token are undefined', () => { + const githubToken = undefined; + const gitTagsGithubToken = undefined; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toBeUndefined(); + }); + + it('returns gitTagsToken when both token are PAT', () => { + const githubToken = 'ghp_github'; + const gitTagsGithubToken = 'ghp_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(gitTagsGithubToken); + }); + + it('returns githubToken is PAT and gitTagsGithubToken is not a PAT', () => { + const githubToken = 'ghp_github'; + const gitTagsGithubToken = 'ghs_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(githubToken); + }); + + it('returns gitTagsToken when both token are set but not PAT', () => { + const githubToken = 'ghs_github'; + const gitTagsGithubToken = 'ghs_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(gitTagsGithubToken); + }); + + it('returns gitTagsToken when gitTagsToken not PAT and gitTagsGithubToken is not set', () => { + const githubToken = undefined; + const gitTagsGithubToken = 'ghs_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(gitTagsGithubToken); + }); + + it('returns githubToken when githubToken not PAT and gitTagsGithubToken is not set', () => { + const githubToken = 'ghs_gitTags'; + const gitTagsGithubToken = undefined; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(githubToken); + }); + + it('take personal assess token over fine grained token', () => { + const githubToken = 'ghp_github'; + const gitTagsGithubToken = 'github_pat_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(githubToken); + }); + + it('take fine grained token over server to server token', () => { + const githubToken = 'github_pat_github'; + const gitTagsGithubToken = 'ghs_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(githubToken); + }); + + it('take git-tags fine grained token', () => { + const githubToken = undefined; + const gitTagsGithubToken = 'github_pat_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(gitTagsGithubToken); + }); + + it('take git-tags unknown token type when no other token is set', () => { + const githubToken = undefined; + const gitTagsGithubToken = 'unknownTokenType_gitTags'; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(gitTagsGithubToken); + }); + + it('take github unknown token type when no other token is set', () => { + const githubToken = 'unknownTokenType'; + const gitTagsGithubToken = undefined; + expect( + takePersonalAccessTokenIfPossible(githubToken, gitTagsGithubToken) + ).toEqual(githubToken); + }); + }); }); diff --git a/lib/modules/manager/composer/utils.ts b/lib/modules/manager/composer/utils.ts index 3571196ad3..330f50bc71 100644 --- a/lib/modules/manager/composer/utils.ts +++ b/lib/modules/manager/composer/utils.ts @@ -4,6 +4,7 @@ import { quote } from 'shlex'; import { GlobalConfig } from '../../../config/global'; import { logger } from '../../../logger'; import type { ToolConstraint } from '../../../util/exec/types'; +import { HostRuleSearch, find as findHostRule } from '../../../util/host-rules'; import { api, id as composerVersioningId } from '../../versioning/composer'; import type { UpdateArtifactsConfig } from '../types'; import type { ComposerConfig, ComposerLock } from './types'; @@ -109,3 +110,66 @@ export function extractConstraints( } return res; } + +export function findGithubToken(search: HostRuleSearch): string | undefined { + return findHostRule(search)?.token?.replace('x-access-token:', ''); +} + +export function isGithubPersonalAccessToken(token: string): boolean { + return token.startsWith('ghp_'); +} + +export function isGithubServerToServerToken(token: string): boolean { + return token.startsWith('ghs_'); +} + +export function isGithubFineGrainedPersonalAccessToken(token: string): boolean { + return token.startsWith('github_pat_'); +} + +export function takePersonalAccessTokenIfPossible( + githubToken: string | undefined, + gitTagsGithubToken: string | undefined +): string | undefined { + if (gitTagsGithubToken && isGithubPersonalAccessToken(gitTagsGithubToken)) { + logger.debug('Using GitHub Personal Access Token (git-tags)'); + return gitTagsGithubToken; + } + + if (githubToken && isGithubPersonalAccessToken(githubToken)) { + logger.debug('Using GitHub Personal Access Token'); + return githubToken; + } + + if ( + gitTagsGithubToken && + isGithubFineGrainedPersonalAccessToken(gitTagsGithubToken) + ) { + logger.debug('Using GitHub Fine-grained Personal Access Token (git-tags)'); + return gitTagsGithubToken; + } + + if (githubToken && isGithubFineGrainedPersonalAccessToken(githubToken)) { + logger.debug('Using GitHub Fine-grained Personal Access Token'); + return githubToken; + } + + if (gitTagsGithubToken) { + if (isGithubServerToServerToken(gitTagsGithubToken)) { + logger.debug('Using GitHub Server-to-Server token (git-tags)'); + } else { + logger.debug('Using unknown GitHub token type (git-tags)'); + } + return gitTagsGithubToken; + } + + if (githubToken) { + if (isGithubServerToServerToken(githubToken)) { + logger.debug('Using GitHub Server-to-Server token'); + } else { + logger.debug('Using unknown GitHub token type'); + } + } + + return githubToken; +} -- GitLab