@@ -0,0 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
@@ -0,0 +1,5 @@
|
||||
# @backstage/plugin-scaffolder-backend-module-gerrit
|
||||
|
||||
The gerrit module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend).
|
||||
|
||||
_This plugin was created through the Backstage CLI_
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-backend-module-gerrit",
|
||||
"description": "The gerrit module for @backstage/plugin-scaffolder-backend",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.cjs.js",
|
||||
"types": "dist/index.d.ts"
|
||||
},
|
||||
"backstage": {
|
||||
"role": "backend-plugin-module"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "backstage-cli package start",
|
||||
"build": "backstage-cli package build",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"clean": "backstage-cli package clean",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/plugin-scaffolder-node": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/integration": "workspace:^",
|
||||
"@backstage/config": "workspace:^",
|
||||
"node-fetch": "^2.6.7",
|
||||
"yaml": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-common": "workspace:^",
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
+3
-2
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock('../helpers', () => {
|
||||
jest.mock('@backstage/plugin-scaffolder-node', () => {
|
||||
return {
|
||||
...jest.requireActual('@backstage/plugin-scaffolder-node'),
|
||||
initRepoAndPush: jest.fn().mockResolvedValue({
|
||||
commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6',
|
||||
}),
|
||||
@@ -34,7 +35,7 @@ import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import { initRepoAndPush } from '../helpers';
|
||||
import { initRepoAndPush } from '@backstage/plugin-scaffolder-node';
|
||||
|
||||
describe('publish:gerrit', () => {
|
||||
const config = new ConfigReader({
|
||||
+6
-3
@@ -22,10 +22,13 @@ import {
|
||||
getGerritRequestOptions,
|
||||
ScmIntegrationRegistry,
|
||||
} from '@backstage/integration';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { getRepoSourceDirectory, parseRepoUrl } from './util';
|
||||
import {
|
||||
createTemplateAction,
|
||||
initRepoAndPush,
|
||||
getRepoSourceDirectory,
|
||||
parseRepoUrl,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import fetch, { Response, RequestInit } from 'node-fetch';
|
||||
import { initRepoAndPush } from '../helpers';
|
||||
import { examples } from './gerrit.examples';
|
||||
|
||||
const createGerritProject = async (
|
||||
+7
-2
@@ -14,14 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
jest.mock('../helpers');
|
||||
jest.mock('@backstage/plugin-scaffolder-node', () => {
|
||||
return {
|
||||
...jest.requireActual('@backstage/plugin-scaffolder-node'),
|
||||
commitAndPushRepo: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
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';
|
||||
import { commitAndPushRepo } from '@backstage/plugin-scaffolder-node';
|
||||
|
||||
describe('publish:gerrit:review', () => {
|
||||
const config = new ConfigReader({
|
||||
+6
-3
@@ -18,9 +18,12 @@ import crypto from 'crypto';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ScmIntegrationRegistry } from '@backstage/integration';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { getRepoSourceDirectory, parseRepoUrl } from './util';
|
||||
import { commitAndPushRepo } from '../helpers';
|
||||
import {
|
||||
createTemplateAction,
|
||||
commitAndPushRepo,
|
||||
getRepoSourceDirectory,
|
||||
parseRepoUrl,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
|
||||
const generateGerritChangeId = (): string => {
|
||||
const changeId = crypto.randomBytes(20).toString('hex');
|
||||
+2
-5
@@ -13,8 +13,5 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// const executeBitMask = 0o000111;
|
||||
// const res = fileMode & executeBitMask;
|
||||
// return res > 0;
|
||||
// };
|
||||
export * from './gerrit';
|
||||
export * from './gerritReview';
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2023 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The gerrit module for @backstage/plugin-scaffolder-backend.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
export * from './actions';
|
||||
@@ -1,319 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Git, getVoidLogger } from '@backstage/backend-common';
|
||||
import { commitAndPushRepo, entityRefToName, 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()
|
||||
.mockResolvedValue('220f19cc36b551763d157f1b5e4a4b446165dbd6'),
|
||||
fetch: jest.fn(),
|
||||
addRemote: jest.fn(),
|
||||
push: jest.fn(),
|
||||
}),
|
||||
},
|
||||
getVoidLogger: jest.requireActual('@backstage/backend-common').getVoidLogger,
|
||||
}));
|
||||
|
||||
const mockedGit = Git.fromAuth({
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
describe('initRepoAndPush', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('with minimal parameters', () => {
|
||||
beforeEach(async () => {
|
||||
await initRepoAndPush({
|
||||
dir: '/test/repo/dir/',
|
||||
remoteUrl: 'git@github.com:test/repo.git',
|
||||
auth: {
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
});
|
||||
|
||||
it('initializes the repo', () => {
|
||||
expect(mockedGit.init).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
defaultBranch: 'master',
|
||||
});
|
||||
});
|
||||
|
||||
it('stages all files in the repo', () => {
|
||||
expect(mockedGit.add).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
filepath: '.',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an initial commit', () => {
|
||||
expect(mockedGit.commit).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
message: 'Initial commit',
|
||||
author: {
|
||||
name: 'Scaffolder',
|
||||
email: 'scaffolder@backstage.io',
|
||||
},
|
||||
committer: {
|
||||
name: 'Scaffolder',
|
||||
email: 'scaffolder@backstage.io',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('adds the appropriate remote', () => {
|
||||
expect(mockedGit.addRemote).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
url: 'git@github.com:test/repo.git',
|
||||
remote: 'origin',
|
||||
});
|
||||
});
|
||||
|
||||
it('pushes to the remote', () => {
|
||||
expect(mockedGit.push).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
remote: 'origin',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('with token', async () => {
|
||||
await initRepoAndPush({
|
||||
dir: '/test/repo/dir/',
|
||||
remoteUrl: 'git@github.com:test/repo.git',
|
||||
auth: {
|
||||
token: 'test-token',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
expect(mockedGit.init).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
defaultBranch: 'master',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows overriding the default branch', async () => {
|
||||
await initRepoAndPush({
|
||||
dir: '/test/repo/dir/',
|
||||
defaultBranch: 'trunk',
|
||||
remoteUrl: 'git@github.com:test/repo.git',
|
||||
auth: {
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
expect(mockedGit.init).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
defaultBranch: 'trunk',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows overriding the author', async () => {
|
||||
await initRepoAndPush({
|
||||
dir: '/test/repo/dir/',
|
||||
gitAuthorInfo: {
|
||||
name: 'Custom Scaffolder Author',
|
||||
email: 'scaffolder@example.org',
|
||||
},
|
||||
remoteUrl: 'git@github.com:test/repo.git',
|
||||
auth: {
|
||||
username: 'test-user',
|
||||
password: 'test-password',
|
||||
},
|
||||
logger: getVoidLogger(),
|
||||
});
|
||||
|
||||
expect(mockedGit.commit).toHaveBeenCalledWith({
|
||||
dir: '/test/repo/dir/',
|
||||
message: 'Initial commit',
|
||||
author: {
|
||||
name: 'Custom Scaffolder Author',
|
||||
email: 'scaffolder@example.org',
|
||||
},
|
||||
committer: {
|
||||
name: 'Custom Scaffolder Author',
|
||||
email: 'scaffolder@example.org',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('entityRefToName', () => {
|
||||
it.each([
|
||||
'user:default/catpants',
|
||||
'group:default/catpants',
|
||||
'user:catpants',
|
||||
'default/catpants',
|
||||
'user:custom/catpants',
|
||||
'group:custom/catpants',
|
||||
'catpants',
|
||||
])('should parse: "%s"', (entityName: string) => {
|
||||
expect(entityRefToName(entityName)).toEqual('catpants');
|
||||
});
|
||||
});
|
||||
@@ -1,253 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 { Git } from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { assertError } from '@backstage/errors';
|
||||
import { Octokit } from 'octokit';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
export async function initRepoAndPush({
|
||||
dir,
|
||||
remoteUrl,
|
||||
auth,
|
||||
logger,
|
||||
defaultBranch = 'master',
|
||||
commitMessage = 'Initial commit',
|
||||
gitAuthorInfo,
|
||||
}: {
|
||||
dir: string;
|
||||
remoteUrl: string;
|
||||
// For use cases where token has to be used with Basic Auth
|
||||
// it has to be provided as password together with a username
|
||||
// which may be a fixed value defined by the provider.
|
||||
auth: { username: string; password: string } | { token: string };
|
||||
logger: Logger;
|
||||
defaultBranch?: string;
|
||||
commitMessage?: string;
|
||||
gitAuthorInfo?: { name?: string; email?: string };
|
||||
}): Promise<{ commitHash: string }> {
|
||||
const git = Git.fromAuth({
|
||||
...auth,
|
||||
logger,
|
||||
});
|
||||
|
||||
await git.init({
|
||||
dir,
|
||||
defaultBranch,
|
||||
});
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
const commitHash = await git.commit({
|
||||
dir,
|
||||
message: commitMessage,
|
||||
author: authorInfo,
|
||||
committer: authorInfo,
|
||||
});
|
||||
await git.addRemote({
|
||||
dir,
|
||||
url: remoteUrl,
|
||||
remote: 'origin',
|
||||
});
|
||||
|
||||
await git.push({
|
||||
dir,
|
||||
remote: 'origin',
|
||||
});
|
||||
|
||||
return { commitHash };
|
||||
}
|
||||
|
||||
export async function commitAndPushRepo({
|
||||
dir,
|
||||
auth,
|
||||
logger,
|
||||
commitMessage,
|
||||
gitAuthorInfo,
|
||||
branch = 'master',
|
||||
remoteRef,
|
||||
}: {
|
||||
dir: string;
|
||||
// For use cases where token has to be used with Basic Auth
|
||||
// it has to be provided as password together with a username
|
||||
// which may be a fixed value defined by the provider.
|
||||
auth: { username: string; password: string } | { token: string };
|
||||
logger: Logger;
|
||||
commitMessage: string;
|
||||
gitAuthorInfo?: { name?: string; email?: string };
|
||||
branch?: string;
|
||||
remoteRef?: string;
|
||||
}): Promise<{ commitHash: string }> {
|
||||
const git = Git.fromAuth({
|
||||
...auth,
|
||||
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',
|
||||
};
|
||||
|
||||
const commitHash = await git.commit({
|
||||
dir,
|
||||
message: commitMessage,
|
||||
author: authorInfo,
|
||||
committer: authorInfo,
|
||||
});
|
||||
|
||||
await git.push({
|
||||
dir,
|
||||
remote: 'origin',
|
||||
remoteRef: remoteRef ?? `refs/heads/${branch}`,
|
||||
});
|
||||
|
||||
return { commitHash };
|
||||
}
|
||||
|
||||
type BranchProtectionOptions = {
|
||||
client: Octokit;
|
||||
owner: string;
|
||||
repoName: string;
|
||||
logger: Logger;
|
||||
requireCodeOwnerReviews: boolean;
|
||||
requiredStatusCheckContexts?: string[];
|
||||
bypassPullRequestAllowances?: {
|
||||
users?: string[];
|
||||
teams?: string[];
|
||||
apps?: string[];
|
||||
};
|
||||
requiredApprovingReviewCount?: number;
|
||||
restrictions?: {
|
||||
users: string[];
|
||||
teams: string[];
|
||||
apps?: string[];
|
||||
};
|
||||
requireBranchesToBeUpToDate?: boolean;
|
||||
requiredConversationResolution?: boolean;
|
||||
defaultBranch?: string;
|
||||
enforceAdmins?: boolean;
|
||||
dismissStaleReviews?: boolean;
|
||||
requiredCommitSigning?: boolean;
|
||||
};
|
||||
|
||||
export const enableBranchProtectionOnDefaultRepoBranch = async ({
|
||||
repoName,
|
||||
client,
|
||||
owner,
|
||||
logger,
|
||||
requireCodeOwnerReviews,
|
||||
bypassPullRequestAllowances,
|
||||
requiredApprovingReviewCount,
|
||||
restrictions,
|
||||
requiredStatusCheckContexts = [],
|
||||
requireBranchesToBeUpToDate = true,
|
||||
requiredConversationResolution = false,
|
||||
defaultBranch = 'master',
|
||||
enforceAdmins = true,
|
||||
dismissStaleReviews = false,
|
||||
requiredCommitSigning = false,
|
||||
}: BranchProtectionOptions): Promise<void> => {
|
||||
const tryOnce = async () => {
|
||||
try {
|
||||
await client.rest.repos.updateBranchProtection({
|
||||
mediaType: {
|
||||
/**
|
||||
* 👇 we need this preview because allowing a custom
|
||||
* reviewer count on branch protection is a preview
|
||||
* feature
|
||||
*
|
||||
* More here: https://docs.github.com/en/rest/overview/api-previews#require-multiple-approving-reviews
|
||||
*/
|
||||
previews: ['luke-cage-preview'],
|
||||
},
|
||||
owner,
|
||||
repo: repoName,
|
||||
branch: defaultBranch,
|
||||
required_status_checks: {
|
||||
strict: requireBranchesToBeUpToDate,
|
||||
contexts: requiredStatusCheckContexts,
|
||||
},
|
||||
restrictions: restrictions ?? null,
|
||||
enforce_admins: enforceAdmins,
|
||||
required_pull_request_reviews: {
|
||||
required_approving_review_count: requiredApprovingReviewCount,
|
||||
require_code_owner_reviews: requireCodeOwnerReviews,
|
||||
bypass_pull_request_allowances: bypassPullRequestAllowances,
|
||||
dismiss_stale_reviews: dismissStaleReviews,
|
||||
},
|
||||
required_conversation_resolution: requiredConversationResolution,
|
||||
});
|
||||
|
||||
if (requiredCommitSigning) {
|
||||
await client.rest.repos.createCommitSignatureProtection({
|
||||
owner,
|
||||
repo: repoName,
|
||||
branch: defaultBranch,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
assertError(e);
|
||||
if (
|
||||
e.message.includes(
|
||||
'Upgrade to GitHub Pro or make this repository public to enable this feature',
|
||||
)
|
||||
) {
|
||||
logger.warn(
|
||||
'Branch protection was not enabled as it requires GitHub Pro for private repositories',
|
||||
);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await tryOnce();
|
||||
} catch (e) {
|
||||
if (!e.message.includes('Branch not found')) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
// GitHub has eventual consistency. Fail silently, wait, and try again.
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
await tryOnce();
|
||||
}
|
||||
};
|
||||
|
||||
export function getGitCommitMessage(
|
||||
gitCommitMessage: string | undefined,
|
||||
config: Config,
|
||||
): string | undefined {
|
||||
return gitCommitMessage
|
||||
? gitCommitMessage
|
||||
: config.getOptionalString('scaffolder.defaultCommitMessage');
|
||||
}
|
||||
|
||||
export function entityRefToName(name: string): string {
|
||||
return name.replace(/^.*[:/]/g, '');
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
export { createPublishAzureAction } from './azure';
|
||||
export { createPublishBitbucketAction } from './bitbucket';
|
||||
export { createPublishBitbucketCloudAction } from './bitbucketCloud';
|
||||
export { createPublishBitbucketServerAction } from './bitbucketServer';
|
||||
export { createPublishBitbucketServerPullRequestAction } from './bitbucketServerPullRequest';
|
||||
export { createPublishGerritAction } from './gerrit';
|
||||
export { createPublishGerritReviewAction } from './gerritReview';
|
||||
export { createPublishGithubAction } from './github';
|
||||
export { createPublishGithubPullRequestAction } from './githubPullRequest';
|
||||
export type {
|
||||
CreateGithubPullRequestClientFactoryInput,
|
||||
CreateGithubPullRequestActionOptions,
|
||||
OctokitWithPullRequestPluginClient,
|
||||
} from './githubPullRequest';
|
||||
export { createPublishGitlabAction } from './gitlab';
|
||||
export { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest';
|
||||
@@ -1,181 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 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 path from 'path';
|
||||
import { getRepoSourceDirectory, isExecutable, parseRepoUrl } from './util';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
|
||||
describe('getRepoSourceDirectory', () => {
|
||||
it('should return workspace root if no sub folder is given', () => {
|
||||
expect(
|
||||
getRepoSourceDirectory(path.join('/', 'var', 'workspace'), undefined),
|
||||
).toEqual(path.join('/', 'var', 'workspace'));
|
||||
});
|
||||
|
||||
it('should return path in workspace if sub folder is given', () => {
|
||||
expect(
|
||||
getRepoSourceDirectory(
|
||||
path.join('/', 'var', 'workspace'),
|
||||
path.join('path', 'of', 'subfolder'),
|
||||
),
|
||||
).toEqual(path.join('/', 'var', 'workspace', 'path', 'of', 'subfolder'));
|
||||
});
|
||||
|
||||
it('should not allow traversal outside the workspace root', () => {
|
||||
// We have to construct the path manually here, as path.join would mitigate the path traversal
|
||||
expect(
|
||||
getRepoSourceDirectory(
|
||||
path.join('/', 'var', 'workspace'),
|
||||
`..${path.sep}secret`,
|
||||
),
|
||||
).toEqual(path.join('/', 'var', 'workspace', 'secret'));
|
||||
expect(
|
||||
getRepoSourceDirectory(
|
||||
path.join('/', 'var', 'workspace'),
|
||||
`.${path.sep}path${path.sep}..${path.sep}..${path.sep}secret`,
|
||||
),
|
||||
).toEqual(path.join('/', 'var', 'workspace', 'secret'));
|
||||
expect(
|
||||
getRepoSourceDirectory(
|
||||
path.join('/', 'var', 'workspace'),
|
||||
path.join('/', 'absolute', 'secret'),
|
||||
),
|
||||
).toEqual(path.join('/', 'var', 'workspace', 'absolute', 'secret'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExecutable', () => {
|
||||
it('should return true for file mode 777', () => {
|
||||
expect(isExecutable(0o100777)).toBe(true);
|
||||
});
|
||||
it('should return true for file mode 775', () => {
|
||||
expect(isExecutable(0o100775)).toBe(true);
|
||||
});
|
||||
it('should return true for file mode 755', () => {
|
||||
expect(isExecutable(0o100755)).toBe(true);
|
||||
});
|
||||
it('should return true for file mode 700', () => {
|
||||
expect(isExecutable(0o100700)).toBe(true);
|
||||
});
|
||||
it('should return true for file mode 770', () => {
|
||||
expect(isExecutable(0o100770)).toBe(true);
|
||||
});
|
||||
it('should return true for file mode 670', () => {
|
||||
expect(isExecutable(0o100670)).toBe(true);
|
||||
});
|
||||
it('should return false for file mode 644', () => {
|
||||
expect(isExecutable(0o100644)).toBe(false);
|
||||
});
|
||||
it('should return false for file mode 600', () => {
|
||||
expect(isExecutable(0o100600)).toBe(false);
|
||||
});
|
||||
it('should return false for file mode 640', () => {
|
||||
expect(isExecutable(0o100640)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseRepoUrl', () => {
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
{
|
||||
host: 'www.gitlab.com',
|
||||
token: 'token',
|
||||
apiBaseUrl: 'https://api.gitlab.com',
|
||||
},
|
||||
],
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'www.bitbucket.net',
|
||||
apiBaseUrl: 'https://api.hosted.bitbucket.net',
|
||||
},
|
||||
{
|
||||
host: 'www.bitbucket.org',
|
||||
username: 'username',
|
||||
appPassword: 'password',
|
||||
apiBaseUrl: 'https://api.hosted.bitbucket.org',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const integrations = ScmIntegrations.fromConfig(config);
|
||||
it('should throw an exception if no parameters are passed (gitlab)', () => {
|
||||
expect(() => parseRepoUrl('www.gitlab.com', integrations)).toThrow(
|
||||
'Invalid repo URL passed to publisher: https://www.gitlab.com/, missing owner',
|
||||
);
|
||||
});
|
||||
it('should return the project ID in the `project` parameter (gitlab)', () => {
|
||||
expect(
|
||||
parseRepoUrl('www.gitlab.com?project=234', integrations),
|
||||
).toStrictEqual({
|
||||
host: 'www.gitlab.com',
|
||||
organization: undefined,
|
||||
owner: undefined,
|
||||
project: '234',
|
||||
repo: null,
|
||||
workspace: undefined,
|
||||
});
|
||||
});
|
||||
it('should throw an exception if the repo parameter is missing, but owner parameter is set', () => {
|
||||
expect(() =>
|
||||
parseRepoUrl('www.gitlab.com?owner=owner', integrations),
|
||||
).toThrow(
|
||||
'Invalid repo URL passed to publisher: https://www.gitlab.com/?owner=owner, missing repo',
|
||||
);
|
||||
});
|
||||
it('should throw an exception if the owner parameter is missing, but repo parameter is set (gitlab)', () => {
|
||||
expect(() =>
|
||||
parseRepoUrl('www.gitlab.com?repo=repo', integrations),
|
||||
).toThrow(
|
||||
'Invalid repo URL passed to publisher: https://www.gitlab.com/?repo=repo, missing owner',
|
||||
);
|
||||
});
|
||||
it('should return the workspace, project and repo (bitbucket.org)', () => {
|
||||
expect(
|
||||
parseRepoUrl(
|
||||
'www.bitbucket.org?project=project&workspace=workspace&repo=repo',
|
||||
integrations,
|
||||
),
|
||||
).toStrictEqual({
|
||||
host: 'www.bitbucket.org',
|
||||
organization: undefined,
|
||||
owner: undefined,
|
||||
project: 'project',
|
||||
repo: 'repo',
|
||||
workspace: 'workspace',
|
||||
});
|
||||
});
|
||||
it('should return the project and repo (another bitbucket instance)', () => {
|
||||
expect(
|
||||
parseRepoUrl('www.bitbucket.net?project=project&repo=repo', integrations),
|
||||
).toStrictEqual({
|
||||
host: 'www.bitbucket.net',
|
||||
organization: undefined,
|
||||
owner: undefined,
|
||||
project: 'project',
|
||||
repo: 'repo',
|
||||
workspace: undefined,
|
||||
});
|
||||
});
|
||||
it('should throw an exception if the workspace parameter is missing for bitbucket.org instance (bitbucket.org)', () => {
|
||||
expect(() =>
|
||||
parseRepoUrl('www.bitbucket.org?project=project&repo=repo', integrations),
|
||||
).toThrow(
|
||||
'Invalid repo URL passed to publisher: https://www.bitbucket.org/?project=project&repo=repo, missing workspace',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -50,12 +50,15 @@
|
||||
"@backstage/plugin-scaffolder-common": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"fs-extra": "10.1.0",
|
||||
"globby": "^11.0.0",
|
||||
"jsonschema": "^1.2.6",
|
||||
"p-limit": "^3.1.0",
|
||||
"winston": "^3.2.1",
|
||||
"zod": "^3.21.4",
|
||||
"zod-to-json-schema": "^3.20.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/config": "workspace:^"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user