1
0
mirror of https://github.com/woodpecker-ci/woodpecker.git synced 2024-12-24 10:07:21 +02:00

Add plugin marketplace (for official plugins) (#451)

Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
Anbraten 2021-10-19 18:54:01 +02:00 committed by GitHub
parent 5990d32fd3
commit 0812a29163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 9193 additions and 1 deletions

View 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

View File

@ -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',

View File

@ -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",

View File

@ -0,0 +1,4 @@
*.log
.DS_Store
node_modules
dist

View 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"
}
}

View 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 };

View 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);
}

View File

@ -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">&lt;&lt; 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;

View File

@ -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;

View 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[];
};

View 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"
}
}

View 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"
}
}

File diff suppressed because it is too large Load Diff