Merge branch 'staged' into aaronpowell/comparing-vally-lint

This commit is contained in:
Aaron Powell
2026-06-23 14:45:11 +10:00
committed by GitHub
260 changed files with 27188 additions and 1673 deletions
@@ -1,5 +1,6 @@
---
description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing
name: Agentic Workflows
description: GitHub Agentic Workflows (gh-aw) - Create, debug, and upgrade AI-powered workflows with intelligent prompt routing.
disable-model-invocation: true
---
@@ -13,13 +14,16 @@ This is a **dispatcher agent** that routes your request to the appropriate speci
- **Creating new workflows**: Routes to `create` prompt
- **Updating existing workflows**: Routes to `update` prompt
- **Debugging workflows**: Routes to `debug` prompt
- **Debugging workflows**: Routes to `debug` prompt
- **Upgrading workflows**: Routes to `upgrade-agentic-workflows` prompt
- **Creating report-generating workflows**: Routes to `report` prompt — consult this whenever the workflow posts status updates, audits, analyses, or any structured output as issues, discussions, or comments
- **Creating shared components**: Routes to `create-shared-agentic-workflow` prompt
- **Fixing Dependabot PRs**: Routes to `dependabot` prompt — use this when Dependabot opens PRs that modify generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`). Never merge those PRs directly; instead update the source `.md` files and rerun `gh aw compile --dependabot` to bundle all fixes
- **Analyzing test coverage**: Routes to `test-coverage` prompt — consult this whenever the workflow reads, analyzes, or reports on test coverage data from PRs or CI runs
- **Rendering ASCII charts in markdown**: Routes to `asciicharts` guide — consult this whenever the workflow needs compact charts that render reliably in GitHub issues, comments, or discussions
- **CLI commands and triggering workflows**: Routes to `cli-commands` guide — consult this whenever the user asks how to run, compile, debug, or manage workflows from the command line, or when they need the MCP tool equivalent of a `gh aw` command
- **Reducing token consumption / cost optimization**: Routes to `token-optimization` guide — consult this whenever the user asks how to reduce token usage, lower costs, speed up workflows, or measure the impact of prompt changes with experiments
- **Choosing workflow architectures and design patterns**: Routes to `patterns` guide — consult this whenever the user asks for strategy, architecture, operating models, or pattern selection for agentic workflows
Workflows may optionally include:
@@ -31,7 +35,7 @@ Workflows may optionally include:
- Workflow files: `.github/workflows/*.md` and `.github/workflows/**/*.md`
- Workflow lock files: `.github/workflows/*.lock.yml`
- Shared components: `.github/workflows/shared/*.md`
- Configuration: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/github-agentic-workflows.md
- Configuration: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md`
## Problems This Solves
@@ -50,30 +54,32 @@ When you interact with this agent, it will:
## Available Prompts
> **Note**: The prompt and reference files listed below are located in the [`github/gh-aw`](https://github.com/github/gh-aw) repository and are **not available locally** in this repository. Load them from their public URLs.
### Create New Workflow
**Load when**: User wants to create a new workflow from scratch, add automation, or design a workflow that doesn't exist yet
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/create-agentic-workflow.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-agentic-workflow.md`
**Use cases**:
- "Create a workflow that triages issues"
- "I need a workflow to label pull requests"
- "Design a weekly research automation"
### Update Existing Workflow
### Update Existing Workflow
**Load when**: User wants to modify, improve, or refactor an existing workflow
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/update-agentic-workflow.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/update-agentic-workflow.md`
**Use cases**:
- "Add web-fetch tool to the issue-classifier workflow"
- "Update the PR reviewer to use discussions instead of issues"
- "Improve the prompt for the weekly-research workflow"
### Debug Workflow
### Debug Workflow
**Load when**: User needs to investigate, audit, debug, or understand a workflow, troubleshoot issues, analyze logs, or fix errors
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/debug-agentic-workflow.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/debug-agentic-workflow.md`
**Use cases**:
- "Why is this workflow failing?"
@@ -83,7 +89,7 @@ When you interact with this agent, it will:
### Upgrade Agentic Workflows
**Load when**: User wants to upgrade workflows to a new gh-aw version or fix deprecations
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/upgrade-agentic-workflows.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/upgrade-agentic-workflows.md`
**Use cases**:
- "Upgrade all workflows to the latest version"
@@ -93,7 +99,7 @@ When you interact with this agent, it will:
### Create a Report-Generating Workflow
**Load when**: The workflow being created or updated produces reports — recurring status updates, audit summaries, analyses, or any structured output posted as a GitHub issue, discussion, or comment
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/report.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/report.md`
**Use cases**:
- "Create a weekly CI health report"
@@ -103,7 +109,7 @@ When you interact with this agent, it will:
### Create Shared Agentic Workflow
**Load when**: User wants to create a reusable workflow component or wrap an MCP server
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/create-shared-agentic-workflow.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/create-shared-agentic-workflow.md`
**Use cases**:
- "Create a shared component for Notion integration"
@@ -113,7 +119,7 @@ When you interact with this agent, it will:
### Fix Dependabot PRs
**Load when**: User needs to close or fix open Dependabot PRs that update dependencies in generated manifest files (`.github/workflows/package.json`, `.github/workflows/requirements.txt`, `.github/workflows/go.mod`)
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/dependabot.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/dependabot.md`
**Use cases**:
- "Fix the open Dependabot PRs for npm dependencies"
@@ -123,7 +129,7 @@ When you interact with this agent, it will:
### Analyze Test Coverage
**Load when**: The workflow reads, analyzes, or reports test coverage — whether triggered by a PR, a schedule, or a slash command. Always consult this prompt before designing the coverage data strategy.
**Prompt file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/test-coverage.md
**Prompt file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/test-coverage.md`
**Use cases**:
- "Create a workflow that comments coverage on PRs"
@@ -133,7 +139,7 @@ When you interact with this agent, it will:
### CLI Commands Reference
**Load when**: The user asks how to run, compile, debug, or manage workflows from the command line; needs the MCP tool equivalent of a `gh aw` command; or is in a restricted environment (e.g., Copilot Cloud) without direct CLI access.
**Reference file**: https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/cli-commands.md
**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md`
**Use cases**:
- "How do I trigger workflow X on the main branch?"
@@ -141,12 +147,36 @@ When you interact with this agent, it will:
- "I'm in Copilot Cloud — how do I compile a workflow?"
- "Show me all available gh aw commands"
### Token Consumption Optimization
**Load when**: The user asks how to reduce token usage, lower workflow costs, make a workflow faster or cheaper, or measure the impact of prompt or configuration changes.
**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/token-optimization.md`
**Use cases**:
- "How do I reduce the token cost of this workflow?"
- "My workflow is too expensive — how do I optimize it?"
- "How do I compare token usage between two runs?"
- "Should I use gh-proxy or the MCP server?"
- "How do I use sub-agents to reduce costs?"
- "How do I measure the impact of a prompt change?"
### Workflow Pattern Selection
**Load when**: The user asks for architecture, strategy, operating model selection, or pattern recommendations for building agentic workflows.
**Reference file**: `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/patterns.md`
**Use cases**:
- "Which pattern should I use for multi-repo rollout?"
- "How should I structure this workflow architecture?"
- "What pattern fits slash-command triage?"
- "Should this be DispatchOps or DailyOps?"
## Instructions
When a user interacts with you:
1. **Identify the task type** from the user's request
2. **Load the appropriate prompt** from the GitHub repository URLs listed above
2. **Load the appropriate prompt** from the URLs listed above
3. **Follow the loaded prompt's instructions** exactly
4. **If uncertain**, ask clarifying questions to determine the right prompt
@@ -185,12 +215,12 @@ gh aw compile --validate
## Important Notes
- Always reference the instructions file at https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/github-agentic-workflows.md for complete documentation
- Always reference the instructions file at `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/github-agentic-workflows.md` for complete documentation
- Use the MCP tool `agentic-workflows` when running in GitHub Copilot Cloud
- Workflows must be compiled to `.lock.yml` files before running in GitHub Actions
- **Bash tools are enabled by default** - Don't restrict bash commands unnecessarily since workflows are sandboxed by the AWF
- Follow security best practices: minimal permissions, explicit network access, no template injection
- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/network.md for the full list of valid ecosystem identifiers and domain patterns.
- **Network configuration**: Use ecosystem identifiers (`node`, `python`, `go`, etc.) or explicit FQDNs in `network.allowed`. Bare shorthands like `npm` or `pypi` are **not** valid. See `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/network.md` for the full list of valid ecosystem identifiers and domain patterns.
- **Single-file output**: When creating a workflow, produce exactly **one** workflow `.md` file. Do not create separate documentation files (architecture docs, runbooks, usage guides, etc.). If documentation is needed, add a brief `## Usage` section inside the workflow file itself.
- **Triggering runs**: Always use `gh aw run <workflow-name>` to trigger a workflow on demand — not `gh workflow run <file>.lock.yml`. `gh aw run` handles workflow resolution by short name, input parsing and validation, and correct run-tracking for agentic workflows. Use `--ref <branch>` to run on a specific branch.
- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see https://github.com/github/gh-aw/blob/v0.71.5/.github/aw/cli-commands.md
- **CLI commands reference**: For a complete guide on all `gh aw` commands and their MCP tool equivalents (for restricted environments), see `https://raw.githubusercontent.com/github/gh-aw/main/.github/aw/cli-commands.md`
+9 -9
View File
@@ -1,9 +1,9 @@
{
"entries": {
"actions/checkout@v6.0.2": {
"actions/checkout@v7.0.0": {
"repo": "actions/checkout",
"version": "v6.0.2",
"sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd"
"version": "v7.0.0",
"sha": "9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0"
},
"actions/download-artifact@v8.0.1": {
"repo": "actions/download-artifact",
@@ -20,15 +20,15 @@
"version": "v7.0.1",
"sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"
},
"github/gh-aw-actions/setup@v0.72.1": {
"github/gh-aw-actions/setup@v0.80.9": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.72.1",
"sha": "bc56a0cad2f450c562810785ef38649c04db812a"
"version": "v0.80.9",
"sha": "8c7d04ebf1ece56cd381446125da3e0f6896294a"
},
"github/gh-aw/actions/setup@v0.72.1": {
"github/gh-aw/actions/setup@v0.80.9": {
"repo": "github/gh-aw/actions/setup",
"version": "v0.72.1",
"sha": "489dbab88cc78e35506b5ccbf08a4037166824ac"
"version": "v0.80.9",
"sha": "a3624368c4e7d877586ff2784b61de73405e2cdd"
}
}
}
+56 -7
View File
@@ -16,6 +16,33 @@
"description": "Drive Microsoft AgentRC from Copilot chat: assess AI readiness, generate Copilot instructions (flat or nested with applyTo globs for monorepos), and manage policies. Produces a self-contained static HTML dashboard at reports/index.html.",
"version": "1.0.0"
},
{
"name": "agent-council",
"description": "A runtime-portable 5-agent quality gate that adjudicates text artifacts before they ship. Five role-conditioned LLM deliberators run a 2-round async protocol with cross-read rebuttal and return one verdict — SHIP, REVISE, or HOLD — plus a structured revision brief and a full audit transcript.",
"version": "0.1.3",
"author": {
"name": "Parth Sangani",
"url": "https://github.com/Avyayalaya"
},
"repository": "https://github.com/Avyayalaya/agent-council",
"license": "MIT",
"keywords": [
"quality-gate",
"multi-agent",
"adjudication",
"council",
"llm-as-judge",
"agent-orchestration",
"review",
"claude-code",
"mcp"
],
"source": {
"source": "github",
"repo": "Avyayalaya/agent-council",
"ref": "v0.1.3"
}
},
{
"name": "ai-ready",
"description": "Analyze any repository and generate AI-ready configuration — AGENTS.md, copilot-instructions.md, CI workflows, issue templates, and more. Mines your PR review patterns and creates files customized to your stack.",
@@ -202,6 +229,30 @@
"description": "Database administration, SQL optimization, and data management tools for PostgreSQL, SQL Server, and general database development best practices.",
"version": "1.0.0"
},
{
"name": "datadog",
"description": "Use Datadog directly in Copilot / VS Code through a preconfigured Datadog MCP server. Query logs, metrics, traces, dashboards, and more through natural conversation.",
"version": "0.7.15",
"author": {
"name": "Datadog",
"url": "https://www.datadoghq.com/"
},
"repository": "https://github.com/datadog-labs/vscode-plugin",
"license": "Apache-2.0",
"keywords": [
"agent",
"copilot",
"datadog",
"monitoring",
"observability",
"vscode"
],
"source": {
"source": "github",
"repo": "datadog-labs/vscode-plugin",
"sha": "b003fcad48c3a935ffe04b6218f5cf58fe2b6760"
}
},
{
"name": "dataverse",
"description": "Build and manage Microsoft Dataverse solutions using natural language. Includes table/column creation, solution lifecycle, data operations, and MCP server configuration.",
@@ -561,7 +612,7 @@
{
"name": "modernize-dotnet",
"description": "AI-powered .NET modernization and upgrade assistant. Helps upgrade .NET Framework and .NET applications to the latest versions of .NET.",
"version": "1.0.1152-preview1",
"version": "1.0.1157-preview1",
"author": {
"name": "Microsoft",
"url": "https://www.microsoft.com"
@@ -901,7 +952,7 @@
{
"name": "ui5",
"description": "SAPUI5 / OpenUI5 plugin for GitHub CoPilot. Create and validate UI5 projects, access API documentation, run UI5 linter, get development guidelines and best practices for UI5 development.",
"version": "0.1.3",
"version": "0.1.4",
"author": {
"name": "SAP SE",
"url": "https://www.sap.com"
@@ -922,14 +973,13 @@
"source": "github",
"repo": "UI5/plugins-coding-agents",
"path": "plugins/ui5",
"ref": "v0.1.3",
"sha": "9b3d7d80356f687725f9584988e4038dbead0d53"
"sha": "80f2d93287054f9d30dd990e842e15bcfca581c9"
}
},
{
"name": "ui5-typescript-conversion",
"description": "SAPUI5 / OpenUI5 plugin for GitHub CoPilot. Convert JavaScript based UI5 projects to TypeScript.",
"version": "0.1.3",
"version": "0.1.4",
"author": {
"name": "SAP SE",
"url": "https://www.sap.com"
@@ -951,8 +1001,7 @@
"source": "github",
"repo": "UI5/plugins-coding-agents",
"path": "plugins/ui5-typescript-conversion",
"ref": "v0.1.3",
"sha": "9b3d7d80356f687725f9584988e4038dbead0d53"
"sha": "80f2d93287054f9d30dd990e842e15bcfca581c9"
}
},
{
+4 -3
View File
@@ -2,10 +2,10 @@
- [ ] I have read and followed the [CONTRIBUTING.md](https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md) guidelines.
- [ ] I have read and followed the [Guidance for submissions involving paid services](https://github.com/github/awesome-copilot/discussions/968).
- [ ] My contribution adds a new instruction, prompt, agent, skill, or workflow file in the correct directory.
- [ ] My contribution adds a new instruction, prompt, agent, skill, workflow, or canvas extension file in the correct directory.
- [ ] The file follows the required naming convention.
- [ ] The content is clearly structured and follows the example format.
- [ ] I have tested my instructions, prompt, agent, skill, or workflow with GitHub Copilot.
- [ ] I have tested my instructions, prompt, agent, skill, workflow, or canvas extension with GitHub Copilot.
- [ ] I have run `npm start` and verified that `README.md` is up to date.
- [ ] I am targeting the `staged` branch for this pull request.
@@ -25,7 +25,8 @@
- [ ] New plugin.
- [ ] New skill file.
- [ ] New agentic workflow.
- [ ] Update to existing instruction, prompt, agent, plugin, skill, or workflow.
- [ ] New canvas extension.
- [ ] Update to existing instruction, prompt, agent, plugin, skill, workflow, or canvas extension.
- [ ] Other (please specify):
---
+80
View File
@@ -0,0 +1,80 @@
---
name: agentic-workflows
description: Route gh-aw workflow design/create/debug/upgrade requests to the right prompts.
---
# Agentic Workflows Router
Use this skill when a user asks to design, create, update, debug, or upgrade GitHub Agentic Workflows in this repository.
This skill is a dispatcher: identify the task type, load the matching workflow prompt/skill file, and follow it directly. Keep responses concise and ask a clarifying question if the correct prompt is unclear.
Read only the files you need:
Load these files from `github/gh-aw` (they are not available locally).
- `.github/aw/agentic-chat.md`
- `.github/aw/agentic-workflows-mcp.md`
- `.github/aw/asciicharts.md`
- `.github/aw/campaign.md`
- `.github/aw/charts-trending.md`
- `.github/aw/charts.md`
- `.github/aw/cli-commands.md`
- `.github/aw/context.md`
- `.github/aw/create-agentic-workflow.md`
- `.github/aw/create-shared-agentic-workflow.md`
- `.github/aw/debug-agentic-workflow.md`
- `.github/aw/dependabot.md`
- `.github/aw/deployment-status.md`
- `.github/aw/experiments.md`
- `.github/aw/github-agentic-workflows.md`
- `.github/aw/github-mcp-server.md`
- `.github/aw/llms.md`
- `.github/aw/mcp-clis.md`
- `.github/aw/memory.md`
- `.github/aw/messages.md`
- `.github/aw/network.md`
- `.github/aw/optimize-agentic-workflow.md`
- `.github/aw/patterns.md`
- `.github/aw/pr-reviewer.md`
- `.github/aw/report.md`
- `.github/aw/reuse.md`
- `.github/aw/safe-outputs-automation.md`
- `.github/aw/safe-outputs-content.md`
- `.github/aw/safe-outputs-management.md`
- `.github/aw/safe-outputs-runtime.md`
- `.github/aw/safe-outputs.md`
- `.github/aw/serena-tool.md`
- `.github/aw/shared-safe-jobs.md`
- `.github/aw/skills.md`
- `.github/aw/subagents.md`
- `.github/aw/syntax-agentic.md`
- `.github/aw/syntax-core.md`
- `.github/aw/syntax-tools-imports.md`
- `.github/aw/syntax.md`
- `.github/aw/test-coverage.md`
- `.github/aw/test-expression.md`
- `.github/aw/token-optimization.md`
- `.github/aw/triggers.md`
- `.github/aw/update-agentic-workflow.md`
- `.github/aw/upgrade-agentic-workflows.md`
- `.github/aw/visual-regression.md`
- `.github/aw/workflow-constraints.md`
- `.github/aw/workflow-editing.md`
- `.github/aw/workflow-patterns.md`
- `.github/skills/agentic-workflow-designer/SKILL.md`
After loading the matching workflow prompt or skill, follow it directly:
- Design workflows from scratch via interview: `skills/agentic-workflow-designer/SKILL.md`
- Create new workflows: `.github/aw/create-agentic-workflow.md`
- Update existing workflows: `.github/aw/update-agentic-workflow.md`
- Debug, audit, or investigate workflows: `.github/aw/debug-agentic-workflow.md`
- Upgrade workflows and fix deprecations: `.github/aw/upgrade-agentic-workflows.md`
- Create shared components or MCP wrappers: `.github/aw/create-shared-agentic-workflow.md`
- Create report-generating workflows: `.github/aw/report.md`
- Fix Dependabot manifest PRs: `.github/aw/dependabot.md`
- Analyze coverage workflows: `.github/aw/test-coverage.md`
- Render compact markdown charts: `.github/aw/asciicharts.md`
- Map CLI commands to MCP usage: `.github/aw/cli-commands.md`
- Choose workflow architecture and patterns: `.github/aw/patterns.md`
- Optimize token usage and cost: `.github/aw/token-optimization.md`
When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt or skill.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Check spelling with codespell
uses: codespell-project/actions-codespell@406322ec52dd7b488e48c1c4b82e2a8b3a1bf630 # v2.1
with:
+2 -2
View File
@@ -21,6 +21,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install gh-aw extension
uses: github/gh-aw-actions/setup-cli@b8068426813005612b960b5ab0b8bd2c27142323 # v0.71.5
uses: github/gh-aw-actions/setup-cli@8c7d04ebf1ece56cd381446125da3e0f6896294a # v0.80.9
with:
version: v0.71.5
version: v0.80.9
File diff suppressed because one or more lines are too long
@@ -9,7 +9,8 @@ concurrency:
cancel-in-progress: false
permissions:
issues: write
pull-requests: write
contents: read
jobs:
sync-merged-pr-labels:
@@ -25,20 +26,6 @@ jobs:
const prNumber = context.payload.pull_request.number;
const staleLabels = ['awaiting-review', 'awaiting-approval', 'ready-for-review', 'rejected'];
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name: 'approved',
color: '1D76DB',
description: 'Submission was approved by a maintainer'
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -275,49 +275,6 @@ jobs:
PR_NUMBER: ${{ steps.approval_pr.outputs.pr-number }}
with:
script: |
const managedLabels = {
'external-plugin': {
color: 'FEF2C0',
description: 'Public external plugin submission'
},
'awaiting-review': {
color: 'FBCA04',
description: 'Submission is waiting for automated intake validation'
},
'ready-for-review': {
color: '0E8A16',
description: 'Submission passed intake validation and is ready for maintainer review'
},
'requires-submitter-fixes': {
color: 'D93F0B',
description: 'Submission has quality-gate findings that submitter must fix before maintainer review'
},
'approved': {
color: '1D76DB',
description: 'Submission was approved by a maintainer'
},
'rejected': {
color: 'B60205',
description: 'Submission was rejected by a maintainer'
}
};
async function ensureLabel(name, config) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color: config.color,
description: config.description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
async function removeLabel(issueNumber, name) {
try {
await github.rest.issues.removeLabel({
@@ -334,7 +291,14 @@ jobs:
}
async function syncIssueLabels(issueNumber, desiredLabels) {
await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config)));
const managedLabels = {
'external-plugin': true,
'awaiting-review': true,
'ready-for-review': true,
'requires-submitter-fixes': true,
'approved': true,
'rejected': true
};
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
@@ -438,49 +402,6 @@ jobs:
PLUGIN_NAME: ${{ steps.parse.outputs.plugin-name }}
with:
script: |
const managedLabels = {
'external-plugin': {
color: 'FEF2C0',
description: 'Public external plugin submission'
},
'awaiting-review': {
color: 'FBCA04',
description: 'Submission is waiting for automated intake validation'
},
'ready-for-review': {
color: '0E8A16',
description: 'Submission passed intake validation and is ready for maintainer review'
},
'requires-submitter-fixes': {
color: 'D93F0B',
description: 'Submission has quality-gate findings that submitter must fix before maintainer review'
},
'approved': {
color: '1D76DB',
description: 'Submission was approved by a maintainer'
},
'rejected': {
color: 'B60205',
description: 'Submission was rejected by a maintainer'
}
};
async function ensureLabel(name, config) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color: config.color,
description: config.description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
async function removeLabel(name) {
try {
await github.rest.issues.removeLabel({
@@ -496,7 +417,6 @@ jobs:
}
}
await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config)));
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -0,0 +1,235 @@
name: External Plugin PR Quality Gates
on:
pull_request_target:
branches: [staged]
paths:
- "plugins/external.json"
types: [opened, synchronize, reopened, edited, ready_for_review]
concurrency:
group: external-plugin-pr-quality-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
jobs:
detect-changed-plugins:
runs-on: ubuntu-latest
outputs:
changed-plugins: ${{ steps.detect.outputs.changed-plugins }}
changed-count: ${{ steps.detect.outputs.changed-count }}
should-run: ${{ steps.detect.outputs.should-run }}
steps:
- name: Detect changed external plugins
id: detect
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const filePath = 'plugins/external.json';
const baseRef = context.payload.pull_request.base.sha;
const headRef = context.payload.pull_request.head.sha;
function normalizePath(value) {
if (!value || value === '/') {
return '';
}
return String(value).trim().replace(/^\/+|\/+$/g, '').toLowerCase();
}
function toIdentity(plugin) {
return [
String(plugin?.name ?? '').trim().toLowerCase(),
String(plugin?.source?.repo ?? '').trim().toLowerCase(),
normalizePath(plugin?.source?.path),
].join('|');
}
async function readExternalJson(ref) {
const response = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: filePath,
ref,
});
const encoded = response.data?.content ?? '';
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
return JSON.parse(decoded);
}
const basePlugins = await readExternalJson(baseRef);
const headPlugins = await readExternalJson(headRef);
const baseByIdentity = new Map(basePlugins.map((plugin) => [toIdentity(plugin), plugin]));
const changedPlugins = headPlugins.filter((plugin) => {
const identity = toIdentity(plugin);
const basePlugin = baseByIdentity.get(identity);
return !basePlugin || JSON.stringify(basePlugin) !== JSON.stringify(plugin);
});
core.setOutput('changed-plugins', JSON.stringify(changedPlugins));
core.setOutput('changed-count', String(changedPlugins.length));
core.setOutput('should-run', changedPlugins.length > 0 ? 'true' : 'false');
run-quality-gates:
runs-on: ubuntu-latest
needs: detect-changed-plugins
if: needs.detect-changed-plugins.outputs.should-run == 'true'
outputs:
quality-result: ${{ steps.quality.outputs.quality-result }}
steps:
- name: Checkout staged branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: staged
persist-credentials: false
submodules: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- name: Install GitHub Copilot CLI
run: npm install -g @github/copilot
- name: Run external plugin PR quality gates
id: quality
env:
CHANGED_PLUGINS_JSON: ${{ needs.detect-changed-plugins.outputs.changed-plugins }}
run: |
result=$(node ./eng/external-plugin-pr-quality-gates.mjs --plugins-json "$CHANGED_PLUGINS_JSON")
{
echo 'quality-result<<EOF'
echo "$result"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
sync-pr-state:
runs-on: ubuntu-latest
needs: [detect-changed-plugins, run-quality-gates]
if: always()
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout staged branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: staged
- name: Sync labels and PR status comment
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
env:
DETECT_JOB_RESULT: ${{ needs.detect-changed-plugins.result }}
SHOULD_RUN: ${{ needs.detect-changed-plugins.outputs.should-run }}
CHANGED_COUNT: ${{ needs.detect-changed-plugins.outputs.changed-count }}
QUALITY_RESULT_JSON: ${{ needs.run-quality-gates.outputs.quality-result }}
QUALITY_JOB_RESULT: ${{ needs.run-quality-gates.result }}
with:
script: |
const path = require('path');
const { pathToFileURL } = require('url');
const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href);
const marker = '<!-- external-plugin-pr-quality -->';
const detectJobResult = process.env.DETECT_JOB_RESULT;
const shouldRun = process.env.SHOULD_RUN === 'true';
const changedCount = Number.parseInt(process.env.CHANGED_COUNT || '0', 10);
const qualityJobResult = process.env.QUALITY_JOB_RESULT;
let qualityResult = {
overall_status: 'not_run',
failure_class: 'none',
checked_plugins: [],
summary: 'No changed external plugin entries were detected in this PR.',
};
if (detectJobResult === 'failure' || detectJobResult === 'cancelled') {
qualityResult = {
overall_status: 'infra_error',
failure_class: 'infra',
checked_plugins: [],
summary: 'External plugin PR change detection failed unexpectedly. Re-run this workflow.',
};
} else if (shouldRun) {
if (qualityJobResult === 'failure' || qualityJobResult === 'cancelled') {
qualityResult = {
overall_status: 'infra_error',
failure_class: 'infra',
checked_plugins: [],
summary: 'External plugin PR quality checks failed unexpectedly. Re-run this workflow.',
};
} else if (process.env.QUALITY_RESULT_JSON) {
qualityResult = JSON.parse(process.env.QUALITY_RESULT_JSON);
} else {
qualityResult = {
overall_status: 'infra_error',
failure_class: 'infra',
checked_plugins: [],
summary: 'External plugin PR quality checks did not return a result payload.',
};
}
}
const stateLabel = qualityResult.failure_class === 'submitter_fixes'
? 'requires-submitter-fixes'
: qualityResult.overall_status === 'pass' || !shouldRun
? 'ready-for-review'
: 'awaiting-review';
const desiredLabels = new Set(['external-plugin', stateLabel]);
await intakeState.syncExternalPluginIntakeLabels({
github,
owner: context.repo.owner,
repo: context.repo.repo,
issueNumber: context.issue.number,
desiredLabels,
});
const checkedPlugins = Array.isArray(qualityResult.checked_plugins) ? qualityResult.checked_plugins : [];
const header = qualityResult.failure_class === 'submitter_fixes'
? '## ⚠️ External plugin PR checks require submitter fixes'
: qualityResult.overall_status === 'pass' || !shouldRun
? '## ✅ External plugin PR checks passed'
: '## ⚠️ External plugin PR checks need maintainer follow-up';
const rows = checkedPlugins.length > 0
? checkedPlugins.map((entry) => {
const name = String(entry?.name || 'unknown');
const quality = entry?.quality || {};
const sourceUrl = String(entry?.source_tree_url || '');
const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository');
const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator;
return `| ${name} | ${quality.skill_validator_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`;
})
: ['| _none_ | not_run | not_run | not_run | _n/a_ |'];
const body = [
marker,
header,
'',
`- **Changed entries detected:** ${changedCount}`,
`- **Workflow state label:** \`${stateLabel}\``,
'',
'### Per-plugin quality summary',
'',
'| Plugin | skill-validator | install smoke test | overall | source tree |',
'|---|---|---|---|---|',
...rows,
'',
String(qualityResult.summary || '').trim() || '_No summary provided._',
].join('\n');
await intakeState.upsertExternalPluginIntakeComment({
github,
owner: context.repo.owner,
repo: context.repo.repo,
issueNumber: context.issue.number,
marker,
body,
});
@@ -180,34 +180,6 @@ jobs:
PLUGIN_NAME: ${{ steps.parse.outputs.plugin-name }}
with:
script: |
const managedLabels = {
're-review-due': {
color: 'FBCA04',
description: 'Approved external plugin is due for six-month re-review'
},
're-review-follow-up': {
color: 'D4C5F9',
description: 'Six-month re-review needs maintainer follow-up before a final decision'
}
};
async function ensureLabel(name, config) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color: config.color,
description: config.description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config)));
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
@@ -26,37 +26,6 @@ jobs:
const rereview = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-rereview.mjs')).href);
const validation = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-validation.mjs')).href);
const managedLabels = {
[rereview.REREVIEW_LABELS.due]: {
color: 'FBCA04',
description: 'Approved external plugin is due for six-month re-review'
},
[rereview.REREVIEW_LABELS.followUp]: {
color: 'D4C5F9',
description: 'Six-month re-review needs maintainer follow-up before a final decision'
},
[rereview.REREVIEW_LABELS.removed]: {
color: 'B60205',
description: 'External plugin was removed from the marketplace after re-review'
}
};
async function ensureLabel(name, config) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color: config.color,
description: config.description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
async function removeLabel(issueNumber, label) {
try {
await github.rest.issues.removeLabel({
@@ -90,8 +59,6 @@ jobs:
return Math.max(0, Math.floor(Math.abs(diff) / (1000 * 60 * 60 * 24)));
}
await Promise.all(Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config)));
const { plugins, errors } = validation.readExternalPlugins({ policy: 'marketplace' });
if (errors.length > 0) {
core.setFailed(errors.join('\n'));
+20 -64
View File
@@ -20,50 +20,18 @@ jobs:
with:
script: |
const managedLabels = {
'targets-main': {
color: 'B60205',
description: 'PR targets main instead of staged'
},
'branched-main': {
color: 'D93F0B',
description: 'PR appears to include plugin files materialized from main'
},
'skills': {
color: '1D76DB',
description: 'PR touches skills'
},
'plugin': {
color: '5319E7',
description: 'PR touches plugins'
},
'agent': {
color: '0E8A16',
description: 'PR touches agents'
},
'instructions': {
color: 'FBCA04',
description: 'PR touches instructions'
},
'new-submission': {
color: '006B75',
description: 'PR adds at least one new contribution'
},
'website-update': {
color: '0052CC',
description: 'PR touches website content or code'
},
'external-plugin': {
color: 'FEF2C0',
description: 'PR updates plugins/external.json'
},
'hooks': {
color: 'C2E0C6',
description: 'PR touches hooks'
},
'workflow': {
color: 'BFD4F2',
description: 'PR touches workflow automation'
}
'targets-main': true,
'branched-main': true,
'skills': true,
'plugin': true,
'agent': true,
'instructions': true,
'new-submission': true,
'website-update': true,
'external-plugin': true,
'hooks': true,
'workflow': true,
'canvas-extension': true
};
const matchesAny = (filename, patterns) => patterns.some((pattern) => pattern.test(filename));
@@ -91,22 +59,6 @@ jobs:
}
}
async function ensureLabel(name, { color, description }) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color,
description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
const files = await listAllFiles();
const filenames = files.map((file) => file.filename);
@@ -139,12 +91,16 @@ jobs:
/^workflows\/.+\.md$/,
/^\.github\/workflows\/.+\.(?:ya?ml|md)$/
],
canvasExtension: [
/^extensions\/[^/]+\//
],
newSubmission: [
/^agents\/.+\.agent\.md$/,
/^instructions\/.+\.instructions\.md$/,
/^skills\/[^/]+\/SKILL\.md$/,
/^hooks\/[^/]+\/(?:README\.md|hooks\.json)$/,
/^plugins\/[^/]+\/\.github\/plugin\/plugin\.json$/,
/^extensions\/[^/]+\/extension\.mjs$/,
/^workflows\/.+\.md$/,
/^\.github\/workflows\/.+\.(?:ya?ml|md)$/,
/^website\//
@@ -197,15 +153,15 @@ jobs:
desiredLabels.add('workflow');
}
if (filenames.some((filename) => matchesAny(filename, patterns.canvasExtension))) {
desiredLabels.add('canvas-extension');
}
if (hasNewSubmission) {
desiredLabels.add('new-submission');
}
}
await Promise.all(
Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config))
);
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -4,6 +4,8 @@ description: "Daily check for new GitHub Copilot features and updates. Opens a P
on:
schedule: daily
workflow_dispatch:
permissions:
contents: read
tools:
bash: ["curl", "gh"]
edit:
@@ -83,4 +85,4 @@ Create a pull request with your changes, using the `staged` branch as the base b
2. What sections of the guide were updated
3. Links to the source announcements
The PR should target the `staged` branch and include the labels `automated-update` and `copilot-updates`.
The PR should target the `staged` branch and include the labels `automated-update` and `copilot-updates`.
File diff suppressed because one or more lines are too long
+8
View File
@@ -133,6 +133,14 @@ jobs:
"${main_publish_ref}:${LEGACY_PUBLISHED_BRANCH}" \
"${marketplace_publish_ref}:${MARKETPLACE_BRANCH}"
git fetch origin "${LEGACY_PUBLISHED_BRANCH}" "${MARKETPLACE_BRANCH}"
if ! git diff --quiet "origin/${LEGACY_PUBLISHED_BRANCH}" "origin/${MARKETPLACE_BRANCH}"; then
echo "Published branch mismatch detected between ${LEGACY_PUBLISHED_BRANCH} and ${MARKETPLACE_BRANCH}"
git --no-pager diff --stat "origin/${LEGACY_PUBLISHED_BRANCH}" "origin/${MARKETPLACE_BRANCH}"
exit 1
fi
echo "Verified published outputs are in sync across ${LEGACY_PUBLISHED_BRANCH} and ${MARKETPLACE_BRANCH}"
- name: Dispatch website deployment
run: gh workflow run deploy-website.yml --ref "${WEBSITE_DEPLOY_REF}"
env:
File diff suppressed because one or more lines are too long
+148
View File
@@ -0,0 +1,148 @@
name: Setup Repository Labels
on:
workflow_dispatch
permissions:
issues: write
jobs:
setup-labels:
runs-on: ubuntu-latest
steps:
- name: Create or update labels
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const labels = {
// Intent labels for PR categorization
'targets-main': {
color: 'B60205',
description: 'PR targets main instead of staged'
},
'branched-main': {
color: 'D93F0B',
description: 'PR appears to include plugin files materialized from main'
},
'skills': {
color: '1D76DB',
description: 'PR touches skills'
},
'plugin': {
color: '5319E7',
description: 'PR touches plugins'
},
'agent': {
color: '0E8A16',
description: 'PR touches agents'
},
'instructions': {
color: 'FBCA04',
description: 'PR touches instructions'
},
'new-submission': {
color: '006B75',
description: 'PR adds at least one new contribution'
},
'website-update': {
color: '0052CC',
description: 'PR touches website content or code'
},
'external-plugin': {
color: 'FEF2C0',
description: 'Public external plugin submission'
},
'hooks': {
color: 'C2E0C6',
description: 'PR touches hooks'
},
'workflow': {
color: 'BFD4F2',
description: 'PR touches workflow automation'
},
// External plugin intake state labels
'awaiting-review': {
color: 'FBCA04',
description: 'Submission is waiting for automated intake validation'
},
'ready-for-review': {
color: '0E8A16',
description: 'Submission passed intake validation and is ready for maintainer review'
},
'requires-submitter-fixes': {
color: 'D93F0B',
description: 'Submission has quality-gate findings that submitter must fix before maintainer review'
},
'approved': {
color: '1D76DB',
description: 'Submission was approved by a maintainer'
},
'rejected': {
color: 'B60205',
description: 'Submission was rejected by a maintainer'
},
// Re-review labels
'removed': {
color: 'B60205',
description: 'External plugin was removed from the marketplace after re-review'
},
're-review-follow-up': {
color: 'D4C5F9',
description: 'Six-month re-review needs maintainer follow-up before a final decision'
},
'awaiting-approval': {
color: 'FBCA04',
description: 'External plugin awaiting maintainer approval'
}
};
let created = 0;
let updated = 0;
let failed = 0;
for (const [name, config] of Object.entries(labels)) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color: config.color,
description: config.description
});
created++;
core.info(`✓ Created label: ${name}`);
} catch (error) {
if (error.status === 422) {
// Label already exists, try to update it
try {
await github.rest.issues.updateLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color: config.color,
description: config.description
});
updated++;
core.info(`✓ Updated label: ${name}`);
} catch (updateError) {
failed++;
core.error(`✗ Failed to update label ${name}: ${updateError.message}`);
}
} else {
failed++;
core.error(`✗ Failed to create label ${name}: ${error.message}`);
}
}
}
core.info(`
Label setup complete:
- Created: ${created}
- Updated: ${updated}
- Failed: ${failed}
- Total: ${Object.keys(labels).length}
`);
if (failed > 0) {
throw new Error(`Failed to setup ${failed} label(s)`);
}
-20
View File
@@ -42,27 +42,7 @@ jobs:
}
};
async function ensureLabel(name, { color, description }) {
try {
await github.rest.issues.createLabel({
owner: context.repo.owner,
repo: context.repo.repo,
name,
color,
description
});
} catch (error) {
if (error.status !== 422) {
throw error;
}
}
}
async function syncManagedLabels(issueNumber, desiredLabels) {
await Promise.all(
Object.entries(managedLabels).map(([name, config]) => ensureLabel(name, config))
);
const currentLabels = await github.paginate(github.rest.issues.listLabelsOnIssue, {
owner: context.repo.owner,
repo: context.repo.repo,
@@ -0,0 +1,135 @@
name: Validate Canvas Extensions
on:
pull_request:
branches: [staged]
types: [opened, synchronize, reopened]
paths:
- "extensions/**"
permissions:
contents: read
pull-requests: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Validate changed canvas extensions
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const fs = require('fs');
const path = require('path');
// Collect changed extension directories from the PR diff
const { execSync } = require('child_process');
const changedFiles = execSync(
`git diff --name-only origin/${{ github.base_ref }}...HEAD`
).toString().trim().split('\n').filter(Boolean);
const EXTENSIONS_DIR = 'extensions';
const EXTERNAL_ASSETS_DIR = 'external-assets';
const changedExtDirs = new Set();
for (const file of changedFiles) {
const parts = file.split('/');
if (parts[0] === EXTENSIONS_DIR && parts.length >= 2) {
const extName = parts[1];
// Skip the external-assets directory — it's not a canvas extension
// Also skip external.json and other files at extensions root level
if (extName !== EXTERNAL_ASSETS_DIR && !extName.includes('.')) {
changedExtDirs.add(path.join(EXTENSIONS_DIR, extName));
}
}
}
if (changedExtDirs.size === 0) {
console.log('No canvas extension directories changed — skipping validation.');
return;
}
console.log(`Validating ${changedExtDirs.size} extension(s): ${[...changedExtDirs].join(', ')}`);
const errors = [];
for (const extDir of changedExtDirs) {
if (!fs.existsSync(extDir)) {
// Directory was deleted — skip
console.log(`${extDir} no longer exists (deleted?), skipping.`);
continue;
}
const extName = path.basename(extDir);
// Rule 1: must contain extension.mjs
const mainFile = path.join(extDir, 'extension.mjs');
if (!fs.existsSync(mainFile)) {
errors.push(
`**\`${extDir}\`**: missing required \`extension.mjs\`. ` +
`Canvas extensions must have their entry point named \`extension.mjs\`.`
);
}
// Rule 2: must contain assets/preview.png
const previewFile = path.join(extDir, 'assets', 'preview.png');
if (!fs.existsSync(previewFile)) {
errors.push(
`**\`${extDir}\`**: missing required \`assets/preview.png\`. ` +
`Canvas extensions must include a screenshot at \`assets/preview.png\` ` +
`so reviewers and users can preview the extension before installing it.`
);
}
}
if (errors.length === 0) {
console.log('✅ All changed canvas extensions pass validation.');
return;
}
const isFork = context.payload.pull_request.head.repo.fork;
const body = [
'❌ **Canvas extension validation failed**',
'',
'The following issue(s) were found in changed canvas extension(s):',
'',
...errors.map(e => `- ${e}`),
'',
'---',
'',
'### Required structure for canvas extensions',
'',
'Each extension folder under `extensions/` must contain:',
'',
'| Path | Required | Description |',
'|------|----------|-------------|',
'| `extension.mjs` | ✅ | Entry point for the canvas extension |',
'| `assets/preview.png` | ✅ | Screenshot shown on the website and in the marketplace |',
'',
'Please add the missing file(s) and push an update to this PR.',
].join('\n');
if (!isFork) {
try {
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
event: 'REQUEST_CHANGES',
body
});
} catch (error) {
core.warning(`Could not post PR review: ${error.message}`);
core.warning(body);
}
} else {
core.warning('PR is from a fork — skipping createReview to avoid permission errors.');
core.warning(body);
}
core.setFailed(`Canvas extension validation failed with ${errors.length} error(s).`);