GitHub Issue + Action submission workflow
An alternative to the built-in submission form is a fully automated GitHub-native approach: contributors fill in a GitHub Issue template, and a GitHub Actions workflow parses the issue, generates the YAML file, commits it to a new branch, and opens a draft pull request, all without leaving GitHub.
This is the pattern used by the Docusaurus Community website.
How it works
- A contributor opens a new issue using your issue template. GitHub renders it as a structured form with dropdowns, checkboxes, and text fields.
- When the issue is opened, the GitHub Action fires. It parses the issue body, validates required fields, generates the YAML, creates a branch, commits the file, and opens a draft PR and then posts the PR link back as a comment on the issue.
- A maintainer reviews and merges the PR.
The workflow won't create the label automatically. You must create the label in your repository settings to match the labels entry in your issue template.
If you have a schemaUrl defined in your plugin configuration, the Action will include it in the generated YAML file. This allows the yaml-language-server to validate the YAML in the PR and provide inline feedback to maintainers.
Issue template
Create .github/ISSUE_TEMPLATE/add-item.yml in your repository. Adapt the tags checkboxes and status dropdown options to match the tags and statuses defined in your plugin configuration.
name: Add an Item
description: Submit a new item to the showcase.
title: "[Submission] "
labels:
- showcase-submission
body:
- type: markdown
attributes:
value: |
Fill in the fields below. A bot will generate your YAML file and open a draft PR automatically.
- type: input
id: name
attributes:
label: Name
description: Display name shown in the showcase.
placeholder: "My Plugin"
validations:
required: true
- type: input
id: id
attributes:
label: ID
description: Unique identifier — lowercase letters, numbers, and hyphens only.
placeholder: "author.my-plugin"
validations:
required: true
- type: textarea
id: description
attributes:
label: Description
description: One sentence describing what this item does.
placeholder: "A plugin that does X."
validations:
required: true
- type: input
id: website
attributes:
label: Website URL
description: Primary URL linked from the card title.
placeholder: "https://example.com"
validations:
required: true
- type: input
id: source
attributes:
label: Source code URL
placeholder: "https://github.com/..."
- type: input
id: author
attributes:
label: Author
placeholder: "yourname"
- type: input
id: preview
attributes:
label: Preview image URL
placeholder: "https://example.com/preview.png"
# Adapt the options below to match the `statuses` defined in your plugin config.
- type: dropdown
id: status
attributes:
label: Status
options:
- maintained
- unmaintained
- unknown
validations:
required: true
- type: input
id: minimumVersion
attributes:
label: Minimum version
placeholder: "3.0.0"
# Adapt the labels below to match the keys defined in your plugin's `tags` config.
- type: checkboxes
id: tags
attributes:
label: Tags
options:
- label: search
- label: api
- label: utility
- label: content
- label: theme
- type: textarea
id: npmPackages
attributes:
label: npm package name(s)
description: One package name per line. Leave blank if not applicable.
placeholder: "my-npm-package"
GitHub Action
Create .github/workflows/create-submission-pr.yml. Update the three variables at the top of the script block to match your repository's layout:
DATA_DIR— path to your showcase data directory (matchesdataDirin your plugin config).LABEL— the issue label that triggers the workflow (must match thelabelsentry in your issue template).SCHEMA_URL— theyaml-language-serverschema URL (matchesschemaUrlin your plugin config, or omit the line if you haven't set one).
name: Create Showcase PR from Issue
on:
issues:
types: [opened]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
create-pr:
if: contains(github.event.issue.labels.*.name, 'showcase-submission')
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
// ── Configure these for your repository ──────────────────────────
const DATA_DIR = 'data/plugins';
const LABEL = 'showcase-submission';
const SCHEMA_URL = 'https://cdn.jsdelivr.net/npm/@homotechsual/docusaurus-plugin-showcase/schema/plugins-preset/1.0.0.json';
// ─────────────────────────────────────────────────────────────────
const body = context.payload.issue.body ?? '';
const issueNumber = context.payload.issue.number;
const issueUrl = context.payload.issue.html_url;
function parseField(body, heading) {
const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`### ${escaped}\\r?\\n\\r?\\n([\\s\\S]*?)(?=\\r?\\n### |$)`);
const match = body.match(regex);
if (!match) return null;
const val = match[1].trim();
return val === '_No response_' || val === '' ? null : val;
}
function parseCheckboxes(body, heading) {
const section = parseField(body, heading);
if (!section) return [];
return section.split('\n')
.filter(line => /^- \[x\]/i.test(line.trim()))
.map(line => line.replace(/^- \[x\] /i, '').trim());
}
const name = parseField(body, 'Name');
const id = parseField(body, 'ID');
const description = parseField(body, 'Description');
const website = parseField(body, 'Website URL');
const source = parseField(body, 'Source code URL');
const author = parseField(body, 'Author');
const preview = parseField(body, 'Preview image URL');
const status = parseField(body, 'Status');
const minimumVersion = parseField(body, 'Minimum version');
const npmPackagesRaw = parseField(body, 'npm package name(s)');
const tags = parseCheckboxes(body, 'Tags');
// Validate required fields
const errors = [];
if (!name) errors.push('Name is required');
if (!id) errors.push('ID is required');
if (!description) errors.push('Description is required');
if (!website) errors.push('Website URL is required');
if (!status) errors.push('Status is required');
if (errors.length > 0) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: [
'Thank you for your submission! Some required fields are missing or invalid:',
'',
...errors.map(e => `- ${e}`),
'',
'Please close this issue and open a new one with all fields filled in.',
].join('\n'),
});
return;
}
const npmPackages = npmPackagesRaw
? npmPackagesRaw.split('\n').map(l => l.trim()).filter(Boolean)
: [];
const filename = `${id}.yaml`;
const branch = `showcase/${id}`;
// Check for duplicate IDs
try {
await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: `${DATA_DIR}/${filename}`,
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `An item with ID \`${id}\` already exists. Please choose a unique ID, then close this issue and open a new one.`,
});
return;
} catch (e) {
// File doesn't exist — expected path
}
// Build YAML
const yamlLines = [];
if (SCHEMA_URL) yamlLines.push(`# yaml-language-server: $schema=${SCHEMA_URL}`);
yamlLines.push(`id: ${id}`);
yamlLines.push(`name: "${name.replace(/"/g, '\\"')}"`);
yamlLines.push(`description: "${description.replace(/"/g, '\\"')}"`);
yamlLines.push(`preview: ${preview || 'null'}`);
yamlLines.push(`website: ${website}`);
yamlLines.push(`source: ${source || 'null'}`);
yamlLines.push(`author: ${author || 'null'}`);
if (tags.length > 0) {
yamlLines.push('tags:');
tags.forEach(t => yamlLines.push(` - ${t}`));
} else {
yamlLines.push('tags: []');
}
yamlLines.push(`minimumVersion: ${minimumVersion || 'null'}`);
yamlLines.push(`status: ${status}`);
if (npmPackages.length > 0) {
yamlLines.push('npmPackages:');
npmPackages.forEach(p => yamlLines.push(` - ${p}`));
} else {
yamlLines.push('npmPackages: []');
}
const yaml = yamlLines.join('\n') + '\n';
// Create branch from main
const mainRef = await github.rest.git.getRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'heads/main',
});
const baseSha = mainRef.data.object.sha;
let actualBranch = branch;
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/heads/${branch}`,
sha: baseSha,
});
} catch (e) {
// Branch already exists — append issue number to make it unique
actualBranch = `${branch}-${issueNumber}`;
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/heads/${actualBranch}`,
sha: baseSha,
});
}
// Commit the YAML file
await github.rest.repos.createOrUpdateFileContents({
owner: context.repo.owner,
repo: context.repo.repo,
path: `${DATA_DIR}/${filename}`,
message: `chore: add ${id} from issue #${issueNumber}`,
content: Buffer.from(yaml).toString('base64'),
branch: actualBranch,
});
// Open a draft PR
const pr = await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Add item: ${name}`,
head: actualBranch,
base: 'main',
body: [
`Adds \`${id}\` to the showcase.`,
'',
`Generated automatically from issue ${issueUrl}.`,
'',
`Closes #${issueNumber}`,
].join('\n'),
draft: true,
});
// Reply on the issue
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: [
`Thank you for your submission! A draft PR has been created: ${pr.data.html_url}`,
'',
'A maintainer will review it shortly.',
].join('\n'),
});
Comparison with the built-in form
Built-in form (submitFormPath) | GitHub Issue + Action | |
|---|---|---|
| Where it lives | Your Docusaurus site | GitHub |
| Contributor needs a GitHub account | No | Yes |
| PR created automatically | No, contributor opens PR manually | Yes, bot opens a draft PR |
| Maintainer review step | Contributor submits PR; maintainer merges | Bot opens draft PR; maintainer reviews and merges |
| Requires GitHub Actions | No | Yes |
| Works without a public GitHub repo | Yes | No |
| Tag / status options are validated | Client-side (form UI) | Server-side (Action script) |
Both approaches produce identical YAML output and are compatible with the same plugin configuration. You can offer both simultaneously, for example, link submitFormPath for contributors who prefer a guided form, and maintain the issue template for those who prefer staying in GitHub.