diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 9c19e6d0..3ab0d3ed 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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): --- diff --git a/.github/workflows/label-pr-intent.yml b/.github/workflows/label-pr-intent.yml index 20de12ad..825fbf2a 100644 --- a/.github/workflows/label-pr-intent.yml +++ b/.github/workflows/label-pr-intent.yml @@ -63,6 +63,10 @@ jobs: 'workflow': { color: 'BFD4F2', description: 'PR touches workflow automation' + }, + 'canvas-extension': { + color: 'E4B9FF', + description: 'PR touches canvas extensions' } }; @@ -139,12 +143,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,6 +205,10 @@ jobs: desiredLabels.add('workflow'); } + if (filenames.some((filename) => matchesAny(filename, patterns.canvasExtension))) { + desiredLabels.add('canvas-extension'); + } + if (hasNewSubmission) { desiredLabels.add('new-submission'); } diff --git a/.github/workflows/validate-canvas-extensions.yml b/.github/workflows/validate-canvas-extensions.yml new file mode 100644 index 00000000..4699b04b --- /dev/null +++ b/.github/workflows/validate-canvas-extensions.yml @@ -0,0 +1,134 @@ +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 + if (extName !== EXTERNAL_ASSETS_DIR) { + 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).`);