Merge pull request #1477 from spotify/mob/create-vcs-step
Create the VCS Stage for the Scaffolder
This commit is contained in:
@@ -24,8 +24,9 @@
|
||||
"@backstage/backend-common": "^0.1.1-alpha.12",
|
||||
"@backstage/catalog-model": "^0.1.1-alpha.12",
|
||||
"@backstage/config": "^0.1.1-alpha.12",
|
||||
"@types/express": "^4.17.6",
|
||||
"@octokit/rest": "^18.0.0",
|
||||
"@types/dockerode": "^2.5.32",
|
||||
"@types/express": "^4.17.6",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dockerode": "^3.2.0",
|
||||
@@ -42,6 +43,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.1.1-alpha.12",
|
||||
"@octokit/types": "^5.0.1",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
"@types/nodegit": "0.26.5",
|
||||
|
||||
+1
-5
@@ -1,7 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Move all template files to the root folder
|
||||
mv ./* ../
|
||||
cd ..
|
||||
rm -rf {{cookiecutter.component_id}}
|
||||
# # # # # # # #
|
||||
echo "Could do something here?"
|
||||
|
||||
+1
-4
@@ -1,9 +1,6 @@
|
||||
name: Frontend CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '.'
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const mockGithubClient = {
|
||||
repos: {
|
||||
createInOrg: jest.fn(),
|
||||
createForAuthenticatedUser: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export class Octokit {
|
||||
constructor() {
|
||||
return mockGithubClient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export const mockIndex = {
|
||||
addAll: jest.fn(),
|
||||
write: jest.fn(),
|
||||
writeTree: jest.fn().mockResolvedValue('mockoid'),
|
||||
};
|
||||
|
||||
export const mockRepo = {
|
||||
refreshIndex: jest.fn().mockResolvedValue(mockIndex),
|
||||
createCommit: jest.fn(),
|
||||
};
|
||||
|
||||
export const mockRemote = {
|
||||
push: jest.fn(),
|
||||
};
|
||||
|
||||
const Repository = { init: jest.fn().mockResolvedValue(mockRepo) };
|
||||
const Remote = { create: jest.fn().mockResolvedValue(mockRemote) };
|
||||
const Signature = { now: jest.fn() };
|
||||
const Cred = {
|
||||
userpassPlaintextNew: jest.fn(),
|
||||
};
|
||||
|
||||
export { Repository, Remote, Signature, Cred };
|
||||
@@ -13,7 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './stages/templater';
|
||||
export * from './stages/templater/cookiecutter';
|
||||
export * from './stages/prepare';
|
||||
export * from './stages';
|
||||
export * from './jobs';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { makeLogStream } from './logger';
|
||||
|
||||
describe('Logger', () => {
|
||||
const mockMeta = { test: 'blob' };
|
||||
|
||||
it('should return empty log lines by default', async () => {
|
||||
const { log } = makeLogStream(mockMeta);
|
||||
|
||||
expect(log).toEqual([]);
|
||||
});
|
||||
it('should add lines to the log when using the logger that is returned', async () => {
|
||||
const { logger, log } = makeLogStream(mockMeta);
|
||||
|
||||
logger.info('TEST LINE');
|
||||
logger.warn('WARN LINE');
|
||||
|
||||
const [first, second] = log;
|
||||
expect(log.length).toBe(2);
|
||||
|
||||
expect(first).toContain('info');
|
||||
expect(first).toContain('TEST LINE');
|
||||
|
||||
expect(second).toContain('warn');
|
||||
expect(second).toContain('WARN LINE');
|
||||
});
|
||||
|
||||
it('should add lines from writing to the stream that is returned', async () => {
|
||||
const { stream, log } = makeLogStream(mockMeta);
|
||||
const textLine = 'SOMETHING';
|
||||
stream.write(textLine);
|
||||
|
||||
expect(log).toContain(textLine);
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ import { PassThrough } from 'stream';
|
||||
import winston from 'winston';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
export const useLogStream = (meta: Record<string, JsonValue>) => {
|
||||
export const makeLogStream = (meta: Record<string, JsonValue>) => {
|
||||
const log: string[] = [];
|
||||
|
||||
// Create an empty stream to collect all the log lines into
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import { JobProcessor } from './processor';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { StageInput } from './types';
|
||||
import { RequiredTemplateValues } from '../stages/templater';
|
||||
|
||||
describe('JobProcessor', () => {
|
||||
const mockEntity: TemplateEntityV1alpha1 = {
|
||||
@@ -41,7 +42,10 @@ describe('JobProcessor', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockValues = { component_id: 'bob' };
|
||||
const mockValues: RequiredTemplateValues = {
|
||||
owner: 'blobby',
|
||||
storePath: 'spotify/mock-repo',
|
||||
};
|
||||
|
||||
describe('create', () => {
|
||||
it('creates should create a new job with a unique id', async () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import * as uuid from 'uuid';
|
||||
import Docker from 'dockerode';
|
||||
import { RequiredTemplateValues, TemplaterBase } from '../stages/templater';
|
||||
import { PreparerBuilder } from '../stages/prepare';
|
||||
import { useLogStream } from './logger';
|
||||
import { makeLogStream } from './logger';
|
||||
|
||||
export type JobProcessorArguments = {
|
||||
preparers: PreparerBuilder;
|
||||
@@ -46,7 +46,7 @@ export class JobProcessor implements Processor {
|
||||
stages: StageInput[];
|
||||
}): Job {
|
||||
const id = uuid.v4();
|
||||
const { logger, stream } = useLogStream({ id });
|
||||
const { logger, stream } = makeLogStream({ id });
|
||||
|
||||
const context: StageContext = {
|
||||
entity,
|
||||
@@ -87,7 +87,7 @@ export class JobProcessor implements Processor {
|
||||
for (const stage of job.stages) {
|
||||
// Create a logger for each stage so we can create seperate
|
||||
// Streams for each step.
|
||||
const { logger, log, stream } = useLogStream({
|
||||
const { logger, log, stream } = makeLogStream({
|
||||
id: job.id,
|
||||
stage: stage.name,
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ export type StageContext<T = {}> = {
|
||||
|
||||
export type ProcessorStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED';
|
||||
|
||||
export interface Stage extends StageInput {
|
||||
export interface StageResult extends StageInput {
|
||||
log: string[];
|
||||
status: ProcessorStatus;
|
||||
startedAt?: number;
|
||||
@@ -47,7 +47,7 @@ export type Job = {
|
||||
id: string;
|
||||
context: StageContext;
|
||||
status: ProcessorStatus;
|
||||
stages: Stage[];
|
||||
stages: StageResult[];
|
||||
error?: Error;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
export * from './templater';
|
||||
@@ -43,6 +43,7 @@ export class FilePreparer implements PreparerBase {
|
||||
|
||||
await fs.copy(parentDirectory, tempDir, {
|
||||
filter: src => src !== location,
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
return tempDir;
|
||||
|
||||
@@ -19,7 +19,6 @@ const mocks = {
|
||||
CheckoutOptions: jest.fn(() => {}),
|
||||
};
|
||||
jest.doMock('nodegit', () => mocks);
|
||||
// require('nodegit');
|
||||
|
||||
import { GithubPreparer } from './github';
|
||||
import {
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock('@octokit/rest');
|
||||
jest.mock('nodegit');
|
||||
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import * as NodeGit from 'nodegit';
|
||||
import { OctokitResponse, ReposCreateInOrgResponseData } from '@octokit/types';
|
||||
import { GithubPublisher } from './github';
|
||||
|
||||
const { mockGithubClient } = require('@octokit/rest') as {
|
||||
mockGithubClient: { repos: jest.Mocked<Octokit['repos']> };
|
||||
};
|
||||
|
||||
const {
|
||||
Repository,
|
||||
mockRepo,
|
||||
mockIndex,
|
||||
Signature,
|
||||
Remote,
|
||||
mockRemote,
|
||||
Cred,
|
||||
} = require('nodegit') as {
|
||||
Repository: jest.Mocked<{ init: any }>;
|
||||
Signature: jest.Mocked<{ now: any }>;
|
||||
Cred: jest.Mocked<{ userpassPlaintextNew: any }>;
|
||||
Remote: jest.Mocked<{ create: any }>;
|
||||
|
||||
mockIndex: jest.Mocked<NodeGit.Index>;
|
||||
mockRepo: jest.Mocked<NodeGit.Repository>;
|
||||
mockRemote: jest.Mocked<NodeGit.Remote>;
|
||||
};
|
||||
|
||||
describe('Github Publisher', () => {
|
||||
const publisher = new GithubPublisher({ client: new Octokit() });
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('publish: createRemoteInGithub', () => {
|
||||
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',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
|
||||
await publisher.publish({
|
||||
values: {
|
||||
isOrg: true,
|
||||
storePath: 'blam/test',
|
||||
owner: 'bob',
|
||||
},
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(mockGithubClient.repos.createInOrg).toHaveBeenCalledWith({
|
||||
org: 'blam',
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
|
||||
await publisher.publish({
|
||||
values: {
|
||||
storePath: 'blam/test',
|
||||
owner: 'bob',
|
||||
},
|
||||
directory: '/tmp/test',
|
||||
});
|
||||
|
||||
expect(
|
||||
mockGithubClient.repos.createForAuthenticatedUser,
|
||||
).toHaveBeenCalledWith({
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('publish: createGitDirectory', () => {
|
||||
const values = {
|
||||
isOrg: true,
|
||||
storePath: 'blam/test',
|
||||
owner: 'lols',
|
||||
};
|
||||
|
||||
const mockDir = '/tmp/test/dir';
|
||||
|
||||
mockGithubClient.repos.createInOrg.mockResolvedValue({
|
||||
data: {
|
||||
clone_url: 'mockclone',
|
||||
},
|
||||
} as OctokitResponse<ReposCreateInOrgResponseData>);
|
||||
it('should call init on the repo with the directory', async () => {
|
||||
await publisher.publish({
|
||||
values,
|
||||
directory: mockDir,
|
||||
});
|
||||
|
||||
expect(Repository.init).toHaveBeenCalledWith(mockDir, 0);
|
||||
});
|
||||
|
||||
it('should call refresh index on the index and write the new files', async () => {
|
||||
await publisher.publish({
|
||||
values,
|
||||
directory: mockDir,
|
||||
});
|
||||
|
||||
expect(mockRepo.refreshIndex).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call add all files and write', async () => {
|
||||
await publisher.publish({
|
||||
values,
|
||||
directory: mockDir,
|
||||
});
|
||||
|
||||
expect(mockIndex.addAll).toHaveBeenCalled();
|
||||
expect(mockIndex.write).toHaveBeenCalled();
|
||||
expect(mockIndex.writeTree).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a commit with on head with the right name and commiter', async () => {
|
||||
const mockSignature = { mockSignature: 'bloblly' };
|
||||
Signature.now.mockReturnValue(mockSignature);
|
||||
|
||||
await publisher.publish({
|
||||
values,
|
||||
directory: mockDir,
|
||||
});
|
||||
|
||||
expect(Signature.now).toHaveBeenCalledTimes(2);
|
||||
expect(Signature.now).toHaveBeenCalledWith(
|
||||
'Scaffolder',
|
||||
'scaffolder@backstage.io',
|
||||
);
|
||||
|
||||
expect(mockRepo.createCommit).toHaveBeenCalledWith(
|
||||
'HEAD',
|
||||
mockSignature,
|
||||
mockSignature,
|
||||
'initial commit',
|
||||
'mockoid',
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a remote with the repo and remote', async () => {
|
||||
await publisher.publish({
|
||||
values,
|
||||
directory: mockDir,
|
||||
});
|
||||
|
||||
expect(Remote.create).toHaveBeenCalledWith(
|
||||
mockRepo,
|
||||
'origin',
|
||||
'mockclone',
|
||||
);
|
||||
});
|
||||
|
||||
it('shoud push to the remote repo', async () => {
|
||||
await publisher.publish({
|
||||
values,
|
||||
directory: mockDir,
|
||||
});
|
||||
|
||||
const [remotes, { callbacks }] = mockRemote.push.mock
|
||||
.calls[0] as NodeGit.PushOptions[];
|
||||
|
||||
expect(remotes).toEqual(['refs/heads/master:refs/heads/master']);
|
||||
|
||||
process.env.GITHUb_ACCESS_TOKEN = 'blob';
|
||||
|
||||
callbacks?.credentials?.();
|
||||
|
||||
expect(Cred.userpassPlaintextNew).toHaveBeenCalledWith(
|
||||
process.env.GITHUB_ACCESS_TOKEN,
|
||||
'x-oauth-basic',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Publisher } from './types';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { RequiredTemplateValues } from '../templater';
|
||||
import { Repository, Remote, Signature, Cred } from 'nodegit';
|
||||
|
||||
export class GithubPublisher implements Publisher {
|
||||
private client: Octokit;
|
||||
constructor({ client }: { client: Octokit }) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async publish({
|
||||
values,
|
||||
directory,
|
||||
}: {
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
directory: string;
|
||||
}): Promise<{ remoteUrl: string }> {
|
||||
const remoteUrl = await this.createRemote(values);
|
||||
await this.pushToRemote(directory, remoteUrl);
|
||||
|
||||
return { remoteUrl };
|
||||
}
|
||||
|
||||
private async createRemote(
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>,
|
||||
) {
|
||||
const [owner, name] = values.storePath.split('/');
|
||||
|
||||
const repoCreationPromise = values.isOrg
|
||||
? this.client.repos.createInOrg({ name, org: owner })
|
||||
: this.client.repos.createForAuthenticatedUser({ name });
|
||||
|
||||
const { data } = await repoCreationPromise;
|
||||
|
||||
return data?.clone_url;
|
||||
}
|
||||
|
||||
private async pushToRemote(directory: string, remote: string): Promise<void> {
|
||||
const repo = await Repository.init(directory, 0);
|
||||
const index = await repo.refreshIndex();
|
||||
await index.addAll();
|
||||
await index.write();
|
||||
const oid = await index.writeTree();
|
||||
await repo.createCommit(
|
||||
'HEAD',
|
||||
Signature.now('Scaffolder', 'scaffolder@backstage.io'),
|
||||
Signature.now('Scaffolder', 'scaffolder@backstage.io'),
|
||||
'initial commit',
|
||||
oid,
|
||||
[],
|
||||
);
|
||||
|
||||
const remoteRepo = await Remote.create(repo, 'origin', remote);
|
||||
await remoteRepo.push(['refs/heads/master:refs/heads/master'], {
|
||||
callbacks: {
|
||||
credentials: () => {
|
||||
return Cred.userpassPlaintextNew(
|
||||
process.env.GITHUB_ACCESS_TOKEN as string,
|
||||
'x-oauth-basic',
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './github';
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import { RequiredTemplateValues } from '../templater';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
/**
|
||||
* Publisher is in charge of taking a folder created by
|
||||
* the templater, and pushing it to a remote storage
|
||||
*/
|
||||
export type Publisher = {
|
||||
/**
|
||||
*
|
||||
* @param opts object containing the template entity from the service
|
||||
* 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 }>;
|
||||
};
|
||||
@@ -45,8 +45,10 @@ describe('CookieCutter Templater', () => {
|
||||
const tempdir = await mkTemp();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
owner: 'blobby',
|
||||
storePath: 'spotify/end-repo',
|
||||
description: 'description',
|
||||
component_id: 'newthing',
|
||||
};
|
||||
|
||||
await cookie.run({ directory: tempdir, values, dockerClient: mockDocker });
|
||||
@@ -65,8 +67,9 @@ describe('CookieCutter Templater', () => {
|
||||
await fs.writeJSON(`${tempdir}/cookiecutter.json`, existingJson);
|
||||
|
||||
const values = {
|
||||
component_id: 'hello',
|
||||
description: 'im something cool',
|
||||
owner: 'blobby',
|
||||
storePath: 'spotify/end-repo',
|
||||
component_id: 'something',
|
||||
};
|
||||
|
||||
await cookie.run({ directory: tempdir, values, dockerClient: mockDocker });
|
||||
@@ -82,8 +85,8 @@ describe('CookieCutter Templater', () => {
|
||||
await fs.writeFile(`${tempdir}/cookiecutter.json`, "{'");
|
||||
|
||||
const values = {
|
||||
component_id: 'hello',
|
||||
description: 'im something cool',
|
||||
owner: 'blobby',
|
||||
storePath: 'spotify/end-repo',
|
||||
};
|
||||
|
||||
await expect(
|
||||
@@ -95,8 +98,9 @@ describe('CookieCutter Templater', () => {
|
||||
const tempdir = await mkTemp();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
owner: 'blobby',
|
||||
storePath: 'spotify/end-repo',
|
||||
component_id: 'newthing',
|
||||
};
|
||||
|
||||
await cookie.run({ directory: tempdir, values, dockerClient: mockDocker });
|
||||
@@ -122,17 +126,18 @@ describe('CookieCutter Templater', () => {
|
||||
const tempdir = await mkTemp();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
owner: 'blobby',
|
||||
storePath: 'spotify/end-repo',
|
||||
component_id: 'newthing',
|
||||
};
|
||||
|
||||
const returnPath = await cookie.run({
|
||||
const { resultDir } = await cookie.run({
|
||||
directory: tempdir,
|
||||
values,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(returnPath.startsWith(`${tempdir}-result`)).toBeTruthy();
|
||||
expect(resultDir.startsWith(`${tempdir}-result`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should pass through the streamer to the run docker helper', async () => {
|
||||
@@ -141,8 +146,9 @@ describe('CookieCutter Templater', () => {
|
||||
const tempdir = await mkTemp();
|
||||
|
||||
const values = {
|
||||
component_id: 'test',
|
||||
description: 'description',
|
||||
owner: 'blobby',
|
||||
storePath: 'spotify/end-repo',
|
||||
component_id: 'newthing',
|
||||
};
|
||||
|
||||
await cookie.run({
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TemplaterBase, TemplaterRunOptions } from '.';
|
||||
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
@@ -18,6 +16,9 @@ import { TemplaterBase, TemplaterRunOptions } from '.';
|
||||
import fs from 'fs-extra';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
import { runDockerContainer } from './helpers';
|
||||
import { TemplaterBase, TemplaterRunOptions } from '.';
|
||||
import path from 'path';
|
||||
import { TemplaterRunResult } from './types';
|
||||
|
||||
export class CookieCutter implements TemplaterBase {
|
||||
private async fetchTemplateCookieCutter(
|
||||
@@ -34,7 +35,7 @@ export class CookieCutter implements TemplaterBase {
|
||||
}
|
||||
}
|
||||
|
||||
public async run(options: TemplaterRunOptions): Promise<string> {
|
||||
public async run(options: TemplaterRunOptions): Promise<TemplaterRunResult> {
|
||||
// First lets grab the default cookiecutter.json file
|
||||
const cookieCutterJson = await this.fetchTemplateCookieCutter(
|
||||
options.directory,
|
||||
@@ -66,6 +67,8 @@ export class CookieCutter implements TemplaterBase {
|
||||
dockerClient: options.dockerClient,
|
||||
});
|
||||
|
||||
return resultDir;
|
||||
return {
|
||||
resultDir: path.resolve(resultDir, options.values.component_id as string),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ describe('helpers', () => {
|
||||
jest
|
||||
.spyOn(mockDocker, 'run')
|
||||
.mockResolvedValue([{ Error: null, StatusCode: 0 }]);
|
||||
jest
|
||||
.spyOn(mockDocker, 'pull')
|
||||
.mockResolvedValue([{ Error: null, StatusCode: 0 }]);
|
||||
});
|
||||
|
||||
describe('runDockerContainer', () => {
|
||||
@@ -34,6 +37,17 @@ describe('helpers', () => {
|
||||
const templateDir = os.tmpdir();
|
||||
const resultDir = os.tmpdir();
|
||||
|
||||
it('will pull the docker container before running', async () => {
|
||||
await runDockerContainer({
|
||||
imageName,
|
||||
args,
|
||||
templateDir,
|
||||
resultDir,
|
||||
dockerClient: mockDocker,
|
||||
});
|
||||
|
||||
expect(mockDocker.pull).toHaveBeenCalledWith(imageName, {});
|
||||
});
|
||||
it('should call the dockerClient run command with the correct arguments passed through', async () => {
|
||||
await runDockerContainer({
|
||||
imageName,
|
||||
|
||||
@@ -44,6 +44,7 @@ export const runDockerContainer = async ({
|
||||
templateDir,
|
||||
dockerClient,
|
||||
}: RunDockerContainerOptions) => {
|
||||
await dockerClient.pull(imageName, {});
|
||||
const [{ Error: error, StatusCode: statusCode }] = await dockerClient.run(
|
||||
imageName,
|
||||
args,
|
||||
|
||||
@@ -13,45 +13,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Writable } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
export interface RequiredTemplateValues {
|
||||
component_id: string;
|
||||
}
|
||||
|
||||
export interface TemplaterRunOptions {
|
||||
directory: string;
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
logStream?: Writable;
|
||||
dockerClient: Docker;
|
||||
}
|
||||
|
||||
export type TemplaterBase = {
|
||||
// runs the templating with the values and returns the directory to push the VCS
|
||||
run(opts: TemplaterRunOptions): Promise<string>;
|
||||
};
|
||||
|
||||
export interface TemplaterConfig {
|
||||
templater?: TemplaterBase;
|
||||
}
|
||||
|
||||
class Templater implements TemplaterBase {
|
||||
templater?: TemplaterBase;
|
||||
|
||||
constructor({ templater }: TemplaterConfig) {
|
||||
this.templater = templater;
|
||||
}
|
||||
|
||||
public async run(opts: TemplaterRunOptions) {
|
||||
return this.templater!.run(opts);
|
||||
}
|
||||
}
|
||||
|
||||
export const createTemplater = (
|
||||
templaterConfig: TemplaterConfig,
|
||||
): TemplaterBase => {
|
||||
return new Templater(templaterConfig);
|
||||
};
|
||||
export * from './cookiecutter';
|
||||
export * from './types';
|
||||
export * from './helpers';
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Writable } from 'stream';
|
||||
import Docker from 'dockerode';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
/**
|
||||
* Currently the required template values. The owner
|
||||
* and where to store the result from templating
|
||||
*/
|
||||
export type RequiredTemplateValues = {
|
||||
owner: string;
|
||||
storePath: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The returned directory from the templater which is ready
|
||||
* to pass to the next stage of the scaffolder which is publishing
|
||||
*/
|
||||
export type TemplaterRunResult = {
|
||||
resultDir: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The values that the templater will recieve. The directory of the
|
||||
* skeleton, with the values from the frontend. A dedicated log stream and a docker
|
||||
* client to run any templater on top of your directory.
|
||||
*/
|
||||
export type TemplaterRunOptions = {
|
||||
directory: string;
|
||||
values: RequiredTemplateValues & Record<string, JsonValue>;
|
||||
logStream?: Writable;
|
||||
dockerClient: Docker;
|
||||
};
|
||||
|
||||
export type TemplaterBase = {
|
||||
// runs the templating with the values and returns the directory to push the VCS
|
||||
run(opts: TemplaterRunOptions): Promise<TemplaterRunResult>;
|
||||
};
|
||||
|
||||
export type TemplaterConfig = {
|
||||
templater?: TemplaterBase;
|
||||
};
|
||||
@@ -17,11 +17,19 @@
|
||||
import { Logger } from 'winston';
|
||||
import Router from 'express-promise-router';
|
||||
import express from 'express';
|
||||
import { PreparerBuilder, TemplaterBase, JobProcessor } from '../scaffolder';
|
||||
import {
|
||||
PreparerBuilder,
|
||||
TemplaterBase,
|
||||
JobProcessor,
|
||||
RequiredTemplateValues,
|
||||
StageContext,
|
||||
GithubPublisher,
|
||||
} from '../scaffolder';
|
||||
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
|
||||
import Docker from 'dockerode';
|
||||
import {} from '@backstage/backend-common';
|
||||
import { StageContext } from '../scaffolder/jobs/types';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { JsonValue } from '@backstage/config';
|
||||
|
||||
export interface RouterOptions {
|
||||
preparers: PreparerBuilder;
|
||||
@@ -34,9 +42,11 @@ export async function createRouter(
|
||||
options: RouterOptions,
|
||||
): Promise<express.Router> {
|
||||
const router = Router();
|
||||
const { preparers, templater, logger: parentLogger, dockerClient } = options;
|
||||
const logger = parentLogger.child({ plugin: 'scaffolder' });
|
||||
|
||||
const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN });
|
||||
const { preparers, templater, logger: parentLogger, dockerClient } = options;
|
||||
const githubPulisher = new GithubPublisher({ client: githubClient });
|
||||
const logger = parentLogger.child({ plugin: 'scaffolder' });
|
||||
const jobProcessor = new JobProcessor();
|
||||
|
||||
router
|
||||
@@ -48,7 +58,9 @@ export async function createRouter(
|
||||
return;
|
||||
}
|
||||
|
||||
res.send(job.stages[Number(params.index)].log.join(''));
|
||||
const { log } = job.stages[Number(params.index)] ?? { log: [] };
|
||||
|
||||
res.send(log.join(''));
|
||||
})
|
||||
.get('/v1/job/:jobId', ({ params }, res) => {
|
||||
const job = jobProcessor.get(params.jobId);
|
||||
@@ -73,37 +85,14 @@ export async function createRouter(
|
||||
error: job.error,
|
||||
});
|
||||
})
|
||||
.post('/v1/jobs', async (_, res) => {
|
||||
// TODO(blam): Create a unique job here and return the ID so that
|
||||
// The end user can poll for updates on the current job
|
||||
|
||||
// TODO(blam): Take this entity from the post body sent from the frontend
|
||||
const mockEntity: TemplateEntityV1alpha1 = {
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Template',
|
||||
metadata: {
|
||||
annotations: {
|
||||
'backstage.io/managed-by-location':
|
||||
'github:https://github.com/benjdlambert/backstage-graphql-template/blob/master/template.yaml',
|
||||
},
|
||||
name: 'graphql-starter',
|
||||
title: 'GraphQL Service',
|
||||
description:
|
||||
'A GraphQL starter template for backstage to get you up and running\nthe best pracices with GraphQL\n',
|
||||
uid: '9cf16bad-16e0-4213-b314-c4eec773c50b',
|
||||
etag: 'ZTkxMjUxMjUtYWY3Yi00MjU2LWFkYWMtZTZjNjU5ZjJhOWM2',
|
||||
|
||||
generation: 1,
|
||||
},
|
||||
spec: {
|
||||
type: 'cookiecutter',
|
||||
path: './template',
|
||||
},
|
||||
};
|
||||
.post('/v1/jobs', async (req, res) => {
|
||||
const template: TemplateEntityV1alpha1 = req.body.template;
|
||||
const values: RequiredTemplateValues & Record<string, JsonValue> =
|
||||
req.body.values;
|
||||
|
||||
const job = jobProcessor.create({
|
||||
entity: mockEntity,
|
||||
values: { component_id: 'blob' },
|
||||
entity: template,
|
||||
values,
|
||||
stages: [
|
||||
{
|
||||
name: 'Prepare the skeleton',
|
||||
@@ -129,15 +118,14 @@ export async function createRouter(
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Create VCS Repo',
|
||||
name: 'Publish template',
|
||||
handler: async (ctx: StageContext<{ resultDir: string }>) => {
|
||||
ctx.logger.info('Should now create the VCS repo');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Push to remote',
|
||||
handler: async ctx => {
|
||||
ctx.logger.info('Should now push to the remote');
|
||||
ctx.logger.info('Should not store the template');
|
||||
const { remoteUrl } = await githubPulisher.publish({
|
||||
values: ctx.values,
|
||||
directory: ctx.resultDir,
|
||||
});
|
||||
return { remoteUrl };
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2248,6 +2248,18 @@
|
||||
dependencies:
|
||||
"@octokit/types" "^2.0.0"
|
||||
|
||||
"@octokit/core@^3.0.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/core/-/core-3.1.0.tgz#9c3c9b23f7504668cfa057f143ccbf0c645a0ac9"
|
||||
integrity sha512-yPyQSmxIXLieEIRikk2w8AEtWkFdfG/LXcw1KvEtK3iP0ENZLW/WYQmdzOKqfSaLhooz4CJ9D+WY79C8ZliACw==
|
||||
dependencies:
|
||||
"@octokit/auth-token" "^2.4.0"
|
||||
"@octokit/graphql" "^4.3.1"
|
||||
"@octokit/request" "^5.4.0"
|
||||
"@octokit/types" "^5.0.0"
|
||||
before-after-hook "^2.1.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/endpoint@^5.5.0":
|
||||
version "5.5.3"
|
||||
resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978"
|
||||
@@ -2257,6 +2269,24 @@
|
||||
is-plain-object "^3.0.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/endpoint@^6.0.1":
|
||||
version "6.0.3"
|
||||
resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487"
|
||||
integrity sha512-Y900+r0gIz+cWp6ytnkibbD95ucEzDSKzlEnaWS52hbCDNcCJYO5mRmWW7HRAnDc7am+N/5Lnd8MppSaTYx1Yg==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.0"
|
||||
is-plain-object "^3.0.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/graphql@^4.3.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.1.tgz#162aed1490320b88ce34775b3f6b8de945529fa9"
|
||||
integrity sha512-qgMsROG9K2KxDs12CO3bySJaYoUu2aic90qpFrv7A8sEBzZ7UFGvdgPKiLw5gOPYEYbS0Xf8Tvf84tJutHPulQ==
|
||||
dependencies:
|
||||
"@octokit/request" "^5.3.0"
|
||||
"@octokit/types" "^5.0.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/plugin-enterprise-rest@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437"
|
||||
@@ -2269,6 +2299,13 @@
|
||||
dependencies:
|
||||
"@octokit/types" "^2.0.1"
|
||||
|
||||
"@octokit/plugin-paginate-rest@^2.2.0":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.3.tgz#a6ad4377e7e7832fb4bdd9d421e600cb7640ac27"
|
||||
integrity sha512-eKTs91wXnJH8Yicwa30jz6DF50kAh7vkcqCQ9D7/tvBAP5KKkg6I2nNof8Mp/65G0Arjsb4QcOJcIEQY+rK1Rg==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.0"
|
||||
|
||||
"@octokit/plugin-request-log@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e"
|
||||
@@ -2282,6 +2319,14 @@
|
||||
"@octokit/types" "^2.0.1"
|
||||
deprecation "^2.3.1"
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods@4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.0.0.tgz#b02a2006dda8e908c3f8ab381dd5475ef5a810a8"
|
||||
integrity sha512-emS6gysz4E9BNi9IrCl7Pm4kR+Az3MmVB0/DoDCmF4U48NbYG3weKyDlgkrz6Jbl4Mu4nDx8YWZwC4HjoTdcCA==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.0"
|
||||
deprecation "^2.3.1"
|
||||
|
||||
"@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801"
|
||||
@@ -2291,6 +2336,15 @@
|
||||
deprecation "^2.0.0"
|
||||
once "^1.4.0"
|
||||
|
||||
"@octokit/request-error@^2.0.0":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0"
|
||||
integrity sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.1"
|
||||
deprecation "^2.0.0"
|
||||
once "^1.4.0"
|
||||
|
||||
"@octokit/request@^5.2.0":
|
||||
version "5.3.2"
|
||||
resolved "https://registry.npmjs.org/@octokit/request/-/request-5.3.2.tgz#1ca8b90a407772a1ee1ab758e7e0aced213b9883"
|
||||
@@ -2305,6 +2359,20 @@
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/request@^5.3.0", "@octokit/request@^5.4.0":
|
||||
version "5.4.5"
|
||||
resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.5.tgz#8df65bd812047521f7e9db6ff118c06ba84ac10b"
|
||||
integrity sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg==
|
||||
dependencies:
|
||||
"@octokit/endpoint" "^6.0.1"
|
||||
"@octokit/request-error" "^2.0.0"
|
||||
"@octokit/types" "^5.0.0"
|
||||
deprecation "^2.0.0"
|
||||
is-plain-object "^3.0.0"
|
||||
node-fetch "^2.3.0"
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/rest@^16.28.4":
|
||||
version "16.43.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz#3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b"
|
||||
@@ -2327,6 +2395,16 @@
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^4.0.0"
|
||||
|
||||
"@octokit/rest@^18.0.0":
|
||||
version "18.0.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.0.tgz#7f401d9ce13530ad743dfd519ae62ce49bcc0358"
|
||||
integrity sha512-4G/a42lry9NFGuuECnua1R1eoKkdBYJap97jYbWDNYBOUboWcM75GJ1VIcfvwDV/pW0lMPs7CEmhHoVrSV5shg==
|
||||
dependencies:
|
||||
"@octokit/core" "^3.0.0"
|
||||
"@octokit/plugin-paginate-rest" "^2.2.0"
|
||||
"@octokit/plugin-request-log" "^1.0.0"
|
||||
"@octokit/plugin-rest-endpoint-methods" "4.0.0"
|
||||
|
||||
"@octokit/types@^2.0.0", "@octokit/types@^2.0.1":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/types/-/types-2.5.0.tgz#f1bbd147e662ae2c79717d518aac686e58257773"
|
||||
@@ -2334,6 +2412,13 @@
|
||||
dependencies:
|
||||
"@types/node" ">= 8"
|
||||
|
||||
"@octokit/types@^5.0.0", "@octokit/types@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/types/-/types-5.0.1.tgz#5459e9a5e9df8565dcc62c17a34491904d71971e"
|
||||
integrity sha512-GorvORVwp244fGKEt3cgt/P+M0MGy4xEDbckw+K5ojEezxyMDgCaYPKVct+/eWQfZXOT7uq0xRpmrl/+hliabA==
|
||||
dependencies:
|
||||
"@types/node" ">= 8"
|
||||
|
||||
"@open-draft/until@^1.0.0":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca"
|
||||
@@ -5243,7 +5328,7 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
before-after-hook@^2.0.0:
|
||||
before-after-hook@^2.0.0, before-after-hook@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635"
|
||||
integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==
|
||||
|
||||
Reference in New Issue
Block a user