Fix prepare, e2e tested with GitHub
This commit is contained in:
@@ -31,16 +31,14 @@ import {
|
||||
export class AzurePreparer implements PreparerBase {
|
||||
private readonly integrations: AzureIntegrationConfig[];
|
||||
private readonly scaffolderToken: string | undefined;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(config: Config, { logger }: { logger: Logger }) {
|
||||
this.logger = logger;
|
||||
this.integrations = readAzureIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.azure') ?? [],
|
||||
);
|
||||
|
||||
if (!this.integrations.length) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
'Integrations for Azure in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
@@ -50,7 +48,7 @@ export class AzurePreparer implements PreparerBase {
|
||||
);
|
||||
|
||||
if (this.scaffolderToken) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the token format under 'scaffolder.azure.api.token' will not be respected in future releases. Please consider using integrations config instead",
|
||||
);
|
||||
}
|
||||
@@ -58,10 +56,11 @@ export class AzurePreparer implements PreparerBase {
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
opts?: PreparerOptions,
|
||||
opts: PreparerOptions,
|
||||
): Promise<string> {
|
||||
const { protocol, location } = parseLocationAnnotation(template);
|
||||
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
|
||||
const workingDirectory = opts.workingDirectory ?? os.tmpdir();
|
||||
const logger = opts.logger;
|
||||
|
||||
if (!['azure/api', 'url'].includes(protocol)) {
|
||||
throw new InputError(
|
||||
@@ -89,9 +88,9 @@ export class AzurePreparer implements PreparerBase {
|
||||
? Git.fromAuth({
|
||||
password: token,
|
||||
username: 'notempty',
|
||||
logger: this.logger,
|
||||
logger,
|
||||
})
|
||||
: Git.fromAuth({ logger: this.logger });
|
||||
: Git.fromAuth({ logger });
|
||||
|
||||
await git.clone({
|
||||
url: repositoryCheckoutUrl,
|
||||
|
||||
@@ -31,16 +31,14 @@ import { Logger } from 'winston';
|
||||
export class BitbucketPreparer implements PreparerBase {
|
||||
private readonly privateToken: string;
|
||||
private readonly username: string;
|
||||
private readonly logger: Logger;
|
||||
private readonly integrations: BitbucketIntegrationConfig[];
|
||||
constructor(config: Config, { logger }: { logger: Logger }) {
|
||||
this.logger = logger;
|
||||
this.integrations = readBitbucketIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.bitbucket') ?? [],
|
||||
);
|
||||
|
||||
if (!this.integrations.length) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
'Integrations for BitBucket in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
@@ -51,7 +49,7 @@ export class BitbucketPreparer implements PreparerBase {
|
||||
config.getOptionalString('scaffolder.bitbucket.api.token') ?? '';
|
||||
|
||||
if (this.username || this.privateToken) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the token format under 'scaffolder.bitbucket.token' will not be respected in future releases. Please consider using integrations config instead",
|
||||
);
|
||||
}
|
||||
@@ -59,10 +57,11 @@ export class BitbucketPreparer implements PreparerBase {
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
opts?: PreparerOptions,
|
||||
opts: PreparerOptions,
|
||||
): Promise<string> {
|
||||
const { protocol, location } = parseLocationAnnotation(template);
|
||||
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
|
||||
const workingDirectory = opts.workingDirectory ?? os.tmpdir();
|
||||
const logger = opts.logger;
|
||||
|
||||
if (!['bitbucket', 'url'].includes(protocol)) {
|
||||
throw new InputError(
|
||||
@@ -90,9 +89,9 @@ export class BitbucketPreparer implements PreparerBase {
|
||||
const git = auth
|
||||
? Git.fromAuth({
|
||||
...auth,
|
||||
logger: this.logger,
|
||||
logger,
|
||||
})
|
||||
: Git.fromAuth({ logger: this.logger });
|
||||
: Git.fromAuth({ logger });
|
||||
|
||||
await git.clone({
|
||||
url: repositoryCheckoutUrl,
|
||||
|
||||
@@ -31,16 +31,14 @@ import {
|
||||
export class GithubPreparer implements PreparerBase {
|
||||
private readonly integrations: GitHubIntegrationConfig[];
|
||||
private readonly scaffolderToken: string | undefined;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(config: Config, { logger }: { logger: Logger }) {
|
||||
this.logger = logger;
|
||||
this.integrations = readGitHubIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.github') ?? [],
|
||||
);
|
||||
|
||||
if (!this.integrations.length) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
'Integrations for Github in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
@@ -48,7 +46,7 @@ export class GithubPreparer implements PreparerBase {
|
||||
this.scaffolderToken = config.getOptionalString('scaffolder.github.token');
|
||||
|
||||
if (this.scaffolderToken) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the token format under 'scaffolder.github.token' will not be respected in future releases. Please consider using integrations config instead",
|
||||
);
|
||||
}
|
||||
@@ -56,10 +54,11 @@ export class GithubPreparer implements PreparerBase {
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
opts?: PreparerOptions,
|
||||
opts: PreparerOptions,
|
||||
): Promise<string> {
|
||||
const { protocol, location } = parseLocationAnnotation(template);
|
||||
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
|
||||
const workingDirectory = opts.workingDirectory ?? os.tmpdir();
|
||||
const logger = opts.logger;
|
||||
|
||||
if (!['github', 'url'].includes(protocol)) {
|
||||
throw new InputError(
|
||||
@@ -87,9 +86,9 @@ export class GithubPreparer implements PreparerBase {
|
||||
? Git.fromAuth({
|
||||
username: token,
|
||||
password: 'x-oauth-basic',
|
||||
logger: this.logger,
|
||||
logger,
|
||||
})
|
||||
: Git.fromAuth({ logger: this.logger });
|
||||
: Git.fromAuth({ logger });
|
||||
|
||||
await git.clone({
|
||||
url: repositoryCheckoutUrl,
|
||||
|
||||
@@ -31,16 +31,14 @@ import { Logger } from 'winston';
|
||||
export class GitlabPreparer implements PreparerBase {
|
||||
private readonly integrations: GitLabIntegrationConfig[];
|
||||
private readonly scaffolderToken: string | undefined;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(config: Config, { logger }: { logger: Logger }) {
|
||||
this.logger = logger;
|
||||
this.integrations = readGitLabIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.gitlab') ?? [],
|
||||
);
|
||||
|
||||
if (!this.integrations.length) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
'Integrations for GitLab in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
@@ -50,7 +48,7 @@ export class GitlabPreparer implements PreparerBase {
|
||||
);
|
||||
|
||||
if (this.scaffolderToken) {
|
||||
this.logger.warn(
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the token format under 'scaffolder.gitlab.api.token' will not be respected in future releases. Please consider using integrations config instead",
|
||||
);
|
||||
}
|
||||
@@ -58,10 +56,11 @@ export class GitlabPreparer implements PreparerBase {
|
||||
|
||||
async prepare(
|
||||
template: TemplateEntityV1alpha1,
|
||||
opts?: PreparerOptions,
|
||||
opts: PreparerOptions,
|
||||
): Promise<string> {
|
||||
const { protocol, location } = parseLocationAnnotation(template);
|
||||
const workingDirectory = opts?.workingDirectory ?? os.tmpdir();
|
||||
const logger = opts.logger;
|
||||
const workingDirectory = opts.workingDirectory ?? os.tmpdir();
|
||||
|
||||
if (!['gitlab', 'gitlab/api', 'url'].includes(protocol)) {
|
||||
throw new InputError(
|
||||
@@ -86,9 +85,9 @@ export class GitlabPreparer implements PreparerBase {
|
||||
? Git.fromAuth({
|
||||
password: token,
|
||||
username: 'oauth2',
|
||||
logger: this.logger,
|
||||
logger,
|
||||
})
|
||||
: Git.fromAuth({ logger: this.logger });
|
||||
: Git.fromAuth({ logger });
|
||||
|
||||
await git.clone({
|
||||
url: repositoryCheckoutUrl,
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
*/
|
||||
import type { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { RemoteProtocol } from '../types';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export type PreparerOptions = {
|
||||
workingDirectory?: string;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
export interface PreparerBase {
|
||||
|
||||
@@ -15,32 +15,55 @@
|
||||
*/
|
||||
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { initRepoAndPush } from './helpers';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { RequiredTemplateValues } from '../templater';
|
||||
import { Config } from '@backstage/config';
|
||||
import { Logger } from 'winston';
|
||||
import {
|
||||
GitHubIntegrationConfig,
|
||||
readGitHubIntegrationConfigs,
|
||||
} from '@backstage/integration';
|
||||
import gitUrlParse from 'git-url-parse';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
export type RepoVisibilityOptions = 'private' | 'internal' | 'public';
|
||||
|
||||
interface GithubPublisherParams {
|
||||
client: Octokit;
|
||||
token: string;
|
||||
repoVisibility: RepoVisibilityOptions;
|
||||
}
|
||||
|
||||
export class GithubPublisher implements PublisherBase {
|
||||
private client: Octokit;
|
||||
private token: string;
|
||||
private repoVisibility: RepoVisibilityOptions;
|
||||
private scaffolderToken: string | undefined;
|
||||
private readonly integrations: GitHubIntegrationConfig[];
|
||||
private readonly apiBaseUrl: string | undefined;
|
||||
private readonly repoVisibility: RepoVisibilityOptions;
|
||||
|
||||
constructor({
|
||||
client,
|
||||
token,
|
||||
repoVisibility = 'public',
|
||||
}: GithubPublisherParams) {
|
||||
this.client = client;
|
||||
this.token = token;
|
||||
this.repoVisibility = repoVisibility;
|
||||
constructor(config: Config, { logger }: { logger: Logger }) {
|
||||
this.integrations = readGitHubIntegrationConfigs(
|
||||
config.getOptionalConfigArray('integrations.github') ?? [],
|
||||
);
|
||||
|
||||
if (!this.integrations.length) {
|
||||
logger.warn(
|
||||
'Integrations for GitHub in Scaffolder are not set. This will cause errors in a future release. Please migrate to using integrations config and specifying tokens under hostnames',
|
||||
);
|
||||
}
|
||||
|
||||
this.scaffolderToken = config.getOptionalString(
|
||||
'scaffolder.github.api.token',
|
||||
);
|
||||
|
||||
this.apiBaseUrl = config.getOptionalString('scaffolder.github.api.baseUrl');
|
||||
|
||||
if (this.scaffolderToken) {
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the token format under 'scaffolder.github.api.token' will not be respected in future releases. Please consider using integrations config instead",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.apiBaseUrl) {
|
||||
logger.warn(
|
||||
"DEPRECATION: Using the apiBaseUrl format under 'scaffolder.github.api.baseUrl' will not be respected in future releases. Please consider using integrations config instead",
|
||||
);
|
||||
}
|
||||
|
||||
this.repoVisibility = (config.getOptionalString(
|
||||
'scaffolder.github.visibility',
|
||||
) ?? 'public') as RepoVisibilityOptions;
|
||||
}
|
||||
|
||||
async publish({
|
||||
@@ -48,13 +71,29 @@ export class GithubPublisher implements PublisherBase {
|
||||
directory,
|
||||
logger,
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const remoteUrl = await this.createRemote(values);
|
||||
const { resource: host, owner, name } = gitUrlParse(values.storePath);
|
||||
const token = this.getToken(host);
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No token provided to create the remote repository');
|
||||
}
|
||||
|
||||
const description = values.description as string;
|
||||
const access = values.access as string;
|
||||
const remoteUrl = await this.createRemote({
|
||||
description,
|
||||
access,
|
||||
host,
|
||||
name,
|
||||
owner,
|
||||
token,
|
||||
});
|
||||
|
||||
await initRepoAndPush({
|
||||
dir: directory,
|
||||
remoteUrl,
|
||||
auth: {
|
||||
username: this.token,
|
||||
username: token ?? '',
|
||||
password: 'x-oauth-basic',
|
||||
},
|
||||
logger,
|
||||
@@ -68,24 +107,34 @@ export class GithubPublisher implements PublisherBase {
|
||||
return { remoteUrl, catalogInfoUrl };
|
||||
}
|
||||
|
||||
private async createRemote(
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>,
|
||||
) {
|
||||
const [owner, name] = values.storePath.split('/');
|
||||
const description = values.description as string;
|
||||
private async createRemote(opts: {
|
||||
access: string;
|
||||
name: string;
|
||||
owner: string;
|
||||
host: string;
|
||||
token: string;
|
||||
description: string;
|
||||
}) {
|
||||
const { access, description, host, owner, name, token } = opts;
|
||||
|
||||
const user = await this.client.users.getByUsername({ username: owner });
|
||||
// create a github client with the config
|
||||
const githubClient = new Octokit({
|
||||
auth: token,
|
||||
baseUrl: this.getBaseUrl(host),
|
||||
});
|
||||
|
||||
const user = await githubClient.users.getByUsername({ username: owner });
|
||||
|
||||
const repoCreationPromise =
|
||||
user.data.type === 'Organization'
|
||||
? this.client.repos.createInOrg({
|
||||
? githubClient.repos.createInOrg({
|
||||
name,
|
||||
org: owner,
|
||||
private: this.repoVisibility !== 'public',
|
||||
visibility: this.repoVisibility,
|
||||
description,
|
||||
})
|
||||
: this.client.repos.createForAuthenticatedUser({
|
||||
: githubClient.repos.createForAuthenticatedUser({
|
||||
name,
|
||||
private: this.repoVisibility === 'private',
|
||||
description,
|
||||
@@ -93,10 +142,9 @@ export class GithubPublisher implements PublisherBase {
|
||||
|
||||
const { data } = await repoCreationPromise;
|
||||
|
||||
const access = values.access as string;
|
||||
if (access?.startsWith(`${owner}/`)) {
|
||||
const [, team] = access.split('/');
|
||||
await this.client.teams.addOrUpdateRepoPermissionsInOrg({
|
||||
await githubClient.teams.addOrUpdateRepoPermissionsInOrg({
|
||||
org: owner,
|
||||
team_slug: team,
|
||||
owner,
|
||||
@@ -105,7 +153,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
});
|
||||
// no need to add access if it's the person who own's the personal account
|
||||
} else if (access && access !== owner) {
|
||||
await this.client.repos.addCollaborator({
|
||||
await githubClient.repos.addCollaborator({
|
||||
owner,
|
||||
repo: name,
|
||||
username: access,
|
||||
@@ -115,4 +163,18 @@ export class GithubPublisher implements PublisherBase {
|
||||
|
||||
return data?.clone_url;
|
||||
}
|
||||
|
||||
private getToken(host: string): string | undefined {
|
||||
return (
|
||||
this.scaffolderToken ||
|
||||
this.integrations.find(c => c.host === host)?.token
|
||||
);
|
||||
}
|
||||
|
||||
private getBaseUrl(host: string): string | undefined {
|
||||
return (
|
||||
this.apiBaseUrl ||
|
||||
this.integrations.find(c => c.host === host)?.apiBaseUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
*/
|
||||
|
||||
import { Logger } from 'winston';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { Gitlab } from '@gitbeaker/node';
|
||||
import { getPersonalAccessTokenHandler, WebApi } from 'azure-devops-node-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
@@ -27,7 +25,7 @@ import {
|
||||
} from '../helpers';
|
||||
import { PublisherBase, PublisherBuilder } from './types';
|
||||
import { RemoteProtocol } from '../types';
|
||||
import { GithubPublisher, RepoVisibilityOptions } from './github';
|
||||
import { GithubPublisher } from './github';
|
||||
import { GitlabPublisher } from './gitlab';
|
||||
import { AzurePublisher } from './azure';
|
||||
import { BitbucketPublisher } from './bitbucket';
|
||||
@@ -80,23 +78,7 @@ export class Publishers implements PublisherBuilder {
|
||||
const githubConfig = config.getOptionalConfig('scaffolder.github');
|
||||
if (githubConfig) {
|
||||
try {
|
||||
const repoVisibility = githubConfig.getString(
|
||||
'visibility',
|
||||
) as RepoVisibilityOptions;
|
||||
|
||||
const githubToken = githubConfig.getString('token');
|
||||
const githubHost =
|
||||
githubConfig.getOptionalString('host') ?? 'https://api.github.com';
|
||||
const githubClient = new Octokit({
|
||||
auth: githubToken,
|
||||
baseUrl: githubHost,
|
||||
});
|
||||
const githubPublisher = new GithubPublisher({
|
||||
client: githubClient,
|
||||
token: githubToken,
|
||||
repoVisibility,
|
||||
});
|
||||
|
||||
const githubPublisher = new GithubPublisher(config, { logger });
|
||||
publishers.register('file', githubPublisher);
|
||||
publishers.register('github', githubPublisher);
|
||||
} catch (e) {
|
||||
@@ -116,12 +98,7 @@ export class Publishers implements PublisherBuilder {
|
||||
const gitLabConfig = config.getOptionalConfig('scaffolder.gitlab');
|
||||
if (gitLabConfig) {
|
||||
try {
|
||||
const gitLabToken = gitLabConfig.getConfig('api').getString('token');
|
||||
const gitLabClient = new Gitlab({
|
||||
host: gitLabConfig.getConfig('api').getOptionalString('baseUrl'),
|
||||
token: gitLabToken,
|
||||
});
|
||||
const gitLabPublisher = new GitlabPublisher(gitLabClient, gitLabToken);
|
||||
const gitLabPublisher = new GitlabPublisher(config, { logger });
|
||||
publishers.register('gitlab', gitLabPublisher);
|
||||
publishers.register('gitlab/api', gitLabPublisher);
|
||||
} catch (e) {
|
||||
|
||||
@@ -36,6 +36,7 @@ export type PublisherBase = {
|
||||
export type PublisherOptions = {
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
directory: string;
|
||||
logger: Logger;
|
||||
};
|
||||
|
||||
export type PublisherResult = {
|
||||
|
||||
@@ -190,7 +190,7 @@ export const TemplatePage = () => {
|
||||
|
||||
if (!parsedUrl.resource) {
|
||||
errors.storePath.addError(
|
||||
'There needs to be a hostname in the storePath',
|
||||
'There needs to be a hostname in the store path',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user