mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2025-01-04 14:48:42 +00:00
Add plugin marketplace (for official plugins) (#451)
Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
parent
5990d32fd3
commit
0812a29163
13 changed files with 9193 additions and 1 deletions
37
.github/ISSUE_TEMPLATE/plugin_index.yml.disabled
vendored
Normal file
37
.github/ISSUE_TEMPLATE/plugin_index.yml.disabled
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
name: "\U0001F4E6 Add plugin to official index"
|
||||||
|
description: Add your plugin to the official index at https://woodpecker-ci.org/plugins
|
||||||
|
labels: ["plugin"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this request!
|
||||||
|
After successfully applying we will create a repository with the name `woodpecker-ci/plugin-<your plugin name>` and grant
|
||||||
|
you write access to it. You will be able to manage things as wanted, just the main branch will be protected and needs approval
|
||||||
|
by a maintainer for security reasons.
|
||||||
|
- type: input
|
||||||
|
id: plugin-name
|
||||||
|
attributes:
|
||||||
|
label: Plugin name
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: plugin-description
|
||||||
|
attributes:
|
||||||
|
label: Short description of the plugin
|
||||||
|
description: A short the description about the plugin.
|
||||||
|
placeholder: Plugin description
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: Validations
|
||||||
|
description: Before submitting the request, please make sure you do the following
|
||||||
|
options:
|
||||||
|
# - label: Follow our [Code of Conduct](https://github.com/woodpecker-ci/woodpecker/blob/master/CODE_OF_CONDUCT.md)
|
||||||
|
# required: true
|
||||||
|
- label: Read the [Contributing Guidelines](https://github.com/woodpecker-ci/woodpecker/blob/master/CONTRIBUTING.md).
|
||||||
|
required: true
|
||||||
|
- label: All plugin code must be licensed under a [FOSS license](https://opensource.org/licenses)
|
||||||
|
required: true
|
|
@ -30,6 +30,11 @@ module.exports = {
|
||||||
position: 'left',
|
position: 'left',
|
||||||
label: 'Docs',
|
label: 'Docs',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
to: '/plugins',
|
||||||
|
position: 'left',
|
||||||
|
label: 'Plugins',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'doc',
|
type: 'doc',
|
||||||
docId: 'migrations',
|
docId: 'migrations',
|
||||||
|
@ -120,6 +125,7 @@ module.exports = {
|
||||||
debug: false, // Set debug to true if you want to inspect the modal
|
debug: false, // Set debug to true if you want to inspect the modal
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
themes: [path.resolve(__dirname, 'plugins', 'woodpecker-plugins', 'dist')],
|
||||||
presets: [
|
presets: [
|
||||||
[
|
[
|
||||||
'@docusaurus/preset-classic',
|
'@docusaurus/preset-classic',
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
"start": "docusaurus start",
|
"start": "docusaurus start",
|
||||||
"build": "docusaurus build",
|
"build": "yarn build:woodpecker-plugins && docusaurus build",
|
||||||
|
"build:woodpecker-plugins": "cd plugins/woodpecker-plugins && yarn && yarn build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
"clear": "docusaurus clear",
|
"clear": "docusaurus clear",
|
||||||
|
|
4
docs/plugins/woodpecker-plugins/.gitignore
vendored
Normal file
4
docs/plugins/woodpecker-plugins/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
dist
|
27
docs/plugins/woodpecker-plugins/package.json
Normal file
27
docs/plugins/woodpecker-plugins/package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "@woodpecker-ci/plugin-index",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"typings": "dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"start": "concurrently 'tsc -w' 'tsc -w -p tsconfig.jsx.json'",
|
||||||
|
"build": "tsc && tsc -p tsconfig.jsx.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@docusaurus/module-type-aliases": "^2.0.0-beta.7",
|
||||||
|
"@docusaurus/theme-classic": "^2.0.0-beta.7",
|
||||||
|
"@docusaurus/types": "^2.0.0-beta.7",
|
||||||
|
"@octokit/openapi-types": "^11.2.0",
|
||||||
|
"@octokit/rest": "^18.12.0",
|
||||||
|
"@tsconfig/docusaurus": "^1.0.4",
|
||||||
|
"@types/marked": "^3.0.1",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
|
"concurrently": "^6.3.0",
|
||||||
|
"marked": "^3.0.7",
|
||||||
|
"typescript": "^4.4.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.4 || ^17.0.0",
|
||||||
|
"react-dom": "^16.8.4 || ^17.0.0"
|
||||||
|
}
|
||||||
|
}
|
125
docs/plugins/woodpecker-plugins/src/index.ts
Normal file
125
docs/plugins/woodpecker-plugins/src/index.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import { LoadContext, Plugin, PluginContentLoadedActions } from '@docusaurus/types';
|
||||||
|
import { Octokit } from '@octokit/rest';
|
||||||
|
import { components as OctokitComponents } from '@octokit/openapi-types';
|
||||||
|
import path from 'path';
|
||||||
|
import { Content, WoodpeckerPlugin, WoodpeckerPluginHeader } from './types';
|
||||||
|
import * as markdown from './markdown';
|
||||||
|
|
||||||
|
const octokit = new Octokit();
|
||||||
|
|
||||||
|
async function getDocs(repoName: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const docsResult = (
|
||||||
|
await octokit.repos.getContent({
|
||||||
|
owner: 'woodpecker-ci',
|
||||||
|
repo: repoName,
|
||||||
|
path: '/docs.md',
|
||||||
|
})
|
||||||
|
).data as OctokitComponents['schemas']['content-file'];
|
||||||
|
|
||||||
|
return Buffer.from(docsResult.content, 'base64').toString('ascii');
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Can't fetch docs file for repository", repoName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadContent(): Promise<Content> {
|
||||||
|
const repositories = (
|
||||||
|
await octokit.rest.search.repos({
|
||||||
|
// search for repos in woodpecker-ci org with the topic: woodpecker-plugin including forks
|
||||||
|
q: 'org:woodpecker-ci topic:woodpecker-plugin fork:true',
|
||||||
|
})
|
||||||
|
).data.items;
|
||||||
|
|
||||||
|
console.log(repositories.map((r) => r.name));
|
||||||
|
|
||||||
|
const plugins = (
|
||||||
|
await Promise.all(
|
||||||
|
repositories.map(async (repo) => {
|
||||||
|
const docs = await getDocs(repo.name);
|
||||||
|
if (!docs) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = markdown.getHeader<WoodpeckerPluginHeader>(docs);
|
||||||
|
const body = markdown.getContent(docs);
|
||||||
|
|
||||||
|
const plugin: WoodpeckerPlugin = {
|
||||||
|
name: header?.name || repo.name,
|
||||||
|
repoName: repo.name,
|
||||||
|
url: repo.html_url,
|
||||||
|
icon: header?.icon,
|
||||||
|
description: header?.description,
|
||||||
|
docs: body,
|
||||||
|
};
|
||||||
|
|
||||||
|
return plugin;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).filter((plugin) => plugin);
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function contentLoaded({
|
||||||
|
content: { plugins },
|
||||||
|
actions,
|
||||||
|
}: {
|
||||||
|
content: Content;
|
||||||
|
actions: PluginContentLoadedActions;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { createData, addRoute } = actions;
|
||||||
|
|
||||||
|
const pluginsJsonPath = await createData('plugins.json', JSON.stringify(plugins));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
plugins.map(async (plugin) => {
|
||||||
|
const pluginJsonPath = await createData(`plugin-${plugin.repoName}.json`, JSON.stringify(plugin));
|
||||||
|
|
||||||
|
addRoute({
|
||||||
|
path: `/plugins/${plugin.repoName}`,
|
||||||
|
component: '@theme/WoodpeckerPlugin',
|
||||||
|
modules: {
|
||||||
|
plugin: pluginJsonPath,
|
||||||
|
},
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
addRoute({
|
||||||
|
path: '/plugins',
|
||||||
|
component: '@theme/WoodpeckerPluginList',
|
||||||
|
modules: {
|
||||||
|
plugins: pluginsJsonPath,
|
||||||
|
},
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function pluginWoodpeckerPluginsIndex(context: LoadContext, options: any): Plugin<Content> {
|
||||||
|
return {
|
||||||
|
name: 'woodpecker-plugins',
|
||||||
|
loadContent,
|
||||||
|
contentLoaded,
|
||||||
|
getThemePath() {
|
||||||
|
return path.join(__dirname, '..', 'dist', 'theme');
|
||||||
|
},
|
||||||
|
getTypeScriptThemePath() {
|
||||||
|
return path.join(__dirname, '..', 'src', 'theme');
|
||||||
|
},
|
||||||
|
getPathsToWatch() {
|
||||||
|
return [path.join(__dirname, '..', 'dist', '**', '*.{js,jsx}')];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSwizzleComponentList = (): string[] => {
|
||||||
|
return ['WoodpeckerPluginList', 'WoodpeckerPlugin'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getSwizzleComponentList };
|
37
docs/plugins/woodpecker-plugins/src/markdown.ts
Normal file
37
docs/plugins/woodpecker-plugins/src/markdown.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import marked from 'marked';
|
||||||
|
|
||||||
|
const tokens = ['---', '---'];
|
||||||
|
const regexHeader = new RegExp('^' + tokens[0] + '([\\s|\\S]*?)' + tokens[1]);
|
||||||
|
const regexContent = new RegExp(
|
||||||
|
'^ *?\\' + tokens[0] + '[^]*?' + tokens[1] + '*'
|
||||||
|
);
|
||||||
|
|
||||||
|
export function getHeader<T = any>(data: string): T {
|
||||||
|
const header = getRawHeader(data);
|
||||||
|
|
||||||
|
const tmpObj = {};
|
||||||
|
const lines = header.trim().split('\n');
|
||||||
|
|
||||||
|
lines.forEach((line, i) => {
|
||||||
|
var arr = line.trim().split(':');
|
||||||
|
tmpObj[arr.shift()] = arr.join(':').trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
return tmpObj as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRawHeader(data: string): string {
|
||||||
|
const header = regexHeader.exec(data);
|
||||||
|
if (!header) {
|
||||||
|
new Error("Can't get the header");
|
||||||
|
}
|
||||||
|
return header[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContent(data): string {
|
||||||
|
const content = data.replace(regexContent, '').replace(/<!--(.*?)-->/gm, '');
|
||||||
|
if (!content) {
|
||||||
|
throw new Error("Can't get the content");
|
||||||
|
}
|
||||||
|
return marked(content);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Layout from '@theme/Layout';
|
||||||
|
import { WoodpeckerPlugin as WoodpeckerPluginType } from '../types';
|
||||||
|
|
||||||
|
export function WoodpeckerPlugin({ plugin }: { plugin: WoodpeckerPluginType }) {
|
||||||
|
return (
|
||||||
|
<Layout
|
||||||
|
title="Woodpecker CI plugins"
|
||||||
|
description="List of Woodpecker-CI plugins"
|
||||||
|
>
|
||||||
|
<main className={clsx("container margin-vert--lg")}>
|
||||||
|
<section>
|
||||||
|
<div className={clsx("container")}>
|
||||||
|
<a href="/plugins"><< Back to plugin list</a>
|
||||||
|
<div className={clsx("row")}>
|
||||||
|
<div className={clsx("col col--10")}>
|
||||||
|
<h1>{plugin.name}</h1>
|
||||||
|
<p>{plugin.description}</p>
|
||||||
|
<a href={plugin.url} target="_blank" rel="noopener noreferrer">
|
||||||
|
{plugin.url}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={clsx("col col--2")}>
|
||||||
|
<img src={plugin.icon} width="150" height="150" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: plugin.docs }} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WoodpeckerPlugin;
|
|
@ -0,0 +1,91 @@
|
||||||
|
import React from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import Layout from '@theme/Layout';
|
||||||
|
import { WoodpeckerPlugin } from '../types';
|
||||||
|
|
||||||
|
function PluginPanel({ plugin }: { plugin: WoodpeckerPlugin }) {
|
||||||
|
const pluginUrl = `/plugins/${plugin.repoName}`;
|
||||||
|
return (
|
||||||
|
<div className={clsx('col col--6')}>
|
||||||
|
<div className={clsx('card margin-horiz--sm margin-vert--md ')}>
|
||||||
|
<div className={clsx('card__header row')}>
|
||||||
|
<div className={clsx('col col--8')}>
|
||||||
|
<a href={pluginUrl}>
|
||||||
|
<h3>{plugin.name}</h3>
|
||||||
|
</a>
|
||||||
|
<p>{plugin.description}</p>
|
||||||
|
</div>
|
||||||
|
<a href={pluginUrl} className={clsx('col col--4 text--right')}>
|
||||||
|
<img src={plugin.icon} width="100" height="100" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={clsx('card__footer')}>
|
||||||
|
<a href={pluginUrl} className={clsx('button button--secondary button--outline button--block ')}>
|
||||||
|
Open {plugin.name}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WoodpeckerPluginList({ plugins }: { plugins: WoodpeckerPlugin[] }) {
|
||||||
|
const applyForIndexUrl =
|
||||||
|
'https://github.com/woodpecker-ci/woodpecker/issues/new?labels=plugin&template=plugin_index.yml';
|
||||||
|
return (
|
||||||
|
<Layout title="Woodpecker CI plugins" description="List of all Woodpecker-CI plugins">
|
||||||
|
<main className="container margin-vert--lg">
|
||||||
|
<section>
|
||||||
|
<div className="container">
|
||||||
|
<div className="row">
|
||||||
|
{plugins.map((plugin, idx) => (
|
||||||
|
<PluginPanel key={idx} plugin={plugin} />
|
||||||
|
))}
|
||||||
|
{/* <div className={clsx('col col--6')}>
|
||||||
|
<div className={clsx('card margin-horiz--sm margin-vert--md ')}>
|
||||||
|
<div className={clsx('card__header row')}>
|
||||||
|
<div className={clsx('col col--8')}>
|
||||||
|
<a href={applyForIndexUrl}>
|
||||||
|
<h3>Add your own plugin</h3>
|
||||||
|
</a>
|
||||||
|
<p>You can simply add your own plugin to this index.</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={applyForIndexUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={clsx('col col--4 text--right')}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="100"
|
||||||
|
height="100"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M88.2357 38.0952H61.9048V11.7643C61.9048 5.29524 56.4714 0 50 0C43.5286 0 38.0952 5.29524 38.0952 11.7643V38.0952H11.7643C5.29524 38.0952 0 43.5286 0 50C0 56.4714 5.29524 61.9048 11.7643 61.9048H38.0952V88.2357C38.0952 94.7048 43.5286 100 50 100C56.4714 100 61.9048 94.7048 61.9048 88.2357V61.9048H88.2357C94.7048 61.9048 100 56.4714 100 50C100 43.5286 94.7048 38.0952 88.2357 38.0952Z"
|
||||||
|
fill="#4CAF50"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={clsx('card__footer')}>
|
||||||
|
<a
|
||||||
|
href={applyForIndexUrl}
|
||||||
|
className={clsx('button button--secondary button--outline button--block ')}
|
||||||
|
>
|
||||||
|
Add your own plugin
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WoodpeckerPluginList;
|
18
docs/plugins/woodpecker-plugins/src/types.ts
Normal file
18
docs/plugins/woodpecker-plugins/src/types.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export type WoodpeckerPluginHeader = {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WoodpeckerPlugin = {
|
||||||
|
name: string;
|
||||||
|
repoName: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
icon: string;
|
||||||
|
docs: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Content = {
|
||||||
|
plugins: WoodpeckerPlugin[];
|
||||||
|
};
|
13
docs/plugins/woodpecker-plugins/tsconfig.json
Normal file
13
docs/plugins/woodpecker-plugins/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
||||||
|
"include": ["src", "types"],
|
||||||
|
"exclude": ["node_modules", "**/__tests__/**/*", "**/dist/**/*", "src/theme"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": false,
|
||||||
|
"pretty": true,
|
||||||
|
"outDir": "./dist"
|
||||||
|
}
|
||||||
|
}
|
13
docs/plugins/woodpecker-plugins/tsconfig.jsx.json
Normal file
13
docs/plugins/woodpecker-plugins/tsconfig.jsx.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/theme"],
|
||||||
|
"exclude": ["node_modules", "**/__tests__/**/*", "**/dist/**/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"jsx": "preserve"
|
||||||
|
}
|
||||||
|
}
|
8783
docs/plugins/woodpecker-plugins/yarn.lock
Normal file
8783
docs/plugins/woodpecker-plugins/yarn.lock
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue