Add Agentic Workflows as a new resource type

Add support for contributing Agentic Workflows — AI-powered repository
automations that run coding agents in GitHub Actions, defined in markdown
with natural language instructions (https://github.github.com/gh-aw).

Changes:
- Create workflows/ directory for community-contributed workflows
- Add workflow metadata parsing (yaml-parser.mjs)
- Add workflow README generation (update-readme.mjs, constants.mjs)
- Add workflow data to website generation (generate-website-data.mjs)
- Update README.md, CONTRIBUTING.md, and AGENTS.md with workflow docs,
  contributing guidelines, and code review checklists

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Bruno Borges
2026-02-20 15:28:28 -08:00
parent 7bebd4a385
commit 997d6302bd
9 changed files with 402 additions and 7 deletions

View File

@@ -127,6 +127,36 @@ Hooks enable automated workflows triggered by specific events during GitHub Copi
- Track usage analytics
- Integrate with external tools and services
- Custom session workflows`,
workflowsSection: `## ⚡ Agentic Workflows
[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 event-triggered and scheduled automation with built-in guardrails and security-first design.`,
workflowsUsage: `### How to Use Agentic Workflows
**What's Included:**
- Each workflow is a folder containing a \`README.md\` and one or more \`.md\` workflow files
- 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)
**To Install:**
- Install the \`gh aw\` CLI extension: \`gh extension install github/gh-aw\`
- Copy the workflow \`.md\` file to your repository's \`.github/workflows/\` directory
- Compile with \`gh aw compile\` to generate the \`.lock.yml\` file
- Commit both the \`.md\` and \`.lock.yml\` files
**To Activate/Use:**
- Workflows run automatically based on their configured triggers (schedules, events, slash commands)
- Use \`gh aw run <workflow>\` to trigger a manual run
- Monitor runs with \`gh aw status\` and \`gh aw logs\`
**When to Use:**
- Automate issue triage and labeling
- Generate daily status reports
- Maintain documentation automatically
- Run scheduled code quality checks
- Respond to slash commands in issues and PRs
- Orchestrate multi-step repository automation`,
};
const vscodeInstallImage =
@@ -152,6 +182,7 @@ const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
const SKILLS_DIR = path.join(ROOT_FOLDER, "skills");
const HOOKS_DIR = path.join(ROOT_FOLDER, "hooks");
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const WORKFLOWS_DIR = path.join(ROOT_FOLDER, "workflows");
const COOKBOOK_DIR = path.join(ROOT_FOLDER, "cookbook");
const MAX_PLUGIN_ITEMS = 50;
@@ -182,6 +213,7 @@ export {
SKILLS_DIR,
TEMPLATES,
vscodeInsidersInstallImage,
vscodeInstallImage
vscodeInstallImage,
WORKFLOWS_DIR
};

View File

@@ -17,13 +17,15 @@ import {
PLUGINS_DIR,
PROMPTS_DIR,
ROOT_FOLDER,
SKILLS_DIR
SKILLS_DIR,
WORKFLOWS_DIR
} from "./constants.mjs";
import { getGitFileDates } from "./utils/git-dates.mjs";
import {
parseFrontmatter,
parseSkillMetadata,
parseHookMetadata,
parseWorkflowMetadata,
parseYamlFile,
} from "./yaml-parser.mjs";
@@ -192,6 +194,67 @@ function generateHooksData(gitDates) {
};
}
/**
* Generate workflows metadata (folder-based, similar to hooks)
*/
function generateWorkflowsData(gitDates) {
const workflows = [];
if (!fs.existsSync(WORKFLOWS_DIR)) {
return {
items: workflows,
filters: {
triggers: [],
tags: [],
},
};
}
const workflowFolders = fs.readdirSync(WORKFLOWS_DIR).filter((file) => {
const filePath = path.join(WORKFLOWS_DIR, file);
return fs.statSync(filePath).isDirectory();
});
const allTriggers = new Set();
const allTags = new Set();
for (const folder of workflowFolders) {
const workflowPath = path.join(WORKFLOWS_DIR, folder);
const metadata = parseWorkflowMetadata(workflowPath);
if (!metadata) continue;
const relativePath = path
.relative(ROOT_FOLDER, workflowPath)
.replace(/\\/g, "/");
const readmeRelativePath = `${relativePath}/README.md`;
(metadata.triggers || []).forEach((t) => allTriggers.add(t));
(metadata.tags || []).forEach((t) => allTags.add(t));
workflows.push({
id: folder,
title: metadata.name,
description: metadata.description,
triggers: metadata.triggers || [],
tags: metadata.tags || [],
assets: metadata.assets || [],
path: relativePath,
readmeFile: readmeRelativePath,
lastUpdated: gitDates.get(readmeRelativePath) || null,
});
}
const sortedWorkflows = workflows.sort((a, b) => a.title.localeCompare(b.title));
return {
items: sortedWorkflows,
filters: {
triggers: Array.from(allTriggers).sort(),
tags: Array.from(allTags).sort(),
},
};
}
/**
* Generate prompts metadata
*/
@@ -606,6 +669,7 @@ function generateSearchIndex(
prompts,
instructions,
hooks,
workflows,
skills,
plugins
) {
@@ -665,6 +729,20 @@ function generateSearchIndex(
});
}
for (const workflow of workflows) {
index.push({
type: "workflow",
id: workflow.id,
title: workflow.title,
description: workflow.description,
path: workflow.readmeFile,
lastUpdated: workflow.lastUpdated,
searchText: `${workflow.title} ${workflow.description} ${workflow.triggers.join(
" "
)} ${workflow.tags.join(" ")}`.toLowerCase(),
});
}
for (const skill of skills) {
index.push({
type: "skill",
@@ -799,7 +877,7 @@ async function main() {
// Load git dates for all resource files (single efficient git command)
console.log("Loading git history for last updated dates...");
const gitDates = getGitFileDates(
["agents/", "prompts/", "instructions/", "hooks/", "skills/", "plugins/"],
["agents/", "prompts/", "instructions/", "hooks/", "workflows/", "skills/", "plugins/"],
ROOT_FOLDER
);
console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
@@ -817,6 +895,12 @@ async function main() {
`✓ Generated ${hooks.length} hooks (${hooksData.filters.hooks.length} hook types, ${hooksData.filters.tags.length} tags)`
);
const workflowsData = generateWorkflowsData(gitDates);
const workflows = workflowsData.items;
console.log(
`✓ Generated ${workflows.length} workflows (${workflowsData.filters.triggers.length} triggers, ${workflowsData.filters.tags.length} tags)`
);
const promptsData = generatePromptsData(gitDates);
const prompts = promptsData.items;
console.log(
@@ -857,6 +941,7 @@ async function main() {
prompts,
instructions,
hooks,
workflows,
skills,
plugins
);
@@ -873,6 +958,11 @@ async function main() {
JSON.stringify(hooksData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "workflows.json"),
JSON.stringify(workflowsData, null, 2)
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "prompts.json"),
JSON.stringify(promptsData, null, 2)
@@ -917,6 +1007,7 @@ async function main() {
instructions: instructions.length,
skills: skills.length,
hooks: hooks.length,
workflows: workflows.length,
plugins: plugins.length,
tools: tools.length,
samples: samplesData.totalRecipes,

View File

@@ -17,12 +17,14 @@ import {
TEMPLATES,
vscodeInsidersInstallImage,
vscodeInstallImage,
WORKFLOWS_DIR,
} from "./constants.mjs";
import {
extractMcpServerConfigs,
parseFrontmatter,
parseSkillMetadata,
parseHookMetadata,
parseWorkflowMetadata,
} from "./yaml-parser.mjs";
const __filename = fileURLToPath(import.meta.url);
@@ -577,6 +579,67 @@ function generateHooksSection(hooksDir) {
return `${TEMPLATES.hooksSection}\n${TEMPLATES.hooksUsage}\n\n${content}`;
}
/**
* Generate the workflows section with a table of all agentic workflows
*/
function generateWorkflowsSection(workflowsDir) {
if (!fs.existsSync(workflowsDir)) {
console.log(`Workflows directory does not exist: ${workflowsDir}`);
return "";
}
// Get all workflow folders (directories)
const workflowFolders = fs.readdirSync(workflowsDir).filter((file) => {
const filePath = path.join(workflowsDir, file);
return fs.statSync(filePath).isDirectory();
});
// Parse each workflow folder
const workflowEntries = workflowFolders
.map((folder) => {
const workflowPath = path.join(workflowsDir, folder);
const metadata = parseWorkflowMetadata(workflowPath);
if (!metadata) return null;
return {
folder,
name: metadata.name,
description: metadata.description,
triggers: metadata.triggers,
tags: metadata.tags,
assets: metadata.assets,
};
})
.filter((entry) => entry !== null)
.sort((a, b) => a.name.localeCompare(b.name));
console.log(`Found ${workflowEntries.length} workflow(s)`);
if (workflowEntries.length === 0) {
return "";
}
// Create table header
let content =
"| Name | Description | Triggers | Bundled Assets |\n| ---- | ----------- | -------- | -------------- |\n";
// Generate table rows for each workflow
for (const workflow of workflowEntries) {
const link = `../workflows/${workflow.folder}/README.md`;
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(
workflow.description
)} | ${triggers} | ${assetsList} |\n`;
}
return `${TEMPLATES.workflowsSection}\n${TEMPLATES.workflowsUsage}\n\n${content}`;
}
/**
* Generate the skills section with a table of all skills
*/
@@ -921,6 +984,7 @@ async function main() {
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
const hooksHeader = TEMPLATES.hooksSection.replace(/^##\s/m, "# ");
const workflowsHeader = TEMPLATES.workflowsSection.replace(/^##\s/m, "# ");
const skillsHeader = TEMPLATES.skillsSection.replace(/^##\s/m, "# ");
const pluginsHeader = TEMPLATES.pluginsSection.replace(
/^##\s/m,
@@ -959,6 +1023,15 @@ async function main() {
registryNames
);
// Generate workflows README
const workflowsReadme = buildCategoryReadme(
generateWorkflowsSection,
WORKFLOWS_DIR,
workflowsHeader,
TEMPLATES.workflowsUsage,
registryNames
);
// Generate skills README
const skillsReadme = buildCategoryReadme(
generateSkillsSection,
@@ -990,6 +1063,7 @@ async function main() {
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.hooks.md"), hooksReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.workflows.md"), workflowsReadme);
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
writeFileIfChanged(
path.join(DOCS_DIR, "README.plugins.md"),

View File

@@ -253,6 +253,67 @@ function parseHookMetadata(hookPath) {
);
}
/**
* Parse workflow metadata from a workflow folder
* @param {string} workflowPath - Path to the workflow folder
* @returns {object|null} Workflow metadata or null on error
*/
function parseWorkflowMetadata(workflowPath) {
return safeFileOperation(
() => {
const readmeFile = path.join(workflowPath, "README.md");
if (!fs.existsSync(readmeFile)) {
return null;
}
const frontmatter = parseFrontmatter(readmeFile);
// Validate required fields
if (!frontmatter?.name || !frontmatter?.description) {
console.warn(
`Invalid workflow at ${workflowPath}: missing name or description in frontmatter`
);
return null;
}
// Extract triggers from frontmatter if present
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 {
name: frontmatter.name,
description: frontmatter.description,
triggers,
tags: frontmatter.tags || [],
assets,
path: workflowPath,
};
},
workflowPath,
null
);
}
/**
* Parse a generic YAML file (used for tools.yml and other config files)
* @param {string} filePath - Path to the YAML file
@@ -276,6 +337,7 @@ export {
parseFrontmatter,
parseSkillMetadata,
parseHookMetadata,
parseWorkflowMetadata,
parseYamlFile,
safeFileOperation,
};