Skip to content

Commit 5fb37f6

Browse files
aaronpowellCopilot
andauthored
feat: add canvas extension validation and labeling (#2017)
- Update PR template to include canvas extension as a contribution type - Add 'canvas-extension' label (color: E4B9FF) to label-pr-intent workflow with auto-detection for PRs touching extensions/** - Add new validate-canvas-extensions.yml workflow that checks: - extension.mjs is present in each changed extension folder - assets/preview.png screenshot is present in each changed extension folder - Posts a REQUEST_CHANGES review with a fix guide on failure Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1dd0e39 commit 5fb37f6

3 files changed

Lines changed: 150 additions & 3 deletions

File tree

.github/pull_request_template.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
- [ ] I have read and followed the [CONTRIBUTING.md](https://github.com/github/awesome-copilot/blob/main/CONTRIBUTING.md) guidelines.
44
- [ ] I have read and followed the [Guidance for submissions involving paid services](https://github.com/github/awesome-copilot/discussions/968).
5-
- [ ] My contribution adds a new instruction, prompt, agent, skill, or workflow file in the correct directory.
5+
- [ ] My contribution adds a new instruction, prompt, agent, skill, workflow, or canvas extension file in the correct directory.
66
- [ ] The file follows the required naming convention.
77
- [ ] The content is clearly structured and follows the example format.
8-
- [ ] I have tested my instructions, prompt, agent, skill, or workflow with GitHub Copilot.
8+
- [ ] I have tested my instructions, prompt, agent, skill, workflow, or canvas extension with GitHub Copilot.
99
- [ ] I have run `npm start` and verified that `README.md` is up to date.
1010
- [ ] I am targeting the `staged` branch for this pull request.
1111

@@ -25,7 +25,8 @@
2525
- [ ] New plugin.
2626
- [ ] New skill file.
2727
- [ ] New agentic workflow.
28-
- [ ] Update to existing instruction, prompt, agent, plugin, skill, or workflow.
28+
- [ ] New canvas extension.
29+
- [ ] Update to existing instruction, prompt, agent, plugin, skill, workflow, or canvas extension.
2930
- [ ] Other (please specify):
3031

3132
---

.github/workflows/label-pr-intent.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ jobs:
6363
'workflow': {
6464
color: 'BFD4F2',
6565
description: 'PR touches workflow automation'
66+
},
67+
'canvas-extension': {
68+
color: 'E4B9FF',
69+
description: 'PR touches canvas extensions'
6670
}
6771
};
6872
@@ -139,12 +143,16 @@ jobs:
139143
/^workflows\/.+\.md$/,
140144
/^\.github\/workflows\/.+\.(?:ya?ml|md)$/
141145
],
146+
canvasExtension: [
147+
/^extensions\/[^/]+\//
148+
],
142149
newSubmission: [
143150
/^agents\/.+\.agent\.md$/,
144151
/^instructions\/.+\.instructions\.md$/,
145152
/^skills\/[^/]+\/SKILL\.md$/,
146153
/^hooks\/[^/]+\/(?:README\.md|hooks\.json)$/,
147154
/^plugins\/[^/]+\/\.github\/plugin\/plugin\.json$/,
155+
/^extensions\/[^/]+\/extension\.mjs$/,
148156
/^workflows\/.+\.md$/,
149157
/^\.github\/workflows\/.+\.(?:ya?ml|md)$/,
150158
/^website\//
@@ -197,6 +205,10 @@ jobs:
197205
desiredLabels.add('workflow');
198206
}
199207
208+
if (filenames.some((filename) => matchesAny(filename, patterns.canvasExtension))) {
209+
desiredLabels.add('canvas-extension');
210+
}
211+
200212
if (hasNewSubmission) {
201213
desiredLabels.add('new-submission');
202214
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
name: Validate Canvas Extensions
2+
3+
on:
4+
pull_request:
5+
branches: [staged]
6+
types: [opened, synchronize, reopened]
7+
paths:
8+
- "extensions/**"
9+
10+
permissions:
11+
contents: read
12+
pull-requests: write
13+
14+
jobs:
15+
validate:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Checkout code
19+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Validate changed canvas extensions
24+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
25+
with:
26+
script: |
27+
const fs = require('fs');
28+
const path = require('path');
29+
30+
// Collect changed extension directories from the PR diff
31+
const { execSync } = require('child_process');
32+
const changedFiles = execSync(
33+
`git diff --name-only origin/${{ github.base_ref }}...HEAD`
34+
).toString().trim().split('\n').filter(Boolean);
35+
36+
const EXTENSIONS_DIR = 'extensions';
37+
const EXTERNAL_ASSETS_DIR = 'external-assets';
38+
39+
const changedExtDirs = new Set();
40+
for (const file of changedFiles) {
41+
const parts = file.split('/');
42+
if (parts[0] === EXTENSIONS_DIR && parts.length >= 2) {
43+
const extName = parts[1];
44+
// Skip the external-assets directory — it's not a canvas extension
45+
if (extName !== EXTERNAL_ASSETS_DIR) {
46+
changedExtDirs.add(path.join(EXTENSIONS_DIR, extName));
47+
}
48+
}
49+
}
50+
51+
if (changedExtDirs.size === 0) {
52+
console.log('No canvas extension directories changed — skipping validation.');
53+
return;
54+
}
55+
56+
console.log(`Validating ${changedExtDirs.size} extension(s): ${[...changedExtDirs].join(', ')}`);
57+
58+
const errors = [];
59+
60+
for (const extDir of changedExtDirs) {
61+
if (!fs.existsSync(extDir)) {
62+
// Directory was deleted — skip
63+
console.log(`${extDir} no longer exists (deleted?), skipping.`);
64+
continue;
65+
}
66+
67+
const extName = path.basename(extDir);
68+
69+
// Rule 1: must contain extension.mjs
70+
const mainFile = path.join(extDir, 'extension.mjs');
71+
if (!fs.existsSync(mainFile)) {
72+
errors.push(
73+
`**\`${extDir}\`**: missing required \`extension.mjs\`. ` +
74+
`Canvas extensions must have their entry point named \`extension.mjs\`.`
75+
);
76+
}
77+
78+
// Rule 2: must contain assets/preview.png
79+
const previewFile = path.join(extDir, 'assets', 'preview.png');
80+
if (!fs.existsSync(previewFile)) {
81+
errors.push(
82+
`**\`${extDir}\`**: missing required \`assets/preview.png\`. ` +
83+
`Canvas extensions must include a screenshot at \`assets/preview.png\` ` +
84+
`so reviewers and users can preview the extension before installing it.`
85+
);
86+
}
87+
}
88+
89+
if (errors.length === 0) {
90+
console.log('✅ All changed canvas extensions pass validation.');
91+
return;
92+
}
93+
94+
const isFork = context.payload.pull_request.head.repo.fork;
95+
const body = [
96+
'❌ **Canvas extension validation failed**',
97+
'',
98+
'The following issue(s) were found in changed canvas extension(s):',
99+
'',
100+
...errors.map(e => `- ${e}`),
101+
'',
102+
'---',
103+
'',
104+
'### Required structure for canvas extensions',
105+
'',
106+
'Each extension folder under `extensions/` must contain:',
107+
'',
108+
'| Path | Required | Description |',
109+
'|------|----------|-------------|',
110+
'| `extension.mjs` | ✅ | Entry point for the canvas extension |',
111+
'| `assets/preview.png` | ✅ | Screenshot shown on the website and in the marketplace |',
112+
'',
113+
'Please add the missing file(s) and push an update to this PR.',
114+
].join('\n');
115+
116+
if (!isFork) {
117+
try {
118+
await github.rest.pulls.createReview({
119+
owner: context.repo.owner,
120+
repo: context.repo.repo,
121+
pull_number: context.issue.number,
122+
event: 'REQUEST_CHANGES',
123+
body
124+
});
125+
} catch (error) {
126+
core.warning(`Could not post PR review: ${error.message}`);
127+
core.warning(body);
128+
}
129+
} else {
130+
core.warning('PR is from a fork — skipping createReview to avoid permission errors.');
131+
core.warning(body);
132+
}
133+
134+
core.setFailed(`Canvas extension validation failed with ${errors.length} error(s).`);

0 commit comments

Comments
 (0)