feat(scaffolder): add a publish:gerrit:review action
Signed-off-by: Thomas Cardonne <thomas.cardonne@adevinta.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Improve `scm/git` wrapper around `isomorphic-git` library :
|
||||
|
||||
- Add `checkout` function,
|
||||
- Add optional `remoteRef` parameter in the `push` function.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Add a `publish:gerrit:review` scaffolder action
|
||||
@@ -339,6 +339,8 @@ export class Git {
|
||||
remote: string;
|
||||
url: string;
|
||||
}): Promise<void>;
|
||||
// (undocumented)
|
||||
checkout(options: { dir: string; ref: string }): Promise<void>;
|
||||
clone(options: {
|
||||
url: string;
|
||||
dir: string;
|
||||
@@ -387,7 +389,11 @@ export class Git {
|
||||
};
|
||||
}): Promise<MergeResult>;
|
||||
// (undocumented)
|
||||
push(options: { dir: string; remote: string }): Promise<PushResult>;
|
||||
push(options: {
|
||||
dir: string;
|
||||
remote: string;
|
||||
remoteRef?: string;
|
||||
}): Promise<PushResult>;
|
||||
readCommit(options: { dir: string; sha: string }): Promise<ReadCommitResult>;
|
||||
resolveRef(options: { dir: string; ref: string }): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -60,6 +60,22 @@ describe('Git', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkout', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
const dir = 'mockdirectory';
|
||||
const ref = 'master';
|
||||
|
||||
await git.checkout({ dir, ref });
|
||||
|
||||
expect(isomorphic.checkout).toHaveBeenCalledWith({
|
||||
fs,
|
||||
dir,
|
||||
ref,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('commit', () => {
|
||||
it('should call isomorphic-git with the correct arguments', async () => {
|
||||
const git = Git.fromAuth({});
|
||||
@@ -310,6 +326,31 @@ describe('Git', () => {
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
it('should call isomorphic-git with remoteRef parameter', async () => {
|
||||
const remote = 'origin';
|
||||
const remoteRef = 'refs/for/master';
|
||||
const dir = '/some/mock/dir';
|
||||
const auth = {
|
||||
username: 'blob',
|
||||
password: 'hunter2',
|
||||
};
|
||||
const git = Git.fromAuth(auth);
|
||||
|
||||
await git.push({ dir, remote, remoteRef });
|
||||
|
||||
expect(isomorphic.push).toHaveBeenCalledWith({
|
||||
fs,
|
||||
http,
|
||||
remote,
|
||||
remoteRef,
|
||||
dir,
|
||||
onProgress: expect.any(Function),
|
||||
headers: {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
onAuth: expect.any(Function),
|
||||
});
|
||||
});
|
||||
it('should pass a function that returns the authorization as the onAuth handler', async () => {
|
||||
const remote = 'origin';
|
||||
const dir = '/some/mock/dir';
|
||||
|
||||
@@ -66,6 +66,13 @@ export class Git {
|
||||
return git.addRemote({ fs, dir, remote, url });
|
||||
}
|
||||
|
||||
async checkout(options: { dir: string; ref: string }): Promise<void> {
|
||||
const { dir, ref } = options;
|
||||
this.config.logger?.info(`Checking out branch {dir=${dir},ref=${ref}}`);
|
||||
|
||||
return git.checkout({ fs, dir, ref });
|
||||
}
|
||||
|
||||
async commit(options: {
|
||||
dir: string;
|
||||
message: string;
|
||||
@@ -190,8 +197,8 @@ export class Git {
|
||||
});
|
||||
}
|
||||
|
||||
async push(options: { dir: string; remote: string }) {
|
||||
const { dir, remote } = options;
|
||||
async push(options: { dir: string; remote: string; remoteRef?: string }) {
|
||||
const { dir, remote, remoteRef } = options;
|
||||
this.config.logger?.info(
|
||||
`Pushing directory to remote {dir=${dir},remote=${remote}}`,
|
||||
);
|
||||
@@ -205,6 +212,7 @@ export class Git {
|
||||
'user-agent': 'git/@isomorphic-git',
|
||||
},
|
||||
remote: remote,
|
||||
remoteRef: remoteRef,
|
||||
onAuth: this.onAuth,
|
||||
});
|
||||
} catch (ex) {
|
||||
|
||||
@@ -321,6 +321,19 @@ export function createPublishGerritAction(options: {
|
||||
sourcePath?: string | undefined;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function createPublishGerritReviewAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
config: Config;
|
||||
}): TemplateAction<{
|
||||
repoUrl: string;
|
||||
branch?: string | undefined;
|
||||
sourcePath?: string | undefined;
|
||||
gitCommitMessage?: string | undefined;
|
||||
gitAuthorName?: string | undefined;
|
||||
gitAuthorEmail?: string | undefined;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export function createPublishGithubAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
createPublishBitbucketCloudAction,
|
||||
createPublishBitbucketServerAction,
|
||||
createPublishGerritAction,
|
||||
createPublishGerritReviewAction,
|
||||
createPublishGithubAction,
|
||||
createPublishGithubPullRequestAction,
|
||||
createPublishGitlabAction,
|
||||
@@ -118,6 +119,10 @@ export const createBuiltinActions = (
|
||||
integrations,
|
||||
config,
|
||||
}),
|
||||
createPublishGerritReviewAction({
|
||||
integrations,
|
||||
config,
|
||||
}),
|
||||
createPublishGithubAction({
|
||||
integrations,
|
||||
config,
|
||||
|
||||
@@ -15,14 +15,16 @@
|
||||
*/
|
||||
|
||||
import { Git, getVoidLogger } from '@backstage/backend-common';
|
||||
import { initRepoAndPush } from './helpers';
|
||||
import { commitAndPushRepo, initRepoAndPush } from './helpers';
|
||||
|
||||
jest.mock('@backstage/backend-common', () => ({
|
||||
Git: {
|
||||
fromAuth: jest.fn().mockReturnValue({
|
||||
init: jest.fn(),
|
||||
add: jest.fn(),
|
||||
checkout: jest.fn(),
|
||||
commit: jest.fn(),
|
||||
fetch: jest.fn(),
|
||||
addRemote: jest.fn(),
|
||||
push: jest.fn(),
|
||||
}),
|
||||
@@ -146,3 +148,142 @@ describe('initRepoAndPush', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('commitAndPushRepo', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('with minimal parameters', () => {
|
||||
beforeEach(async () => {
|
||||
await commitAndPushRepo({
|
||||
dir: '/test/repo/dir/',
|
||||
auth: {
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
commitMessage: 'commit message',
|
||||
});
|
||||
});
|
||||
|
||||
it('fetches commits', () => {
|
||||
expect(mockedGit.fetch).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
});
|
||||
});
|
||||
|
||||
it('checkouts to master', () => {
|
||||
expect(mockedGit.checkout).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
ref: 'master',
|
||||
});
|
||||
});
|
||||
|
||||
it('stages all files in the repo', () => {
|
||||
expect(mockedGit.add).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
filepath: '.',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a commit', () => {
|
||||
expect(mockedGit.commit).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
message: 'commit message',
|
||||
author: {
|
||||
name: 'Scaffolder',
|
||||
email: 'scaffolder@backstage.io',
|
||||
},
|
||||
committer: {
|
||||
name: 'Scaffolder',
|
||||
email: 'scaffolder@backstage.io',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('pushes to the remote', () => {
|
||||
expect(mockedGit.push).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
remote: 'origin',
|
||||
remoteRef: 'refs/heads/master',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('allows overriding the default branch', async () => {
|
||||
await commitAndPushRepo({
|
||||
dir: '/test/repo/dir/',
|
||||
auth: {
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
commitMessage: 'commit message',
|
||||
branch: 'otherbranch',
|
||||
});
|
||||
|
||||
expect(mockedGit.checkout).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
ref: 'otherbranch',
|
||||
});
|
||||
expect(mockedGit.push).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
remote: 'origin',
|
||||
remoteRef: 'refs/heads/otherbranch',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows overriding the remote ref', async () => {
|
||||
await commitAndPushRepo({
|
||||
dir: '/test/repo/dir/',
|
||||
auth: {
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
commitMessage: 'commit message',
|
||||
remoteRef: 'refs/for/master',
|
||||
});
|
||||
|
||||
expect(mockedGit.checkout).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
ref: 'master',
|
||||
});
|
||||
expect(mockedGit.push).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
remote: 'origin',
|
||||
remoteRef: 'refs/for/master',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows overriding the author', async () => {
|
||||
await commitAndPushRepo({
|
||||
dir: '/test/repo/dir/',
|
||||
commitMessage: 'commit message',
|
||||
gitAuthorInfo: {
|
||||
name: 'Custom Scaffolder Author',
|
||||
email: 'scaffolder@example.org',
|
||||
},
|
||||
auth: {
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
branch: 'master',
|
||||
});
|
||||
|
||||
expect(mockedGit.commit).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
message: 'commit message',
|
||||
author: {
|
||||
name: 'Custom Scaffolder Author',
|
||||
email: 'scaffolder@example.org',
|
||||
},
|
||||
committer: {
|
||||
name: 'Custom Scaffolder Author',
|
||||
email: 'scaffolder@example.org',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,6 +127,53 @@ export async function initRepoAndPush({
|
||||
});
|
||||
}
|
||||
|
||||
export async function commitAndPushRepo({
|
||||
dir,
|
||||
auth,
|
||||
logger,
|
||||
commitMessage,
|
||||
gitAuthorInfo,
|
||||
branch = 'master',
|
||||
remoteRef,
|
||||
}: {
|
||||
dir: string;
|
||||
auth: { username: string; password: string };
|
||||
logger: Logger;
|
||||
commitMessage: string;
|
||||
gitAuthorInfo?: { name?: string; email?: string };
|
||||
branch?: string;
|
||||
remoteRef?: string;
|
||||
}): Promise<void> {
|
||||
const git = Git.fromAuth({
|
||||
username: auth.username,
|
||||
password: auth.password,
|
||||
logger,
|
||||
});
|
||||
|
||||
await git.fetch({ dir });
|
||||
await git.checkout({ dir, ref: branch });
|
||||
await git.add({ dir, filepath: '.' });
|
||||
|
||||
// use provided info if possible, otherwise use fallbacks
|
||||
const authorInfo = {
|
||||
name: gitAuthorInfo?.name ?? 'Scaffolder',
|
||||
email: gitAuthorInfo?.email ?? 'scaffolder@backstage.io',
|
||||
};
|
||||
|
||||
await git.commit({
|
||||
dir,
|
||||
message: commitMessage,
|
||||
author: authorInfo,
|
||||
committer: authorInfo,
|
||||
});
|
||||
|
||||
await git.push({
|
||||
dir,
|
||||
remote: 'origin',
|
||||
remoteRef: remoteRef ?? `refs/heads/${branch}`,
|
||||
});
|
||||
}
|
||||
|
||||
type BranchProtectionOptions = {
|
||||
client: Octokit;
|
||||
owner: string;
|
||||
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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('../helpers');
|
||||
|
||||
import { createPublishGerritReviewAction } from './gerritReview';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import { commitAndPushRepo } from '../helpers';
|
||||
|
||||
describe('publish:gerrit:review', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gerrit: [
|
||||
{
|
||||
host: 'gerrithost.org',
|
||||
username: 'gerrituser',
|
||||
password: 'usertoken',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
const action = createPublishGerritReviewAction({ integrations, config });
|
||||
const mockContext = {
|
||||
input: {
|
||||
repoUrl:
|
||||
'gerrithost.org?owner=owner&workspace=parent&project=project&repo=repo',
|
||||
gitCommitMessage: 'Review from backstage',
|
||||
},
|
||||
workspacePath: 'workspace',
|
||||
logger: getVoidLogger(),
|
||||
logStream: new PassThrough(),
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should throw an error when the repoUrl is not well formed', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'gerrithost.org?workspace=w&owner=o',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/missing repo/);
|
||||
});
|
||||
|
||||
it('should throw an error when no commit message is provided', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: { repoUrl: 'gerrithost.org?workspace=w&owner=o&repo=r' },
|
||||
}),
|
||||
).rejects.toThrow(/Missing gitCommitMessage input/);
|
||||
});
|
||||
|
||||
it('should throw if there is no integration config provided', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
...mockContext,
|
||||
input: {
|
||||
repoUrl: 'missing.com?workspace=w&owner=o&repo=repo',
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow(/No matching integration configuration/);
|
||||
});
|
||||
|
||||
it('can correctly create a review', async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
await action.handler(mockContext);
|
||||
|
||||
expect(commitAndPushRepo).toHaveBeenCalledWith({
|
||||
dir: mockContext.workspacePath,
|
||||
auth: { username: 'gerrituser', password: 'usertoken' },
|
||||
logger: mockContext.logger,
|
||||
commitMessage: expect.stringContaining(
|
||||
'Review from backstage\n\nChange-Id:',
|
||||
),
|
||||
gitAuthorInfo: {},
|
||||
branch: 'master',
|
||||
remoteRef: 'refs/for/master',
|
||||
});
|
||||
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'repoContentsUrl',
|
||||
'https://gerrithost.org/repo/+/refs/heads/master',
|
||||
);
|
||||
expect(mockContext.output).toHaveBeenCalledWith(
|
||||
'reviewUrl',
|
||||
expect.stringMatching(new RegExp('^https://gerrithost.org/#/q/I')),
|
||||
);
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* 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 crypto from 'crypto';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { createTemplateAction } from '../../createTemplateAction';
|
||||
import { getRepoSourceDirectory, parseRepoUrl } from './util';
|
||||
import { commitAndPushRepo } from '../helpers';
|
||||
|
||||
const generateGerritChangeId = (): string => {
|
||||
const changeId = crypto.randomBytes(20).toString('hex');
|
||||
return `I${changeId}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new action that creates a Gerrit review
|
||||
* @public
|
||||
*/
|
||||
export function createPublishGerritReviewAction(options: {
|
||||
integrations: ScmIntegrationRegistry;
|
||||
config: Config;
|
||||
}) {
|
||||
const { integrations, config } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
repoUrl: string;
|
||||
branch?: string;
|
||||
sourcePath?: string;
|
||||
gitCommitMessage?: string;
|
||||
gitAuthorName?: string;
|
||||
gitAuthorEmail?: string;
|
||||
}>({
|
||||
id: 'publish:gerrit:review',
|
||||
description: 'Creates a new Gerrit review.',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['repoUrl', 'gitCommitMessage'],
|
||||
properties: {
|
||||
repoUrl: {
|
||||
title: 'Repository Location',
|
||||
type: 'string',
|
||||
},
|
||||
branch: {
|
||||
title: 'Repository branch',
|
||||
type: 'string',
|
||||
description:
|
||||
'Branch of the repository the review will be created on',
|
||||
},
|
||||
sourcePath: {
|
||||
type: 'string',
|
||||
title: 'Working Subdirectory',
|
||||
description:
|
||||
'Subdirectory of working directory containing the repository',
|
||||
},
|
||||
gitCommitMessage: {
|
||||
title: 'Git Commit Message',
|
||||
type: 'string',
|
||||
description: `Sets the commit message on the repository.`,
|
||||
},
|
||||
gitAuthorName: {
|
||||
title: 'Default Author Name',
|
||||
type: 'string',
|
||||
description: `Sets the default author name for the commit. The default value is 'Scaffolder'`,
|
||||
},
|
||||
gitAuthorEmail: {
|
||||
title: 'Default Author Email',
|
||||
type: 'string',
|
||||
description: `Sets the default author email for the commit.`,
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reviewUrl: {
|
||||
title: 'A URL to the review',
|
||||
type: 'string',
|
||||
},
|
||||
repoContentsUrl: {
|
||||
title: 'A URL to the root of the repository',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const {
|
||||
repoUrl,
|
||||
branch = 'master',
|
||||
sourcePath,
|
||||
gitAuthorName,
|
||||
gitAuthorEmail,
|
||||
gitCommitMessage,
|
||||
} = ctx.input;
|
||||
const { host, repo } = parseRepoUrl(repoUrl, integrations);
|
||||
|
||||
if (!gitCommitMessage) {
|
||||
throw new InputError(`Missing gitCommitMessage input`);
|
||||
}
|
||||
|
||||
const integrationConfig = integrations.gerrit.byHost(host);
|
||||
|
||||
if (!integrationConfig) {
|
||||
throw new InputError(
|
||||
`No matching integration configuration for host ${host}, please check your integrations config`,
|
||||
);
|
||||
}
|
||||
|
||||
const auth = {
|
||||
username: integrationConfig.config.username!,
|
||||
password: integrationConfig.config.password!,
|
||||
};
|
||||
const gitAuthorInfo = {
|
||||
name: gitAuthorName
|
||||
? gitAuthorName
|
||||
: config.getOptionalString('scaffolder.defaultAuthor.name'),
|
||||
email: gitAuthorEmail
|
||||
? gitAuthorEmail
|
||||
: config.getOptionalString('scaffolder.defaultAuthor.email'),
|
||||
};
|
||||
const changeId = generateGerritChangeId();
|
||||
const commitMessage = `${gitCommitMessage}\n\nChange-Id: ${changeId}`;
|
||||
|
||||
await commitAndPushRepo({
|
||||
dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath),
|
||||
auth,
|
||||
logger: ctx.logger,
|
||||
commitMessage,
|
||||
gitAuthorInfo,
|
||||
branch,
|
||||
remoteRef: `refs/for/${branch}`,
|
||||
});
|
||||
|
||||
const repoContentsUrl = `${integrationConfig.config.gitilesBaseUrl}/${repo}/+/refs/heads/${branch}`;
|
||||
const reviewUrl = `${integrationConfig.config.baseUrl}/#/q/${changeId}`;
|
||||
ctx.logger?.info(`Review available on ${reviewUrl}`);
|
||||
ctx.output('repoContentsUrl', repoContentsUrl);
|
||||
ctx.output('reviewUrl', reviewUrl);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export { createPublishBitbucketCloudAction } from './bitbucketCloud';
|
||||
export { createPublishBitbucketServerAction } from './bitbucketServer';
|
||||
export { createPublishFileAction } from './file';
|
||||
export { createPublishGerritAction } from './gerrit';
|
||||
export { createPublishGerritReviewAction } from './gerritReview';
|
||||
export { createPublishGithubAction } from './github';
|
||||
export { createPublishGithubPullRequestAction } from './githubPullRequest';
|
||||
export type {
|
||||
|
||||
Reference in New Issue
Block a user