Merge pull request #21890 from ch007m/issue-21866
Create a new action to publish on gitea
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': patch
|
||||
---
|
||||
|
||||
Validate if the `baseUrl` is a valid URL
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-node': patch
|
||||
---
|
||||
|
||||
Add gitea as new type to be used from integrations configuration
|
||||
@@ -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) {
|
||||
|
||||
@@ -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` ;-)
|
||||
@@ -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,
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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:^"
|
||||
|
||||
Reference in New Issue
Block a user