diff --git a/.changeset/nasty-nails-appear.md b/.changeset/nasty-nails-appear.md new file mode 100644 index 0000000000..fdb35f052c --- /dev/null +++ b/.changeset/nasty-nails-appear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-gitlab': minor +--- + +Add Gitlab Scaffolder Plugin diff --git a/plugins/scaffolder-backend-module-gitlab/.eslintrc.js b/plugins/scaffolder-backend-module-gitlab/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md new file mode 100644 index 0000000000..35353cf9c1 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/README.md @@ -0,0 +1,162 @@ +# scaffolder-backend-module-gitlab + +Welcome to the Gitlab Module for Scaffolder. + +Here you can find all Gitlab related features to improve your scaffolder: + +## Getting started + +## From your Backstage root directory + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-gitlab +``` + +Configure the action: +(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options): + +```typescript +// packages/backend/src/plugins/scaffolder.ts + +import { + createGitlabProjectAccessTokenAction, + createGitlabProjectAccessTokenAction, + createGitlabProjectDeployTokenAction, +} from '@backstage/plugin-scaffolder-backend-module-gitlab'; + +// Create BuiltIn Actions +const builtInActions = createBuiltinActions({ + integrations, + catalogClient, + config: env.config, + reader: env.reader, +}); + +// Add Gitlab Actions +const actions = [ + ...builtInActions, + createGitlabProjectAccessTokenAction({ + integrations: integrations, + }), + createGitlabProjectAccessTokenAction({ + integrations: integrations, + }), + createGitlabProjectDeployTokenAction({ + integrations: integrations, + }), +]; + +// Create Scaffolder Router +return await createRouter({ + containerRunner, + catalogClient, + actions, + logger: env.logger, + config: env.config, + database: env.database, + reader: env.reader, +}); +``` + +After that you can use the action in your template: + +```yaml +apiVersion: scaffolder.backstage.io/v1beta3 +kind: Template +metadata: + name: gitlab-demo + title: Gitlab DEMO + description: Scaffolder Gitlab Demo +spec: + owner: backstage/techdocs-core + type: service + + parameters: + - title: Fill in some steps + required: + - name + properties: + name: + title: Name + type: string + description: Unique name of the component + ui:autofocus: true + ui:options: + rows: 5 + - title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - gitlab.com + + steps: + - id: fetch + name: Fetch + action: fetch:template + input: + url: https://github.com/TEMPLATE + values: + name: ${{ parameters.name }} + + - id: publish + name: Publish + action: publish:gitlab + input: + description: This is ${{ parameters.name }} + repoUrl: ${{ parameters.repoUrl }} + sourcePath: pimcore + defaultBranch: main + + - id: gitlab-deploy-token + name: Create Deploy Token + action: gitlab:projectDeployToken:create + input: + repoUrl: ${{ parameters.repoUrl }} + projectId: "${{ steps['publish'].output.projectId }}" + name: ${{ parameters.name }}-secret + username: ${{ parameters.name }}-secret + scopes: ['read_registry'] + + - id: gitlab-access-token + name: Gitlab Project Access Token + action: gitlab:projectAccessToken:create + input: + repoUrl: ${{ parameters.repoUrl }} + projectId: "${{ steps['publish-manifest'].output.projectId }}" + name: ${{ parameters.name }}-access-token + accessLevel: 40 + scopes: ['read_repository', 'write_repository'] + + - id: gitlab-project-variable + name: Gitlab Project Variable + action: gitlab:projectVariable:create + input: + repoUrl: ${{ parameters.repoUrl }} + projectId: "${{ steps['publish'].output.projectId }}" + key: 'VARIABLE_NAME' + value: "${{ steps['gitlab-access-token'].output.access_token }}" + variableType: 'env_var' + masked: true + variableProtected: false + raw: false + environmentScope: '*' + + - id: register + name: Register + action: catalog:register + input: + repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} + catalogInfoPath: '/catalog-info.yaml' + + output: + links: + - title: Repository + url: ${{ steps['publish'].output.remoteUrl }} +``` diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md new file mode 100644 index 0000000000..6db732fdcc --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/api-report.md @@ -0,0 +1,48 @@ +## API Report File for "@backstage/plugin-scaffolder-backend-module-gitlab" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; + +// @public +export const createGitlabProjectAccessTokenAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction<{ + repoUrl: string; + projectId: string | number; + name: string; + accessLevel: number; + scopes: string[]; + token?: string | undefined; +}>; + +// @public +export const createGitlabProjectDeployTokenAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction<{ + repoUrl: string; + projectId: string | number; + name: string; + username: string; + scopes: string[]; + token?: string | undefined; +}>; + +// @public +export const createGitlabProjectVariableAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction<{ + repoUrl: string; + projectId: string | number; + key: string; + value: string; + variableType: string; + variableProtected: boolean; + masked: boolean; + raw: boolean; + environmentScope: string; + token?: string | undefined; +}>; +``` diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json new file mode 100644 index 0000000000..5e02ccc89f --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/plugin-scaffolder-backend-module-gitlab", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin-module" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/scaffolder-backend-module-gitlab" + }, + "keywords": [ + "backstage" + ], + "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/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/integration": "workspace:^", + "@backstage/plugin-scaffolder-node": "workspace:^", + "@gitbeaker/node": "^35.8.0" + }, + "devDependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts new file mode 100644 index 0000000000..0f39e3bc33 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts @@ -0,0 +1,112 @@ +/* + * 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 { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { getToken } from '../util'; + +/** + * Creates a `gitlab:create-project-access-token` Scaffolder action. + * + * @param options - Templating configuration. + * @public + */ +export const createGitlabProjectAccessTokenAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + return createTemplateAction<{ + repoUrl: string; + projectId: string | number; + name: string; + accessLevel: number; + scopes: string[]; + token?: string; + }>({ + id: 'gitlab:projectAccessToken:create', + schema: { + input: { + required: ['projectId', 'repoUrl'], + type: 'object', + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + projectId: { + title: 'Project ID', + type: ['string', 'number'], + }, + name: { + title: 'Deploy Token Name', + type: 'string', + }, + accessLevel: { + title: 'Access Level of the Token', + type: 'number', + }, + scopes: { + title: 'Scopes', + type: 'array', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitLab', + }, + }, + }, + output: { + type: 'object', + properties: { + access_token: { + title: 'Access Token', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`); + const { repoUrl, projectId, name, accessLevel, scopes } = ctx.input; + const { token, integrationConfig } = getToken( + repoUrl, + ctx.input.token, + integrations, + ); + + const response = await fetch( + `${integrationConfig.config.baseUrl}/api/v4/projects/${projectId}/access_tokens`, + { + method: 'POST', // *GET, POST, PUT, DELETE, etc. + headers: { + 'PRIVATE-TOKEN': token, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + name: name, + scopes: scopes, + access_level: accessLevel, + }), + }, + ); + + const result = await response.json(); + + ctx.output('access_token', result.token); + }, + }); +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts new file mode 100644 index 0000000000..1d3ae804c1 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { createGitlabProjectDeployTokenAction } from './createGitlabProjectDeployTokenAction'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; + +const mockGitlabClient = { + ProjectDeployTokens: { + add: jest.fn(), + }, +}; +jest.mock('@gitbeaker/node', () => ({ + Gitlab: class { + constructor() { + return mockGitlabClient; + } + }, +})); + +describe('gitlab:create-deploy-token', () => { + const config = new ConfigReader({ + integrations: { + gitlab: [ + { + host: 'gitlab.com', + token: 'tokenlols', + apiBaseUrl: 'https://api.gitlab.com', + }, + { + host: 'hosted.gitlab.com', + apiBaseUrl: 'https://api.hosted.gitlab.com', + }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + const action = createGitlabProjectDeployTokenAction({ integrations }); + const mockContext = { + input: { + repoUrl: 'gitlab.com?repo=repo&owner=owner', + projectId: '123', + name: 'tokenname', + username: 'tokenuser', + scopes: ['read_repository'], + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should work when there is a token provided through ctx.input', async () => { + mockGitlabClient.ProjectDeployTokens.add.mockResolvedValue({ + token: 'TOKEN', + username: 'User', + }); + + await action.handler({ + ...mockContext, + input: { + repoUrl: 'hosted.gitlab.com?repo=bob&owner=owner', + projectId: '123', + name: 'tokenname', + username: 'tokenuser', + scopes: ['read_repository'], + }, + }); + + expect(mockGitlabClient.ProjectDeployTokens.add).toHaveBeenCalledWith( + '123', + 'tokenname', + ['read_repository'], + { username: 'tokenuser' }, + ); + + expect(mockContext.output).toHaveBeenCalledWith('deploy_token', 'TOKEN'); + expect(mockContext.output).toHaveBeenCalledWith('user', 'User'); + }); +}); diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts new file mode 100644 index 0000000000..35a53c87e2 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts @@ -0,0 +1,120 @@ +/* + * 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 { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { Gitlab } from '@gitbeaker/node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { DeployTokenScope } from '@gitbeaker/core/dist/types/templates/ResourceDeployTokens'; +import { getToken } from '../util'; +import { InputError } from '@backstage/errors'; + +/** + * Creates a `gitlab:create-project-deploy-token` Scaffolder action. + * + * @param options - Templating configuration. + * @public + */ +export const createGitlabProjectDeployTokenAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + return createTemplateAction<{ + repoUrl: string; + projectId: string | number; + name: string; + username: string; + scopes: string[]; + token?: string; + }>({ + id: 'gitlab:projectDeployToken:create', + schema: { + input: { + required: ['projectId', 'repoUrl'], + type: 'object', + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + projectId: { + title: 'Project ID', + type: ['string', 'number'], + }, + name: { + title: 'Deploy Token Name', + type: 'string', + }, + username: { + title: 'Deploy Token Username', + type: 'string', + }, + scopes: { + title: 'Scopes', + type: 'array', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitLab', + }, + }, + }, + output: { + type: 'object', + properties: { + deploy_token: { + title: 'Deploy Token', + type: 'string', + }, + user: { + title: 'User', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`); + const { repoUrl, projectId, name, username, scopes } = ctx.input; + const { token, integrationConfig } = getToken( + repoUrl, + ctx.input.token, + integrations, + ); + + const api = new Gitlab({ + host: integrationConfig.config.baseUrl, + token: token, + }); + + const deployToken = await api.ProjectDeployTokens.add( + projectId, + name, + scopes as DeployTokenScope[], + { + username: username, + }, + ); + + if (!deployToken.hasOwnProperty('token')) { + throw new InputError(`No deploy_token given from gitlab instance`); + } + + ctx.output('deploy_token', deployToken.token as string); + ctx.output('user', deployToken.username); + }, + }); +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts new file mode 100644 index 0000000000..847bbc2057 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts @@ -0,0 +1,139 @@ +/* + * 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 { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { getToken } from '../util'; +import { Gitlab } from '@gitbeaker/node'; + +/** + * Creates a `gitlab:create-project-variable` Scaffolder action. + * + * @param options - Templating configuration. + * @public + */ +export const createGitlabProjectVariableAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + return createTemplateAction<{ + repoUrl: string; + projectId: string | number; + key: string; + value: string; + variableType: string; + variableProtected: boolean; + masked: boolean; + raw: boolean; + environmentScope: string; + token?: string; + }>({ + id: 'gitlab:projectVariable:create', + schema: { + input: { + required: [ + 'repoUrl', + 'projectId', + 'key', + 'value', + 'variableType', + 'variableProtected', + 'masked', + 'raw', + 'environmentScope', + ], + type: 'object', + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + projectId: { + title: 'Project ID', + type: ['string', 'number'], + }, + key: { + title: + 'The key of a variable; must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed', + type: 'string', + }, + value: { + title: 'The value of a variable', + type: 'string', + }, + variableType: { + title: 'Variable Type (env_var or file)', + type: 'string', + }, + variableProtected: { + title: 'Whether the variable is protected. Default: false', + type: 'boolean', + }, + masked: { + title: 'Whether the variable is masked. Default: false', + type: 'boolean', + }, + raw: { + title: 'Whether the variable is expandable. Default: false', + type: 'boolean', + }, + environmentScope: { + title: 'The environment_scope of the variable. Default: *', + type: 'string', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitLab', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + projectId, + key, + value, + variableType, + variableProtected, + masked, + raw, + environmentScope, + } = ctx.input; + const { token, integrationConfig } = getToken( + repoUrl, + ctx.input.token, + integrations, + ); + + const api = new Gitlab({ + host: integrationConfig.config.baseUrl, + token: token, + }); + + await api.ProjectVariables.create(projectId, { + key: key, + value: value, + variable_type: variableType, + protected: variableProtected, + masked: masked, + raw: raw, + environment_scope: environmentScope, + }); + }, + }); +}; diff --git a/plugins/scaffolder-backend-module-gitlab/src/index.ts b/plugins/scaffolder-backend-module-gitlab/src/index.ts new file mode 100644 index 0000000000..11209e19bd --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/index.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +/** + * A module for the scaffolder backend that lets you create gitlab project access tokens or deploy tokens + * + * @packageDocumentation + */ + +export * from './actions/createGitlabProjectDeployTokenAction'; +export * from './actions/createGitlabProjectAccessTokenAction'; +export * from './actions/createGitlabProjectVariableAction'; diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts new file mode 100644 index 0000000000..fc6667d6d3 --- /dev/null +++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts @@ -0,0 +1,57 @@ +/* + * 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 { InputError } from '@backstage/errors'; +import { + GitLabIntegration, + ScmIntegrationRegistry, +} from '@backstage/integration'; + +export const parseRepoHost = (repoUrl: string): string => { + let parsed; + try { + parsed = new URL(`https://${repoUrl}`); + } catch (error) { + throw new InputError( + `Invalid repo URL passed to publisher, got ${repoUrl}, ${error}`, + ); + } + return parsed.host; +}; + +export const getToken = ( + repoUrl: string, + inputToken: string | null | undefined, + integrations: ScmIntegrationRegistry, +): { token: string; integrationConfig: GitLabIntegration } => { + const host = parseRepoHost(repoUrl); + const integrationConfig = integrations.gitlab.byHost(host); + + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + const token = inputToken || integrationConfig.config.token!; + const tokenType = inputToken ? 'oauthToken' : 'token'; + + if (tokenType === 'oauthToken') { + throw new InputError(`OAuth Token is currently not supported`); + } + + return { token: token, integrationConfig: integrationConfig }; +}; diff --git a/yarn.lock b/yarn.lock index 8e28fd42bb..436474b158 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7500,6 +7500,21 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab": + version: 0.0.0-use.local + resolution: "@backstage/plugin-scaffolder-backend-module-gitlab@workspace:plugins/scaffolder-backend-module-gitlab" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/integration": "workspace:^" + "@backstage/plugin-scaffolder-node": "workspace:^" + "@gitbeaker/node": ^35.8.0 + languageName: unknown + linkType: soft + "@backstage/plugin-scaffolder-backend-module-rails@workspace:^, @backstage/plugin-scaffolder-backend-module-rails@workspace:plugins/scaffolder-backend-module-rails": version: 0.0.0-use.local resolution: "@backstage/plugin-scaffolder-backend-module-rails@workspace:plugins/scaffolder-backend-module-rails" @@ -9856,7 +9871,7 @@ __metadata: languageName: node linkType: hard -"@gitbeaker/node@npm:^35.1.0": +"@gitbeaker/node@npm:^35.1.0, @gitbeaker/node@npm:^35.8.0": version: 35.8.1 resolution: "@gitbeaker/node@npm:35.8.1" dependencies: