Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14b1f7e9bc | ||
|
|
0196385345 |
369
.github/workflows/docs-to-issues.yml
vendored
Normal file
369
.github/workflows/docs-to-issues.yml
vendored
Normal file
@@ -0,0 +1,369 @@
|
||||
name: Convert Docs to Issues
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
paths:
|
||||
- 'docs/issues/**/*.md'
|
||||
- '!docs/issues/created/**'
|
||||
- '!docs/issues/_TEMPLATE.md'
|
||||
- '!docs/issues/README.md'
|
||||
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run (no issues created)'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
file_path:
|
||||
description: 'Specific file to process (optional)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
convert-docs:
|
||||
name: Convert Markdown to Issues
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'github-actions[bot]'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install gray-matter
|
||||
|
||||
- name: Detect changed files
|
||||
id: changes
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Manual file specification
|
||||
const manualFile = '${{ github.event.inputs.file_path }}';
|
||||
if (manualFile) {
|
||||
if (fs.existsSync(manualFile)) {
|
||||
core.setOutput('files', JSON.stringify([manualFile]));
|
||||
return;
|
||||
} else {
|
||||
core.setFailed(`File not found: ${manualFile}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get changed files from commit
|
||||
const { data: commit } = await github.rest.repos.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: context.sha
|
||||
});
|
||||
|
||||
const changedFiles = (commit.files || [])
|
||||
.filter(f => f.filename.startsWith('docs/issues/'))
|
||||
.filter(f => !f.filename.startsWith('docs/issues/created/'))
|
||||
.filter(f => !f.filename.includes('_TEMPLATE'))
|
||||
.filter(f => !f.filename.includes('README'))
|
||||
.filter(f => f.filename.endsWith('.md'))
|
||||
.filter(f => f.status !== 'removed')
|
||||
.map(f => f.filename);
|
||||
|
||||
console.log('Changed issue files:', changedFiles);
|
||||
core.setOutput('files', JSON.stringify(changedFiles));
|
||||
|
||||
- name: Process issue files
|
||||
id: process
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
env:
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const matter = require('gray-matter');
|
||||
|
||||
const files = JSON.parse('${{ steps.changes.outputs.files }}');
|
||||
const isDryRun = process.env.DRY_RUN === 'true';
|
||||
const createdIssues = [];
|
||||
const errors = [];
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No issue files to process');
|
||||
core.setOutput('created_count', 0);
|
||||
core.setOutput('created_issues', '[]');
|
||||
core.setOutput('errors', '[]');
|
||||
return;
|
||||
}
|
||||
|
||||
// Label color map
|
||||
const labelColors = {
|
||||
testing: 'BFD4F2',
|
||||
feature: 'A2EEEF',
|
||||
enhancement: '84B6EB',
|
||||
bug: 'D73A4A',
|
||||
documentation: '0075CA',
|
||||
backend: '1D76DB',
|
||||
frontend: '5EBEFF',
|
||||
security: 'EE0701',
|
||||
ui: '7057FF',
|
||||
caddy: '1F6FEB',
|
||||
'needs-triage': 'FBCA04',
|
||||
acl: 'C5DEF5',
|
||||
regression: 'D93F0B',
|
||||
'manual-testing': 'BFD4F2',
|
||||
'bulk-acl': '006B75',
|
||||
'error-handling': 'D93F0B',
|
||||
'ui-ux': '7057FF',
|
||||
integration: '0E8A16',
|
||||
performance: 'EDEDED',
|
||||
'cross-browser': '5319E7',
|
||||
plus: 'FFD700',
|
||||
beta: '0052CC',
|
||||
alpha: '5319E7',
|
||||
high: 'D93F0B',
|
||||
medium: 'FBCA04',
|
||||
low: '0E8A16',
|
||||
critical: 'B60205',
|
||||
architecture: '006B75',
|
||||
database: '006B75',
|
||||
'post-beta': '006B75'
|
||||
};
|
||||
|
||||
// Helper: Ensure label exists
|
||||
async function ensureLabel(name) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: name
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: name,
|
||||
color: labelColors[name.toLowerCase()] || '666666'
|
||||
});
|
||||
console.log(`Created label: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Parse markdown file
|
||||
function parseIssueFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const { data: frontmatter, content: body } = matter(content);
|
||||
|
||||
// Extract title: frontmatter > first H1 > filename
|
||||
let title = frontmatter.title;
|
||||
if (!title) {
|
||||
const h1Match = body.match(/^#\s+(.+)$/m);
|
||||
title = h1Match ? h1Match[1] : path.basename(filePath, '.md').replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
// Build labels array
|
||||
const labels = [...(frontmatter.labels || [])];
|
||||
if (frontmatter.priority) labels.push(frontmatter.priority);
|
||||
if (frontmatter.type) labels.push(frontmatter.type);
|
||||
|
||||
return {
|
||||
title,
|
||||
body: body.trim(),
|
||||
labels: [...new Set(labels)],
|
||||
assignees: frontmatter.assignees || [],
|
||||
milestone: frontmatter.milestone,
|
||||
parent_issue: frontmatter.parent_issue,
|
||||
create_sub_issues: frontmatter.create_sub_issues || false
|
||||
};
|
||||
}
|
||||
|
||||
// Helper: Extract sub-issues from H2 sections
|
||||
function extractSubIssues(body, parentLabels) {
|
||||
const sections = [];
|
||||
const lines = body.split('\n');
|
||||
let currentSection = null;
|
||||
let currentBody = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const h2Match = line.match(/^##\s+(?:Sub-Issue\s*#?\d*:?\s*)?(.+)$/);
|
||||
if (h2Match) {
|
||||
if (currentSection) {
|
||||
sections.push({
|
||||
title: currentSection,
|
||||
body: currentBody.join('\n').trim(),
|
||||
labels: [...parentLabels]
|
||||
});
|
||||
}
|
||||
currentSection = h2Match[1].trim();
|
||||
currentBody = [];
|
||||
} else if (currentSection) {
|
||||
currentBody.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSection) {
|
||||
sections.push({
|
||||
title: currentSection,
|
||||
body: currentBody.join('\n').trim(),
|
||||
labels: [...parentLabels]
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
// Process each file
|
||||
for (const filePath of files) {
|
||||
console.log(`\nProcessing: ${filePath}`);
|
||||
|
||||
try {
|
||||
const parsed = parseIssueFile(filePath);
|
||||
console.log(` Title: ${parsed.title}`);
|
||||
console.log(` Labels: ${parsed.labels.join(', ')}`);
|
||||
|
||||
if (isDryRun) {
|
||||
console.log(' [DRY RUN] Would create issue');
|
||||
createdIssues.push({ file: filePath, title: parsed.title, dryRun: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure labels exist
|
||||
for (const label of parsed.labels) {
|
||||
await ensureLabel(label);
|
||||
}
|
||||
|
||||
// Create the main issue
|
||||
const issueBody = parsed.body +
|
||||
`\n\n---\n*Auto-created from [${path.basename(filePath)}](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${context.sha}/${filePath})*`;
|
||||
|
||||
const issueResponse = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: parsed.title,
|
||||
body: issueBody,
|
||||
labels: parsed.labels,
|
||||
assignees: parsed.assignees
|
||||
});
|
||||
|
||||
const issueNumber = issueResponse.data.number;
|
||||
console.log(` Created issue #${issueNumber}`);
|
||||
|
||||
// Handle sub-issues
|
||||
if (parsed.create_sub_issues) {
|
||||
const subIssues = extractSubIssues(parsed.body, parsed.labels);
|
||||
for (const sub of subIssues) {
|
||||
for (const label of sub.labels) {
|
||||
await ensureLabel(label);
|
||||
}
|
||||
const subResponse = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `[${parsed.title}] ${sub.title}`,
|
||||
body: sub.body + `\n\n---\n*Sub-issue of #${issueNumber}*`,
|
||||
labels: sub.labels,
|
||||
assignees: parsed.assignees
|
||||
});
|
||||
console.log(` Created sub-issue #${subResponse.data.number}: ${sub.title}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Link to parent issue if specified
|
||||
if (parsed.parent_issue) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parsed.parent_issue,
|
||||
body: `Sub-issue created: #${issueNumber}`
|
||||
});
|
||||
}
|
||||
|
||||
createdIssues.push({
|
||||
file: filePath,
|
||||
title: parsed.title,
|
||||
issueNumber
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(` Error processing ${filePath}: ${error.message}`);
|
||||
errors.push({ file: filePath, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
core.setOutput('created_count', createdIssues.length);
|
||||
core.setOutput('created_issues', JSON.stringify(createdIssues));
|
||||
core.setOutput('errors', JSON.stringify(errors));
|
||||
|
||||
if (errors.length > 0) {
|
||||
core.warning(`${errors.length} file(s) had errors`);
|
||||
}
|
||||
|
||||
- name: Move processed files
|
||||
if: steps.process.outputs.created_count != '0' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
mkdir -p docs/issues/created
|
||||
CREATED_ISSUES='${{ steps.process.outputs.created_issues }}'
|
||||
echo "$CREATED_ISSUES" | jq -r '.[].file' | while read file; do
|
||||
if [ -f "$file" ] && [ ! -z "$file" ]; then
|
||||
filename=$(basename "$file")
|
||||
timestamp=$(date +%Y%m%d)
|
||||
mv "$file" "docs/issues/created/${timestamp}-${filename}"
|
||||
echo "Moved: $file -> docs/issues/created/${timestamp}-${filename}"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Commit moved files
|
||||
if: steps.process.outputs.created_count != '0' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add docs/issues/
|
||||
git diff --staged --quiet || git commit -m "chore: move processed issue files to created/ [skip ci]"
|
||||
git push
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Docs to Issues Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
CREATED='${{ steps.process.outputs.created_issues }}'
|
||||
ERRORS='${{ steps.process.outputs.errors }}'
|
||||
DRY_RUN='${{ github.event.inputs.dry_run }}'
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "🔍 **Dry Run Mode** - No issues were actually created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "### Created Issues" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -n "$CREATED" ] && [ "$CREATED" != "[]" ] && [ "$CREATED" != "null" ]; then
|
||||
echo "$CREATED" | jq -r '.[] | "- \(.title) (#\(.issueNumber // "dry-run"))"' >> $GITHUB_STEP_SUMMARY || echo "_Parse error_" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "_No issues created_" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Errors" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -n "$ERRORS" ] && [ "$ERRORS" != "[]" ] && [ "$ERRORS" != "null" ]; then
|
||||
echo "$ERRORS" | jq -r '.[] | "- ❌ \(.file): \(.error)"' >> $GITHUB_STEP_SUMMARY || echo "_Parse error_" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "_No errors_" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
85
docs/issues/README.md
Normal file
85
docs/issues/README.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# docs/issues - Issue Specification Files
|
||||
|
||||
This directory contains markdown files that are automatically converted to GitHub Issues when merged to `main` or `development`.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Create a markdown file** in this directory using the template format
|
||||
2. **Add YAML frontmatter** with issue metadata (title, labels, priority, etc.)
|
||||
3. **Merge to main/development** - the `docs-to-issues.yml` workflow runs
|
||||
4. **GitHub Issue is created** with your specified metadata
|
||||
5. **File is moved** to `docs/issues/created/` to prevent duplicates
|
||||
|
||||
## Quick Start
|
||||
|
||||
Copy `_TEMPLATE.md` and fill in your issue details:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "My New Issue"
|
||||
labels:
|
||||
- feature
|
||||
- backend
|
||||
priority: medium
|
||||
---
|
||||
|
||||
# My New Issue
|
||||
|
||||
Description of the issue...
|
||||
```
|
||||
|
||||
## Frontmatter Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `title` | Yes* | Issue title (*or uses first H1 as fallback) |
|
||||
| `labels` | No | Array of labels to apply |
|
||||
| `priority` | No | `critical`, `high`, `medium`, `low` |
|
||||
| `milestone` | No | Milestone name |
|
||||
| `assignees` | No | Array of GitHub usernames |
|
||||
| `parent_issue` | No | Parent issue number for linking |
|
||||
| `create_sub_issues` | No | If `true`, each `## Section` becomes a sub-issue |
|
||||
|
||||
## Sub-Issues
|
||||
|
||||
To create multiple related issues from one file, set `create_sub_issues: true`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Main Testing Issue"
|
||||
labels: [testing]
|
||||
create_sub_issues: true
|
||||
---
|
||||
|
||||
# Main Testing Issue
|
||||
|
||||
Overview content for the parent issue.
|
||||
|
||||
## Unit Testing
|
||||
|
||||
This section becomes a separate issue.
|
||||
|
||||
## Integration Testing
|
||||
|
||||
This section becomes another separate issue.
|
||||
```
|
||||
|
||||
## Manual Trigger
|
||||
|
||||
You can manually run the workflow with:
|
||||
|
||||
```bash
|
||||
# Dry run (no issues created)
|
||||
gh workflow run docs-to-issues.yml -f dry_run=true
|
||||
|
||||
# Process specific file
|
||||
gh workflow run docs-to-issues.yml -f file_path=docs/issues/my-issue.md
|
||||
```
|
||||
|
||||
## Labels
|
||||
|
||||
Labels are automatically created if they don't exist. Common labels:
|
||||
|
||||
- **Priority**: `critical`, `high`, `medium`, `low`
|
||||
- **Type**: `feature`, `bug`, `enhancement`, `testing`, `documentation`
|
||||
- **Component**: `backend`, `frontend`, `ui`, `security`, `caddy`, `database`
|
||||
45
docs/issues/_TEMPLATE.md
Normal file
45
docs/issues/_TEMPLATE.md
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
# REQUIRED: Issue title
|
||||
title: "Your Issue Title"
|
||||
|
||||
# OPTIONAL: Labels to apply (will be created if missing)
|
||||
labels:
|
||||
- feature # feature, bug, enhancement, testing, documentation
|
||||
- backend # backend, frontend, ui, security, caddy, database
|
||||
|
||||
# OPTIONAL: Priority (creates matching label)
|
||||
priority: medium # critical, high, medium, low
|
||||
|
||||
# OPTIONAL: Milestone name
|
||||
milestone: "v0.2.0-beta.2"
|
||||
|
||||
# OPTIONAL: GitHub usernames to assign
|
||||
assignees: []
|
||||
|
||||
# OPTIONAL: Parent issue number for linking
|
||||
# parent_issue: 42
|
||||
|
||||
# OPTIONAL: Parse ## sections as separate sub-issues
|
||||
# create_sub_issues: true
|
||||
---
|
||||
|
||||
# Issue Title
|
||||
|
||||
## Description
|
||||
|
||||
Clear description of the issue or feature request.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Criterion 1
|
||||
- [ ] Criterion 2
|
||||
|
||||
## Related Issues
|
||||
|
||||
- #XX - Related issue description
|
||||
1
docs/issues/created/.gitkeep
Normal file
1
docs/issues/created/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# Processed issue files are moved here after GitHub Issues are created
|
||||
910
docs/plans/docs_to_issues_workflow.md
Normal file
910
docs/plans/docs_to_issues_workflow.md
Normal file
@@ -0,0 +1,910 @@
|
||||
# docs/issues to GitHub Issues Workflow - Implementation Plan
|
||||
|
||||
**Version:** 1.0
|
||||
**Date:** 2025-12-13
|
||||
**Status:** 📋 PLANNING
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This document provides a comprehensive plan for a GitHub Actions workflow that automatically converts markdown files in `docs/issues/` into GitHub Issues, applies labels, and adds them to the project board.
|
||||
|
||||
---
|
||||
|
||||
## 1. Research Findings
|
||||
|
||||
### 1.1 Current docs/issues File Analysis
|
||||
|
||||
Analyzed 8 existing files in `docs/issues/`:
|
||||
|
||||
| File | Structure | Has Frontmatter | Labels Present | Title Pattern |
|
||||
|------|-----------|-----------------|----------------|---------------|
|
||||
| `ACL-testing-tasks.md` | Free-form + metadata section | ❌ | Inline suggestion | H1 + metadata block |
|
||||
| `Additional_Security.md` | Markdown lists | ❌ | ❌ | H3 title |
|
||||
| `bulk-acl-subissues.md` | Multi-issue spec | ❌ | Inline per section | Inline titles |
|
||||
| `bulk-acl-testing.md` | Full issue template | ❌ | Inline section | H1 title |
|
||||
| `hectate.md` | Feature spec | ❌ | ❌ | H1 title |
|
||||
| `orthrus.md` | Feature spec | ❌ | ❌ | H1 title |
|
||||
| `plex-remote-access-helper.md` | Issue template | ❌ | Inline section | `## Issue Title` |
|
||||
| `rotating-loading-animations.md` | Enhancement spec | ❌ | `**Issue Type**` line | H1 title |
|
||||
|
||||
**Key Finding:** No files use YAML frontmatter. Most have ad-hoc metadata embedded in markdown body.
|
||||
|
||||
### 1.2 Existing Workflow Patterns
|
||||
|
||||
From `.github/workflows/`:
|
||||
|
||||
| Workflow | Pattern | Relevance |
|
||||
|----------|---------|-----------|
|
||||
| `auto-label-issues.yml` | `actions/github-script` for issue manipulation | Label creation, issue API |
|
||||
| `propagate-changes.yml` | Complex `github-script` with file analysis | File path detection |
|
||||
| `auto-add-to-project.yml` | `actions/add-to-project` action | Project board integration |
|
||||
| `create-labels.yml` | Label creation/update logic | Label management |
|
||||
|
||||
### 1.3 Project Board Configuration
|
||||
|
||||
- Project URL stored in `secrets.PROJECT_URL`
|
||||
- PAT token: `secrets.ADD_TO_PROJECT_PAT`
|
||||
- Uses `actions/add-to-project@v1.0.2` for automatic addition
|
||||
|
||||
---
|
||||
|
||||
## 2. Markdown File Format Specification
|
||||
|
||||
### 2.1 Required Frontmatter Format
|
||||
|
||||
All files in `docs/issues/` should use YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Issue Title Here"
|
||||
labels:
|
||||
- testing
|
||||
- feature
|
||||
- backend
|
||||
assignees:
|
||||
- username1
|
||||
- username2
|
||||
milestone: "v0.2.0-beta.2"
|
||||
priority: high # critical, high, medium, low
|
||||
type: feature # feature, bug, enhancement, testing, documentation
|
||||
parent_issue: 42 # Optional: Link as sub-issue
|
||||
create_sub_issues: true # Optional: Parse ## sections as separate issues
|
||||
---
|
||||
|
||||
# Issue Title (H1 fallback if frontmatter title missing)
|
||||
|
||||
Issue body content starts here...
|
||||
```
|
||||
|
||||
### 2.2 Frontmatter Fields
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `title` | Yes* | string | Issue title (*or H1 fallback) |
|
||||
| `labels` | No | array | Labels to apply (created if missing) |
|
||||
| `assignees` | No | array | GitHub usernames |
|
||||
| `milestone` | No | string | Milestone name |
|
||||
| `priority` | No | enum | Maps to priority label |
|
||||
| `type` | No | enum | Maps to type label |
|
||||
| `parent_issue` | No | number | Parent issue number for linking |
|
||||
| `create_sub_issues` | No | boolean | Parse H2 sections as sub-issues |
|
||||
|
||||
### 2.3 Sub-Issue Detection
|
||||
|
||||
When `create_sub_issues: true`, each `## Section Title` becomes a sub-issue:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Main Testing Issue"
|
||||
labels: [testing]
|
||||
create_sub_issues: true
|
||||
---
|
||||
|
||||
# Main Testing Issue
|
||||
|
||||
Overview text (goes in parent issue).
|
||||
|
||||
## Sub-Issue #1: Unit Testing
|
||||
|
||||
This section becomes a separate issue titled "Unit Testing"
|
||||
with body content from this section.
|
||||
|
||||
## Sub-Issue #2: Integration Testing
|
||||
|
||||
This section becomes another separate issue.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Workflow Implementation
|
||||
|
||||
### 3.1 Workflow File: `.github/workflows/docs-to-issues.yml`
|
||||
|
||||
```yaml
|
||||
name: Convert Docs to Issues
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- development
|
||||
paths:
|
||||
- 'docs/issues/**/*.md'
|
||||
- '!docs/issues/created/**'
|
||||
|
||||
# Allow manual trigger
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run (no issues created)'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
file_path:
|
||||
description: 'Specific file to process (optional)'
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
convert-docs:
|
||||
name: Convert Markdown to Issues
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor != 'github-actions[bot]'
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
with:
|
||||
fetch-depth: 2 # Need previous commit for diff
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install gray-matter
|
||||
|
||||
- name: Detect changed files
|
||||
id: changes
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Manual file specification
|
||||
const manualFile = '${{ github.event.inputs.file_path }}';
|
||||
if (manualFile) {
|
||||
if (fs.existsSync(manualFile)) {
|
||||
core.setOutput('files', JSON.stringify([manualFile]));
|
||||
return;
|
||||
} else {
|
||||
core.setFailed(`File not found: ${manualFile}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get changed files from commit
|
||||
const { data: commit } = await github.rest.repos.getCommit({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: context.sha
|
||||
});
|
||||
|
||||
const changedFiles = (commit.files || [])
|
||||
.filter(f => f.filename.startsWith('docs/issues/'))
|
||||
.filter(f => !f.filename.startsWith('docs/issues/created/'))
|
||||
.filter(f => f.filename.endsWith('.md'))
|
||||
.filter(f => f.status !== 'removed')
|
||||
.map(f => f.filename);
|
||||
|
||||
console.log('Changed issue files:', changedFiles);
|
||||
core.setOutput('files', JSON.stringify(changedFiles));
|
||||
|
||||
- name: Process issue files
|
||||
id: process
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
env:
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const matter = require('gray-matter');
|
||||
|
||||
const files = JSON.parse('${{ steps.changes.outputs.files }}');
|
||||
const isDryRun = process.env.DRY_RUN === 'true';
|
||||
const createdIssues = [];
|
||||
const errors = [];
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No issue files to process');
|
||||
core.setOutput('created_count', 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper: Ensure label exists
|
||||
async function ensureLabel(name) {
|
||||
try {
|
||||
await github.rest.issues.getLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: name
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.status === 404) {
|
||||
// Create label with default color
|
||||
const colors = {
|
||||
testing: 'BFD4F2',
|
||||
feature: 'A2EEEF',
|
||||
enhancement: '84B6EB',
|
||||
bug: 'D73A4A',
|
||||
documentation: '0075CA',
|
||||
backend: '1D76DB',
|
||||
frontend: '5EBEFF',
|
||||
security: 'EE0701',
|
||||
ui: '7057FF',
|
||||
caddy: '1F6FEB',
|
||||
'needs-triage': 'FBCA04',
|
||||
acl: 'C5DEF5',
|
||||
regression: 'D93F0B',
|
||||
'manual-testing': 'BFD4F2',
|
||||
'bulk-acl': '006B75',
|
||||
'error-handling': 'D93F0B',
|
||||
'ui-ux': '7057FF',
|
||||
integration: '0E8A16',
|
||||
performance: 'EDEDED',
|
||||
'cross-browser': '5319E7',
|
||||
plus: 'FFD700',
|
||||
beta: '0052CC',
|
||||
alpha: '5319E7',
|
||||
high: 'D93F0B',
|
||||
medium: 'FBCA04',
|
||||
low: '0E8A16',
|
||||
critical: 'B60205'
|
||||
};
|
||||
await github.rest.issues.createLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: name,
|
||||
color: colors[name.toLowerCase()] || '666666'
|
||||
});
|
||||
console.log(`Created label: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper: Parse markdown file
|
||||
function parseIssueFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const { data: frontmatter, content: body } = matter(content);
|
||||
|
||||
// Extract title: frontmatter > first H1 > filename
|
||||
let title = frontmatter.title;
|
||||
if (!title) {
|
||||
const h1Match = body.match(/^#\s+(.+)$/m);
|
||||
title = h1Match ? h1Match[1] : path.basename(filePath, '.md');
|
||||
}
|
||||
|
||||
// Build labels array
|
||||
const labels = [...(frontmatter.labels || [])];
|
||||
if (frontmatter.priority) labels.push(frontmatter.priority);
|
||||
if (frontmatter.type) labels.push(frontmatter.type);
|
||||
|
||||
return {
|
||||
title,
|
||||
body: body.trim(),
|
||||
labels: [...new Set(labels)],
|
||||
assignees: frontmatter.assignees || [],
|
||||
milestone: frontmatter.milestone,
|
||||
parent_issue: frontmatter.parent_issue,
|
||||
create_sub_issues: frontmatter.create_sub_issues || false
|
||||
};
|
||||
}
|
||||
|
||||
// Helper: Extract sub-issues from H2 sections
|
||||
function extractSubIssues(body, parentLabels) {
|
||||
const sections = [];
|
||||
const regex = /^##\s+(?:Sub-Issue\s*#?\d*:?\s*)?(.+?)$([\s\S]*?)(?=^##\s|\Z)/gm;
|
||||
let match;
|
||||
|
||||
while ((match = regex.exec(body)) !== null) {
|
||||
const sectionTitle = match[1].trim();
|
||||
const sectionBody = match[2].trim();
|
||||
|
||||
// Extract inline labels like **Labels**: `testing`, `acl`
|
||||
const inlineLabels = [];
|
||||
const labelMatch = sectionBody.match(/\*\*Labels?\*\*:?\s*[`']?([^`'\n]+)[`']?/i);
|
||||
if (labelMatch) {
|
||||
labelMatch[1].split(',').forEach(l => {
|
||||
const label = l.replace(/[`']/g, '').trim();
|
||||
if (label) inlineLabels.push(label);
|
||||
});
|
||||
}
|
||||
|
||||
sections.push({
|
||||
title: sectionTitle,
|
||||
body: sectionBody,
|
||||
labels: [...parentLabels, ...inlineLabels]
|
||||
});
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
// Process each file
|
||||
for (const filePath of files) {
|
||||
console.log(`\nProcessing: ${filePath}`);
|
||||
|
||||
try {
|
||||
const parsed = parseIssueFile(filePath);
|
||||
console.log(` Title: ${parsed.title}`);
|
||||
console.log(` Labels: ${parsed.labels.join(', ')}`);
|
||||
|
||||
if (isDryRun) {
|
||||
console.log(' [DRY RUN] Would create issue');
|
||||
createdIssues.push({ file: filePath, title: parsed.title, dryRun: true });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure labels exist
|
||||
for (const label of parsed.labels) {
|
||||
await ensureLabel(label);
|
||||
}
|
||||
|
||||
// Create the main issue
|
||||
const issueResponse = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: parsed.title,
|
||||
body: parsed.body + `\n\n---\n*Auto-created from [${path.basename(filePath)}](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${context.sha}/${filePath})*`,
|
||||
labels: parsed.labels,
|
||||
assignees: parsed.assignees
|
||||
});
|
||||
|
||||
const issueNumber = issueResponse.data.number;
|
||||
console.log(` Created issue #${issueNumber}`);
|
||||
|
||||
// Handle sub-issues
|
||||
if (parsed.create_sub_issues) {
|
||||
const subIssues = extractSubIssues(parsed.body, parsed.labels);
|
||||
for (const sub of subIssues) {
|
||||
for (const label of sub.labels) {
|
||||
await ensureLabel(label);
|
||||
}
|
||||
const subResponse = await github.rest.issues.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: `[${parsed.title}] ${sub.title}`,
|
||||
body: sub.body + `\n\n---\n*Sub-issue of #${issueNumber}*`,
|
||||
labels: sub.labels,
|
||||
assignees: parsed.assignees
|
||||
});
|
||||
console.log(` Created sub-issue #${subResponse.data.number}: ${sub.title}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Link to parent issue if specified
|
||||
if (parsed.parent_issue) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: parsed.parent_issue,
|
||||
body: `Sub-issue created: #${issueNumber}`
|
||||
});
|
||||
}
|
||||
|
||||
createdIssues.push({
|
||||
file: filePath,
|
||||
title: parsed.title,
|
||||
issueNumber
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(` Error processing ${filePath}: ${error.message}`);
|
||||
errors.push({ file: filePath, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
core.setOutput('created_count', createdIssues.length);
|
||||
core.setOutput('created_issues', JSON.stringify(createdIssues));
|
||||
core.setOutput('errors', JSON.stringify(errors));
|
||||
|
||||
if (errors.length > 0) {
|
||||
core.warning(`${errors.length} file(s) had errors`);
|
||||
}
|
||||
|
||||
- name: Move processed files
|
||||
if: steps.process.outputs.created_count != '0' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
mkdir -p docs/issues/created
|
||||
CREATED_ISSUES='${{ steps.process.outputs.created_issues }}'
|
||||
echo "$CREATED_ISSUES" | jq -r '.[].file' | while read file; do
|
||||
if [ -f "$file" ] && [ ! -z "$file" ]; then
|
||||
filename=$(basename "$file")
|
||||
timestamp=$(date +%Y%m%d)
|
||||
mv "$file" "docs/issues/created/${timestamp}-${filename}"
|
||||
echo "Moved: $file -> docs/issues/created/${timestamp}-${filename}"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Commit moved files
|
||||
if: steps.process.outputs.created_count != '0' && github.event.inputs.dry_run != 'true'
|
||||
run: |
|
||||
git config --local user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git add docs/issues/
|
||||
git diff --staged --quiet || git commit -m "chore: move processed issue files to created/ [skip ci]"
|
||||
git push
|
||||
|
||||
- name: Add to project board
|
||||
if: steps.process.outputs.created_count != '0' && github.event.inputs.dry_run != 'true'
|
||||
continue-on-error: true
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||
env:
|
||||
PROJECT_URL: ${{ secrets.PROJECT_URL }}
|
||||
ADD_TO_PROJECT_PAT: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
with:
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const projectUrl = process.env.PROJECT_URL;
|
||||
if (!projectUrl) {
|
||||
console.log('PROJECT_URL not configured, skipping project board');
|
||||
return;
|
||||
}
|
||||
|
||||
const createdIssues = JSON.parse('${{ steps.process.outputs.created_issues }}');
|
||||
console.log(`Would add ${createdIssues.length} issues to project board`);
|
||||
// Note: Actual project board integration requires GraphQL API
|
||||
// The actions/add-to-project action handles this automatically
|
||||
// for issues created via normal flow
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
echo "## Docs to Issues Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
CREATED='${{ steps.process.outputs.created_issues }}'
|
||||
ERRORS='${{ steps.process.outputs.errors }}'
|
||||
DRY_RUN='${{ github.event.inputs.dry_run }}'
|
||||
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
echo "🔍 **Dry Run Mode** - No issues were actually created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "### Created Issues" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -n "$CREATED" ] && [ "$CREATED" != "[]" ]; then
|
||||
echo "$CREATED" | jq -r '.[] | "- \(.title) (#\(.issueNumber // "dry-run"))"' >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "_No issues created_" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Errors" >> $GITHUB_STEP_SUMMARY
|
||||
if [ -n "$ERRORS" ] && [ "$ERRORS" != "[]" ]; then
|
||||
echo "$ERRORS" | jq -r '.[] | "- ❌ \(.file): \(.error)"' >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "_No errors_" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. File Conversion Templates
|
||||
|
||||
### 4.1 Updated ACL-testing-tasks.md (Example)
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "ACL: Test and validate ACL changes (feature/beta-release)"
|
||||
labels:
|
||||
- testing
|
||||
- needs-triage
|
||||
- acl
|
||||
- regression
|
||||
priority: high
|
||||
milestone: "v0.2.0-beta.2"
|
||||
create_sub_issues: false
|
||||
---
|
||||
|
||||
# ACL: Test and validate ACL changes (feature/beta-release)
|
||||
|
||||
**Repository:** Wikid82/Charon
|
||||
**Branch:** feature/beta-release
|
||||
|
||||
## Purpose
|
||||
|
||||
Create a tracked issue and sub-tasks to validate ACL-related changes...
|
||||
|
||||
[rest of content unchanged]
|
||||
```
|
||||
|
||||
### 4.2 Updated bulk-acl-subissues.md (Example with Sub-Issues)
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Bulk ACL Testing - Sub-Issues"
|
||||
labels:
|
||||
- testing
|
||||
- manual-testing
|
||||
- bulk-acl
|
||||
priority: medium
|
||||
milestone: "v0.2.0-beta.2"
|
||||
create_sub_issues: true
|
||||
---
|
||||
|
||||
# Bulk ACL Testing - Sub-Issues
|
||||
|
||||
## Basic Functionality Testing
|
||||
|
||||
**Labels**: `testing`, `manual-testing`, `bulk-acl`
|
||||
|
||||
Test the core functionality of the bulk ACL feature...
|
||||
|
||||
## ACL Removal Testing
|
||||
|
||||
**Labels**: `testing`, `manual-testing`, `bulk-acl`
|
||||
|
||||
Test the ability to remove access lists...
|
||||
|
||||
[each ## section becomes a sub-issue]
|
||||
```
|
||||
|
||||
### 4.3 Template for New Issue Files
|
||||
|
||||
Create `docs/issues/_TEMPLATE.md`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
# REQUIRED: Issue title
|
||||
title: "Your Issue Title"
|
||||
|
||||
# OPTIONAL: Labels to apply (will be created if missing)
|
||||
labels:
|
||||
- feature # feature, bug, enhancement, testing, documentation
|
||||
- backend # backend, frontend, ui, security, caddy, database
|
||||
|
||||
# OPTIONAL: Priority (creates matching label)
|
||||
priority: medium # critical, high, medium, low
|
||||
|
||||
# OPTIONAL: Milestone name
|
||||
milestone: "v0.2.0-beta.2"
|
||||
|
||||
# OPTIONAL: GitHub usernames to assign
|
||||
assignees: []
|
||||
|
||||
# OPTIONAL: Parent issue number for linking
|
||||
# parent_issue: 42
|
||||
|
||||
# OPTIONAL: Parse ## sections as separate sub-issues
|
||||
# create_sub_issues: true
|
||||
---
|
||||
|
||||
# Issue Title
|
||||
|
||||
## Description
|
||||
|
||||
Clear description of the issue or feature request.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [ ] Task 1
|
||||
- [ ] Task 2
|
||||
- [ ] Task 3
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Criterion 1
|
||||
- [ ] Criterion 2
|
||||
|
||||
## Related Issues
|
||||
|
||||
- #XX - Related issue description
|
||||
|
||||
---
|
||||
|
||||
*Issue specification created: YYYY-MM-DD*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Files to Update
|
||||
|
||||
### 5.1 Existing docs/issues Files Needing Frontmatter
|
||||
|
||||
| File | Action Needed |
|
||||
|------|---------------|
|
||||
| `ACL-testing-tasks.md` | Add frontmatter with extracted metadata |
|
||||
| `Additional_Security.md` | Add frontmatter, convert to issue format |
|
||||
| `bulk-acl-subissues.md` | Add frontmatter with `create_sub_issues: true` |
|
||||
| `bulk-acl-testing.md` | Add frontmatter with extracted metadata |
|
||||
| `hectate.md` | Add frontmatter (feature spec) |
|
||||
| `orthrus.md` | Add frontmatter (feature spec) |
|
||||
| `plex-remote-access-helper.md` | Add frontmatter, already has metadata section |
|
||||
| `rotating-loading-animations.md` | Add frontmatter, extract inline metadata |
|
||||
|
||||
### 5.2 New Files to Create
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `.github/workflows/docs-to-issues.yml` | Main workflow |
|
||||
| `docs/issues/_TEMPLATE.md` | Template for new issues |
|
||||
| `docs/issues/created/.gitkeep` | Directory for processed files |
|
||||
| `docs/issues/README.md` | Documentation for contributors |
|
||||
|
||||
---
|
||||
|
||||
## 6. Directory Structure
|
||||
|
||||
```
|
||||
docs/
|
||||
├── issues/
|
||||
│ ├── README.md # How to create issue files
|
||||
│ ├── _TEMPLATE.md # Template for new issues
|
||||
│ ├── created/ # Processed files moved here
|
||||
│ │ └── .gitkeep
|
||||
│ ├── ACL-testing-tasks.md # Pending (needs frontmatter)
|
||||
│ ├── Additional_Security.md # Pending
|
||||
│ ├── bulk-acl-subissues.md # Pending
|
||||
│ └── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Duplicate Prevention Strategy
|
||||
|
||||
### 7.1 File-Based Tracking
|
||||
|
||||
- **Processed files move to `docs/issues/created/`** with timestamp prefix
|
||||
- **Workflow excludes `created/` directory** from processing
|
||||
- **Git history provides audit trail** of when files were converted
|
||||
|
||||
### 7.2 Issue Tracking
|
||||
|
||||
Each created issue includes footer:
|
||||
```markdown
|
||||
---
|
||||
*Auto-created from [filename.md](link-to-source-commit)*
|
||||
```
|
||||
|
||||
### 7.3 Manual Override
|
||||
|
||||
- Use `workflow_dispatch` with specific file path to reprocess
|
||||
- Files in `created/` can be moved back for reprocessing
|
||||
|
||||
---
|
||||
|
||||
## 8. Project Board Integration
|
||||
|
||||
### 8.1 Automatic Addition
|
||||
|
||||
The workflow leverages the existing `auto-add-to-project.yml` which triggers on `issues: [opened]`.
|
||||
|
||||
When the workflow creates an issue, the auto-add workflow automatically adds it to the project board (if `PROJECT_URL` secret is configured).
|
||||
|
||||
### 8.2 Manual Configuration
|
||||
|
||||
If not using `auto-add-to-project.yml`, configure in the docs-to-issues workflow:
|
||||
|
||||
```yaml
|
||||
- name: Add to project
|
||||
uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2
|
||||
with:
|
||||
project-url: ${{ secrets.PROJECT_URL }}
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Testing Strategy
|
||||
|
||||
### 9.1 Dry Run Testing
|
||||
|
||||
```bash
|
||||
# Manually trigger with dry_run=true
|
||||
gh workflow run docs-to-issues.yml -f dry_run=true
|
||||
|
||||
# Test specific file
|
||||
gh workflow run docs-to-issues.yml -f dry_run=true -f file_path=docs/issues/ACL-testing-tasks.md
|
||||
```
|
||||
|
||||
### 9.2 Local Testing
|
||||
|
||||
```bash
|
||||
# Parse frontmatter locally
|
||||
node -e "
|
||||
const matter = require('gray-matter');
|
||||
const fs = require('fs');
|
||||
const result = matter(fs.readFileSync('docs/issues/ACL-testing-tasks.md', 'utf8'));
|
||||
console.log(JSON.stringify(result.data, null, 2));
|
||||
"
|
||||
```
|
||||
|
||||
### 9.3 Validation Checklist
|
||||
|
||||
- [ ] Frontmatter parses correctly for all files
|
||||
- [ ] Labels are created when missing
|
||||
- [ ] Issue titles are correct
|
||||
- [ ] Issue bodies include full content
|
||||
- [ ] Sub-issues are created correctly (if `create_sub_issues: true`)
|
||||
- [ ] Files move to `created/` after processing
|
||||
- [ ] Auto-add-to-project triggers for new issues
|
||||
- [ ] Commit message includes `[skip ci]` to prevent loops
|
||||
|
||||
---
|
||||
|
||||
## 10. Implementation Phases
|
||||
|
||||
### Phase 1: Setup (15 min)
|
||||
1. Create `.github/workflows/docs-to-issues.yml`
|
||||
2. Create `docs/issues/created/.gitkeep`
|
||||
3. Create `docs/issues/_TEMPLATE.md`
|
||||
4. Create `docs/issues/README.md`
|
||||
|
||||
### Phase 2: File Migration (30 min)
|
||||
1. Add frontmatter to existing files (in order of priority)
|
||||
2. Test with dry_run mode
|
||||
3. Create one test issue to verify
|
||||
|
||||
### Phase 3: Validation (15 min)
|
||||
1. Verify issue creation
|
||||
2. Verify label creation
|
||||
3. Verify project board integration
|
||||
4. Verify file move to `created/`
|
||||
|
||||
---
|
||||
|
||||
## 11. Risk Assessment
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Duplicate issues | Low | File move + exclude pattern |
|
||||
| Label spam | Low | Predefined color map |
|
||||
| Rate limiting | Medium | Process files sequentially |
|
||||
| Malformed frontmatter | Medium | Try-catch with error logging |
|
||||
| Project board auth failure | Low | `continue-on-error: true` |
|
||||
|
||||
---
|
||||
|
||||
## 12. Definition of Done
|
||||
|
||||
- [ ] Workflow file created and committed
|
||||
- [ ] All existing docs/issues files have valid frontmatter
|
||||
- [ ] Dry run succeeds with no errors
|
||||
- [ ] At least one test issue created successfully
|
||||
- [ ] File moved to `created/` after processing
|
||||
- [ ] Labels created automatically
|
||||
- [ ] Project board integration verified (if configured)
|
||||
- [ ] Documentation in `docs/issues/README.md`
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Label Color Reference
|
||||
|
||||
```javascript
|
||||
const labelColors = {
|
||||
// Priority
|
||||
critical: 'B60205',
|
||||
high: 'D93F0B',
|
||||
medium: 'FBCA04',
|
||||
low: '0E8A16',
|
||||
|
||||
// Milestone
|
||||
alpha: '5319E7',
|
||||
beta: '0052CC',
|
||||
'post-beta': '006B75',
|
||||
|
||||
// Type
|
||||
feature: 'A2EEEF',
|
||||
bug: 'D73A4A',
|
||||
enhancement: '84B6EB',
|
||||
documentation: '0075CA',
|
||||
testing: 'BFD4F2',
|
||||
|
||||
// Component
|
||||
backend: '1D76DB',
|
||||
frontend: '5EBEFF',
|
||||
ui: '7057FF',
|
||||
security: 'EE0701',
|
||||
caddy: '1F6FEB',
|
||||
database: '006B75',
|
||||
|
||||
// Custom
|
||||
'needs-triage': 'FBCA04',
|
||||
acl: 'C5DEF5',
|
||||
'bulk-acl': '006B75',
|
||||
'manual-testing': 'BFD4F2',
|
||||
regression: 'D93F0B',
|
||||
plus: 'FFD700'
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Appendix B: Frontmatter Conversion Examples
|
||||
|
||||
### B.1 hectate.md (Feature Spec)
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Hecate: Tunnel & Pathway Manager"
|
||||
labels:
|
||||
- feature
|
||||
- backend
|
||||
- architecture
|
||||
priority: medium
|
||||
milestone: "post-beta"
|
||||
type: feature
|
||||
---
|
||||
```
|
||||
|
||||
### B.2 orthrus.md (Feature Spec)
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Orthrus: Remote Socket Proxy Agent"
|
||||
labels:
|
||||
- feature
|
||||
- backend
|
||||
- architecture
|
||||
priority: medium
|
||||
milestone: "post-beta"
|
||||
type: feature
|
||||
---
|
||||
```
|
||||
|
||||
### B.3 plex-remote-access-helper.md
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Plex Remote Access Helper & CGNAT Solver"
|
||||
labels:
|
||||
- beta
|
||||
- feature
|
||||
- plus
|
||||
- ui
|
||||
- caddy
|
||||
priority: medium
|
||||
milestone: "beta"
|
||||
type: feature
|
||||
parent_issue: 44
|
||||
---
|
||||
```
|
||||
|
||||
### B.4 rotating-loading-animations.md
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Enhancement: Rotating Thematic Loading Animations"
|
||||
labels:
|
||||
- enhancement
|
||||
- frontend
|
||||
- ui
|
||||
priority: low
|
||||
type: enhancement
|
||||
---
|
||||
```
|
||||
|
||||
### B.5 Additional_Security.md
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: "Additional Security Threats to Consider"
|
||||
labels:
|
||||
- security
|
||||
- documentation
|
||||
- architecture
|
||||
priority: medium
|
||||
type: documentation
|
||||
---
|
||||
```
|
||||
Reference in New Issue
Block a user