Merge pull request #21890 from ch007m/issue-21866

Create a new action to publish on gitea
This commit is contained in:
Ben Lambert
2024-02-16 14:48:42 +01:00
committed by GitHub
20 changed files with 1030 additions and 17 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-scaffolder-backend-module-gitea': minor
'@backstage/plugin-scaffolder-backend': minor
---
Created a gitea module for the scaffolder. This module provides a new action "publish:gitea" able to create a gitea repository owned by an organization. See: https://gitea.com/api/swagger#/organization/createOrgRepo
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Validate if the `baseUrl` is a valid URL
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-node': patch
---
Add gitea as new type to be used from integrations configuration
+5 -1
View File
@@ -16,7 +16,7 @@
import { Config } from '@backstage/config';
import { trimEnd } from 'lodash';
import { isValidHost } from '../helpers';
import { isValidHost, isValidUrl } from '../helpers';
/**
* The configuration for a single Gitea integration.
@@ -62,6 +62,10 @@ export function readGiteaConfig(config: Config): GiteaIntegrationConfig {
throw new Error(
`Invalid Gitea integration config, '${host}' is not a valid host`,
);
} else if (baseUrl && !isValidUrl(baseUrl)) {
throw new Error(
`Invalid Gitea integration config, '${baseUrl}' is not a valid baseUrl`,
);
}
if (baseUrl) {
+18 -16
View File
@@ -30,6 +30,22 @@ describe('gitea core', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
describe('getGiteaFileCatalogInfoCntentsUrl', () => {
it('can create an url from arguments', () => {
const config: GiteaIntegrationConfig = {
host: 'gitea.com',
};
expect(
getGiteaFileContentsUrl(
config,
'https://gitea.com/org1/repo1/src/branch/main/catalog-info.yaml',
),
).toEqual(
'https://gitea.com/api/v1/repos/org1/repo1/contents/catalog-info.yaml?ref=main',
);
});
});
describe('getGiteaFileContentsUrl', () => {
it('can create an url from arguments', () => {
const config: GiteaIntegrationConfig = {
@@ -94,24 +110,10 @@ describe('gitea core', () => {
});
});
describe('getGerritRequestOptions', () => {
it('adds token header when only a password is specified', () => {
const authRequest: GiteaIntegrationConfig = {
host: 'gerrit.com',
password: 'P',
};
const anonymousRequest: GiteaIntegrationConfig = {
host: 'gerrit.com',
};
expect(
(getGiteaRequestOptions(authRequest).headers as any).Authorization,
).toEqual('token P');
expect(getGiteaRequestOptions(anonymousRequest).headers).toBeUndefined();
});
describe('getGiteaRequestOptions', () => {
it('adds basic auth when username and password are specified', () => {
const authRequest: GiteaIntegrationConfig = {
host: 'gerrit.com',
host: 'gitea.com',
username: 'username',
password: 'P',
};
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,110 @@
# scaffolder-backend-module-gitea
Welcome to the `publish:gitea` action of the `scaffolder-gitea-backend`.
## Getting started
To use this action, you will have to add the package using the following command to be executed at the root of your backstage project:
```bash
yarn add --cwd packages/backend
@backstage/plugin-scaffolder-backend-module-gitea
```
Configure the action (if not yet done):
(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options):
Before to create a template, include to your `app-config.yaml` file the
gitea host and credentials under the `integrations:` section
```yaml
integrations:
gitea:
- host: gitea.com
username: '<GITEA_USER>'
password: '<GITEA_PASSWORD>'
- host: localhost:3333
username: '<GITEA_LOCALHOST_USER>'
password: '<GITEA_LOCALHOST_PASSWORD>'
```
**NOTE**: As backstage will issue HTTPS/TLS requests to the gitea instance, it is needed to configure `gitea` with a valid certificate or at least with a
self-signed certificate `gitea cert --host localhost -ca`. Don't forget to set the env var `NODE_EXTRA_CA_CERTS` to point to the CA file before launching backstage !
When done, you can create a template which:
- Declare the `RepoUrlPicker` within the `spec/parameters` section to select the gitea hosts
- Include a step able to publish by example the newly generated project using the action `action: publish:gitea`
```yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: quarkus-web-template
title: Quarkus Hello world
description: Create a simple microservice using Quarkus
tags:
- java
- quarkus
spec:
owner: quarkus
type: service
parameters:
- title: Git repository Information
required:
- repoUrl
properties:
repoUrl:
title: Repository Location
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- gitea.<YOUR_DOMAIN>:<PORT>
- localhost:<PORT>
steps:
- id: template
name: Generating component
action: fetch:template
input:
url: ./skeleton
copyWithoutTemplating:
- .github/workflows/*
values:
component_id: ${{ parameters.component_id }}
namespace: ${{ parameters.component_id }}-dev
description: ${{ parameters.description }}
group_id: ${{ parameters.group_id }}
artifact_id: ${{ parameters.artifact_id }}
java_package_name: ${{ parameters.java_package_name }}
owner: ${{ parameters.owner }}
destination: ${{ (parameters.repoUrl | parseRepoUrl).owner }}/${{ (parameters.repoUrl | parseRepoUrl).repo }
port: 8080
- id: publish
name: Publishing to a gitea git repository
action: publish:gitea
input:
description: This is ${{ parameters.component_id }}
repoUrl: ${{ parameters.repoUrl }}
defaultBranch: main
- id: register
if: ${{ parameters.dryRun !== true }}
name: Register
action: catalog:register
input:
repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
catalogInfoPath: 'main/catalog-info.yaml'
output:
links:
- title: Source Code Repository
url: ${{ steps.publish.output.remoteUrl }}
- title: Open Component in catalog
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}
```
Access the newly gitea repository created using the `repoContentsUrl` ;-)
+32
View File
@@ -0,0 +1,32 @@
## API Report File for "@backstage/plugin-scaffolder-backend-module-gitea"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
import { Config } from '@backstage/config';
import { JsonObject } from '@backstage/types';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
// @public
export function createPublishGiteaAction(options: {
integrations: ScmIntegrationRegistry;
config: Config;
}): TemplateAction<
{
repoUrl: string;
description: string;
defaultBranch?: string | undefined;
gitCommitMessage?: string | undefined;
gitAuthorName?: string | undefined;
gitAuthorEmail?: string | undefined;
sourcePath?: string | undefined;
},
JsonObject
>;
// @public
const giteaModule: () => BackendFeature;
export default giteaModule;
```
@@ -0,0 +1,9 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-plugin-scaffolder-backend-module-gitea
title: '@backstage/plugin-scaffolder-backend-module-gitea'
spec:
lifecycle: experimental
type: backstage-backend-plugin-module
owner: maintainers
@@ -0,0 +1,57 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-gitea",
"description": "The gitea 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"
},
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/scaffolder-backend-module-gitea"
},
"backstage": {
"role": "backend-plugin-module"
},
"exports": {
".": "./src/index.ts",
"./package.json": "./package.json"
},
"typesVersions": {
"*": {
"package.json": [
"package.json"
]
}
},
"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/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-scaffolder-node": "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"
]
}
@@ -0,0 +1,158 @@
/*
* 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.
*/
import { TemplateExample } from '@backstage/plugin-scaffolder-node';
import yaml from 'yaml';
export const examples: TemplateExample[] = [
{
description:
'Initializes a Gitea repository using the content of the workspace and publish it to Gitea with default configuration.',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
},
},
],
}),
},
{
description: 'Initializes a Gitea repository with a description.',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
description: 'Initialize a gitea repository',
},
},
],
}),
},
{
description:
'Initializes a Gitea repository with a default Branch, if not set defaults to main',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
defaultBranch: 'main',
},
},
],
}),
},
{
description:
'Initializes a Gitea repository with an initial commit message, if not set defaults to initial commit',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
gitCommitMessage: 'Initial Commit Message',
},
},
],
}),
},
{
description:
'Initializes a Gitea repository with a repo Author Name, if not set defaults to Scaffolder',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
gitAuthorName: 'John Doe',
},
},
],
}),
},
{
description: 'Initializes a Gitea repository with a repo Author Email',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
gitAuthorEmail: 'johndoe@email.com',
},
},
],
}),
},
{
description:
'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
sourcePath: 'repository/',
},
},
],
}),
},
{
description: 'Initializes a Gitea repository with all properties being set',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
description: 'Initialize a gitea repository',
defaultBranch: 'staging',
gitCommitMessage: 'Initial Commit Message',
gitAuthorName: 'John Doe',
gitAuthorEmail: 'johndoe@email.com',
sourcePath: 'repository/',
},
},
],
}),
},
];
@@ -0,0 +1,160 @@
/*
* 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.
*/
import { PassThrough } from 'stream';
import { ScmIntegrations } from '@backstage/integration';
import { ConfigReader } from '@backstage/config';
import { getVoidLogger } from '@backstage/backend-common';
import { createPublishGiteaAction } from './gitea';
import { initRepoAndPush } from '@backstage/plugin-scaffolder-node';
import { rest } from 'msw';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { setupServer } from 'msw/node';
jest.mock('@backstage/plugin-scaffolder-node', () => {
return {
...jest.requireActual('@backstage/plugin-scaffolder-node'),
initRepoAndPush: jest.fn().mockResolvedValue({
commitHash: '220f19cc36b551763d157f1b5e4a4b446165dbd6',
}),
};
});
describe('publish:gitea', () => {
const config = new ConfigReader({
integrations: {
gitea: [
{
host: 'gitea.com',
username: 'gitea_user',
password: 'gitea_password',
},
],
},
});
const description = 'for the lols';
const integrations = ScmIntegrations.fromConfig(config);
const action = createPublishGiteaAction({ integrations, config });
const mockContext = {
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
description,
},
workspacePath: 'lol',
logger: getVoidLogger(),
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory: jest.fn(),
};
const server = setupServer();
setupRequestMockHandlers(server);
beforeEach(() => {
jest.resetAllMocks();
});
it('should throw an error when the repoUrl is not well formed', async () => {
await expect(
action.handler({
...mockContext,
input: { repoUrl: 'gitea.com?owner=o', description },
}),
).rejects.toThrow(/missing repo/);
});
it('should throw if there is no integration config provided for missing.com host', async () => {
await expect(
action.handler({
...mockContext,
input: { repoUrl: 'missing.com?repo=repo', description },
}),
).rejects.toThrow(/No matching integration configuration/);
});
it('should throw if there is no repositoryId returned', async () => {
server.use(
rest.get('https://gitea.com/api/v1/orgs/org1', (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json({
id: 1,
name: 'org1',
visibility: 'public',
repo_admin_change_team_access: false,
username: 'org1',
}),
);
}),
rest.get(
'https://gitea.com/org1/repo/src/branch/main',
(_req, res, ctx) => {
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
},
),
rest.post('https://gitea.com/api/v1/orgs/org1/repos', (req, res, ctx) => {
// Basic auth must match the user and password defined part of the config
expect(req.headers.get('Authorization')).toBe(
'basic Z2l0ZWFfdXNlcjpnaXRlYV9wYXNzd29yZA==',
);
expect(req.body).toEqual({
name: 'repo',
description,
});
return res(
ctx.status(201),
ctx.set('Content-Type', 'application/json'),
ctx.json({}),
);
}),
);
await action.handler({
...mockContext,
input: {
...mockContext.input,
repoUrl: 'gitea.com?repo=repo&owner=org1',
},
});
expect(initRepoAndPush).toHaveBeenCalledWith({
dir: mockContext.workspacePath,
remoteUrl: 'https://gitea.com/org1/repo.git',
defaultBranch: 'main',
auth: { username: 'gitea_user', password: 'gitea_password' },
logger: mockContext.logger,
commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'),
gitAuthorInfo: {
email: undefined,
name: undefined,
},
});
expect(mockContext.output).toHaveBeenCalledWith(
'repoContentsUrl',
'https://gitea.com/org1/repo/src/branch/main/',
);
});
afterEach(() => {
jest.resetAllMocks();
});
});
@@ -0,0 +1,346 @@
/*
* 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.
*/
import { InputError } from '@backstage/errors';
import { Config } from '@backstage/config';
import {
getGiteaRequestOptions,
GiteaIntegrationConfig,
ScmIntegrationRegistry,
} from '@backstage/integration';
import {
ActionContext,
createTemplateAction,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { examples } from './gitea.examples';
import crypto from 'crypto';
const checkGiteaContentUrl = async (
config: GiteaIntegrationConfig,
options: {
owner?: string;
repo: string;
defaultBranch?: string;
},
): Promise<Response> => {
const { owner, repo, defaultBranch } = options;
let response: Response;
const getOptions: RequestInit = {
method: 'GET',
};
try {
response = await fetch(
`${config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}`,
getOptions,
);
} catch (e) {
throw new Error(
`Unable to get the repository: ${owner}/${repo} metadata , ${e}`,
);
}
return response;
};
const checkGiteaOrg = async (
config: GiteaIntegrationConfig,
options: {
owner: string;
},
): Promise<void> => {
const { owner } = options;
let response: Response;
// check first if the org = owner exists
const getOptions: RequestInit = {
method: 'GET',
headers: {
...getGiteaRequestOptions(config).headers,
'Content-Type': 'application/json',
},
};
try {
response = await fetch(
`${config.baseUrl}/api/v1/orgs/${owner}`,
getOptions,
);
} catch (e) {
throw new Error(`Unable to get the Organization: ${owner}, ${e}`);
}
if (response.status !== 200) {
throw new Error(
`Organization ${owner} do not exist. Please create it first !`,
);
}
};
const createGiteaProject = async (
config: GiteaIntegrationConfig,
options: {
projectName: string;
owner?: string;
description: string;
},
): Promise<void> => {
const { projectName, description, owner } = options;
/*
Several options exist to create a repository using either the user or organisation
User: https://gitea.com/api/swagger#/user/createCurrentUserRepo
Api: URL/api/v1/user/repos
Remark: The user is the username defined part of the backstage integration config for the gitea URL !
Org: https://gitea.com/api/swagger#/organization/createOrgRepo
Api: URL/api/v1/orgs/${org_owner}/repos
This is the default scenario that we support currently
*/
let response: Response;
const postOptions: RequestInit = {
method: 'POST',
body: JSON.stringify({
name: projectName,
description,
}),
headers: {
...getGiteaRequestOptions(config).headers,
'Content-Type': 'application/json',
},
};
try {
response = await fetch(
`${config.baseUrl}/api/v1/orgs/${owner}/repos`,
postOptions,
);
} catch (e) {
throw new Error(`Unable to create repository, ${e}`);
}
if (response.status !== 201) {
throw new Error(
`Unable to create repository, ${response.status} ${
response.statusText
}, ${await response.text()}`,
);
}
};
const generateCommitMessage = (
config: Config,
commitSubject?: string,
): string => {
const changeId = crypto.randomBytes(20).toString('hex');
const msg = `${
config.getOptionalString('scaffolder.defaultCommitMessage') || commitSubject
}\n\nChange-Id: I${changeId}`;
return msg;
};
async function checkAvailabilityGiteaRepository(
maxDuration: number,
integrationConfig: GiteaIntegrationConfig,
options: {
owner?: string;
repo: string;
defaultBranch: string;
ctx: ActionContext<any>;
},
) {
const startTimestamp = Date.now();
const { owner, repo, defaultBranch, ctx } = options;
const sleep = (ms: number | undefined) => new Promise(r => setTimeout(r, ms));
let response: Response;
while (Date.now() - startTimestamp < maxDuration) {
if (ctx.signal?.aborted) return;
response = await checkGiteaContentUrl(integrationConfig, {
owner,
repo,
defaultBranch,
});
if (response.status !== 200) {
// Repository is not yet available/accessible ...
await sleep(1000);
}
}
}
/**
* Creates a new action that initializes a git repository using the content of the workspace.
* and publishes it to a Gitea instance.
* @public
*/
export function createPublishGiteaAction(options: {
integrations: ScmIntegrationRegistry;
config: Config;
}) {
const { integrations, config } = options;
return createTemplateAction<{
repoUrl: string;
description: string;
defaultBranch?: string;
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
sourcePath?: string;
}>({
id: 'publish:gitea',
description:
'Initializes a git repository using the content of the workspace, and publishes it to Gitea.',
examples,
schema: {
input: {
type: 'object',
required: ['repoUrl'],
properties: {
repoUrl: {
title: 'Repository Location',
type: 'string',
},
description: {
title: 'Repository Description',
type: 'string',
},
defaultBranch: {
title: 'Default Branch',
type: 'string',
description: `Sets the default branch on the repository. The default value is 'main'`,
},
gitCommitMessage: {
title: 'Git Commit Message',
type: 'string',
description: `Sets the commit message on the repository. The default value is 'initial commit'`,
},
gitAuthorName: {
title: 'Default Author Name',
type: 'string',
description: `Sets the default author name for the commit. The default value is 'Scaffolder'`,
},
gitAuthorEmail: {
title: 'Default Author Email',
type: 'string',
description: `Sets the default author email for the commit.`,
},
sourcePath: {
title: 'Source Path',
type: 'string',
description: `Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.`,
},
},
},
output: {
type: 'object',
properties: {
remoteUrl: {
title: 'A URL to the repository with the provider',
type: 'string',
},
repoContentsUrl: {
title: 'A URL to the root of the repository',
type: 'string',
},
commitHash: {
title: 'The git commit hash of the initial commit',
type: 'string',
},
},
},
},
async handler(ctx) {
const {
repoUrl,
description,
defaultBranch = 'main',
gitAuthorName,
gitAuthorEmail,
gitCommitMessage = 'initial commit',
sourcePath,
} = ctx.input;
const { repo, host, owner } = parseRepoUrl(repoUrl, integrations);
const integrationConfig = integrations.gitea.byHost(host);
if (!integrationConfig) {
throw new InputError(
`No matching integration configuration for host ${host}, please check your integrations config`,
);
}
const { username, password } = integrationConfig.config;
if (!username || !password) {
throw new Error('Credentials for the gitea ${host} required.');
}
// check if the org exists within the gitea server
if (owner) {
await checkGiteaOrg(integrationConfig.config, { owner });
}
await createGiteaProject(integrationConfig.config, {
description,
owner: owner,
projectName: repo,
});
const auth = {
username: username,
password: password,
};
const gitAuthorInfo = {
name: gitAuthorName
? gitAuthorName
: config.getOptionalString('scaffolder.defaultAuthor.name'),
email: gitAuthorEmail
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
// The owner to be used should be either the org name or user authenticated with the gitea server
const remoteUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}.git`;
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath),
remoteUrl,
auth,
defaultBranch,
logger: ctx.logger,
commitMessage: generateCommitMessage(config, gitCommitMessage),
gitAuthorInfo,
});
// Check if the gitea repo URL is available before to exit
const maxDuration = 20000; // 20 seconds
await checkAvailabilityGiteaRepository(
maxDuration,
integrationConfig.config,
{
owner,
repo,
defaultBranch,
ctx,
},
);
const repoContentsUrl = `${integrationConfig.config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}/`;
ctx.output('remoteUrl', remoteUrl);
ctx.output('commitHash', commitResult?.commitHash);
ctx.output('repoContentsUrl', repoContentsUrl);
},
});
}
@@ -0,0 +1,16 @@
/*
* 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.
*/
export * from './gitea';
@@ -0,0 +1,24 @@
/*
* 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.
*/
/**
* A module for the scaffolder backend that lets you interact with gitea
*
* @packageDocumentation
*/
export * from './actions';
export { giteaModule as default } from './module';
@@ -0,0 +1,48 @@
/*
* Copyright 2024 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 {
createBackendModule,
coreServices,
} from '@backstage/backend-plugin-api';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createPublishGiteaAction } from './actions';
import { ScmIntegrations } from '@backstage/integration';
/**
* @public
* The Gitea Module for the Scaffolder Backend
*/
export const giteaModule = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'gitea',
register({ registerInit }) {
registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
config: coreServices.rootConfig,
},
async init({ scaffolder, config }) {
const integrations = ScmIntegrations.fromConfig(config);
scaffolder.addActions(
createPublishGiteaAction({
integrations,
config,
}),
);
},
});
},
});
+1
View File
@@ -64,6 +64,7 @@
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-gerrit": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-gitea": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-github": "workspace:^",
"@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
@@ -81,6 +81,8 @@ import {
createPublishGitlabMergeRequestAction,
} from '@backstage/plugin-scaffolder-backend-module-gitlab';
import { createPublishGiteaAction } from '@backstage/plugin-scaffolder-backend-module-gitea';
/**
* The options passed to {@link createBuiltinActions}
* @public
@@ -158,6 +160,10 @@ export const createBuiltinActions = (
integrations,
config,
}),
createPublishGiteaAction({
integrations,
config,
}),
createPublishGithubAction({
integrations,
config,
@@ -92,6 +92,10 @@ export const parseRepoUrl = (
}
break;
}
case 'gitea': {
checkRequiredParams(parsed, 'repo');
break;
}
case 'gerrit': {
checkRequiredParams(parsed, 'repo');
break;
+19
View File
@@ -8492,6 +8492,24 @@ __metadata:
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-gitea@workspace:^, @backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-gitea@workspace:plugins/scaffolder-backend-module-gitea"
dependencies:
"@backstage/backend-common": "workspace:^"
"@backstage/backend-plugin-api": "workspace:^"
"@backstage/backend-test-utils": "workspace:^"
"@backstage/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/plugin-scaffolder-node": "workspace:^"
msw: ^1.0.0
node-fetch: ^2.6.7
yaml: ^2.0.0
languageName: unknown
linkType: soft
"@backstage/plugin-scaffolder-backend-module-github@workspace:^, @backstage/plugin-scaffolder-backend-module-github@workspace:plugins/scaffolder-backend-module-github":
version: 0.0.0-use.local
resolution: "@backstage/plugin-scaffolder-backend-module-github@workspace:plugins/scaffolder-backend-module-github"
@@ -8616,6 +8634,7 @@ __metadata:
"@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-bitbucket-server": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-gerrit": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-gitea": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-github": "workspace:^"
"@backstage/plugin-scaffolder-backend-module-gitlab": "workspace:^"
"@backstage/plugin-scaffolder-common": "workspace:^"