New plugin to publish a git repository on gitea

Signed-off-by: cmoulliard <cmoulliard@redhat.com>
This commit is contained in:
cmoulliard
2023-12-14 19:03:53 +01:00
parent a5e494cbaf
commit 211ec25212
13 changed files with 579 additions and 2 deletions
@@ -77,8 +77,8 @@ describe('scaffolderModule factory', () => {
'templating package.json.hbs',
'templating index.ts.hbs',
'copying index.ts',
'copying example.test.ts',
'copying example.ts',
'copying gitea.test.ts',
'copying gitea.ts',
'copying index.ts',
'Installing:',
`moving plugins${sep}scaffolder-backend-module-test`,
@@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
@@ -0,0 +1,7 @@
# @backstage/plugin-scaffolder-backend-module-gitea
## 0.1.0-next.0
### Minor Changes
- TODO: Add Gitea Scaffolder Plugin
@@ -0,0 +1,5 @@
# backstage-plugin-scaffolder-backend-module-gitea
The gitea module for [@backstage/plugin-scaffolder-backend](https://www.npmjs.com/package/@backstage/plugin-scaffolder-backend).
_This plugin was created through the Backstage CLI_
+20
View File
@@ -0,0 +1,20 @@
## 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 { JsonObject } from '@backstage/types';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
// @public
export function createAcmeExampleAction(): TemplateAction<
{
myParameter: string;
},
JsonObject
>;
// Warnings were encountered during analysis:
//
// src/index.d.ts:2:25 - (tsdoc-characters-after-block-tag) The token "@backstage" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@"
```
@@ -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,41 @@
{
"name": "backstage-plugin-scaffolder-backend-module-gitea",
"description": "The gitea module for @backstage/plugin-scaffolder-backend",
"version": "0.1.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "backend-plugin-module"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-scaffolder-backend": "workspace:^",
"@backstage/plugin-scaffolder-node": "workspace:^",
"node-fetch": "^2.6.7",
"yaml": "^2.0.0"
},
"devDependencies": {
"@backstage/backend-common": "workspace:^",
"@backstage/cli": "workspace:^"
},
"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 of contents in 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 master',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
defaultBranch: 'staging',
},
},
],
}),
},
{
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,49 @@
/*
* 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 { createAcmeExampleAction } from './gitea';
import { getVoidLogger } from '@backstage/backend-common';
describe('acme:example', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should call action', async () => {
const action = createAcmeExampleAction();
const logger = getVoidLogger();
jest.spyOn(logger, 'info');
await action.handler({
input: {
myParameter: 'test',
},
workspacePath: '/tmp',
logger,
logStream: new PassThrough(),
output: jest.fn(),
createTemporaryDirectory() {
// Usage of mock-fs is recommended for testing of filesystem operations
throw new Error('Not implemented');
},
});
expect(logger.info).toHaveBeenCalledWith(
'Running example template with parameters: test',
);
});
});
@@ -0,0 +1,232 @@
/*
* 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 {
createTemplateAction,
getRepoSourceDirectory,
initRepoAndPush,
parseRepoUrl,
} from '@backstage/plugin-scaffolder-node';
import { examples } from './gitea.examples';
import fetch, { RequestInit, Response } from 'node-fetch';
import crypto from 'crypto';
const createGiteaProject = async (
config: GiteaIntegrationConfig,
options: {
projectName: string;
parent: string;
owner?: string;
description: string;
},
): Promise<void> => {
const { projectName, parent, owner, description } = options;
const fetchOptions: RequestInit = {
method: 'PUT',
body: JSON.stringify({
parent,
description,
owners: owner ? [owner] : [],
create_empty_commit: false,
}),
headers: {
...getGiteaRequestOptions(config).headers,
'Content-Type': 'application/json',
},
};
// TODO
const response: Response = await fetch(
`${config.baseUrl}/a/projects/${encodeURIComponent(projectName)}`,
fetchOptions,
);
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;
};
/**
* 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 'master'`,
},
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, workspace } = 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`,
);
}
if (!workspace) {
throw new InputError(
`Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`,
);
}
await createGiteaProject(integrationConfig.config, {
description,
owner: owner,
projectName: repo,
parent: workspace,
});
const auth = {
username: integrationConfig.config.username!,
password: integrationConfig.config.password!,
};
const gitAuthorInfo = {
name: gitAuthorName
? gitAuthorName
: config.getOptionalString('scaffolder.defaultAuthor.name'),
email: gitAuthorEmail
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
// TODO
const remoteUrl = `${integrationConfig.config.baseUrl}/a/${repo}`;
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath),
remoteUrl,
auth,
defaultBranch,
logger: ctx.logger,
commitMessage: generateCommitMessage(config, gitCommitMessage),
gitAuthorInfo,
});
const repoContentsUrl = `${integrationConfig.config.baseUrl}/${repo}/+/refs/heads/${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,23 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A module for the scaffolder backend that lets you interact with gitea
*
* @packageDocumentation
*/
export * from './actions';
+16
View File
@@ -22051,6 +22051,22 @@ __metadata:
languageName: node
linkType: hard
"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/cli": "workspace:^"
"@backstage/config": "workspace:^"
"@backstage/errors": "workspace:^"
"@backstage/integration": "workspace:^"
"@backstage/plugin-scaffolder-backend": "workspace:^"
"@backstage/plugin-scaffolder-node": "workspace:^"
node-fetch: ^2.6.7
yaml: ^2.0.0
languageName: unknown
linkType: soft
"badge-maker@npm:^3.3.0":
version: 3.3.1
resolution: "badge-maker@npm:3.3.1"