Simplify workflows to flat .md files instead of folders

Workflows are now standalone .md files in workflows/ — no subfolders
or README.md needed. Each file contains both the metadata frontmatter
(name, description, triggers, tags) and the agentic workflow definition
(on, permissions, safe-outputs) in a single file.

Updated all build scripts, CI workflows, docs, and review checklists.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Bruno Borges
2026-02-20 15:53:03 -08:00
parent e83cc6efee
commit 53401cb560
8 changed files with 64 additions and 112 deletions

View File

@@ -27,11 +27,9 @@ jobs:
exit_code=0 exit_code=0
found=0 found=0
# Find all .md files in workflows/ subfolders (excluding README.md) # Find all .md files directly in workflows/
for workflow_file in workflows/*/*.md; do for workflow_file in workflows/*.md; do
[ -f "$workflow_file" ] || continue [ -f "$workflow_file" ] || continue
basename=$(basename "$workflow_file")
[ "$basename" = "README.md" ] && continue
found=$((found + 1)) found=$((found + 1))
echo "::group::Compiling $workflow_file" echo "::group::Compiling $workflow_file"
@@ -45,7 +43,7 @@ jobs:
done done
if [ "$found" -eq 0 ]; then if [ "$found" -eq 0 ]; then
echo "No workflow .md files found to validate (README.md files are excluded)." echo "No workflow .md files found to validate."
else else
echo "Validated $found workflow file(s)." echo "Validated $found workflow file(s)."
fi fi

View File

@@ -21,7 +21,7 @@ The Awesome GitHub Copilot repository is a community-driven collection of custom
├── instructions/ # Coding standards and guidelines (.instructions.md files) ├── instructions/ # Coding standards and guidelines (.instructions.md files)
├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets) ├── skills/ # Agent Skills folders (each with SKILL.md and optional bundled assets)
├── hooks/ # Automated workflow hooks (folders with README.md + hooks.json) ├── hooks/ # Automated workflow hooks (folders with README.md + hooks.json)
├── workflows/ # Agentic Workflows (folders with README.md + workflow .md files) ├── workflows/ # Agentic Workflows (.md files for GitHub Actions automation)
├── plugins/ # Installable plugin packages (folders with plugin.json) ├── plugins/ # Installable plugin packages (folders with plugin.json)
├── docs/ # Documentation for different resource types ├── docs/ # Documentation for different resource types
├── eng/ # Build and automation scripts ├── eng/ # Build and automation scripts
@@ -98,14 +98,14 @@ All agent files (`*.agent.md`), prompt files (`*.prompt.md`), and instruction fi
- Follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks) - Follow the [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- Optionally includes `tags` field for categorization - Optionally includes `tags` field for categorization
#### Workflow Folders (workflows/*/README.md) #### Workflow Files (workflows/*.md)
- Each workflow is a folder containing a `README.md` file with frontmatter and one or more `.md` workflow files - Each workflow is a standalone `.md` file in the `workflows/` directory
- README.md must have `name` field (human-readable name) - Must have `name` field (human-readable name)
- README.md must have `description` field (wrapped in single quotes, not empty) - Must have `description` field (wrapped in single quotes, not empty)
- README.md should have `triggers` field (array of trigger types, e.g., `['schedule', 'issues']`) - Should have `triggers` field (array of trigger types, e.g., `['schedule', 'issues']`)
- Workflow `.md` files contain YAML frontmatter (`on`, `permissions`, `safe-outputs`) and natural language instructions - Contains agentic workflow frontmatter (`on`, `permissions`, `safe-outputs`) and natural language instructions
- Folder names should be lower case with words separated by hyphens - File names should be lower case with words separated by hyphens
- Can include bundled assets (scripts, configuration files) - Only `.md` files are accepted — `.yml`, `.yaml`, and `.lock.yml` files are blocked by CI
- Optionally includes `tags` field for categorization - Optionally includes `tags` field for categorization
- Follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw) - Follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw)
@@ -139,12 +139,11 @@ When adding a new agent, prompt, instruction, skill, hook, workflow, or plugin:
**For Workflows:** **For Workflows:**
1. Create a new folder in `workflows/` with a descriptive name 1. Create a new `.md` file in `workflows/` with a descriptive name (e.g., `daily-issues-report.md`)
2. Create `README.md` with proper frontmatter (name, description, triggers, tags) 2. Include frontmatter with `name`, `description`, `triggers`, plus agentic workflow fields (`on`, `permissions`, `safe-outputs`)
3. Add one or more `.md` workflow files with `on`, `permissions`, and `safe-outputs` frontmatter 3. Compile with `gh aw compile --validate` to verify it's valid
4. Add any bundled scripts or assets to the folder 4. Update the README.md by running: `npm run build`
5. Update the README.md by running: `npm run build` 5. Verify the workflow appears in the generated README
6. Verify the workflow appears in the generated README
**For Skills:** **For Skills:**
@@ -263,14 +262,15 @@ For hook folders (hooks/*/):
- [ ] Follows [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks) - [ ] Follows [GitHub Copilot hooks specification](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/coding-agent/use-hooks)
- [ ] Optionally includes `tags` array field for categorization - [ ] Optionally includes `tags` array field for categorization
For workflow folders (workflows/*/): For workflow files (workflows/*.md):
- [ ] Folder contains a README.md file with markdown front matter - [ ] File has markdown front matter
- [ ] Has `name` field with human-readable name - [ ] Has `name` field with human-readable name
- [ ] Has non-empty `description` field wrapped in single quotes - [ ] Has non-empty `description` field wrapped in single quotes
- [ ] Has `triggers` array field listing workflow trigger types - [ ] Has `triggers` array field listing workflow trigger types
- [ ] Folder name is lower case with hyphens - [ ] File name is lower case with hyphens
- [ ] Contains at least one `.md` workflow file with `on` and `permissions` in frontmatter - [ ] Contains `on` and `permissions` in frontmatter
- [ ] Workflow uses least-privilege permissions and safe outputs - [ ] Workflow uses least-privilege permissions and safe outputs
- [ ] No `.yml`, `.yaml`, or `.lock.yml` files included
- [ ] Follows [GitHub Agentic Workflows specification](https://github.github.com/gh-aw) - [ ] Follows [GitHub Agentic Workflows specification](https://github.github.com/gh-aw)
- [ ] Optionally includes `tags` array field for categorization - [ ] Optionally includes `tags` array field for categorization

View File

@@ -165,21 +165,14 @@ plugins/my-plugin-id/
[Agentic Workflows](https://github.github.com/gh-aw) are AI-powered repository automations that run coding agents in GitHub Actions. Defined in markdown with natural language instructions, they enable scheduled and event-triggered automation with built-in guardrails. [Agentic Workflows](https://github.github.com/gh-aw) are AI-powered repository automations that run coding agents in GitHub Actions. Defined in markdown with natural language instructions, they enable scheduled and event-triggered automation with built-in guardrails.
1. **Create a new workflow folder**: Add a new folder in the `workflows/` directory with a descriptive name (e.g., `daily-issues-report`) 1. **Create your workflow file**: Add a new `.md` file in the `workflows/` directory (e.g., `daily-issues-report.md`)
2. **Create a `README.md`**: Add a `README.md` with frontmatter containing `name`, `description`, `triggers`, and optionally `tags` 2. **Include frontmatter**: Add `name`, `description`, `triggers`, and optionally `tags` at the top, followed by agentic workflow frontmatter (`on`, `permissions`, `safe-outputs`) and natural language instructions
3. **Add workflow files**: Include one or more `.md` workflow files with YAML frontmatter (`on`, `permissions`, `safe-outputs`) and natural language instructions 3. **Test locally**: Compile with `gh aw compile --validate` to verify it's valid
4. **Add optional assets**: Include any helper scripts or configuration files referenced by the workflow 4. **Update the README**: Run `npm run build` to update the generated README tables
5. **Update the README**: Run `npm run build` to update the generated README tables
#### Workflow folder structure > **Note:** Only `.md` files are accepted — do not include compiled `.lock.yml` or `.yml` files. CI will block them.
``` #### Workflow file example
workflows/daily-issues-report/
├── README.md # Workflow documentation with frontmatter
└── daily-issues-report.md # Agentic workflow file
```
#### README.md frontmatter example
```markdown ```markdown
--- ---
@@ -187,13 +180,6 @@ name: 'Daily Issues Report'
description: 'Generates a daily summary of open issues and recent activity as a GitHub issue' description: 'Generates a daily summary of open issues and recent activity as a GitHub issue'
triggers: ['schedule'] triggers: ['schedule']
tags: ['reporting', 'issues', 'automation'] tags: ['reporting', 'issues', 'automation']
---
```
#### Workflow file example
```markdown
---
on: on:
schedule: daily on weekdays schedule: daily on weekdays
permissions: permissions:
@@ -220,9 +206,9 @@ Create a daily summary of open issues for the team.
- **Security first**: Use least-privilege permissions and safe outputs instead of direct write access - **Security first**: Use least-privilege permissions and safe outputs instead of direct write access
- **Clear instructions**: Write clear natural language instructions in the workflow body - **Clear instructions**: Write clear natural language instructions in the workflow body
- **Descriptive names**: Use lowercase folder names with hyphens - **Descriptive names**: Use lowercase filenames with hyphens (e.g., `daily-issues-report.md`)
- **Test locally**: Use `gh aw run` to test workflows before contributing - **Test locally**: Use `gh aw compile --validate` to verify your workflow compiles
- **Documentation**: Include a thorough README explaining what the workflow does and how to use it - **No compiled files**: Only submit the `.md` source — `.lock.yml` and `.yml` files are not accepted
- Learn more at the [Agentic Workflows documentation](https://github.github.com/gh-aw) - Learn more at the [Agentic Workflows documentation](https://github.github.com/gh-aw)
## Submitting Your Contribution ## Submitting Your Contribution

View File

@@ -5,7 +5,7 @@
### How to Use Agentic Workflows ### How to Use Agentic Workflows
**What's Included:** **What's Included:**
- Each workflow is a folder containing a `README.md` and one or more `.md` workflow files - Each workflow is a single `.md` file with YAML frontmatter and natural language instructions
- Workflows are compiled to `.lock.yml` GitHub Actions files via `gh aw compile` - Workflows are compiled to `.lock.yml` GitHub Actions files via `gh aw compile`
- Workflows follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw) - Workflows follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw)

View File

@@ -135,7 +135,7 @@ Hooks enable automated workflows triggered by specific events during GitHub Copi
workflowsUsage: `### How to Use Agentic Workflows workflowsUsage: `### How to Use Agentic Workflows
**What's Included:** **What's Included:**
- Each workflow is a folder containing a \`README.md\` and one or more \`.md\` workflow files - Each workflow is a single \`.md\` file with YAML frontmatter and natural language instructions
- Workflows are compiled to \`.lock.yml\` GitHub Actions files via \`gh aw compile\` - Workflows are compiled to \`.lock.yml\` GitHub Actions files via \`gh aw compile\`
- Workflows follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw) - Workflows follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw)

View File

@@ -195,7 +195,7 @@ function generateHooksData(gitDates) {
} }
/** /**
* Generate workflows metadata (folder-based, similar to hooks) * Generate workflows metadata (flat .md files)
*/ */
function generateWorkflowsData(gitDates) { function generateWorkflowsData(gitDates) {
const workflows = []; const workflows = [];
@@ -210,37 +210,34 @@ function generateWorkflowsData(gitDates) {
}; };
} }
const workflowFolders = fs.readdirSync(WORKFLOWS_DIR).filter((file) => { const workflowFiles = fs.readdirSync(WORKFLOWS_DIR).filter((file) => {
const filePath = path.join(WORKFLOWS_DIR, file); return file.endsWith(".md") && file !== ".gitkeep";
return fs.statSync(filePath).isDirectory();
}); });
const allTriggers = new Set(); const allTriggers = new Set();
const allTags = new Set(); const allTags = new Set();
for (const folder of workflowFolders) { for (const file of workflowFiles) {
const workflowPath = path.join(WORKFLOWS_DIR, folder); const filePath = path.join(WORKFLOWS_DIR, file);
const metadata = parseWorkflowMetadata(workflowPath); const metadata = parseWorkflowMetadata(filePath);
if (!metadata) continue; if (!metadata) continue;
const relativePath = path const relativePath = path
.relative(ROOT_FOLDER, workflowPath) .relative(ROOT_FOLDER, filePath)
.replace(/\\/g, "/"); .replace(/\\/g, "/");
const readmeRelativePath = `${relativePath}/README.md`;
(metadata.triggers || []).forEach((t) => allTriggers.add(t)); (metadata.triggers || []).forEach((t) => allTriggers.add(t));
(metadata.tags || []).forEach((t) => allTags.add(t)); (metadata.tags || []).forEach((t) => allTags.add(t));
const id = path.basename(file, ".md");
workflows.push({ workflows.push({
id: folder, id,
title: metadata.name, title: metadata.name,
description: metadata.description, description: metadata.description,
triggers: metadata.triggers || [], triggers: metadata.triggers || [],
tags: metadata.tags || [], tags: metadata.tags || [],
assets: metadata.assets || [],
path: relativePath, path: relativePath,
readmeFile: readmeRelativePath, lastUpdated: gitDates.get(relativePath) || null,
lastUpdated: gitDates.get(readmeRelativePath) || null,
}); });
} }
@@ -735,7 +732,7 @@ function generateSearchIndex(
id: workflow.id, id: workflow.id,
title: workflow.title, title: workflow.title,
description: workflow.description, description: workflow.description,
path: workflow.readmeFile, path: workflow.path,
lastUpdated: workflow.lastUpdated, lastUpdated: workflow.lastUpdated,
searchText: `${workflow.title} ${workflow.description} ${workflow.triggers.join( searchText: `${workflow.title} ${workflow.description} ${workflow.triggers.join(
" " " "

View File

@@ -588,26 +588,24 @@ function generateWorkflowsSection(workflowsDir) {
return ""; return "";
} }
// Get all workflow folders (directories) // Get all .md workflow files (flat, no subfolders)
const workflowFolders = fs.readdirSync(workflowsDir).filter((file) => { const workflowFiles = fs.readdirSync(workflowsDir).filter((file) => {
const filePath = path.join(workflowsDir, file); return file.endsWith(".md") && file !== ".gitkeep";
return fs.statSync(filePath).isDirectory();
}); });
// Parse each workflow folder // Parse each workflow file
const workflowEntries = workflowFolders const workflowEntries = workflowFiles
.map((folder) => { .map((file) => {
const workflowPath = path.join(workflowsDir, folder); const filePath = path.join(workflowsDir, file);
const metadata = parseWorkflowMetadata(workflowPath); const metadata = parseWorkflowMetadata(filePath);
if (!metadata) return null; if (!metadata) return null;
return { return {
folder, file,
name: metadata.name, name: metadata.name,
description: metadata.description, description: metadata.description,
triggers: metadata.triggers, triggers: metadata.triggers,
tags: metadata.tags, tags: metadata.tags,
assets: metadata.assets,
}; };
}) })
.filter((entry) => entry !== null) .filter((entry) => entry !== null)
@@ -621,20 +619,16 @@ function generateWorkflowsSection(workflowsDir) {
// Create table header // Create table header
let content = let content =
"| Name | Description | Triggers | Bundled Assets |\n| ---- | ----------- | -------- | -------------- |\n"; "| Name | Description | Triggers |\n| ---- | ----------- | -------- |\n";
// Generate table rows for each workflow // Generate table rows for each workflow
for (const workflow of workflowEntries) { for (const workflow of workflowEntries) {
const link = `../workflows/${workflow.folder}/README.md`; const link = `../workflows/${workflow.file}`;
const triggers = workflow.triggers.length > 0 ? workflow.triggers.join(", ") : "N/A"; const triggers = workflow.triggers.length > 0 ? workflow.triggers.join(", ") : "N/A";
const assetsList =
workflow.assets.length > 0
? workflow.assets.map((a) => `\`${a}\``).join("<br />")
: "None";
content += `| [${workflow.name}](${link}) | ${formatTableCell( content += `| [${workflow.name}](${link}) | ${formatTableCell(
workflow.description workflow.description
)} | ${triggers} | ${assetsList} |\n`; )} | ${triggers} |\n`;
} }
return `${TEMPLATES.workflowsSection}\n${TEMPLATES.workflowsUsage}\n\n${content}`; return `${TEMPLATES.workflowsSection}\n${TEMPLATES.workflowsUsage}\n\n${content}`;

View File

@@ -254,24 +254,23 @@ function parseHookMetadata(hookPath) {
} }
/** /**
* Parse workflow metadata from a workflow folder * Parse workflow metadata from a standalone .md workflow file
* @param {string} workflowPath - Path to the workflow folder * @param {string} filePath - Path to the workflow .md file
* @returns {object|null} Workflow metadata or null on error * @returns {object|null} Workflow metadata or null on error
*/ */
function parseWorkflowMetadata(workflowPath) { function parseWorkflowMetadata(filePath) {
return safeFileOperation( return safeFileOperation(
() => { () => {
const readmeFile = path.join(workflowPath, "README.md"); if (!fs.existsSync(filePath)) {
if (!fs.existsSync(readmeFile)) {
return null; return null;
} }
const frontmatter = parseFrontmatter(readmeFile); const frontmatter = parseFrontmatter(filePath);
// Validate required fields // Validate required fields
if (!frontmatter?.name || !frontmatter?.description) { if (!frontmatter?.name || !frontmatter?.description) {
console.warn( console.warn(
`Invalid workflow at ${workflowPath}: missing name or description in frontmatter` `Invalid workflow at ${filePath}: missing name or description in frontmatter`
); );
return null; return null;
} }
@@ -279,37 +278,15 @@ function parseWorkflowMetadata(workflowPath) {
// Extract triggers from frontmatter if present // Extract triggers from frontmatter if present
const triggers = frontmatter.triggers || []; const triggers = frontmatter.triggers || [];
// List bundled assets (all files except README.md), recursing through subdirectories
const getAllFiles = (dirPath, arrayOfFiles = []) => {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const filePath = path.join(dirPath, file);
if (fs.statSync(filePath).isDirectory()) {
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
} else {
const relativePath = path.relative(workflowPath, filePath);
if (relativePath !== "README.md") {
arrayOfFiles.push(relativePath.replace(/\\/g, "/"));
}
}
});
return arrayOfFiles;
};
const assets = getAllFiles(workflowPath).sort();
return { return {
name: frontmatter.name, name: frontmatter.name,
description: frontmatter.description, description: frontmatter.description,
triggers, triggers,
tags: frontmatter.tags || [], tags: frontmatter.tags || [],
assets, path: filePath,
path: workflowPath,
}; };
}, },
workflowPath, filePath,
null null
); );
} }