You've already forked woodpecker
mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-05-22 08:55:42 +02:00
Add plugin marketplace (for official plugins) (#451)
Co-authored-by: 6543 <6543@obermui.de>
This commit is contained in:
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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[];
|
||||
};
|
||||
Reference in New Issue
Block a user