Skip to content

Commit

Permalink
[WIP] Add logic to submit PRs to repos using the right GitHub Integra…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
rm3l committed Apr 8, 2024
1 parent 14055a9 commit 6f38749
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 26 deletions.
4 changes: 4 additions & 0 deletions plugins/bulk-import-backend/src/openapi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ declare namespace Components {
* organization which the repository is part of
*/
organization?: string;
/**
* default branch
*/
defaultBranch?: string;
};
/**
* content of the catalog-info.yaml to include in the import Pull Request.
Expand Down
10 changes: 8 additions & 2 deletions plugins/bulk-import-backend/src/openapidocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,10 @@ const OPENAPI = `
"organization": {
"type": "string",
"description": "organization which the repository is part of"
},
"defaultBranch": {
"type": "string",
"description": "default branch"
}
}
},
Expand Down Expand Up @@ -416,7 +420,8 @@ const OPENAPI = `
"repository": {
"name": "pet-app",
"url": "https://github.com/my-org/pet-app",
"organization": "my-org"
"organization": "my-org",
"defaultBranch": "main"
},
"github": {
"pullRequest": {
Expand All @@ -429,7 +434,8 @@ const OPENAPI = `
"repository": {
"name": "project-zero",
"url": "https://ghe.example.com/my-other-org/project-zero",
"organization": "my-other-org"
"organization": "my-other-org",
"defaultBranch": "dev"
},
"github": {
"pullRequest": {
Expand Down
5 changes: 5 additions & 0 deletions plugins/bulk-import-backend/src/schema/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ components:
organization:
type: string
description: organization which the repository is part of
defaultBranch:
type: string
description: default branch
catalogInfoContent:
type: string
description: content of the catalog-info.yaml to include in the import Pull Request.
Expand Down Expand Up @@ -320,6 +323,7 @@ components:
name: 'pet-app'
url: 'https://github.com/my-org/pet-app'
organization: 'my-org'
defaultBranch: main
github:
pullRequest:
title: 'Add default catalog-info.yaml to import to Red Hat Developer Hub'
Expand All @@ -328,6 +332,7 @@ components:
name: 'project-zero'
url: 'https://ghe.example.com/my-other-org/project-zero'
organization: 'my-other-org'
defaultBranch: dev
github:
pullRequest:
title: 'Add custom catalog-info.yaml to import to Red Hat Developer Hub'
Expand Down
130 changes: 110 additions & 20 deletions plugins/bulk-import-backend/src/service/githubApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
GithubRepositoryResponse,
isGithubAppCredential,
} from '../types';
import {Components} from "../openapi";
import gitUrlParse from "git-url-parse";

export class GithubApiService {
private readonly logger: Logger;
Expand All @@ -39,15 +41,15 @@ export class GithubApiService {
this.logger = logger;
this.integrations = ScmIntegrations.fromConfig(config);
this.githubCredentialsProvider =
CustomGithubCredentialsProvider.fromIntegrations(this.integrations);
CustomGithubCredentialsProvider.fromIntegrations(this.integrations);
}

/**
* Creates the GithubRepoFetchError to be stored in the returned errors array of the returned GithubRepositoryResponse object
*/
private createCredentialError(
credential: ExtendedGithubCredentials,
err?: Error,
credential: ExtendedGithubCredentials,
err?: Error,
): GithubRepoFetchError | undefined {
if (err) {
if (isGithubAppCredential(credential)) {
Expand Down Expand Up @@ -86,18 +88,18 @@ export class GithubApiService {
* If any errors occurs, adds them to the provided errors Map<number, GithubRepoFetchError>
*/
private async addGithubAppRepositories(
octokit: Octokit,
credential: GithubAppCredentials,
repositories: Map<string, GithubRepository>,
errors: Map<number, GithubRepoFetchError>,
octokit: Octokit,
credential: GithubAppCredentials,
repositories: Map<string, GithubRepository>,
errors: Map<number, GithubRepoFetchError>,
): Promise<void> {
try {
const repos = await octokit.paginate(
octokit.apps.listReposAccessibleToInstallation,
octokit.apps.listReposAccessibleToInstallation,
);
// The return type of the paginate method is incorrect for apps.listReposAccessibleToInstallation
const accessibleRepos: RestEndpointMethodTypes['apps']['listReposAccessibleToInstallation']['response']['data']['repositories'] =
repos.repositories ?? repos;
repos.repositories ?? repos;
accessibleRepos.forEach(repo => {
const githubRepo: GithubRepository = {
name: repo.name,
Expand All @@ -110,11 +112,11 @@ export class GithubApiService {
});
} catch (err) {
this.logger.error(
`Fetching repositories with access token for github app ${credential.appId}, failed with ${err}`,
`Fetching repositories with access token for github app ${credential.appId}, failed with ${err}`,
);
const credentialError = this.createCredentialError(
credential,
err as Error,
credential,
err as Error,
);
if (credentialError) {
errors.set(credential.appId, credentialError);
Expand All @@ -127,10 +129,10 @@ export class GithubApiService {
* If any errors occurs, adds them to the provided errors Map<number, GithubRepoFetchError>
*/
private async addGithubTokenRepositories(
octokit: Octokit,
credential: GithubCredentials,
repositories: Map<string, GithubRepository>,
errors: Map<number, GithubRepoFetchError>,
octokit: Octokit,
credential: GithubCredentials,
repositories: Map<string, GithubRepository>,
errors: Map<number, GithubRepoFetchError>,
): Promise<void> {

try {
Expand All @@ -155,11 +157,11 @@ export class GithubApiService {
});
} catch (err) {
this.logger.error(
`Fetching repositories with token from token failed with ${err}`,
`Fetching repositories with token from token failed with ${err}`,
);
const credentialError = this.createCredentialError(
credential,
err as Error,
credential,
err as Error,
);
if (credentialError) {
errors.set(-1, credentialError);
Expand Down Expand Up @@ -236,4 +238,92 @@ export class GithubApiService {
errors: Array.from(errors.values()),
};
}
}

async submitPrToRepo(logger: Logger, input: {
repoUrl: string,
gitUrl: gitUrlParse.GitUrl,
prTitle: string,
prBody: string,
catalogInfoContent: string,
}): Promise<{ pr: string }> {
const branchName = "backstage-bulk-import";
const fileName = "catalog-info.yaml";
const ghConfig = this.integrations.github.byUrl(input.repoUrl)?.config;
if (!ghConfig) {
throw new Error(`Could not find GH integration from ${input.repoUrl}`);
}

const owner = input.gitUrl.organization;
const repo = input.gitUrl.name;

const credentials = await this.githubCredentialsProvider.getAllCredentials({
host: ghConfig.host,
});
if (credentials.length === 0) {
throw new Error(`No credentials for GH integration`);
}
for (const credential of credentials) {
if ('error' in credential) {
if (credential.error?.name !== 'NotFoundError') {
this.logger.error(
`Obtaining the Access Token Github App with appId: ${credential.appId} failed with ${credential.error}`,
);
const credentialError = this.createCredentialError(credential);
if (credentialError) {
logger.debug(`${credential.appId}: ${credentialError}`);
}
}
continue;
}
// const baseUrl =
// ghHost === 'github.com'
// ? 'https://api.github.com'
// : `https://${ghHost}/api/v3`;
const octo = new Octokit({
baseUrl: ghConfig.apiBaseUrl ?? 'https://api.github.com',
auth: credential.token,
});
try {
const repoData = await octo.rest.repos.get({
owner,
repo
});
const parentRef = await octo.rest.git.getRef({
owner,
repo,
ref: `heads/${repoData.data.default_branch}`
});
await octo.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${branchName}`,
sha: parentRef.data.object.sha
});
await octo.rest.repos.createOrUpdateFileContents({
owner,
repo,
path: fileName,
message: `Add ${fileName} config file`,
content: btoa(input.catalogInfoContent),
branch: branchName
});
const pullRequestResponse = await octo.rest.pulls.create({
owner,
repo,
title: input.prTitle,
body: input.prBody,
head: branchName,
base: repoData.data.default_branch
});

return {
pr: pullRequestResponse.data.html_url,
}
} catch(e) {
logger.warn(`Couldn't create PR: ${e}`);
}
}

throw new Error("Tried all possible GitHub credentials, but could not create PR. Please try again later...");
}
}
40 changes: 38 additions & 2 deletions plugins/bulk-import-backend/src/service/handlers/bulkImports.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {Logger} from "winston";
import {Paths} from "../../openapi";
import {GithubApiService} from "../githubApiService";
import {CatalogInfoGenerator} from "../../helpers";
import gitUrlParse from "git-url-parse";

export async function findAllImports(
logger: Logger,
Expand All @@ -11,9 +14,42 @@ export async function findAllImports(

export async function createImportJobs(
logger: Logger,
req: Paths.CreateImportJobs.RequestBody,
githubApiService: GithubApiService,
catalogInfoGenerator: CatalogInfoGenerator,
importRequests: Paths.CreateImportJobs.RequestBody,
): Promise<Paths.CreateImportJobs.Responses.$202> {
logger.debug('Creating new bulk import jobs from request..');
logger.debug(`Handling request to import ${importRequests?.length ?? 0} repo(s)..`);

if (!importRequests) {
return [];
}

for (const req of importRequests) {
if (!req.repository || !req.repository.url) {
continue;
}
const gitUrl = gitUrlParse(req.repository.url);
// TODO: Check if repo is already imported

// TODO: Check if there is already a pending import for this repo

// Create PR
const prToRepo = await githubApiService.submitPrToRepo(logger, {
repoUrl: req.repository.url,
gitUrl: gitUrl,
catalogInfoContent: req.catalogInfoContent ?? catalogInfoGenerator.generateDefaultCatalogInfoContent(gitUrl.owner, gitUrl.organization, gitUrl.name),
prTitle: req.github?.pullRequest?.title ?? `Add catalog-info.yaml`,
prBody: req.github?.pullRequest?.body ?? `This pull request adds a **Backstage entity metadata file** to this repository so that the component can be added to the Red Hat Developer Hub software catalog.
After this pull request is merged, the component will become available.
For more information, read an [overview of the Backstage software catalog](https://backstage.io/docs/features/software-catalog/).`,
});
logger.debug(`Created new PR from request: ${prToRepo.pr}`);

// TODO: Create Location
}

// TODO: implement
throw new Error("createImportJobs: not implemented yet");
}
4 changes: 2 additions & 2 deletions plugins/bulk-import-backend/src/service/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export async function createRouter(
req.header('authorization'),
);
await permissionCheck(permissions, backstageToken);
const repos = await findAllRepositories(logger, githubApiService);
const repos = await findAllRepositories(logger, githubApiService, catalogInfoGenerator);
return res.json(repos);
},
);
Expand Down Expand Up @@ -152,7 +152,7 @@ export async function createRouter(
req.header('authorization'),
);
await permissionCheck(permissions, backstageToken);
const imports = await createImportJobs(logger, c.request.requestBody);
const imports = await createImportJobs(logger, githubApiService, catalogInfoGenerator, c.request.requestBody);
return res.json(imports);
},
);
Expand Down

0 comments on commit 6f38749

Please sign in to comment.