mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-24 04:15:14 +00:00
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:
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user