mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-30 12:20:33 +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',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
to: '/plugins',
|
||||
position: 'left',
|
||||
label: 'Plugins',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
docId: 'migrations',
|
||||
|
@ -120,6 +125,7 @@ module.exports = {
|
|||
debug: false, // Set debug to true if you want to inspect the modal
|
||||
},
|
||||
},
|
||||
themes: [path.resolve(__dirname, 'plugins', 'woodpecker-plugins', 'dist')],
|
||||
presets: [
|
||||
[
|
||||
'@docusaurus/preset-classic',
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"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",
|
||||
"deploy": "docusaurus deploy",
|
||||
"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