ci: use issure form template to submit (#72)

Co-authored-by: Really Him <hesereallyhim@proton.me>
This commit is contained in:
Really Him
2025-10-15 18:12:03 -04:00
committed by GitHub
parent 49ef8c9d8c
commit a1f78fbe20
3 changed files with 572 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
name: "Git Command Submission"
description: "Submit a new Git command entry using the required format from dev-docs/FORMAT.md."
title: "Add: <command name>"
labels:
- "new command"
body:
- type: markdown
attributes:
value: |
# Contribution Format Guide
**Purpose:** This template captures the required format for documenting Git commands in this repository. All contributors should follow this structure when adding or updating entries in `toc-source.json`.
**How to Contribute:**
- When you want to add a new Git command or script, copy the template below and fill in each field as described.
- Place your new entry in `toc-source.json` using this format.
- Submit your changes via a Pull Request (PR).
- Well-formatted contributions make it easy to generate documentation and keep the project organized.
**Where to Find This:**
- This template reflects `dev-docs/FORMAT.md` (always up to date).
- Main documentation and contribution guidelines may also reference this file.
- type: input
id: name
attributes:
label: "Name"
placeholder: "git example-command"
description: |
Type: string
Description: The official name/title of the command or script.
validations:
required: true
- type: input
id: category
attributes:
label: "Category"
placeholder: "History"
description: |
Type: string
Description: The category this command belongs to (e.g., Collaboration, History, Worktree).
validations:
required: true
- type: input
id: short_description
attributes:
label: "Short Description"
placeholder: "Summarize what the command does."
description: |
Type: string
Description: A concise summary of what the command does.
validations:
required: true
- type: textarea
id: long_description
attributes:
label: "Long Description"
description: |
Type: string (optional)
Description: A more detailed explanation of the command, its context, and use cases.
Please provide plain text or Markdown.
placeholder: "Explain when and why to use this command..."
validations:
required: false
- type: input
id: command
attributes:
label: "Command"
description: |
Type: string (optional)
Description: The full command syntax, including placeholders for arguments.
placeholder: "git example-command <arguments>"
validations:
required: false
- type: textarea
id: examples
attributes:
label: "Examples"
description: |
Type: list of objects
Each object:
- `code`: string (the command as used)
- `description`: string (what the example does)
Provide as valid JSON. Example:
[
{
"code": "git clean -n -d",
"description": "Preview what will be deleted (dry run)."
}
]
validations:
required: true
- type: textarea
id: steps
attributes:
label: "Steps"
description: |
Type: list of strings
Description: Step-by-step instructions for using the command.
Provide as a JSON array of strings.
placeholder: |
[
"First step...",
"Second step..."
]
validations:
required: true
- type: textarea
id: flags
attributes:
label: "Flags"
description: |
Type: object (optional)
Each key: flag (e.g., `-f`, `-d`)
Each value: string (description of the flag)
Provide as a JSON object.
placeholder: |
{
"-f": "Forces the action."
}
validations:
required: false
- type: textarea
id: prerequisites
attributes:
label: "Prerequisites"
description: |
Type: list of strings (optional)
Description: Requirements before using the command.
Provide as a JSON array of strings.
placeholder: |
[
"Have a clean working tree."
]
validations:
required: false
- type: textarea
id: warnings
attributes:
label: "Warnings"
description: |
Type: list of strings (optional)
Description: Important cautions or risks.
Provide as a JSON array of strings.
placeholder: |
[
"This will permanently delete files."
]
validations:
required: false
- type: textarea
id: protips
attributes:
label: "ProTips"
description: |
Type: list of strings (optional)
Description: Short, actionable tips to help users be more successful with this command. Use 12 lines per tip.
Provide as a JSON array of strings.
placeholder: |
[
"Use `-n` for a dry run before deleting files."
]
validations:
required: false
- type: textarea
id: tags
attributes:
label: "Tags"
description: |
Type: list of strings
Description: Keywords for search and categorization.
Provide as a JSON array of strings.
placeholder: |
[
"clean",
"workspace"
]
validations:
required: true
- type: input
id: author
attributes:
label: "Author"
description: |
Type: string
Description: Who contributed this entry.
placeholder: "your-github-handle"
validations:
required: true
- type: input
id: last_updated
attributes:
label: "Last Updated"
description: |
Type: string (date)
Description: Last update date (YYYY-MM-DD).
placeholder: "2025-10-14"
validations:
required: true
- type: textarea
id: links
attributes:
label: "Links"
description: |
Type: list of objects (optional)
Each object:
- `label`: string (e.g., \"Official Docs\")
- `url`: string (link to resource)
Provide as valid JSON.
placeholder: |
[
{
"label": "Official Docs",
"url": "https://git-scm.com/docs/git-clean"
}
]
validations:
required: false
- type: textarea
id: related_commands
attributes:
label: "Related Commands"
description: |
Type: list of strings (optional)
Description: Other commands related to this one.
Provide as a JSON array of strings.
placeholder: |
[
"git status"
]
validations:
required: false

38
.github/workflows/issue-to-toc.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
name: "Convert Issue Submissions to TOC PR"
on:
issues:
types:
- opened
permissions:
contents: write
pull-requests: write
issues: read
jobs:
build:
if: ${{ contains(join(github.event.issue.labels.*.name, ','), 'new command') }}
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Generate toc entry from issue
run: node scripts/issue-to-toc.js
env:
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Create pull request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: issue/${{ github.event.issue.number }}-toc-entry
delete-branch: true
commit-message: Add command from issue #${{ github.event.issue.number }}
title: Add command from issue #${{ github.event.issue.number }}
body: |
## Summary
- Generated from issue #${{ github.event.issue.number }} using the Git command submission template.
- Adds a new entry to `toc-source.json`.

301
scripts/issue-to-toc.js Normal file
View File

@@ -0,0 +1,301 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ISSUE_BODY = process.env.ISSUE_BODY;
const ISSUE_NUMBER = process.env.ISSUE_NUMBER || "unknown";
const WORKSPACE = process.env.GITHUB_WORKSPACE || process.cwd();
if (!ISSUE_BODY) {
console.error("ISSUE_BODY environment variable is required.");
process.exit(1);
}
const FIELD_CONFIG = [
{ heading: "Name", key: "Name", required: true, parser: asString },
{ heading: "Category", key: "category", required: true, parser: asString },
{
heading: "Short Description",
key: "short_description",
required: true,
parser: asString,
},
{
heading: "Long Description",
key: "long_description",
required: false,
parser: asOptionalString,
},
{ heading: "Command", key: "command", required: false, parser: asOptionalString },
{
heading: "Examples",
key: "examples",
required: true,
parser: asExamples,
},
{
heading: "Steps",
key: "steps",
required: true,
parser: asStringArray,
},
{ heading: "Flags", key: "flags", required: false, parser: asFlags },
{
heading: "Prerequisites",
key: "prerequisites",
required: false,
parser: asOptionalStringArray,
},
{
heading: "Warnings",
key: "warnings",
required: false,
parser: asOptionalStringArray,
},
{
heading: "ProTips",
key: "protips",
required: false,
parser: asOptionalStringArray,
},
{
heading: "Tags",
key: "tags",
required: true,
parser: asStringArray,
},
{ heading: "Author", key: "author", required: true, parser: asString },
{
heading: "Last Updated",
key: "last_updated",
required: true,
parser: asDateString,
},
{ heading: "Links", key: "links", required: false, parser: asLinks },
{
heading: "Related Commands",
key: "related_commands",
required: false,
parser: asOptionalStringArray,
},
];
const sections = parseSections(ISSUE_BODY);
const entry = {};
for (const field of FIELD_CONFIG) {
const rawValue = sections[field.heading] ?? "";
if (!rawValue && field.required) {
fail(`Field "${field.heading}" is required but was not provided.`);
}
const parsedValue = field.parser(rawValue, field.heading);
if (parsedValue !== undefined) {
entry[field.key] = parsedValue;
}
}
const tocPath = path.join(WORKSPACE, "toc-source.json");
const toc = readJson(tocPath);
ensureUniqueName(entry.Name, toc);
toc.push(entry);
writeJson(tocPath, toc);
console.log(`Entry for "${entry.Name}" added to toc-source.json from issue #${ISSUE_NUMBER}.`);
function parseSections(body) {
const normalized = body.replace(/\r\n/g, "\n").trim();
const regex = /### ([^\n]+)\n([\s\S]*?)(?=(?:\n### [^\n]+)|$)/g;
const result = {};
let match;
while ((match = regex.exec(normalized)) !== null) {
const heading = match[1].trim();
const value = match[2] ?? "";
result[heading] = value;
}
return result;
}
function cleanupValue(value) {
if (!value) return "";
let cleaned = value.trim();
const isNoResponse = (text) =>
text.replace(/\s+/g, " ").toLowerCase() === "_no response_";
if (isNoResponse(cleaned)) {
return "";
}
if (cleaned.startsWith("```")) {
const lines = cleaned.split(/\r?\n/);
if (lines[0].startsWith("```")) {
lines.shift();
const last = lines[lines.length - 1];
if (last && last.startsWith("```")) {
lines.pop();
}
cleaned = lines.join("\n").trim();
if (isNoResponse(cleaned)) {
return "";
}
}
}
return cleaned;
}
function asString(raw, field) {
const value = cleanupValue(raw);
if (!value) {
fail(`Field "${field}" must be a non-empty string.`);
}
return value;
}
function asOptionalString(raw) {
const value = cleanupValue(raw);
return value ? value : undefined;
}
function asDateString(raw, field) {
const value = asString(raw, field);
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
fail(`Field "${field}" must follow the YYYY-MM-DD format.`);
}
return value;
}
function parseJson(raw, field) {
const value = cleanupValue(raw);
if (!value) {
return undefined;
}
try {
return JSON.parse(value);
} catch (error) {
fail(`Field "${field}" must contain valid JSON. ${error.message}`);
}
}
function asStringArray(raw, field) {
const parsed = parseJson(raw, field);
if (!Array.isArray(parsed) || parsed.length === 0) {
fail(`Field "${field}" must be a non-empty JSON array of strings.`);
}
parsed.forEach((item, index) => {
if (typeof item !== "string" || !item.trim()) {
fail(`Field "${field}" must contain only non-empty strings (problem at index ${index}).`);
}
});
return parsed;
}
function asOptionalStringArray(raw, field) {
const value = cleanupValue(raw);
if (!value) {
return undefined;
}
const parsed = parseJson(value, field);
if (!Array.isArray(parsed)) {
fail(`Field "${field}" must be a JSON array of strings.`);
}
parsed.forEach((item, index) => {
if (typeof item !== "string" || !item.trim()) {
fail(`Field "${field}" must contain only non-empty strings (problem at index ${index}).`);
}
});
return parsed;
}
function asExamples(raw, field) {
const parsed = parseJson(raw, field);
if (!Array.isArray(parsed) || parsed.length === 0) {
fail(`Field "${field}" must be a non-empty JSON array of objects with "code" and "description" strings.`);
}
parsed.forEach((item, index) => {
if (!item || typeof item !== "object") {
fail(`Field "${field}" must contain objects (problem at index ${index}).`);
}
if (typeof item.code !== "string" || !item.code.trim()) {
fail(`Field "${field}" example ${index} is missing a non-empty "code" string.`);
}
if (typeof item.description !== "string" || !item.description.trim()) {
fail(`Field "${field}" example ${index} is missing a non-empty "description" string.`);
}
});
return parsed;
}
function asFlags(raw, field) {
const value = cleanupValue(raw);
if (!value) {
return undefined;
}
const parsed = parseJson(value, field);
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
fail(`Field "${field}" must be a JSON object of flag descriptions.`);
}
Object.entries(parsed).forEach(([flag, description]) => {
if (typeof flag !== "string" || !flag.trim()) {
fail(`Field "${field}" contains a flag with an invalid name.`);
}
if (typeof description !== "string" || !description.trim()) {
fail(`Field "${field}" flag "${flag}" must have a non-empty description string.`);
}
});
return parsed;
}
function asLinks(raw, field) {
const value = cleanupValue(raw);
if (!value) {
return undefined;
}
const parsed = parseJson(value, field);
if (!Array.isArray(parsed)) {
fail(`Field "${field}" must be a JSON array of link objects.`);
}
parsed.forEach((item, index) => {
if (!item || typeof item !== "object") {
fail(`Field "${field}" must contain objects (problem at index ${index}).`);
}
if (typeof item.label !== "string" || !item.label.trim()) {
fail(`Field "${field}" link ${index} is missing a non-empty "label" string.`);
}
if (typeof item.url !== "string" || !item.url.trim()) {
fail(`Field "${field}" link ${index} is missing a non-empty "url" string.`);
}
});
return parsed;
}
function ensureUniqueName(name, toc) {
const exists = toc.some((entry) => {
if (entry && typeof entry === "object") {
if (entry.Name === name) return true;
if (Array.isArray(entry.subtoc)) {
return entry.subtoc.some((sub) => sub && sub.Name === name);
}
}
return false;
});
if (exists) {
fail(`An entry with the Name "${name}" already exists in toc-source.json.`);
}
}
function readJson(filePath) {
try {
const raw = fs.readFileSync(filePath, "utf8");
return JSON.parse(raw);
} catch (error) {
fail(`Unable to read or parse JSON file at ${filePath}. ${error.message}`);
}
}
function writeJson(filePath, data) {
const serialized = JSON.stringify(data, null, 2) + "\n";
fs.writeFileSync(filePath, serialized, "utf8");
}
function fail(message) {
console.error(message);
process.exit(1);
}