diff --git a/plugins/scaffolder-backend-module-gerrit/.eslintrc.js b/plugins/scaffolder-backend-module-gerrit/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gerrit/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-gerrit/README.md b/plugins/scaffolder-backend-module-gerrit/README.md new file mode 100644 index 0000000000..f1492f5b35 --- /dev/null +++ b/plugins/scaffolder-backend-module-gerrit/README.md @@ -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_ diff --git a/plugins/scaffolder-backend-module-gerrit/package.json b/plugins/scaffolder-backend-module-gerrit/package.json new file mode 100644 index 0000000000..f3797accb3 --- /dev/null +++ b/plugins/scaffolder-backend-module-gerrit/package.json @@ -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" + ] +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.examples.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.examples.ts similarity index 100% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.examples.ts rename to plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.examples.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts similarity index 97% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.test.ts rename to plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts index 73dcc47225..b56c27f59e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.test.ts @@ -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({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.ts similarity index 97% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts rename to plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.ts index 67ed868c4c..b7369fd5d8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerrit.ts @@ -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 ( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.test.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts similarity index 93% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.test.ts rename to plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts index 55d7bde5a7..eeae3871ee 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.test.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.test.ts @@ -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({ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.ts similarity index 96% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts rename to plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.ts index 73e74f5cbc..bd8d0321ab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerritReview.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/gerritReview.ts @@ -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'); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts b/plugins/scaffolder-backend-module-gerrit/src/actions/index.ts similarity index 84% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts rename to plugins/scaffolder-backend-module-gerrit/src/actions/index.ts index 7ad21e34ba..1918679320 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.ts +++ b/plugins/scaffolder-backend-module-gerrit/src/actions/index.ts @@ -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'; diff --git a/plugins/scaffolder-backend-module-gerrit/src/index.ts b/plugins/scaffolder-backend-module-gerrit/src/index.ts new file mode 100644 index 0000000000..21fdd116e7 --- /dev/null +++ b/plugins/scaffolder-backend-module-gerrit/src/index.ts @@ -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'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts deleted file mode 100644 index 7362e99892..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.test.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts deleted file mode 100644 index 5ac92128ca..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ /dev/null @@ -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 => { - 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, ''); -} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts deleted file mode 100644 index bed3a2316c..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ /dev/null @@ -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'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts deleted file mode 100644 index 5446109056..0000000000 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/util.test.ts +++ /dev/null @@ -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', - ); - }); -}); diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index 5f5702a035..877a465052 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -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:^" },