refactor repo create, repo push

Signed-off-by: Marco Crivellaro <marco.crive@gmail.com>
This commit is contained in:
Marco Crivellaro
2022-06-22 11:02:42 +01:00
parent e150231eb9
commit 4271cc7ecc
5 changed files with 369 additions and 351 deletions
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { assertError, InputError } from '@backstage/errors';
import { InputError } from '@backstage/errors';
import {
GithubCredentialsProvider,
ScmIntegrationRegistry,
@@ -21,7 +21,10 @@ import {
import { Octokit } from 'octokit';
import { createTemplateAction } from '../../createTemplateAction';
import { parseRepoUrl } from '../publish/util';
import { getOctokitOptions } from './helpers';
import {
createGithubRepoWithCollaboratorsAndTopics,
getOctokitOptions,
} from './helpers';
import * as inputProps from './inputProperties';
import * as outputProps from './outputProperties';
@@ -58,6 +61,11 @@ export function createGithubRepoCreateAction(options: {
team: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
| {
/** @deprecated This field is deprecated in favor of team */
username: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
>;
token?: string;
topics?: string[];
@@ -107,136 +115,37 @@ export function createGithubRepoCreateAction(options: {
token: providedToken,
} = ctx.input;
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
}
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
repoUrl: repoUrl,
});
const client = new Octokit(octokitOptions);
const user = await client.rest.users.getByUsername({
username: owner,
});
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
const repoCreationPromise =
user.data.type === 'Organization'
? client.rest.repos.createInOrg({
name: repo,
org: owner,
private: repoVisibility === 'private',
visibility: repoVisibility,
description: description,
delete_branch_on_merge: deleteBranchOnMerge,
allow_merge_commit: allowMergeCommit,
allow_squash_merge: allowSquashMerge,
allow_rebase_merge: allowRebaseMerge,
})
: client.rest.repos.createForAuthenticatedUser({
name: repo,
private: repoVisibility === 'private',
description: description,
delete_branch_on_merge: deleteBranchOnMerge,
allow_merge_commit: allowMergeCommit,
allow_squash_merge: allowSquashMerge,
allow_rebase_merge: allowRebaseMerge,
});
let newRepo;
try {
newRepo = (await repoCreationPromise).data;
} catch (e) {
assertError(e);
if (e.message === 'Resource not accessible by integration') {
ctx.logger.warn(
`The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`,
);
}
throw new Error(
`Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`,
);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
}
if (access?.startsWith(`${owner}/`)) {
const [, team] = access.split('/');
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: team,
owner,
repo,
permission: 'admin',
});
// No need to add access if it's the person who owns the personal account
} else if (access && access !== owner) {
await client.rest.repos.addCollaborator({
owner,
repo,
username: access,
permission: 'admin',
});
}
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
client,
repo,
owner,
repoVisibility,
description,
deleteBranchOnMerge,
allowMergeCommit,
allowSquashMerge,
allowRebaseMerge,
access,
collaborators,
topics,
ctx.logger,
);
if (collaborators) {
for (const collaborator of collaborators) {
try {
if ('user' in collaborator) {
await client.rest.repos.addCollaborator({
owner,
repo,
username: collaborator.user,
permission: collaborator.access,
});
} else if ('team' in collaborator) {
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: collaborator.team,
owner,
repo,
permission: collaborator.access,
});
}
} catch (e) {
assertError(e);
const name = extractCollaboratorName(collaborator);
ctx.logger.warn(
`Skipping ${collaborator.access} access for ${name}, ${e.message}`,
);
}
}
}
if (topics) {
try {
await client.rest.repos.replaceAllTopics({
owner,
repo,
names: topics.map(t => t.toLowerCase()),
});
} catch (e) {
assertError(e);
ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`);
}
}
const remoteUrl = newRepo.clone_url;
ctx.output('remoteUrl', remoteUrl);
ctx.output('remoteUrl', newRepo.clone_url);
},
});
}
function extractCollaboratorName(
collaborator: { user: string } | { team: string } | { username: string },
) {
if ('username' in collaborator) return collaborator.username;
if ('user' in collaborator) return collaborator.user;
return collaborator.team;
}
@@ -14,19 +14,15 @@
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { assertError, InputError } from '@backstage/errors';
import { InputError } from '@backstage/errors';
import {
GithubCredentialsProvider,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { Octokit } from 'octokit';
import { createTemplateAction } from '../../createTemplateAction';
import {
enableBranchProtectionOnDefaultRepoBranch,
initRepoAndPush,
} from '../helpers';
import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util';
import { getOctokitOptions } from './helpers';
import { parseRepoUrl } from '../publish/util';
import { getOctokitOptions, initRepoPushAndProtect } from './helpers';
import * as inputProps from './inputProperties';
import * as outputProps from './outputProperties';
@@ -117,48 +113,24 @@ export function createGithubRepoPushAction(options: {
const remoteUrl = targetRepo.data.clone_url;
const repoContentsUrl = `${targetRepo.data.html_url}/blob/${defaultBranch}`;
const gitAuthorInfo = {
name: gitAuthorName
? gitAuthorName
: config.getOptionalString('scaffolder.defaultAuthor.name'),
email: gitAuthorEmail
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
await initRepoPushAndProtect(
remoteUrl,
octokitOptions.auth,
ctx.workspacePath,
ctx.input.sourcePath,
defaultBranch,
auth: {
username: 'x-access-token',
password: octokitOptions.auth,
},
logger: ctx.logger,
commitMessage: gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
});
if (protectDefaultBranch) {
try {
await enableBranchProtectionOnDefaultRepoBranch({
owner,
client,
repoName: repo,
logger: ctx.logger,
defaultBranch,
requireCodeOwnerReviews,
requiredStatusCheckContexts,
});
} catch (e) {
assertError(e);
ctx.logger.warn(
`Skipping: default branch protection on '${repo}', ${e.message}`,
);
}
}
protectDefaultBranch,
owner,
client,
repo,
requireCodeOwnerReviews,
requiredStatusCheckContexts,
config,
ctx.logger,
gitCommitMessage,
gitAuthorName,
gitAuthorEmail,
);
ctx.output('remoteUrl', remoteUrl);
ctx.output('repoContentsUrl', repoContentsUrl);
@@ -13,14 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InputError } from '@backstage/errors';
import { Config } from '@backstage/config';
import { assertError, InputError } from '@backstage/errors';
import {
DefaultGithubCredentialsProvider,
GithubCredentialsProvider,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { OctokitOptions } from '@octokit/core/dist-types/types';
import { parseRepoUrl } from '../publish/util';
import { Octokit } from 'octokit';
import { Logger } from 'winston';
import {
enableBranchProtectionOnDefaultRepoBranch,
initRepoAndPush,
} from '../helpers';
import { getRepoSourceDirectory, parseRepoUrl } from '../publish/util';
const DEFAULT_TIMEOUT_MS = 60_000;
@@ -83,3 +90,213 @@ export async function getOctokitOptions(options: {
previews: ['nebula-preview'],
};
}
export async function createGithubRepoWithCollaboratorsAndTopics(
client: Octokit,
repo: string,
owner: string,
repoVisibility: 'private' | 'internal' | 'public',
description: string | undefined,
deleteBranchOnMerge: boolean,
allowMergeCommit: boolean,
allowSquashMerge: boolean,
allowRebaseMerge: boolean,
access: string | undefined,
collaborators:
| (
| {
user: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
| {
team: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
| {
/** @deprecated This field is deprecated in favor of team */
username: string;
access: 'pull' | 'push' | 'admin' | 'maintain' | 'triage';
}
)[]
| undefined,
topics: string[] | undefined,
logger: Logger,
) {
const user = await client.rest.users.getByUsername({
username: owner,
});
const repoCreationPromise =
user.data.type === 'Organization'
? client.rest.repos.createInOrg({
name: repo,
org: owner,
private: repoVisibility === 'private',
visibility: repoVisibility,
description: description,
delete_branch_on_merge: deleteBranchOnMerge,
allow_merge_commit: allowMergeCommit,
allow_squash_merge: allowSquashMerge,
allow_rebase_merge: allowRebaseMerge,
})
: client.rest.repos.createForAuthenticatedUser({
name: repo,
private: repoVisibility === 'private',
description: description,
delete_branch_on_merge: deleteBranchOnMerge,
allow_merge_commit: allowMergeCommit,
allow_squash_merge: allowSquashMerge,
allow_rebase_merge: allowRebaseMerge,
});
let newRepo;
try {
newRepo = (await repoCreationPromise).data;
} catch (e) {
assertError(e);
if (e.message === 'Resource not accessible by integration') {
logger.warn(
`The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`,
);
}
throw new Error(
`Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`,
);
}
if (access?.startsWith(`${owner}/`)) {
const [, team] = access.split('/');
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: team,
owner,
repo,
permission: 'admin',
});
// No need to add access if it's the person who owns the personal account
} else if (access && access !== owner) {
await client.rest.repos.addCollaborator({
owner,
repo,
username: access,
permission: 'admin',
});
}
if (collaborators) {
for (const collaborator of collaborators) {
try {
if ('user' in collaborator) {
await client.rest.repos.addCollaborator({
owner,
repo,
username: collaborator.user,
permission: collaborator.access,
});
} else if ('team' in collaborator) {
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: collaborator.team,
owner,
repo,
permission: collaborator.access,
});
}
} catch (e) {
assertError(e);
const name = extractCollaboratorName(collaborator);
logger.warn(
`Skipping ${collaborator.access} access for ${name}, ${e.message}`,
);
}
}
}
if (topics) {
try {
await client.rest.repos.replaceAllTopics({
owner,
repo,
names: topics.map(t => t.toLowerCase()),
});
} catch (e) {
assertError(e);
logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`);
}
}
return newRepo;
}
export async function initRepoPushAndProtect(
remoteUrl: string,
password: string,
workspacePath: string,
sourcePath: string | undefined,
defaultBranch: string,
protectDefaultBranch: boolean,
owner: string,
client: Octokit,
repo: string,
requireCodeOwnerReviews: boolean,
requiredStatusCheckContexts: string[],
config: Config,
logger: any,
gitCommitMessage?: string,
gitAuthorName?: string,
gitAuthorEmail?: string,
) {
const gitAuthorInfo = {
name: gitAuthorName
? gitAuthorName
: config.getOptionalString('scaffolder.defaultAuthor.name'),
email: gitAuthorEmail
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
const commitMessage = gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage');
await initRepoAndPush({
dir: getRepoSourceDirectory(workspacePath, sourcePath),
remoteUrl,
defaultBranch,
auth: {
username: 'x-access-token',
password,
},
logger,
commitMessage,
gitAuthorInfo,
});
if (protectDefaultBranch) {
try {
await enableBranchProtectionOnDefaultRepoBranch({
owner,
client,
repoName: repo,
logger,
defaultBranch,
requireCodeOwnerReviews,
requiredStatusCheckContexts,
});
} catch (e) {
assertError(e);
logger.warn(
`Skipping: default branch protection on '${repo}', ${e.message}`,
);
}
}
}
function extractCollaboratorName(
collaborator: { user: string } | { team: string } | { username: string },
) {
if ('username' in collaborator) return collaborator.username;
if ('user' in collaborator) return collaborator.user;
return collaborator.team;
}
@@ -14,12 +14,13 @@
* limitations under the License.
*/
import { SpawnOptionsWithoutStdio, spawn } from 'child_process';
import { Git } from '@backstage/backend-common';
import { Config } from '@backstage/config';
import { assertError } from '@backstage/errors';
import { spawn, SpawnOptionsWithoutStdio } from 'child_process';
import { Octokit } from 'octokit';
import { PassThrough, Writable } from 'stream';
import { Logger } from 'winston';
import { Git } from '@backstage/backend-common';
import { Octokit } from 'octokit';
import { assertError } from '@backstage/errors';
/** @public */
export type RunCommandOptions = {
@@ -200,3 +201,12 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({
await tryOnce();
}
};
export function getGitCommitMessage(
gitCommitMessage: string | undefined,
config: Config,
): string | undefined {
return gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage');
}
@@ -14,22 +14,21 @@
* limitations under the License.
*/
import { Config } from '@backstage/config';
import { assertError, InputError } from '@backstage/errors';
import { InputError } from '@backstage/errors';
import {
GithubCredentialsProvider,
ScmIntegrationRegistry,
} from '@backstage/integration';
import { Octokit } from 'octokit';
import { createTemplateAction } from '../../createTemplateAction';
import { getOctokitOptions } from '../github/helpers';
import {
createGithubRepoWithCollaboratorsAndTopics,
getOctokitOptions,
initRepoPushAndProtect,
} from '../github/helpers';
import * as inputProps from '../github/inputProperties';
import * as outputProps from '../github/outputProperties';
import {
enableBranchProtectionOnDefaultRepoBranch,
initRepoAndPush,
} from '../helpers';
import { getRepoSourceDirectory, parseRepoUrl } from './util';
import { parseRepoUrl } from './util';
/**
* Creates a new action that initializes a git repository of the content in the workspace
* and publishes it to GitHub.
@@ -137,192 +136,103 @@ export function createPublishGithubAction(options: {
token: providedToken,
} = ctx.input;
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
repoUrl: repoUrl,
});
const client = new Octokit(octokitOptions);
const { owner, repo } = parseRepoUrl(repoUrl, integrations);
if (!owner) {
throw new InputError('Invalid repository owner provided in repoUrl');
}
const octokitOptions = await getOctokitOptions({
integrations,
credentialsProvider: githubCredentialsProvider,
token: providedToken,
repoUrl,
});
const client = new Octokit(octokitOptions);
const user = await client.rest.users.getByUsername({
username: owner,
});
const repoCreationPromise =
user.data.type === 'Organization'
? client.rest.repos.createInOrg({
name: repo,
org: owner,
private: repoVisibility === 'private',
visibility: repoVisibility,
description: description,
delete_branch_on_merge: deleteBranchOnMerge,
allow_merge_commit: allowMergeCommit,
allow_squash_merge: allowSquashMerge,
allow_rebase_merge: allowRebaseMerge,
})
: client.rest.repos.createForAuthenticatedUser({
name: repo,
private: repoVisibility === 'private',
description: description,
delete_branch_on_merge: deleteBranchOnMerge,
allow_merge_commit: allowMergeCommit,
allow_squash_merge: allowSquashMerge,
allow_rebase_merge: allowRebaseMerge,
});
let newRepo;
try {
newRepo = (await repoCreationPromise).data;
} catch (e) {
assertError(e);
if (e.message === 'Resource not accessible by integration') {
ctx.logger.warn(
`The GitHub app or token provided may not have the required permissions to create the ${user.data.type} repository ${owner}/${repo}.`,
);
}
throw new Error(
`Failed to create the ${user.data.type} repository ${owner}/${repo}, ${e.message}`,
);
}
if (access?.startsWith(`${owner}/`)) {
const [, team] = access.split('/');
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: team,
owner,
repo,
permission: 'admin',
});
// No need to add access if it's the person who owns the personal account
} else if (access && access !== owner) {
await client.rest.repos.addCollaborator({
owner,
repo,
username: access,
permission: 'admin',
});
}
if (collaborators) {
for (const collaborator of collaborators) {
try {
if ('user' in collaborator) {
await client.rest.repos.addCollaborator({
owner,
repo,
username: collaborator.user,
permission: collaborator.access,
});
} else if ('username' in collaborator) {
ctx.logger.warn(
'The field `username` is deprecated in favor of `team` and will be removed in the future.',
);
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: collaborator.username,
owner,
repo,
permission: collaborator.access,
});
} else if ('team' in collaborator) {
await client.rest.teams.addOrUpdateRepoPermissionsInOrg({
org: owner,
team_slug: collaborator.team,
owner,
repo,
permission: collaborator.access,
});
}
} catch (e) {
assertError(e);
const name = extractCollaboratorName(collaborator);
ctx.logger.warn(
`Skipping ${collaborator.access} access for ${name}, ${e.message}`,
);
}
}
}
if (topics) {
try {
await client.rest.repos.replaceAllTopics({
owner,
repo,
names: topics.map(t => t.toLowerCase()),
});
} catch (e) {
assertError(e);
ctx.logger.warn(`Skipping topics ${topics.join(' ')}, ${e.message}`);
}
}
const newRepo = await createGithubRepoWithCollaboratorsAndTopics(
client,
repo,
owner,
repoVisibility,
description,
deleteBranchOnMerge,
allowMergeCommit,
allowSquashMerge,
allowRebaseMerge,
access,
collaborators,
topics,
ctx.logger,
);
const remoteUrl = newRepo.clone_url;
const repoContentsUrl = `${newRepo.html_url}/blob/${defaultBranch}`;
const gitAuthorInfo = {
name: gitAuthorName
? gitAuthorName
: config.getOptionalString('scaffolder.defaultAuthor.name'),
email: gitAuthorEmail
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
// const gitAuthorInfo = {
// name: gitAuthorName
// ? gitAuthorName
// : config.getOptionalString('scaffolder.defaultAuthor.name'),
// email: gitAuthorEmail
// ? gitAuthorEmail
// : config.getOptionalString('scaffolder.defaultAuthor.email'),
// };
await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
// await initRepoAndPush({
// dir: getRepoSourceDirectory(ctx.workspacePath, ctx.input.sourcePath),
// remoteUrl,
// defaultBranch,
// auth: {
// username: 'x-access-token',
// password: octokitOptions.auth,
// },
// logger: ctx.logger,
// commitMessage: gitCommitMessage
// ? gitCommitMessage
// : config.getOptionalString('scaffolder.defaultCommitMessage'),
// gitAuthorInfo,
// });
// if (protectDefaultBranch) {
// try {
// await enableBranchProtectionOnDefaultRepoBranch({
// owner,
// client,
// repoName: newRepo.name,
// logger: ctx.logger,
// defaultBranch,
// requireCodeOwnerReviews,
// requiredStatusCheckContexts,
// });
// } catch (e) {
// assertError(e);
// ctx.logger.warn(
// `Skipping: default branch protection on '${newRepo.name}', ${e.message}`,
// );
// }
// }
await initRepoPushAndProtect(
remoteUrl,
octokitOptions.auth,
ctx.workspacePath,
ctx.input.sourcePath,
defaultBranch,
auth: {
username: 'x-access-token',
password: octokitOptions.auth,
},
logger: ctx.logger,
commitMessage: gitCommitMessage
? gitCommitMessage
: config.getOptionalString('scaffolder.defaultCommitMessage'),
gitAuthorInfo,
});
if (protectDefaultBranch) {
try {
await enableBranchProtectionOnDefaultRepoBranch({
owner,
client,
repoName: newRepo.name,
logger: ctx.logger,
defaultBranch,
requireCodeOwnerReviews,
requiredStatusCheckContexts,
});
} catch (e) {
assertError(e);
ctx.logger.warn(
`Skipping: default branch protection on '${newRepo.name}', ${e.message}`,
);
}
}
protectDefaultBranch,
owner,
client,
repo,
requireCodeOwnerReviews,
requiredStatusCheckContexts,
config,
ctx.logger,
gitCommitMessage,
gitAuthorName,
gitAuthorEmail,
);
ctx.output('remoteUrl', remoteUrl);
ctx.output('repoContentsUrl', repoContentsUrl);
},
});
}
function extractCollaboratorName(
collaborator: { user: string } | { team: string } | { username: string },
) {
if ('username' in collaborator) return collaborator.username;
if ('user' in collaborator) return collaborator.user;
return collaborator.team;
}