diff --git a/workspaces/x2a/.changeset/large-signs-serve.md b/workspaces/x2a/.changeset/large-signs-serve.md new file mode 100644 index 00000000000..a11474b599e --- /dev/null +++ b/workspaces/x2a/.changeset/large-signs-serve.md @@ -0,0 +1,7 @@ +--- +'@red-hat-developer-hub/backstage-plugin-scaffolder-backend-module-x2a': patch +'@red-hat-developer-hub/backstage-plugin-x2a-common': patch +'@red-hat-developer-hub/backstage-plugin-x2a': patch +--- + +Add GitLab support. diff --git a/workspaces/x2a/README.md b/workspaces/x2a/README.md index ab429f58a28..c5a1c06d898 100644 --- a/workspaces/x2a/README.md +++ b/workspaces/x2a/README.md @@ -22,7 +22,7 @@ See the [backend plugin README](./plugins/x2a-backend/README.md) for detailed co ### Prerequisites -- Node.js >20 +- Node.js >22 - Yarn package manager - Kubernetes cluster access (optional, for Kubernetes features) @@ -34,12 +34,28 @@ See the [backend plugin README](./plugins/x2a-backend/README.md) for detailed co yarn install ``` -2. Start the development environment with just the plugin loaded: +2. **Optional:** Update `app-config.yaml` based on your environment. + - **`auth:`** + - Configure authentication providers for sign-in and SCM access (GitHub, GitLab). See [Backstage auth docs](https://backstage.io/docs/auth/). + - Based on your options of auth-providers, mind updating the `conversion-project-template.yaml` for source and target repository URLs. + - **`x2a:`** - Provide LLM credentials, Ansible Automation Platform connection details, and Kubernetes resource limits. See [x2a-convertor technical details](https://github.com/x2ansible/x2a-convertor?tab=readme-ov-file#technical-details). + +3. Start the development environment with just the plugin loaded: + + **GitHub OAuth**: [Create a GitHub OAuth application](https://github.com/settings/developers). + + **GitLab OAuth:** When [creating a GitLab OAuth application](https://gitlab.com/-/user_settings/applications), request these scopes: + - `read_api`, `read_user`, `read_repository`, `write_repository` + - `openid`, `profile`, `email` ```sh export AUTH_GITHUB_CLIENT_ID=.... # Optional if "guest" user is not enough export AUTH_GITHUB_CLIENT_SECRET=... # Optional if "guest" user is not enough + # For GitLab auth (create app at https://gitlab.com/-/user_settings/applications): + export AUTH_GITLAB_CLIENT_ID=... + export AUTH_GITLAB_CLIENT_SECRET=... + yarn dev ``` @@ -50,6 +66,8 @@ See the [backend plugin README](./plugins/x2a-backend/README.md) for detailed co ```sh export AUTH_GITHUB_CLIENT_ID=.... # Optional if "guest" user is not enough export AUTH_GITHUB_CLIENT_SECRET=... # Optional if "guest" user is not enough + export AUTH_GITLAB_CLIENT_ID=... + export AUTH_GITLAB_CLIENT_SECRET=... yarn start ``` @@ -192,7 +210,7 @@ The plugin uses the Kubernetes client library to interact with Kubernetes cluste By default, it loads configuration from your local `~/.kube/config` file. -Additional methods are planed to be implemented later. +Additional methods are planned to be implemented later. ### Local Development Setup @@ -224,7 +242,7 @@ Loaded Kubernetes configuration from ~/.kube/config - `yarn test` - Run tests - `yarn lint` - Run linter -- `prettier:fix` - Fix by prettier +- `yarn prettier:fix` - Fix code formatting - `yarn build:all` - Build all packages - `yarn clean` - Clean build artifacts diff --git a/workspaces/x2a/app-config.yaml b/workspaces/x2a/app-config.yaml index cd572486717..221d1829452 100644 --- a/workspaces/x2a/app-config.yaml +++ b/workspaces/x2a/app-config.yaml @@ -33,15 +33,19 @@ backend: # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir integrations: - github: - - host: github.com - # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information - # about setting up the GitHub integration here: https://backstage.io/docs/integrations/github/locations#configuration - token: ${GITHUB_TOKEN} - ### Example for how to add your GitHub Enterprise instance using the API: - # - host: ghe.example.net - # apiBaseUrl: https://ghe.example.net/api/v3 - # token: ${GHE_TOKEN} +# So far no need for integrations +# github: +# - host: github.com +# # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information +# # about setting up the GitHub integration here: https://backstage.io/docs/integrations/github/locations#configuration +# token: ${GITHUB_TOKEN} +# gitlab: +# - host: gitlab.cee.redhat.com +# token: ${GITLAB_TOKEN} +### Example for how to add your GitHub Enterprise instance using the API: +# - host: ghe.example.net +# apiBaseUrl: https://ghe.example.net/api/v3 +# token: ${GHE_TOKEN} proxy: ### Example for how to add a proxy endpoint for the frontend. @@ -62,6 +66,10 @@ techdocs: publisher: type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. +# Use redirect flow instead of popup; fixes "Missing session cookie" on refresh +# when the popup and main window have different cookie contexts +#enableExperimentalRedirectFlow: true + auth: # see https://backstage.io/docs/auth/ to learn about auth providers environment: development @@ -70,12 +78,13 @@ auth: guest: {} gitlab: development: + # Create an OAuth app on https://gitlab.com/-/user_settings/applications clientId: ${AUTH_GITLAB_CLIENT_ID} clientSecret: ${AUTH_GITLAB_CLIENT_SECRET} + audience: https://gitlab.com signIn: resolvers: - - resolver: emailMatchingUserEntityProfileEmail - # - resolver: usernameMatchingUserEntityName + - resolver: usernameMatchingUserEntityName github: development: clientId: ${AUTH_GITHUB_CLIENT_ID} diff --git a/workspaces/x2a/package.json b/workspaces/x2a/package.json index 1faff0245aa..243348c0881 100644 --- a/workspaces/x2a/package.json +++ b/workspaces/x2a/package.json @@ -59,7 +59,8 @@ "@backstage/backend-defaults": "0.13.1", "@backstage/backend-plugin-api": "1.5.0", "@backstage-community/plugin-rbac-backend": "~7.4.0", - "@backstage/plugin-permission-react": "0.4.36" + "@backstage/plugin-permission-react": "0.4.36", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.9" }, "prettier": "@backstage/cli/config/prettier", "lint-staged": { diff --git a/workspaces/x2a/packages/app/src/App.tsx b/workspaces/x2a/packages/app/src/App.tsx index a87240bfc1c..c9c3fc2e2f9 100644 --- a/workspaces/x2a/packages/app/src/App.tsx +++ b/workspaces/x2a/packages/app/src/App.tsx @@ -56,7 +56,7 @@ import { X2APage, x2aPluginTranslations, } from '@red-hat-developer-hub/backstage-plugin-x2a'; -import { githubAuthApiRef } from '@backstage/core-plugin-api'; +import { githubAuthApiRef, gitlabAuthApiRef } from '@backstage/core-plugin-api'; const app = createApp({ apis, @@ -90,6 +90,12 @@ const app = createApp({ message: 'Sign in using GitHub', apiRef: githubAuthApiRef, }, + { + id: 'gitlab-auth-provider', + title: 'GitLab', + message: 'Sign in using GitLab', + apiRef: gitlabAuthApiRef, + }, ]} /> ), diff --git a/workspaces/x2a/packages/backend/package.json b/workspaces/x2a/packages/backend/package.json index e098e1bafaa..f5e85c460e8 100644 --- a/workspaces/x2a/packages/backend/package.json +++ b/workspaces/x2a/packages/backend/package.json @@ -27,6 +27,7 @@ "@backstage/plugin-app-backend": "^0.5.8", "@backstage/plugin-auth-backend": "^0.25.6", "@backstage/plugin-auth-backend-module-github-provider": "^0.3.9", + "@backstage/plugin-auth-backend-module-gitlab-provider": "^0.3.9", "@backstage/plugin-auth-backend-module-guest-provider": "^0.2.14", "@backstage/plugin-auth-node": "^0.6.9", "@backstage/plugin-catalog-backend": "^3.2.0", @@ -40,6 +41,7 @@ "@backstage/plugin-proxy-backend": "^0.6.8", "@backstage/plugin-scaffolder-backend": "^3.0.1", "@backstage/plugin-scaffolder-backend-module-github": "^0.9.2", + "@backstage/plugin-scaffolder-backend-module-gitlab": "^0.11.3", "@backstage/plugin-scaffolder-backend-module-notifications": "^0.1.16", "@backstage/plugin-search-backend": "^2.0.8", "@backstage/plugin-search-backend-module-catalog": "^0.3.10", diff --git a/workspaces/x2a/packages/backend/src/index.ts b/workspaces/x2a/packages/backend/src/index.ts index 4d9cdf051df..78630314976 100644 --- a/workspaces/x2a/packages/backend/src/index.ts +++ b/workspaces/x2a/packages/backend/src/index.ts @@ -24,6 +24,7 @@ backend.add(import('@backstage/plugin-proxy-backend')); // scaffolder plugin backend.add(import('@backstage/plugin-scaffolder-backend')); backend.add(import('@backstage/plugin-scaffolder-backend-module-github')); +backend.add(import('@backstage/plugin-scaffolder-backend-module-gitlab')); backend.add( import('@backstage/plugin-scaffolder-backend-module-notifications'), ); @@ -36,6 +37,8 @@ backend.add(import('@backstage/plugin-auth-backend')); // See https://backstage.io/docs/backend-system/building-backends/migrating#the-auth-plugin backend.add(import('@backstage/plugin-auth-backend-module-guest-provider')); backend.add(import('@backstage/plugin-auth-backend-module-github-provider')); +backend.add(import('@backstage/plugin-auth-backend-module-gitlab-provider')); + // See https://backstage.io/docs/auth/guest/provider // catalog plugin diff --git a/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts b/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts index 5d358951cd3..dc7992709a5 100644 --- a/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts +++ b/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.test.ts @@ -63,7 +63,7 @@ describe('x2a:project:create', () => { abbreviation: 'PRJ', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: false, + areTargetAndSourceRepoShared: false, targetRepoUrl: 'https://github.com/org/target-repo', targetRepoBranch: 'main', }, @@ -126,7 +126,7 @@ describe('x2a:project:create', () => { abbreviation: 'ABBR', sourceRepoUrl: 'https://github.com/org/repo2', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: true, + areTargetAndSourceRepoShared: true, targetRepoBranch: 'main', }, secrets: { @@ -195,7 +195,7 @@ describe('x2a:project:create', () => { ownedByGroup: 'group:default/team-a', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: true, + areTargetAndSourceRepoShared: true, targetRepoBranch: 'main', }, secrets: { @@ -252,7 +252,7 @@ describe('x2a:project:create', () => { ownedByGroup: ' group:default/team-b ', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: true, + areTargetAndSourceRepoShared: true, targetRepoBranch: 'main', }, secrets: { @@ -265,6 +265,69 @@ describe('x2a:project:create', () => { expect(createProjectBody.ownedByGroup).toBe('group:default/team-b'); }); + it('should pass userPrompt to the init-phase API when provided', async () => { + const createdProject = { + id: 'project-uuid-prompt', + abbreviation: 'PRM', + name: 'Prompt Project', + description: '', + createdAt: '2025-01-01T00:00:00.000Z', + createdBy: 'user:default/jane', + }; + + let runRequestBody: Record = {}; + mockFetch.mockImplementation((_url: string, options?: RequestInit) => { + const body = options?.body ? JSON.parse(options.body as string) : {}; + if (body.sourceRepoAuth && body.targetRepoAuth) { + runRequestBody = body; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ + status: 'pending', + jobId: 'init-job-prompt', + }), + }); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(createdProject), + }); + }); + + const action = createProjectAction(mockDiscoveryApi, { + fetchApi: { fetch: mockFetch }, + }); + const mockContext = createMockActionContext({ + input: { + name: 'Prompt Project', + abbreviation: 'PRM', + sourceRepoUrl: 'https://github.com/org/repo', + sourceRepoBranch: 'main', + areTargetAndSourceRepoShared: true, + targetRepoBranch: 'main', + userPrompt: 'Convert this to Ansible playbook with best practices', + }, + secrets: { + SRC_USER_OAUTH_TOKEN: 'mock-source-token', + }, + }); + + await action.handler(mockContext); + + expect(runRequestBody).toMatchObject({ + userPrompt: 'Convert this to Ansible playbook with best practices', + }); + expect(mockContext.output).toHaveBeenCalledWith( + 'projectId', + 'project-uuid-prompt', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'initJobId', + 'init-job-prompt', + ); + }); + it('should omit ownedByGroup from the request body when not provided', async () => { const createdProject = { id: 'project-uuid-no-group', @@ -304,7 +367,7 @@ describe('x2a:project:create', () => { abbreviation: 'NOG', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: true, + areTargetAndSourceRepoShared: true, targetRepoBranch: 'main', }, secrets: { @@ -361,7 +424,7 @@ describe('x2a:project:create', () => { abbreviation: 'TKN', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: false, + areTargetAndSourceRepoShared: false, targetRepoUrl: 'https://github.com/org/target-repo', targetRepoBranch: 'main', }, @@ -418,7 +481,7 @@ describe('x2a:project:create', () => { abbreviation: 'LOG', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: false, + areTargetAndSourceRepoShared: false, targetRepoUrl: 'https://github.com/org/target-repo', targetRepoBranch: 'main', }, @@ -439,6 +502,257 @@ describe('x2a:project:create', () => { ); }); + describe('augmentRepoToken', () => { + const createdProject = { + id: 'project-augment', + abbreviation: 'AUG', + name: 'Augment Project', + description: '', + createdAt: '2025-01-01T00:00:00.000Z', + createdBy: 'user:default/test', + }; + + it('should pass tokens as-is for GitHub URLs (no oauth2: prefix)', async () => { + let runRequestBody: { + sourceRepoAuth?: { token: string }; + targetRepoAuth?: { token: string }; + } = {}; + mockFetch.mockImplementation((_url: string, options?: RequestInit) => { + const body = options?.body ? JSON.parse(options.body as string) : {}; + if (body.sourceRepoAuth && body.targetRepoAuth) { + runRequestBody = body; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ status: 'pending', jobId: 'init-job-augment' }), + }); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(createdProject), + }); + }); + + const action = createProjectAction(mockDiscoveryApi, { + fetchApi: { fetch: mockFetch }, + }); + const mockContext = createMockActionContext({ + input: { + name: 'Augment Project', + abbreviation: 'AUG', + sourceRepoUrl: 'https://github.com/org/source-repo', + sourceRepoBranch: 'main', + areTargetAndSourceRepoShared: false, + targetRepoUrl: 'https://github.com/org/target-repo', + targetRepoBranch: 'main', + }, + secrets: { + SRC_USER_OAUTH_TOKEN: 'gh-source-token', + TGT_USER_OAUTH_TOKEN: 'gh-target-token', + }, + }); + + await action.handler(mockContext); + + expect(runRequestBody.sourceRepoAuth?.token).toBe('gh-source-token'); + expect(runRequestBody.targetRepoAuth?.token).toBe('gh-target-token'); + }); + + it('should prefix tokens with oauth2: for GitLab URLs', async () => { + let runRequestBody: { + sourceRepoAuth?: { token: string }; + targetRepoAuth?: { token: string }; + } = {}; + mockFetch.mockImplementation((_url: string, options?: RequestInit) => { + const body = options?.body ? JSON.parse(options.body as string) : {}; + if (body.sourceRepoAuth && body.targetRepoAuth) { + runRequestBody = body; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ status: 'pending', jobId: 'init-job-augment' }), + }); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(createdProject), + }); + }); + + const action = createProjectAction(mockDiscoveryApi, { + fetchApi: { fetch: mockFetch }, + }); + const mockContext = createMockActionContext({ + input: { + name: 'Augment Project', + abbreviation: 'AUG', + sourceRepoUrl: 'https://gitlab.com/org/source-repo', + sourceRepoBranch: 'main', + areTargetAndSourceRepoShared: false, + targetRepoUrl: 'https://gitlab.com/org/target-repo', + targetRepoBranch: 'main', + }, + secrets: { + SRC_USER_OAUTH_TOKEN: 'gl-source-token', + TGT_USER_OAUTH_TOKEN: 'gl-target-token', + }, + }); + + await action.handler(mockContext); + + expect(runRequestBody.sourceRepoAuth?.token).toBe( + 'oauth2:gl-source-token', + ); + expect(runRequestBody.targetRepoAuth?.token).toBe( + 'oauth2:gl-target-token', + ); + }); + + it('should prefix source token with oauth2: when source is GitLab (shared repos)', async () => { + let runRequestBody: { + sourceRepoAuth?: { token: string }; + targetRepoAuth?: { token: string }; + } = {}; + mockFetch.mockImplementation((_url: string, options?: RequestInit) => { + const body = options?.body ? JSON.parse(options.body as string) : {}; + if (body.sourceRepoAuth && body.targetRepoAuth) { + runRequestBody = body; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ status: 'pending', jobId: 'init-job-augment' }), + }); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(createdProject), + }); + }); + + const action = createProjectAction(mockDiscoveryApi, { + fetchApi: { fetch: mockFetch }, + }); + const mockContext = createMockActionContext({ + input: { + name: 'Augment Project', + abbreviation: 'AUG', + sourceRepoUrl: 'https://gitlab.com/org/shared-repo', + sourceRepoBranch: 'main', + areTargetAndSourceRepoShared: true, + targetRepoBranch: 'main', + }, + secrets: { + SRC_USER_OAUTH_TOKEN: 'gl-shared-token', + }, + }); + + await action.handler(mockContext); + + expect(runRequestBody.sourceRepoAuth?.token).toBe( + 'oauth2:gl-shared-token', + ); + expect(runRequestBody.targetRepoAuth?.token).toBe( + 'oauth2:gl-shared-token', + ); + }); + + it('should use plain token for source (GitHub) and oauth2: for target (GitLab)', async () => { + let runRequestBody: { + sourceRepoAuth?: { token: string }; + targetRepoAuth?: { token: string }; + } = {}; + mockFetch.mockImplementation((_url: string, options?: RequestInit) => { + const body = options?.body ? JSON.parse(options.body as string) : {}; + if (body.sourceRepoAuth && body.targetRepoAuth) { + runRequestBody = body; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ status: 'pending', jobId: 'init-job-augment' }), + }); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(createdProject), + }); + }); + + const action = createProjectAction(mockDiscoveryApi, { + fetchApi: { fetch: mockFetch }, + }); + const mockContext = createMockActionContext({ + input: { + name: 'Augment Project', + abbreviation: 'AUG', + sourceRepoUrl: 'https://github.com/org/source-repo', + sourceRepoBranch: 'main', + areTargetAndSourceRepoShared: false, + targetRepoUrl: 'https://gitlab.com/org/target-repo', + targetRepoBranch: 'main', + }, + secrets: { + SRC_USER_OAUTH_TOKEN: 'gh-source-token', + TGT_USER_OAUTH_TOKEN: 'gl-target-token', + }, + }); + + await action.handler(mockContext); + + expect(runRequestBody.sourceRepoAuth?.token).toBe('gh-source-token'); + expect(runRequestBody.targetRepoAuth?.token).toBe( + 'oauth2:gl-target-token', + ); + }); + + it('should use oauth2: for source (GitLab) and plain token for target (GitHub)', async () => { + let runRequestBody: { + sourceRepoAuth?: { token: string }; + targetRepoAuth?: { token: string }; + } = {}; + mockFetch.mockImplementation((_url: string, options?: RequestInit) => { + const body = options?.body ? JSON.parse(options.body as string) : {}; + if (body.sourceRepoAuth && body.targetRepoAuth) { + runRequestBody = body; + return Promise.resolve({ + ok: true, + json: () => + Promise.resolve({ status: 'pending', jobId: 'init-job-augment' }), + }); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve(createdProject), + }); + }); + + const action = createProjectAction(mockDiscoveryApi, { + fetchApi: { fetch: mockFetch }, + }); + const mockContext = createMockActionContext({ + input: { + name: 'Augment Project', + abbreviation: 'AUG', + sourceRepoUrl: 'https://gitlab.com/org/source-repo', + sourceRepoBranch: 'main', + areTargetAndSourceRepoShared: false, + targetRepoUrl: 'https://github.com/org/target-repo', + targetRepoBranch: 'main', + }, + secrets: { + SRC_USER_OAUTH_TOKEN: 'gl-source-token', + TGT_USER_OAUTH_TOKEN: 'gh-target-token', + }, + }); + + await action.handler(mockContext); + + expect(runRequestBody.sourceRepoAuth?.token).toBe( + 'oauth2:gl-source-token', + ); + expect(runRequestBody.targetRepoAuth?.token).toBe('gh-target-token'); + }); + }); + describe('failing scenarios', () => { it('should throw when target repository URL is missing (not shared)', async () => { const action = createProjectAction(mockDiscoveryApi); @@ -448,7 +762,7 @@ describe('x2a:project:create', () => { abbreviation: 'P', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: false, + areTargetAndSourceRepoShared: false, targetRepoBranch: 'main', // targetRepoUrl omitted }, @@ -471,7 +785,7 @@ describe('x2a:project:create', () => { abbreviation: 'P', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: false, + areTargetAndSourceRepoShared: false, targetRepoUrl: 'https://github.com/org/target', targetRepoBranch: 'main', }, @@ -494,7 +808,7 @@ describe('x2a:project:create', () => { abbreviation: 'P', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: false, + areTargetAndSourceRepoShared: false, targetRepoUrl: 'https://github.com/org/target', targetRepoBranch: 'main', }, @@ -525,7 +839,7 @@ describe('x2a:project:create', () => { abbreviation: 'P', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: true, + areTargetAndSourceRepoShared: true, targetRepoBranch: 'main', }, secrets: { @@ -565,7 +879,7 @@ describe('x2a:project:create', () => { abbreviation: 'P', sourceRepoUrl: 'https://github.com/org/repo', sourceRepoBranch: 'main', - areTargeAndSourceRepoShared: true, + areTargetAndSourceRepoShared: true, targetRepoBranch: 'main', }, secrets: { diff --git a/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts b/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts index 28fb92096ed..d79935371f7 100644 --- a/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts +++ b/workspaces/x2a/plugins/scaffolder-backend-module-x2a/src/actions/createProject.ts @@ -20,6 +20,8 @@ import { Project, ProjectsPost, ProjectsProjectIdRunPost200Response, + augmentRepoToken, + getAuthTokenDescriptor, normalizeRepoUrl, } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; @@ -67,7 +69,7 @@ export function createProjectAction( z.string({ description: 'The URL of the source repository' }), sourceRepoBranch: z => z.string({ description: 'The branch of the source repository' }), - areTargeAndSourceRepoShared: z => + areTargetAndSourceRepoShared: z => z.boolean({ description: 'Whether the target and source repositories are shared', @@ -109,30 +111,47 @@ export function createProjectAction( }); // Create the project in the x2a database - const targetRepoUrl = ctx.input.areTargeAndSourceRepoShared - ? ctx.input.sourceRepoUrl + const sourceRepoUrl = ctx.input.sourceRepoUrl; + const targetRepoUrl = ctx.input.areTargetAndSourceRepoShared + ? sourceRepoUrl : ctx.input.targetRepoUrl; if (!targetRepoUrl) { throw new Error('Target repository URL is required'); } - const sourceRepoToken = ctx.secrets?.SRC_USER_OAUTH_TOKEN; + let sourceRepoToken = ctx.secrets?.SRC_USER_OAUTH_TOKEN; if (!sourceRepoToken) { throw new Error('Source repository token is required'); } - const targetRepoToken = ctx.input.areTargeAndSourceRepoShared + + let targetRepoToken = ctx.input.areTargetAndSourceRepoShared ? sourceRepoToken : ctx.secrets?.TGT_USER_OAUTH_TOKEN; if (!targetRepoToken) { throw new Error('Target repository token is required'); } + sourceRepoToken = augmentRepoToken( + sourceRepoToken, + getAuthTokenDescriptor({ + repoUrl: sourceRepoUrl, + readOnly: true, + }), + ); + targetRepoToken = augmentRepoToken( + targetRepoToken, + getAuthTokenDescriptor({ + repoUrl: targetRepoUrl, + readOnly: false, + }), + ); + const body: ProjectsPost['body'] = { name: ctx.input.name, description: ctx.input.description ?? '', abbreviation: ctx.input.abbreviation, ownedByGroup: ctx.input.ownedByGroup?.trim() ?? undefined, - sourceRepoUrl: normalizeRepoUrl(ctx.input.sourceRepoUrl), + sourceRepoUrl: normalizeRepoUrl(sourceRepoUrl), targetRepoUrl: normalizeRepoUrl(targetRepoUrl), sourceRepoBranch: ctx.input.sourceRepoBranch, targetRepoBranch: ctx.input.targetRepoBranch, @@ -143,17 +162,19 @@ export function createProjectAction( try { const response = await api.projectsPost({ body }, { token: token }); if (!response.ok) { - const error = (await response.json()) as any; + const error = (await response.json()) as { message?: string }; ctx.logger.error( `Project creation response status: ${response.status}, error: ${JSON.stringify(error)}`, ); - throw new Error(error); + const msg = error?.message ?? JSON.stringify(error); + throw new Error(msg); } project = await response.json(); } catch (error) { - ctx.logger.error(`Error creating project: ${JSON.stringify(error)}`); - throw new Error(error as string); + const message = error instanceof Error ? error.message : String(error); + ctx.logger.error(`Error creating project: ${message}`); + throw error; } // The project is created, trigger the init-phase automatically @@ -183,19 +204,19 @@ export function createProjectAction( ); if (!initResponse.ok) { - const error = (await initResponse.json()) as any; + const error = (await initResponse.json()) as { message?: string }; ctx.logger.error( `Init-phase response status: ${initResponse.status}, error: ${JSON.stringify(error)}`, ); - throw new Error(error); + const msg = error?.message ?? JSON.stringify(error); + throw new Error(msg); } initResponseData = await initResponse.json(); } catch (error) { - ctx.logger.error( - `Error triggering init-phase: ${JSON.stringify(error)}`, - ); - throw new Error(error as string); + const message = error instanceof Error ? error.message : String(error); + ctx.logger.error(`Error triggering init-phase: ${message}`); + throw error; } ctx.logger.info( diff --git a/workspaces/x2a/plugins/x2a-common/package.json b/workspaces/x2a/plugins/x2a-common/package.json index ae90e7ef2d2..5cefa8d2dde 100644 --- a/workspaces/x2a/plugins/x2a-common/package.json +++ b/workspaces/x2a/plugins/x2a-common/package.json @@ -48,6 +48,7 @@ "@eloycoto" ], "dependencies": { + "@backstage/core-plugin-api": "^1.10.9", "@backstage/plugin-permission-common": "^0.9.4" } } diff --git a/workspaces/x2a/plugins/x2a-common/report.api.md b/workspaces/x2a/plugins/x2a-common/report.api.md index 428a7b26f7f..e4c332ce162 100644 --- a/workspaces/x2a/plugins/x2a-common/report.api.md +++ b/workspaces/x2a/plugins/x2a-common/report.api.md @@ -40,6 +40,23 @@ export interface Artifact { // @public (undocumented) export type ArtifactType = 'migration_plan' | 'module_migration_plan' | 'migrated_sources' | 'project_metadata'; +// @public +export const augmentRepoToken: (token: string, authDescriptor: AuthTokenDescriptor) => string; + +// @public +export interface AuthToken { + provider: string; + token: string; +} + +// @public +export type AuthTokenDescriptor = { + provider: string; + customProviderApiId?: string; + scope?: string | string[]; + tokenType?: 'openId' | 'oauth'; +}; + // @public export const CREATE_CHEF_PROJECT_TEMPLATE_PATH = "/create/templates/default/chef-conversion-project-template"; @@ -75,6 +92,12 @@ export class DefaultApiClient { projectsProjectIdRunPost(request: ProjectsProjectIdRunPost, options?: RequestOptions): Promise>; } +// @public +export const getAuthTokenDescriptor: ({ repoUrl, readOnly, }: { + repoUrl: string; + readOnly: boolean; +}) => AuthTokenDescriptor; + // @public (undocumented) export interface GitRepoAuth { token: string; diff --git a/workspaces/x2a/plugins/x2a-common/src/utils/augmentRepoToken.ts b/workspaces/x2a/plugins/x2a-common/src/utils/augmentRepoToken.ts new file mode 100644 index 00000000000..bc4c67d756b --- /dev/null +++ b/workspaces/x2a/plugins/x2a-common/src/utils/augmentRepoToken.ts @@ -0,0 +1,34 @@ +/* + * Copyright Red Hat, Inc. + * + * 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 governing permissions and limitations under the License. + */ + +import { AuthTokenDescriptor } from './tokenDescriptorTypes'; + +/** + * Augments the repository token for the given provider. + * + * For GitLab, it adds a user preceding the token. + * @public + */ +export const augmentRepoToken = ( + token: string, + authDescriptor: AuthTokenDescriptor, +): string => { + if (authDescriptor.provider === 'gitlab') { + // The git push on gitlab requires an user name preceding the token. + // https://docs.gitlab.com/api/oauth2/#access-git-over-https-with-access-token + return `oauth2:${token}`; + } + return token; +}; diff --git a/workspaces/x2a/plugins/x2a/src/repoAuth/getAuthTokenDescriptor.ts b/workspaces/x2a/plugins/x2a-common/src/utils/getAuthTokenDescriptor.ts similarity index 92% rename from workspaces/x2a/plugins/x2a/src/repoAuth/getAuthTokenDescriptor.ts rename to workspaces/x2a/plugins/x2a-common/src/utils/getAuthTokenDescriptor.ts index 279a7d1c489..047cf36a185 100644 --- a/workspaces/x2a/plugins/x2a/src/repoAuth/getAuthTokenDescriptor.ts +++ b/workspaces/x2a/plugins/x2a-common/src/utils/getAuthTokenDescriptor.ts @@ -1,5 +1,3 @@ -import { AuthTokenDescriptor } from './tokenDescriptorTypes'; - /** * Copyright Red Hat, Inc. * @@ -14,6 +12,14 @@ import { AuthTokenDescriptor } from './tokenDescriptorTypes'; * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific governing permissions and limitations under the License. */ +import { AuthTokenDescriptor } from './tokenDescriptorTypes'; + +/** + * Returns the AuthTokenDescriptor for the given repository URL. + * Determines the provider. + * + * @public + */ export const getAuthTokenDescriptor = ({ repoUrl, readOnly, diff --git a/workspaces/x2a/plugins/x2a-common/src/utils/index.ts b/workspaces/x2a/plugins/x2a-common/src/utils/index.ts index f564e7a1746..c2dff818584 100644 --- a/workspaces/x2a/plugins/x2a-common/src/utils/index.ts +++ b/workspaces/x2a/plugins/x2a-common/src/utils/index.ts @@ -16,3 +16,6 @@ export * from './normalizeRepoUrl'; export * from './toSorted'; +export * from './getAuthTokenDescriptor'; +export * from './tokenDescriptorTypes'; +export * from './augmentRepoToken'; diff --git a/workspaces/x2a/plugins/x2a/src/repoAuth/tokenDescriptorTypes.ts b/workspaces/x2a/plugins/x2a-common/src/utils/tokenDescriptorTypes.ts similarity index 95% rename from workspaces/x2a/plugins/x2a/src/repoAuth/tokenDescriptorTypes.ts rename to workspaces/x2a/plugins/x2a-common/src/utils/tokenDescriptorTypes.ts index b589ac5c14e..690ffa6ad96 100644 --- a/workspaces/x2a/plugins/x2a/src/repoAuth/tokenDescriptorTypes.ts +++ b/workspaces/x2a/plugins/x2a-common/src/utils/tokenDescriptorTypes.ts @@ -13,8 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { OAuthScope } from '@backstage/core-plugin-api'; +/** + * Auth token object + * @public + */ export interface AuthToken { /** * The auth token provider name @@ -28,6 +31,8 @@ export interface AuthToken { /** * Descriptor for authentication token configuration + * + * @public */ export type AuthTokenDescriptor = { /** @@ -46,7 +51,7 @@ export type AuthTokenDescriptor = { /** * OAuth scope for token permissions (e.g., 'repo' for GitHub repository write permissions) */ - scope?: OAuthScope; + scope?: string | string[]; /** * Type of authentication token to request. If not provided, the default is 'oauth'. diff --git a/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePage.tsx b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePage.tsx index 1ef4e387cce..00197b2f314 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePage.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ModulePage/ModulePage.tsx @@ -24,14 +24,17 @@ import { ResponseErrorPanel, } from '@backstage/core-components'; import { Grid } from '@material-ui/core'; -import { ModulePhase } from '@red-hat-developer-hub/backstage-plugin-x2a-common'; +import { + getAuthTokenDescriptor, + ModulePhase, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; import Alert from '@material-ui/lab/Alert'; import AlertTitle from '@material-ui/lab/AlertTitle'; import { moduleRouteRef } from '../../routes'; import { useClientService } from '../../ClientService'; import { useTranslation } from '../../hooks/useTranslation'; -import { getAuthTokenDescriptor, useRepoAuthentication } from '../../repoAuth'; +import { useRepoAuthentication } from '../../repoAuth'; import { ArtifactsCard } from './ArtifactsCard'; import { ModuleDetailsCard } from './ModuleDetailsCard'; import { PhasesCard } from './PhasesCard'; diff --git a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx index fbc90239545..93e6d682021 100644 --- a/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx +++ b/workspaces/x2a/plugins/x2a/src/components/ModuleTable/ModuleTable.tsx @@ -16,6 +16,7 @@ import { useCallback, useMemo, useState } from 'react'; import { Artifact, + getAuthTokenDescriptor, MigrationPhase, Module, ModulePhase, @@ -31,7 +32,7 @@ import AlertTitle from '@material-ui/lab/AlertTitle'; import { useTranslation } from '../../hooks/useTranslation'; import { useClientService } from '../../ClientService'; import { Artifacts } from './Artifacts'; -import { getAuthTokenDescriptor, useRepoAuthentication } from '../../repoAuth'; +import { useRepoAuthentication } from '../../repoAuth'; import { CurrentPhaseCell } from './CurrentPhaseCell'; import { ModuleStatusCell } from './ModuleStatusCell'; import { TimingCell } from './TimingCell'; diff --git a/workspaces/x2a/plugins/x2a/src/repoAuth/index.ts b/workspaces/x2a/plugins/x2a/src/repoAuth/index.ts index 55e46029ea4..07f3083af15 100644 --- a/workspaces/x2a/plugins/x2a/src/repoAuth/index.ts +++ b/workspaces/x2a/plugins/x2a/src/repoAuth/index.ts @@ -13,5 +13,3 @@ * See the License for the specific governing permissions and limitations under the License. */ export * from './useRepoAuth'; -export * from './getAuthTokenDescriptor'; -export * from './tokenDescriptorTypes'; diff --git a/workspaces/x2a/plugins/x2a/src/repoAuth/useRepoAuth.ts b/workspaces/x2a/plugins/x2a/src/repoAuth/useRepoAuth.ts index cd65cc4679e..b4af4b473a6 100644 --- a/workspaces/x2a/plugins/x2a/src/repoAuth/useRepoAuth.ts +++ b/workspaces/x2a/plugins/x2a/src/repoAuth/useRepoAuth.ts @@ -22,7 +22,11 @@ import { useApi, useApp, } from '@backstage/core-plugin-api'; -import { AuthToken, AuthTokenDescriptor } from './tokenDescriptorTypes'; +import { + augmentRepoToken, + AuthToken, + AuthTokenDescriptor, +} from '@red-hat-developer-hub/backstage-plugin-x2a-common'; const isAuthApi = (api: any): api is OAuthApi => { return api && typeof api.getAccessToken === 'function'; @@ -58,14 +62,17 @@ export const useRepoAuthentication = () => { const getToken = useCallback( async (tokenDescriptor: AuthTokenDescriptor): Promise => { - let authApi = githubAuthApi; - if (tokenDescriptor.provider.toLocaleLowerCase('en-US') === 'gitlab') { - authApi = gitlabAuthApi; + let token; + if (tokenDescriptor.provider === 'github') { + token = await getProviderToken(githubAuthApi, tokenDescriptor); + } else if (tokenDescriptor.provider === 'gitlab') { + token = await getProviderToken(gitlabAuthApi, tokenDescriptor); + } else { + throw new Error(`Unsupported provider: ${tokenDescriptor.provider}`); } - const token = await getProviderToken(authApi, tokenDescriptor); return { - token, + token: augmentRepoToken(token, tokenDescriptor), provider: tokenDescriptor.provider, }; }, diff --git a/workspaces/x2a/templates/conversion-project-template.yaml b/workspaces/x2a/templates/conversion-project-template.yaml index 3ee70429bec..594f360659a 100644 --- a/workspaces/x2a/templates/conversion-project-template.yaml +++ b/workspaces/x2a/templates/conversion-project-template.yaml @@ -42,7 +42,7 @@ spec: ui:order: - sourceRepoUrl - sourceRepoBranch - - areTargeAndSourceRepoShared + - areTargetAndSourceRepoShared - targetRepoUrl - targetRepoBranch required: @@ -53,55 +53,55 @@ spec: sourceRepoUrl: title: Conversion source repository description: | - The Owner should be your GitHub username. The repository name should be a name that already exists in your GitHub account and contains a Chef cookbook or directory to convert. + The Owner should be your SCM (Source Code Management) username. The repository name should be a name that already exists in your SCM account and contains a Chef cookbook or directory to convert. type: string ui:field: RepoUrlPicker ui:options: requestUserCredentials: secretsKey: SRC_USER_OAUTH_TOKEN - #additionalScopes: - # github: - # - workflow allowedHosts: + # This is to be updated per production deployment along the auth in the app-config.yaml file - github.com + - gitlab.com sourceRepoBranch: title: Conversion source repository branch type: string description: The branch of the source repository to use for the conversion. default: 'main' - areTargeAndSourceRepoShared: + areTargetAndSourceRepoShared: title: Use the same repository for target and source type: boolean description: If checked, the target repository will be the same as the source repository. default: true - # TODO: Can be replaced by RepoBranchPicker but it does not work nicely. https://backstage.io/docs/features/software-templates/writing-templates#the-repository-branch-picker + # Can be replaced by RepoBranchPicker but it does not work nicely. https://backstage.io/docs/features/software-templates/writing-templates#the-repository-branch-picker targetRepoBranch: title: Conversion target repository branch type: string description: The branch of the target repository to use for the conversion. default: 'main' dependencies: - areTargeAndSourceRepoShared: + areTargetAndSourceRepoShared: allOf: - if: properties: - areTargeAndSourceRepoShared: + areTargetAndSourceRepoShared: const: false then: properties: targetRepoUrl: title: Conversion target repository description: | - The Owner should be your GitHub username. The repository name should be a name that already exists in your GitHub account. It will be populated by converted Ansible sources and intermediary artifacts. + The Owner should be your SCM (Source Code Management) username. The repository name should be a name that already exists in your SCM account. It will be populated by converted Ansible sources and intermediary artifacts. type: string ui:field: RepoUrlPicker ui:options: requestUserCredentials: secretsKey: TGT_USER_OAUTH_TOKEN allowedHosts: + # This is to be updated per production deployment along the auth in the app-config.yaml file - github.com - + - gitlab.com - title: Conversion parameters properties: userPrompt: @@ -123,7 +123,7 @@ spec: ownedByGroup: ${{ parameters.ownedByGroup }} sourceRepoUrl: ${{ parameters.sourceRepoUrl }} sourceRepoBranch: ${{ parameters.sourceRepoBranch }} - areTargeAndSourceRepoShared: ${{ parameters.areTargeAndSourceRepoShared }} + areTargetAndSourceRepoShared: ${{ parameters.areTargetAndSourceRepoShared }} targetRepoUrl: ${{ parameters.targetRepoUrl }} targetRepoBranch: ${{ parameters.targetRepoBranch }} userPrompt: ${{ parameters.userPrompt }} diff --git a/workspaces/x2a/yarn.lock b/workspaces/x2a/yarn.lock index d8092c9cfd5..8b3a90d9e26 100644 --- a/workspaces/x2a/yarn.lock +++ b/workspaces/x2a/yarn.lock @@ -3030,6 +3030,20 @@ __metadata: languageName: node linkType: hard +"@backstage/plugin-auth-backend-module-gitlab-provider@npm:0.3.9": + version: 0.3.9 + resolution: "@backstage/plugin-auth-backend-module-gitlab-provider@npm:0.3.9" + dependencies: + "@backstage/backend-plugin-api": "npm:^1.5.0" + "@backstage/plugin-auth-node": "npm:^0.6.9" + express: "npm:^4.18.2" + passport: "npm:^0.7.0" + passport-gitlab2: "npm:^5.0.0" + zod: "npm:^3.22.4" + checksum: 10c0/6547514048e0f71e973996e259e4fc43e3c00b2443cec613a273d03ee32f2993b461f7fa694cf997f19fc02b9e6b7f94dcc0f02604feeda246e8f38db3d0a8c3 + languageName: node + linkType: hard + "@backstage/plugin-auth-backend-module-guest-provider@npm:^0.2.14": version: 0.2.15 resolution: "@backstage/plugin-auth-backend-module-guest-provider@npm:0.2.15" @@ -3884,21 +3898,21 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-scaffolder-backend-module-gitlab@npm:^0.11.2": - version: 0.11.2 - resolution: "@backstage/plugin-scaffolder-backend-module-gitlab@npm:0.11.2" +"@backstage/plugin-scaffolder-backend-module-gitlab@npm:^0.11.2, @backstage/plugin-scaffolder-backend-module-gitlab@npm:^0.11.3": + version: 0.11.3 + resolution: "@backstage/plugin-scaffolder-backend-module-gitlab@npm:0.11.3" dependencies: - "@backstage/backend-plugin-api": "npm:^1.6.2" + "@backstage/backend-plugin-api": "npm:^1.7.0" "@backstage/config": "npm:^1.3.6" "@backstage/errors": "npm:^1.2.7" - "@backstage/integration": "npm:^1.19.2" - "@backstage/plugin-scaffolder-node": "npm:^0.12.4" + "@backstage/integration": "npm:^1.20.0" + "@backstage/plugin-scaffolder-node": "npm:^0.12.5" "@gitbeaker/requester-utils": "npm:^41.2.0" "@gitbeaker/rest": "npm:^41.2.0" luxon: "npm:^3.0.0" yaml: "npm:^2.0.0" zod: "npm:^3.25.76" - checksum: 10c0/ddfbd0f8bc1db4b7e56726c108f8b0ec9c476a3c781e5c7d9ce121c8844b24363c51afc89415e0496000d86094145335a972d6f5aa6d88d02cc0b006d14341fb + checksum: 10c0/83c6a7889c2e0051e71691426fb5e8f9e3f2cb4b6f546711dce7636cc8d2e85cbcdce75c63738b1c7d5003f8160ec30a61c7356ed3ee5de6aef51bdc9d38179e languageName: node linkType: hard @@ -4043,16 +4057,16 @@ __metadata: languageName: node linkType: hard -"@backstage/plugin-scaffolder-node@npm:^0.12.3, @backstage/plugin-scaffolder-node@npm:^0.12.4": - version: 0.12.4 - resolution: "@backstage/plugin-scaffolder-node@npm:0.12.4" +"@backstage/plugin-scaffolder-node@npm:^0.12.3, @backstage/plugin-scaffolder-node@npm:^0.12.4, @backstage/plugin-scaffolder-node@npm:^0.12.5": + version: 0.12.5 + resolution: "@backstage/plugin-scaffolder-node@npm:0.12.5" dependencies: - "@backstage/backend-plugin-api": "npm:^1.6.2" + "@backstage/backend-plugin-api": "npm:^1.7.0" "@backstage/catalog-model": "npm:^1.7.6" "@backstage/errors": "npm:^1.2.7" - "@backstage/integration": "npm:^1.19.2" - "@backstage/plugin-permission-common": "npm:^0.9.5" - "@backstage/plugin-scaffolder-common": "npm:^1.7.5" + "@backstage/integration": "npm:^1.20.0" + "@backstage/plugin-permission-common": "npm:^0.9.6" + "@backstage/plugin-scaffolder-common": "npm:^1.7.6" "@backstage/types": "npm:^1.2.2" "@isomorphic-git/pgp-plugin": "npm:^0.0.7" concat-stream: "npm:^2.0.0" @@ -4062,12 +4076,12 @@ __metadata: jsonschema: "npm:^1.5.0" lodash: "npm:^4.17.21" p-limit: "npm:^3.1.0" - tar: "npm:^6.1.12" + tar: "npm:^7.5.6" winston: "npm:^3.2.1" winston-transport: "npm:^4.7.0" zod: "npm:^3.25.76" zod-to-json-schema: "npm:^3.25.1" - checksum: 10c0/98f96505b42413defdbce53d8c0b1cca2d5d867755d71f5584af3271f4fcf34b506d7e1bef421a6cc5d5d157de4fd9dffc740bec30e4a776b5a99244c678acbb + checksum: 10c0/f1e401744991049f9f7bbc0fb1c8d1cdff85082748004b7d428f0681e0944b865c61484285730e28b483443b6595128b45379a24d9cc4967940b21068bfd2b49 languageName: node linkType: hard @@ -11965,6 +11979,7 @@ __metadata: resolution: "@red-hat-developer-hub/backstage-plugin-x2a-common@workspace:plugins/x2a-common" dependencies: "@backstage/cli": "npm:^0.34.5" + "@backstage/core-plugin-api": "npm:^1.10.9" "@backstage/plugin-permission-common": "npm:^0.9.4" cross-fetch: "npm:^4.1.0" uri-template: "npm:^2.0.0" @@ -17148,6 +17163,7 @@ __metadata: "@backstage/plugin-app-backend": "npm:^0.5.8" "@backstage/plugin-auth-backend": "npm:^0.25.6" "@backstage/plugin-auth-backend-module-github-provider": "npm:^0.3.9" + "@backstage/plugin-auth-backend-module-gitlab-provider": "npm:^0.3.9" "@backstage/plugin-auth-backend-module-guest-provider": "npm:^0.2.14" "@backstage/plugin-auth-node": "npm:^0.6.9" "@backstage/plugin-catalog-backend": "npm:^3.2.0" @@ -17161,6 +17177,7 @@ __metadata: "@backstage/plugin-proxy-backend": "npm:^0.6.8" "@backstage/plugin-scaffolder-backend": "npm:^3.0.1" "@backstage/plugin-scaffolder-backend-module-github": "npm:^0.9.2" + "@backstage/plugin-scaffolder-backend-module-gitlab": "npm:^0.11.3" "@backstage/plugin-scaffolder-backend-module-notifications": "npm:^0.1.16" "@backstage/plugin-search-backend": "npm:^2.0.8" "@backstage/plugin-search-backend-module-catalog": "npm:^0.3.10" @@ -29251,7 +29268,16 @@ __metadata: languageName: node linkType: hard -"passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.7.0": +"passport-gitlab2@npm:^5.0.0": + version: 5.0.0 + resolution: "passport-gitlab2@npm:5.0.0" + dependencies: + passport-oauth2: "npm:^1.4.0" + checksum: 10c0/703b1c26e7cc085fb089b6d00111765b9ca5695c1cf73c1d2d352996ae8ea70e737c4e759f6d43814053633dabf5fe299498dca0c26ecc79f7a32c9cccbd8257 + languageName: node + linkType: hard + +"passport-oauth2@npm:1.x.x, passport-oauth2@npm:^1.4.0, passport-oauth2@npm:^1.7.0": version: 1.8.0 resolution: "passport-oauth2@npm:1.8.0" dependencies: @@ -33947,16 +33973,16 @@ __metadata: languageName: node linkType: hard -"tar@npm:^7.5.4": - version: 7.5.7 - resolution: "tar@npm:7.5.7" +"tar@npm:^7.5.4, tar@npm:^7.5.6": + version: 7.5.9 + resolution: "tar@npm:7.5.9" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10c0/51f261afc437e1112c3e7919478d6176ea83f7f7727864d8c2cce10f0b03a631d1911644a567348c3063c45abdae39718ba97abb073d22aa3538b9a53ae1e31c + checksum: 10c0/e870beb1b2477135ca2abe86b2d18f7b35d0a4e3a37bbc523d3b8f7adca268dfab543f26528a431d569897f8c53a7cac745cdfbc4411c2f89aeeacc652b81b0a languageName: node linkType: hard