Consolidate scripts and automate report management (#1540)

* removing old scripts

* consolidated folder

* Updating usage of scripts

* Adding script to generate an open PR report, rather than making AI gen it each time

* Adding step to close old quality report discussions
This commit is contained in:
Aaron Powell
2026-04-28 17:29:40 +10:00
committed by GitHub
parent f7a7ef7c28
commit 2f972ba80c
9 changed files with 409 additions and 321 deletions

View File

@@ -43,7 +43,7 @@ jobs:
run: npm run build run: npm run build
- name: Fix line endings - name: Fix line endings
run: bash scripts/fix-line-endings.sh run: bash eng/fix-line-endings.sh
- name: Publish to main - name: Publish to main
run: | run: |

View File

@@ -297,6 +297,84 @@ jobs:
} }
core.setOutput('comment_count', String(commentParts.length)); core.setOutput('comment_count', String(commentParts.length));
- name: Close old report discussions
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const RETENTION_DAYS = 5;
const CATEGORY_NAME = 'Skill Quality Reports';
const TITLE_PREFIX = 'Skill Quality Report — ';
const cutoff = new Date();
cutoff.setUTCDate(cutoff.getUTCDate() - RETENTION_DAYS);
let after = null;
let closedCount = 0;
let reachedCutoff = false;
while (!reachedCutoff) {
const result = await github.graphql(`
query($owner: String!, $repo: String!, $after: String) {
repository(owner: $owner, name: $repo) {
discussions(first: 100, after: $after, orderBy: { field: CREATED_AT, direction: ASC }) {
nodes {
id
title
createdAt
closed
url
category {
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`, {
owner: context.repo.owner,
repo: context.repo.repo,
after,
});
const discussions = result.repository.discussions;
for (const discussion of discussions.nodes) {
if (new Date(discussion.createdAt) >= cutoff) {
reachedCutoff = true;
break;
}
if (discussion.category?.name !== CATEGORY_NAME) continue;
if (!discussion.title.startsWith(TITLE_PREFIX)) continue;
if (discussion.closed) continue;
await github.graphql(`
mutation($discussionId: ID!) {
closeDiscussion(input: { discussionId: $discussionId }) {
discussion {
id
}
}
}
`, { discussionId: discussion.id });
closedCount++;
console.log(`Closed old report discussion: ${discussion.url}`);
}
if (reachedCutoff || !discussions.pageInfo.hasNextPage) {
break;
}
after = discussions.pageInfo.endCursor;
}
console.log(`Closed ${closedCount} report discussion(s) older than ${RETENTION_DAYS} days.`);
# ── Create Discussion (preferred) or Issue (fallback) ──────── # ── Create Discussion (preferred) or Issue (fallback) ────────
- name: Create Discussion - name: Create Discussion
id: create-discussion id: create-discussion

View File

@@ -57,18 +57,21 @@ npm run skill:create -- --name <skill-name>
All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets. Hooks are folders containing a `README.md` with frontmatter and a `hooks.json` configuration file: All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must include proper markdown front matter. Agent Skills are folders containing a `SKILL.md` file with frontmatter and optional bundled assets. Hooks are folders containing a `README.md` with frontmatter and a `hooks.json` configuration file:
#### Agent Files (*.agent.md) #### Agent Files (\*.agent.md)
- Must have `description` field (wrapped in single quotes) - Must have `description` field (wrapped in single quotes)
- File names should be lower case with words separated by hyphens - File names should be lower case with words separated by hyphens
- Recommended to include `tools` field - Recommended to include `tools` field
- Strongly recommended to specify `model` field - Strongly recommended to specify `model` field
#### Instruction Files (*.instructions.md) #### Instruction Files (\*.instructions.md)
- Must have `description` field (wrapped in single quotes, not empty) - Must have `description` field (wrapped in single quotes, not empty)
- Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`) - Must have `applyTo` field specifying file patterns (e.g., `'**.js, **.ts'`)
- File names should be lower case with words separated by hyphens - File names should be lower case with words separated by hyphens
#### Agent Skills (skills/*/SKILL.md) #### Agent Skills (skills/\*/SKILL.md)
- Each skill is a folder containing a `SKILL.md` file - Each skill is a folder containing a `SKILL.md` file
- SKILL.md must have `name` field (lowercase with hyphens, matching folder name, max 64 characters) - SKILL.md must have `name` field (lowercase with hyphens, matching folder name, max 64 characters)
- SKILL.md must have `description` field (wrapped in single quotes, 10-1024 characters) - SKILL.md must have `description` field (wrapped in single quotes, 10-1024 characters)
@@ -78,7 +81,8 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
- Asset files should be reasonably sized (under 5MB per file) - Asset files should be reasonably sized (under 5MB per file)
- Skills follow the [Agent Skills specification](https://agentskills.io/specification) - Skills follow the [Agent Skills specification](https://agentskills.io/specification)
#### Hook Folders (hooks/*/README.md) #### Hook Folders (hooks/\*/README.md)
- Each hook is a folder containing a `README.md` file with frontmatter - Each hook is a folder containing a `README.md` file with frontmatter
- README.md must have `name` field (human-readable name) - README.md must have `name` field (human-readable name)
- README.md must have `description` field (wrapped in single quotes, not empty) - README.md must have `description` field (wrapped in single quotes, not empty)
@@ -89,7 +93,8 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
- 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 Files (workflows/*.md) #### Workflow Files (workflows/\*.md)
- Each workflow is a standalone `.md` file in the `workflows/` directory - Each workflow is a standalone `.md` file in the `workflows/` directory
- Must have `name` field (human-readable name) - Must have `name` field (human-readable name)
- Must have `description` field (wrapped in single quotes, not empty) - Must have `description` field (wrapped in single quotes, not empty)
@@ -98,7 +103,8 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
- Only `.md` files are accepted — `.yml`, `.yaml`, and `.lock.yml` files are blocked by CI - Only `.md` files are accepted — `.yml`, `.yaml`, and `.lock.yml` files are blocked by CI
- Follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw/reference/workflow-structure/) - Follow the [GitHub Agentic Workflows specification](https://github.github.com/gh-aw/reference/workflow-structure/)
#### Plugin Folders (plugins/*) #### Plugin Folders (plugins/\*)
- Each plugin is a folder containing a `.github/plugin/plugin.json` file with metadata - Each plugin is a folder containing a `.github/plugin/plugin.json` file with metadata
- plugin.json must have `name` field (matching the folder name) - plugin.json must have `name` field (matching the folder name)
- plugin.json must have `description` field (describing the plugin's purpose) - plugin.json must have `description` field (describing the plugin's purpose)
@@ -112,12 +118,14 @@ All agent files (`*.agent.md`) and instruction files (`*.instructions.md`) must
When adding a new agent, instruction, skill, hook, workflow, or plugin: When adding a new agent, instruction, skill, hook, workflow, or plugin:
**For Agents and Instructions:** **For Agents and Instructions:**
1. Create the file with proper front matter 1. Create the file with proper front matter
2. Add the file to the appropriate directory 2. Add the file to the appropriate directory
3. Update the README.md by running: `npm run build` 3. Update the README.md by running: `npm run build`
4. Verify the resource appears in the generated README 4. Verify the resource appears in the generated README
**For Hooks:** **For Hooks:**
1. Create a new folder in `hooks/` with a descriptive name 1. Create a new folder in `hooks/` with a descriptive name
2. Create `README.md` with proper frontmatter (name, description, hooks, tags) 2. Create `README.md` with proper frontmatter (name, description, hooks, tags)
3. Create `hooks.json` with hook configuration following GitHub Copilot hooks spec 3. Create `hooks.json` with hook configuration following GitHub Copilot hooks spec
@@ -126,16 +134,16 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
6. Update the README.md by running: `npm run build` 6. Update the README.md by running: `npm run build`
7. Verify the hook appears in the generated README 7. Verify the hook appears in the generated README
**For Workflows:** **For Workflows:**
1. Create a new `.md` file in `workflows/` with a descriptive name (e.g., `daily-issues-report.md`) 1. Create a new `.md` file in `workflows/` with a descriptive name (e.g., `daily-issues-report.md`)
2. Include frontmatter with `name` and `description`, plus agentic workflow fields (`on`, `permissions`, `safe-outputs`) 2. Include frontmatter with `name` and `description`, plus agentic workflow fields (`on`, `permissions`, `safe-outputs`)
3. Compile with `gh aw compile --validate` to verify it's valid 3. Compile with `gh aw compile --validate` to verify it's valid
4. Update the README.md by running: `npm run build` 4. Update the README.md by running: `npm run build`
5. Verify the workflow appears in the generated README 5. Verify the workflow appears in the generated README
**For Skills:** **For Skills:**
1. Run `npm run skill:create` to scaffold a new skill folder 1. Run `npm run skill:create` to scaffold a new skill folder
2. Edit the generated SKILL.md file with your instructions 2. Edit the generated SKILL.md file with your instructions
3. Add any bundled assets (scripts, templates, data) to the skill folder 3. Add any bundled assets (scripts, templates, data) to the skill folder
@@ -144,6 +152,7 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
6. Verify the skill appears in the generated README 6. Verify the skill appears in the generated README
**For Plugins:** **For Plugins:**
1. Run `npm run plugin:create -- --name <plugin-name>` to scaffold a new plugin 1. Run `npm run plugin:create -- --name <plugin-name>` to scaffold a new plugin
2. Define agents, commands, and skills in `plugin.json` using Claude Code spec fields 2. Define agents, commands, and skills in `plugin.json` using Claude Code spec fields
3. Edit the generated `plugin.json` with your metadata 3. Edit the generated `plugin.json` with your metadata
@@ -152,6 +161,7 @@ When adding a new agent, instruction, skill, hook, workflow, or plugin:
6. Verify the plugin appears in `.github/plugin/marketplace.json` 6. Verify the plugin appears in `.github/plugin/marketplace.json`
**For External Plugins:** **For External Plugins:**
1. Edit `plugins/external.json` and add an entry with `name`, `source`, `description`, and `version` 1. Edit `plugins/external.json` and add an entry with `name`, `source`, `description`, and `version`
2. The `source` field should be an object specifying a GitHub repo, git URL, npm package, or pip package (see [CONTRIBUTING.md](CONTRIBUTING.md#adding-external-plugins)) 2. The `source` field should be an object specifying a GitHub repo, git URL, npm package, or pip package (see [CONTRIBUTING.md](CONTRIBUTING.md#adding-external-plugins))
3. Run `npm run build` to regenerate marketplace.json 3. Run `npm run build` to regenerate marketplace.json
@@ -168,25 +178,28 @@ npm run skill:validate
npm run build npm run build
# Fix line endings (required before committing) # Fix line endings (required before committing)
bash scripts/fix-line-endings.sh bash eng/fix-line-endings.sh
``` ```
Before committing: Before committing:
- Ensure all markdown front matter is correctly formatted - Ensure all markdown front matter is correctly formatted
- Verify file names follow the lower-case-with-hyphens convention - Verify file names follow the lower-case-with-hyphens convention
- Run `npm run build` to update the README - Run `npm run build` to update the README
- **Always run `bash scripts/fix-line-endings.sh`** to normalize line endings (CRLF → LF) - **Always run `bash eng/fix-line-endings.sh`** to normalize line endings (CRLF → LF)
- Check that your new resource appears correctly in the README - Check that your new resource appears correctly in the README
## Code Style Guidelines ## Code Style Guidelines
### Markdown Files ### Markdown Files
- Use proper front matter with required fields - Use proper front matter with required fields
- Keep descriptions concise and informative - Keep descriptions concise and informative
- Wrap description field values in single quotes - Wrap description field values in single quotes
- Use lower-case file names with hyphens as separators - Use lower-case file names with hyphens as separators
### JavaScript/Node.js Scripts ### JavaScript/Node.js Scripts
- Located in `eng/` and `scripts/` directories - Located in `eng/` and `scripts/` directories
- Follow Node.js ES module conventions (`.mjs` extension) - Follow Node.js ES module conventions (`.mjs` extension)
- Use clear, descriptive function and variable names - Use clear, descriptive function and variable names
@@ -201,29 +214,32 @@ When creating a pull request:
2. **Front matter validation**: Ensure all markdown files have the required front matter fields 2. **Front matter validation**: Ensure all markdown files have the required front matter fields
3. **File naming**: Verify all new files follow the lower-case-with-hyphens naming convention 3. **File naming**: Verify all new files follow the lower-case-with-hyphens naming convention
4. **Build check**: Run `npm run build` before committing to verify README generation 4. **Build check**: Run `npm run build` before committing to verify README generation
5. **Line endings**: **Always run `bash scripts/fix-line-endings.sh`** to normalize line endings to LF (Unix-style) 5. **Line endings**: **Always run `bash eng/fix-line-endings.sh`** to normalize line endings to LF (Unix-style)
6. **Description**: Provide a clear description of what your agent/instruction does 6. **Description**: Provide a clear description of what your agent/instruction does
7. **Testing**: If adding a plugin, run `npm run plugin:validate` to ensure validity 7. **Testing**: If adding a plugin, run `npm run plugin:validate` to ensure validity
### Pre-commit Checklist ### Pre-commit Checklist
Before submitting your PR, ensure you have: Before submitting your PR, ensure you have:
- [ ] Run `npm install` (or `npm ci`) to install dependencies - [ ] Run `npm install` (or `npm ci`) to install dependencies
- [ ] Run `npm run build` to generate the updated README.md - [ ] Run `npm run build` to generate the updated README.md
- [ ] Run `bash scripts/fix-line-endings.sh` to normalize line endings - [ ] Run `bash eng/fix-line-endings.sh` to normalize line endings
- [ ] Verified that all new files have proper front matter - [ ] Verified that all new files have proper front matter
- [ ] Tested that your contribution works with GitHub Copilot - [ ] Tested that your contribution works with GitHub Copilot
- [ ] Checked that file names follow the naming convention - [ ] Checked that file names follow the naming convention
### Code Review Checklist ### Code Review Checklist
For instruction files (*.instructions.md): For instruction files (\*.instructions.md):
- [ ] Has markdown front matter - [ ] Has markdown front matter
- [ ] Has non-empty `description` field wrapped in single quotes - [ ] Has non-empty `description` field wrapped in single quotes
- [ ] Has `applyTo` field with file patterns - [ ] Has `applyTo` field with file patterns
- [ ] File name is lower case with hyphens - [ ] File name is lower case with hyphens
For agent files (*.agent.md): For agent files (\*.agent.md):
- [ ] Has markdown front matter - [ ] Has markdown front matter
- [ ] Has non-empty `description` field wrapped in single quotes - [ ] Has non-empty `description` field wrapped in single quotes
- [ ] Has `name` field with human-readable name (e.g., "Address Comments" not "address-comments") - [ ] Has `name` field with human-readable name (e.g., "Address Comments" not "address-comments")
@@ -231,7 +247,8 @@ For agent files (*.agent.md):
- [ ] Includes `model` field (strongly recommended) - [ ] Includes `model` field (strongly recommended)
- [ ] Considers using `tools` field - [ ] Considers using `tools` field
For skills (skills/*/): For skills (skills/\*/):
- [ ] Folder contains a SKILL.md file - [ ] Folder contains a SKILL.md file
- [ ] SKILL.md has markdown front matter - [ ] SKILL.md has markdown front matter
- [ ] Has `name` field matching folder name (lowercase with hyphens, max 64 characters) - [ ] Has `name` field matching folder name (lowercase with hyphens, max 64 characters)
@@ -240,7 +257,8 @@ For skills (skills/*/):
- [ ] Any bundled assets are referenced in SKILL.md - [ ] Any bundled assets are referenced in SKILL.md
- [ ] Bundled assets are under 5MB per file - [ ] Bundled assets are under 5MB per file
For hook folders (hooks/*/): For hook folders (hooks/\*/):
- [ ] Folder contains a README.md file with markdown front matter - [ ] Folder contains a README.md file with 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
@@ -250,7 +268,8 @@ 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 files (workflows/*.md): For workflow files (workflows/\*.md):
- [ ] File has 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
@@ -260,7 +279,8 @@ For workflow files (workflows/*.md):
- [ ] No `.yml`, `.yaml`, or `.lock.yml` files included - [ ] No `.yml`, `.yaml`, or `.lock.yml` files included
- [ ] Follows [GitHub Agentic Workflows specification](https://github.github.com/gh-aw/reference/workflow-structure/) - [ ] Follows [GitHub Agentic Workflows specification](https://github.github.com/gh-aw/reference/workflow-structure/)
For plugins (plugins/*/): For plugins (plugins/\*/):
- [ ] Directory contains a `.github/plugin/plugin.json` file - [ ] Directory contains a `.github/plugin/plugin.json` file
- [ ] Directory contains a `README.md` file - [ ] Directory contains a `README.md` file
- [ ] `plugin.json` has `name` field matching the directory name (lowercase with hyphens) - [ ] `plugin.json` has `name` field matching the directory name (lowercase with hyphens)
@@ -275,6 +295,7 @@ For plugins (plugins/*/):
## Contributing ## Contributing
This is a community-driven project. Contributions are welcome! Please see: This is a community-driven project. Contributions are welcome! Please see:
- [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines - [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines
- [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community standards - [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for community standards
- [SECURITY.md](SECURITY.md) for security policies - [SECURITY.md](SECURITY.md) for security policies

291
eng/generate-open-pr-report.mjs Executable file
View File

@@ -0,0 +1,291 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { ROOT_FOLDER } from "./constants.mjs";
import { setupGracefulShutdown } from "./utils/graceful-shutdown.mjs";
const DEFAULT_REPO = "github/awesome-copilot";
const DEFAULT_LIMIT = 500;
const DEFAULT_CMD_TIMEOUT = 30_000;
const REPORT_DEFINITIONS = [
{
heading: "PRs that target `main`",
fileName: "prs-targeting-main.json",
predicate: (pr) => pr.targetBranch === "main"
},
{
heading: "PRs that target `staged` which are passing all checks and have less than 10 files",
fileName: "prs-staged-passing-under-10-files.json",
predicate: (pr) => pr.targetBranch === "staged" && pr.checksPass && pr.fileCount < 10
},
{
heading: "PRs that target `staged` which have between 10 and 50 files",
fileName: "prs-staged-10-to-50-files.json",
predicate: (pr) => pr.targetBranch === "staged" && pr.fileCount >= 10 && pr.fileCount <= 50
},
{
heading: "PRs that target `staged` with greater than 50 files",
fileName: "prs-staged-over-50-files.json",
predicate: (pr) => pr.targetBranch === "staged" && pr.fileCount > 50
}
];
setupGracefulShutdown("generate-open-pr-report");
function printUsage() {
console.log(`Usage: node eng/generate-open-pr-report.mjs [--repo owner/name] [--output-dir path] [--limit N]
Generate open PR reports for a GitHub repository.
Outputs:
- open-pr-report.md
- prs-targeting-main.json
- prs-staged-passing-under-10-files.json
- prs-staged-10-to-50-files.json
- prs-staged-over-50-files.json
Options:
--repo GitHub repository in owner/name format (default: ${DEFAULT_REPO})
--output-dir Directory for generated reports (default: <repo-root>/reports)
--limit Max number of open PRs to fetch (default: ${DEFAULT_LIMIT})
--help, -h Show this help text`);
}
function parseArgs(argv) {
const options = {
repo: DEFAULT_REPO,
outputDir: path.join(ROOT_FOLDER, "reports"),
limit: DEFAULT_LIMIT
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--help" || arg === "-h") {
printUsage();
process.exit(0);
}
if (arg === "--repo") {
options.repo = argv[i + 1] ?? "";
i += 1;
continue;
}
if (arg === "--output-dir") {
options.outputDir = argv[i + 1] ?? "";
i += 1;
continue;
}
if (arg === "--limit") {
options.limit = Number.parseInt(argv[i + 1] ?? "", 10);
i += 1;
continue;
}
throw new Error(`Unknown option: ${arg}`);
}
if (!options.repo || !options.repo.includes("/")) {
throw new Error("--repo must be in owner/name format.");
}
if (!Number.isInteger(options.limit) || options.limit < 1) {
throw new Error("--limit must be a positive integer.");
}
if (!options.outputDir) {
throw new Error("--output-dir is required.");
}
return options;
}
function ensureCommandAvailable(command) {
try {
execFileSync(command, ["--version"], {
stdio: "ignore",
timeout: DEFAULT_CMD_TIMEOUT
});
} catch (error) {
throw new Error(`Missing required command: ${command}`);
}
}
function runGhJson(args) {
const output = execFileSync("gh", args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: DEFAULT_CMD_TIMEOUT
});
return JSON.parse(output);
}
function getCheckState(statusCheckRollup) {
if (!Array.isArray(statusCheckRollup) || statusCheckRollup.length === 0) {
return "NONE";
}
if (statusCheckRollup.some((check) => check.status !== "COMPLETED")) {
return "PENDING";
}
const failureConclusions = new Set([
"FAILURE",
"TIMED_OUT",
"ACTION_REQUIRED",
"CANCELLED",
"STALE",
"STARTUP_FAILURE"
]);
if (statusCheckRollup.some((check) => failureConclusions.has(check.conclusion ?? ""))) {
return "FAILURE";
}
const successConclusions = new Set(["SUCCESS", "NEUTRAL", "SKIPPED"]);
const allSuccessful = statusCheckRollup.every((check) => successConclusions.has(check.conclusion ?? ""));
return allSuccessful ? "SUCCESS" : "FAILURE";
}
function normalizePullRequest(pr) {
const checkState = getCheckState(pr.statusCheckRollup);
return {
id: pr.number,
title: pr.title,
author: pr.author?.login ?? "ghost",
checksPass: checkState === "SUCCESS",
checkState,
targetBranch: pr.baseRefName,
fileCount: pr.changedFiles,
createdAt: pr.createdAt,
updatedAt: pr.updatedAt,
createdAgeDays: getAgeInDays(pr.createdAt),
updatedAgeDays: getAgeInDays(pr.updatedAt),
url: pr.url
};
}
function getCheckLabel(pr) {
if (pr.checkState === "SUCCESS") {
return "Yes";
}
if (pr.checkState === "PENDING") {
return "Pending";
}
if (pr.checkState === "NONE") {
return "No checks";
}
return "No";
}
function escapeMarkdownCell(value) {
return String(value).replaceAll("|", "\\|");
}
function getAgeInDays(timestamp) {
const milliseconds = Date.now() - new Date(timestamp).getTime();
return Math.max(0, Math.floor(milliseconds / (24 * 60 * 60 * 1000)));
}
function formatTimestampWithAge(timestamp) {
return `${timestamp.slice(0, 10)} (${getAgeInDays(timestamp)}d ago)`;
}
function renderTable(prs) {
const lines = [
"| PR title + ID | Author | Whether checks pass | Created | Updated | Link to PR |",
"| --- | --- | --- | --- | --- | --- |"
];
if (prs.length === 0) {
lines.push("| None | - | - | - | - | - |");
return lines.join("\n");
}
for (const pr of prs) {
lines.push(
`| ${escapeMarkdownCell(pr.title)} (#${pr.id}) | ${escapeMarkdownCell(pr.author)} | ${getCheckLabel(pr)} | ${formatTimestampWithAge(pr.createdAt)} | ${formatTimestampWithAge(pr.updatedAt)} | [Link](${pr.url}) |`
);
}
return lines.join("\n");
}
function renderMarkdownReport(repo, generatedAt, categorizedReports) {
const sections = [
"# Open PR report",
"",
`**Repository:** \`${repo}\` `,
`**Generated:** \`${generatedAt}\``
];
for (const report of categorizedReports) {
sections.push("", `## ${report.heading}`, "", renderTable(report.items));
}
return `${sections.join("\n")}\n`;
}
function writeJsonReport(filePath, items) {
fs.writeFileSync(filePath, `${JSON.stringify(items, null, 2)}\n`);
}
function generateOpenPrReport() {
const options = parseArgs(process.argv.slice(2));
ensureCommandAvailable("gh");
console.log(`Fetching open PRs from ${options.repo}...`);
const pullRequests = runGhJson([
"pr",
"list",
"--repo",
options.repo,
"--state",
"open",
"--limit",
String(options.limit),
"--json",
"number,title,url,author,baseRefName,changedFiles,createdAt,updatedAt,statusCheckRollup"
]);
const normalizedPullRequests = pullRequests.map(normalizePullRequest);
const categorizedReports = REPORT_DEFINITIONS.map((report) => ({
...report,
items: normalizedPullRequests.filter(report.predicate)
}));
fs.mkdirSync(options.outputDir, { recursive: true });
for (const report of categorizedReports) {
writeJsonReport(path.join(options.outputDir, report.fileName), report.items);
}
const markdownReport = renderMarkdownReport(
options.repo,
new Date().toISOString(),
categorizedReports
);
const markdownFilePath = path.join(options.outputDir, "open-pr-report.md");
fs.writeFileSync(markdownFilePath, markdownReport);
console.log(`Generated reports in ${options.outputDir}:`);
console.log(" open-pr-report.md");
for (const report of categorizedReports) {
console.log(` ${report.fileName}`);
}
}
generateOpenPrReport();

0
eng/generate-website-data.mjs Normal file → Executable file
View File

View File

@@ -1,137 +0,0 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { ROOT_FOLDER, SKILLS_DIR } from "./constants.mjs";
import { parseFrontmatter } from "./yaml-parser.mjs";
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
/**
* Convert a prompt file to a skill folder
* @param {string} promptFilePath - Full path to the prompt file
* @returns {object} Result with success status and details
*/
function convertPromptToSkill(promptFilePath) {
const filename = path.basename(promptFilePath);
const baseName = filename.replace(".prompt.md", "");
console.log(`\nConverting: ${baseName}`);
// Parse the prompt file frontmatter
const frontmatter = parseFrontmatter(promptFilePath);
const content = fs.readFileSync(promptFilePath, "utf8");
// Extract the content after frontmatter
const frontmatterEndMatch = content.match(/^---\n[\s\S]*?\n---\n/);
const mainContent = frontmatterEndMatch
? content.substring(frontmatterEndMatch[0].length).trim()
: content.trim();
// Create skill folder
const skillFolderPath = path.join(SKILLS_DIR, baseName);
if (fs.existsSync(skillFolderPath)) {
console.log(` ⚠️ Skill folder already exists: ${baseName}`);
return { success: false, reason: "already-exists", name: baseName };
}
fs.mkdirSync(skillFolderPath, { recursive: true });
// Build new frontmatter for SKILL.md
const skillFrontmatter = {
name: baseName,
description: frontmatter?.description || `Skill converted from ${filename}`,
};
// Build SKILL.md content
const skillContent = `---
name: ${skillFrontmatter.name}
description: '${skillFrontmatter.description.replace(/'/g, "'''")}'
---
${mainContent}
`;
// Write SKILL.md
const skillFilePath = path.join(skillFolderPath, "SKILL.md");
fs.writeFileSync(skillFilePath, skillContent, "utf8");
console.log(` ✓ Created skill: ${baseName}`);
return { success: true, name: baseName, path: skillFolderPath };
}
/**
* Main migration function
*/
function main() {
console.log("=".repeat(60));
console.log("Starting Prompt to Skills Migration");
console.log("=".repeat(60));
// Check if prompts directory exists
if (!fs.existsSync(PROMPTS_DIR)) {
console.error(`Error: Prompts directory not found: ${PROMPTS_DIR}`);
process.exit(1);
}
// Get all prompt files
const promptFiles = fs
.readdirSync(PROMPTS_DIR)
.filter((file) => file.endsWith(".prompt.md"))
.map((file) => path.join(PROMPTS_DIR, file));
console.log(`Found ${promptFiles.length} prompt files to convert\n`);
const results = {
success: [],
alreadyExists: [],
failed: [],
};
// Convert each prompt
for (const promptFile of promptFiles) {
try {
const result = convertPromptToSkill(promptFile);
if (result.success) {
results.success.push(result.name);
} else if (result.reason === "already-exists") {
results.alreadyExists.push(result.name);
} else {
results.failed.push(result.name);
}
} catch (error) {
const baseName = path.basename(promptFile, ".prompt.md");
console.error(` ✗ Error converting ${baseName}: ${error.message}`);
results.failed.push(baseName);
}
}
// Print summary
console.log("\n" + "=".repeat(60));
console.log("Migration Summary");
console.log("=".repeat(60));
console.log(`✓ Successfully converted: ${results.success.length}`);
console.log(`⚠ Already existed: ${results.alreadyExists.length}`);
console.log(`✗ Failed: ${results.failed.length}`);
console.log(`Total processed: ${promptFiles.length}`);
if (results.failed.length > 0) {
console.log("\nFailed conversions:");
results.failed.forEach((name) => console.log(` - ${name}`));
}
if (results.alreadyExists.length > 0) {
console.log("\nSkipped (already exist):");
results.alreadyExists.forEach((name) => console.log(` - ${name}`));
}
console.log("\n✅ Migration complete!");
console.log(
"\nNext steps:\n" +
"1. Run 'npm run skill:validate' to validate all new skills\n" +
"2. Update plugin manifests to reference skills instead of commands\n" +
"3. Remove prompts directory after testing\n"
);
}
// Run migration
main();

View File

@@ -1,165 +0,0 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { PLUGINS_DIR } from "./constants.mjs";
/**
* Convert commands references to skills references in a plugin.json
* @param {string} pluginJsonPath - Path to the plugin.json file
* @returns {object} Result with success status and details
*/
function updatePluginManifest(pluginJsonPath) {
const pluginDir = path.dirname(path.dirname(path.dirname(pluginJsonPath)));
const pluginName = path.basename(pluginDir);
console.log(`\nProcessing plugin: ${pluginName}`);
// Read and parse plugin.json
let plugin;
try {
const content = fs.readFileSync(pluginJsonPath, "utf8");
plugin = JSON.parse(content);
} catch (error) {
console.log(` ✗ Error reading/parsing: ${error.message}`);
return { success: false, name: pluginName, reason: "parse-error" };
}
// Check if plugin has commands field
if (!plugin.commands || !Array.isArray(plugin.commands)) {
console.log(` No commands field found`);
return { success: false, name: pluginName, reason: "no-commands" };
}
const commandCount = plugin.commands.length;
console.log(` Found ${commandCount} command(s) to convert`);
// Validate and convert commands to skills format
// Commands: "./commands/foo.md" → Skills: "./skills/foo/"
const validCommands = plugin.commands.filter((cmd) => {
if (typeof cmd !== "string") {
console.log(` ⚠ Skipping non-string command entry: ${JSON.stringify(cmd)}`);
return false;
}
if (!cmd.startsWith("./commands/") || !cmd.endsWith(".md")) {
console.log(` ⚠ Skipping command with unexpected format: ${cmd}`);
return false;
}
return true;
});
const skills = validCommands.map((cmd) => {
const basename = path.basename(cmd, ".md");
return `./skills/${basename}/`;
});
// Initialize skills array if it doesn't exist or is not an array
if (!Array.isArray(plugin.skills)) {
plugin.skills = [];
}
// Add converted commands to skills array, de-duplicating entries
const allSkills = new Set(plugin.skills);
for (const skillPath of skills) {
allSkills.add(skillPath);
}
plugin.skills = Array.from(allSkills);
// Remove commands field
delete plugin.commands;
// Write updated plugin.json
try {
fs.writeFileSync(
pluginJsonPath,
JSON.stringify(plugin, null, 2) + "\n",
"utf8"
);
console.log(` ✓ Converted ${commandCount} command(s) to skills`);
return { success: true, name: pluginName, count: commandCount };
} catch (error) {
console.log(` ✗ Error writing file: ${error.message}`);
return { success: false, name: pluginName, reason: "write-error" };
}
}
/**
* Main function to update all plugin manifests
*/
function main() {
console.log("=".repeat(60));
console.log("Updating Plugin Manifests: Commands → Skills");
console.log("=".repeat(60));
// Check if plugins directory exists
if (!fs.existsSync(PLUGINS_DIR)) {
console.error(`Error: Plugins directory not found: ${PLUGINS_DIR}`);
process.exit(1);
}
// Find all plugin.json files
const pluginDirs = fs
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
console.log(`Found ${pluginDirs.length} plugin directory(ies)\n`);
const results = {
updated: [],
noCommands: [],
failed: [],
};
// Process each plugin
for (const dirName of pluginDirs) {
const pluginJsonPath = path.join(
PLUGINS_DIR,
dirName,
".github/plugin",
"plugin.json"
);
if (!fs.existsSync(pluginJsonPath)) {
console.log(`\nSkipping ${dirName}: no plugin.json found`);
continue;
}
const result = updatePluginManifest(pluginJsonPath);
if (result.success) {
results.updated.push({ name: result.name, count: result.count });
} else if (result.reason === "no-commands") {
results.noCommands.push(result.name);
} else {
results.failed.push(result.name);
}
}
// Print summary
console.log("\n" + "=".repeat(60));
console.log("Update Summary");
console.log("=".repeat(60));
console.log(`✓ Updated plugins: ${results.updated.length}`);
console.log(` No commands field: ${results.noCommands.length}`);
console.log(`✗ Failed: ${results.failed.length}`);
console.log(`Total processed: ${pluginDirs.length}`);
if (results.updated.length > 0) {
console.log("\nUpdated plugins:");
results.updated.forEach(({ name, count }) =>
console.log(` - ${name} (${count} command(s) → skills)`)
);
}
if (results.failed.length > 0) {
console.log("\nFailed updates:");
results.failed.forEach((name) => console.log(` - ${name}`));
}
console.log("\n✅ Plugin manifest updates complete!");
console.log(
"\nNext steps:\n" +
"1. Run 'npm run plugin:validate' to validate all updated plugins\n" +
"2. Test that plugins work correctly\n"
);
}
// Run the update
main();