Merge pull request #3351 from mfrinnstrom/scaffolder-support-add-azure-components
[Scaffolder] support adding scaffolded components in Azure DevOps to catalog
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Move constructing the catalog-info.yaml URL for scaffolded components to the publishers
|
||||
@@ -41,7 +41,7 @@ describe('Azure Publisher', () => {
|
||||
describe('publish: createRemoteInAzure', () => {
|
||||
it('should use azure-devops-node-api to create a repo in the given project', async () => {
|
||||
mockGitApi.createRepository.mockResolvedValue({
|
||||
remoteUrl: 'mockclone',
|
||||
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
|
||||
} as { remoteUrl: string });
|
||||
|
||||
const result = await publisher.publish({
|
||||
@@ -52,7 +52,11 @@ describe('Azure Publisher', () => {
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ remoteUrl: 'mockclone' });
|
||||
expect(result).toEqual({
|
||||
remoteUrl: 'https://dev.azure.com/organization/project/_git/repo',
|
||||
catalogInfoUrl:
|
||||
'https://dev.azure.com/organization/project/_git/repo?path=%2Fcatalog-info.yaml',
|
||||
});
|
||||
expect(mockGitApi.createRepository).toHaveBeenCalledWith(
|
||||
{
|
||||
name: 'repo',
|
||||
@@ -61,7 +65,7 @@ describe('Azure Publisher', () => {
|
||||
);
|
||||
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
'mockclone',
|
||||
'https://dev.azure.com/organization/project/_git/repo',
|
||||
'notempty',
|
||||
'fake-token',
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PublisherBase } from './types';
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { GitApi } from 'azure-devops-node-api/GitApi';
|
||||
import { GitRepositoryCreateOptions } from 'azure-devops-node-api/interfaces/GitInterfaces';
|
||||
import { pushToRemoteUserPass } from './helpers';
|
||||
@@ -33,14 +33,12 @@ export class AzurePublisher implements PublisherBase {
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
}: {
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
directory: string;
|
||||
}): Promise<{ remoteUrl: string }> {
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const remoteUrl = await this.createRemote(values);
|
||||
await pushToRemoteUserPass(directory, remoteUrl, 'notempty', this.token);
|
||||
const catalogInfoUrl = `${remoteUrl}?path=%2Fcatalog-info.yaml`;
|
||||
|
||||
return { remoteUrl };
|
||||
return { remoteUrl, catalogInfoUrl };
|
||||
}
|
||||
|
||||
private async createRemote(
|
||||
|
||||
@@ -53,7 +53,7 @@ describe('GitHub Publisher', () => {
|
||||
it('should use octokit to create a repo in an organisation if the organisation property is set', async () => {
|
||||
mockGithubClient.repos.createInOrg.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'mockclone',
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
mockGithubClient.users.getByUsername.mockResolvedValue({
|
||||
@@ -71,7 +71,11 @@ describe('GitHub Publisher', () => {
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ remoteUrl: 'mockclone' });
|
||||
expect(result).toEqual({
|
||||
remoteUrl: 'https://github.com/backstage/backstage.git',
|
||||
catalogInfoUrl:
|
||||
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
});
|
||||
expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({
|
||||
org: 'blam',
|
||||
name: 'test',
|
||||
@@ -89,7 +93,7 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
'mockclone',
|
||||
'https://github.com/backstage/backstage.git',
|
||||
'abc',
|
||||
'x-oauth-basic',
|
||||
);
|
||||
@@ -98,7 +102,7 @@ describe('GitHub Publisher', () => {
|
||||
it('should use octokit to create a repo in the authed user if the organisation property is not set', async () => {
|
||||
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'mockclone',
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
mockGithubClient.users.getByUsername.mockResolvedValue({
|
||||
@@ -116,7 +120,11 @@ describe('GitHub Publisher', () => {
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ remoteUrl: 'mockclone' });
|
||||
expect(result).toEqual({
|
||||
remoteUrl: 'https://github.com/backstage/backstage.git',
|
||||
catalogInfoUrl:
|
||||
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
});
|
||||
expect(
|
||||
mockGithubClient.repos.createForAuthenticatedUser,
|
||||
).toHaveBeenCalledWith({
|
||||
@@ -126,7 +134,7 @@ describe('GitHub Publisher', () => {
|
||||
expect(mockGithubClient.repos.addCollaborator).not.toHaveBeenCalled();
|
||||
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
'mockclone',
|
||||
'https://github.com/backstage/backstage.git',
|
||||
'abc',
|
||||
'x-oauth-basic',
|
||||
);
|
||||
@@ -136,7 +144,7 @@ describe('GitHub Publisher', () => {
|
||||
it('should invite other user in the authed user', async () => {
|
||||
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'mockclone',
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
mockGithubClient.users.getByUsername.mockResolvedValue({
|
||||
@@ -155,7 +163,11 @@ describe('GitHub Publisher', () => {
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ remoteUrl: 'mockclone' });
|
||||
expect(result).toEqual({
|
||||
remoteUrl: 'https://github.com/backstage/backstage.git',
|
||||
catalogInfoUrl:
|
||||
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
});
|
||||
expect(
|
||||
mockGithubClient.repos.createForAuthenticatedUser,
|
||||
).toHaveBeenCalledWith({
|
||||
@@ -171,7 +183,7 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
'mockclone',
|
||||
'https://github.com/backstage/backstage.git',
|
||||
'abc',
|
||||
'x-oauth-basic',
|
||||
);
|
||||
@@ -188,7 +200,7 @@ describe('GitHub Publisher', () => {
|
||||
it('creates a private repository in the organization with visibility set to internal', async () => {
|
||||
mockGithubClient.repos.createInOrg.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'mockclone',
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
mockGithubClient.users.getByUsername.mockResolvedValue({
|
||||
@@ -206,7 +218,11 @@ describe('GitHub Publisher', () => {
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ remoteUrl: 'mockclone' });
|
||||
expect(result).toEqual({
|
||||
remoteUrl: 'https://github.com/backstage/backstage.git',
|
||||
catalogInfoUrl:
|
||||
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
});
|
||||
expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({
|
||||
org: 'blam',
|
||||
name: 'test',
|
||||
@@ -215,7 +231,7 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
'mockclone',
|
||||
'https://github.com/backstage/backstage.git',
|
||||
'abc',
|
||||
'x-oauth-basic',
|
||||
);
|
||||
@@ -232,7 +248,7 @@ describe('GitHub Publisher', () => {
|
||||
it('creates a private repository', async () => {
|
||||
mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'mockclone',
|
||||
clone_url: 'https://github.com/backstage/backstage.git',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
mockGithubClient.users.getByUsername.mockResolvedValue({
|
||||
@@ -249,7 +265,11 @@ describe('GitHub Publisher', () => {
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(result).toEqual({ remoteUrl: 'mockclone' });
|
||||
expect(result).toEqual({
|
||||
remoteUrl: 'https://github.com/backstage/backstage.git',
|
||||
catalogInfoUrl:
|
||||
'https://github.com/backstage/backstage/blob/master/catalog-info.yaml',
|
||||
});
|
||||
expect(
|
||||
mockGithubClient.repos.createForAuthenticatedUser,
|
||||
).toHaveBeenCalledWith({
|
||||
@@ -258,7 +278,7 @@ describe('GitHub Publisher', () => {
|
||||
});
|
||||
expect(pushToRemoteUserPass).toHaveBeenCalledWith(
|
||||
'/tmp/test',
|
||||
'mockclone',
|
||||
'https://github.com/backstage/backstage.git',
|
||||
'abc',
|
||||
'x-oauth-basic',
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PublisherBase } from './types';
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { pushToRemoteUserPass } from './helpers';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
@@ -46,10 +46,7 @@ export class GithubPublisher implements PublisherBase {
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
}: {
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
directory: string;
|
||||
}): Promise<{ remoteUrl: string }> {
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const remoteUrl = await this.createRemote(values);
|
||||
await pushToRemoteUserPass(
|
||||
directory,
|
||||
@@ -57,8 +54,12 @@ export class GithubPublisher implements PublisherBase {
|
||||
this.token,
|
||||
'x-oauth-basic',
|
||||
);
|
||||
const catalogInfoUrl = remoteUrl.replace(
|
||||
/\.git$/,
|
||||
'/blob/master/catalog-info.yaml',
|
||||
);
|
||||
|
||||
return { remoteUrl };
|
||||
return { remoteUrl, catalogInfoUrl };
|
||||
}
|
||||
|
||||
private async createRemote(
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PublisherBase } from './types';
|
||||
import { PublisherBase, PublisherOptions, PublisherResult } from './types';
|
||||
import { Gitlab } from '@gitbeaker/core';
|
||||
import { pushToRemoteUserPass } from './helpers';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
@@ -32,10 +32,7 @@ export class GitlabPublisher implements PublisherBase {
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
}: {
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
directory: string;
|
||||
}): Promise<{ remoteUrl: string }> {
|
||||
}: PublisherOptions): Promise<PublisherResult> {
|
||||
const remoteUrl = await this.createRemote(values);
|
||||
await pushToRemoteUserPass(directory, remoteUrl, 'oauth2', this.token);
|
||||
|
||||
|
||||
@@ -29,11 +29,17 @@ export type PublisherBase = {
|
||||
* catalog, plus the values from the form and the directory that has
|
||||
* been templated
|
||||
*/
|
||||
publish(opts: {
|
||||
entity: TemplateEntityV1alpha1;
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
directory: string;
|
||||
}): Promise<{ remoteUrl: string }>;
|
||||
publish(opts: PublisherOptions): Promise<PublisherResult>;
|
||||
};
|
||||
|
||||
export type PublisherOptions = {
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
directory: string;
|
||||
};
|
||||
|
||||
export type PublisherResult = {
|
||||
remoteUrl: string;
|
||||
catalogInfoUrl?: string;
|
||||
};
|
||||
|
||||
export type PublisherBuilder = {
|
||||
|
||||
@@ -156,12 +156,11 @@ export async function createRouter(
|
||||
handler: async (ctx: StageContext<{ resultDir: string }>) => {
|
||||
const publisher = publishers.get(ctx.entity);
|
||||
ctx.logger.info('Will now store the template');
|
||||
const { remoteUrl } = await publisher.publish({
|
||||
entity: ctx.entity,
|
||||
const result = await publisher.publish({
|
||||
values: ctx.values,
|
||||
directory: ctx.resultDir,
|
||||
});
|
||||
return { remoteUrl };
|
||||
return result;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -106,18 +106,10 @@ export const TemplatePage = () => {
|
||||
);
|
||||
|
||||
const handleCreateComplete = async (job: Job) => {
|
||||
const target = job.metadata.remoteUrl?.replace(
|
||||
/\.git$/,
|
||||
// TODO(Rugvip): This is not the location we want. As part of scaffolder v2 we
|
||||
// want this to be more flexible, but before that we might want
|
||||
// to update all templates to use catalog-info.yaml instead.
|
||||
'/blob/master/component-info.yaml',
|
||||
);
|
||||
|
||||
if (!target) {
|
||||
if (!job.metadata.catalogInfoUrl) {
|
||||
errorApi.post(
|
||||
new Error(
|
||||
`Failed to find component-info.yaml file in ${job.metadata.remoteUrl}.`,
|
||||
`Failed to find catalog-info.yaml file in ${job.metadata.remoteUrl}.`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
@@ -125,7 +117,7 @@ export const TemplatePage = () => {
|
||||
|
||||
const {
|
||||
entities: [createdEntity],
|
||||
} = await catalogApi.addLocation({ target });
|
||||
} = await catalogApi.addLocation({ target: job.metadata.catalogInfoUrl });
|
||||
|
||||
setEntity((createdEntity as any) as TemplateEntityV1alpha1);
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ export type Job = {
|
||||
entity: any;
|
||||
values: any;
|
||||
remoteUrl?: string;
|
||||
catalogInfoUrl?: string;
|
||||
};
|
||||
status: 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
|
||||
stages: Stage[];
|
||||
|
||||
Reference in New Issue
Block a user