Skip to content

Commit 336d745

Browse files
authored
Merge branch 'main' into add-pester-migration-skill
2 parents 56d67f0 + 235bdb1 commit 336d745

48 files changed

Lines changed: 8346 additions & 163 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-website.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ on:
1111
- instructions
1212
- hooks
1313
- workflows
14+
- extensions
15+
- .schemas/canvas.schema.json
1416

1517
permissions:
1618
contents: read

.github/workflows/skill-check.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ jobs:
1818
runs-on: ubuntu-latest
1919
steps:
2020
- name: Checkout code
21-
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
21+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
2222
with:
2323
fetch-depth: 0
2424

2525
- name: Setup Node.js
26-
uses: actions/setup-node@3235b876344febd2b5f2414c5edc3a01b7f10a06 # v4.2.0
26+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
2727
with:
2828
node-version: 20
2929

@@ -155,7 +155,7 @@ jobs:
155155
156156
- name: Upload results
157157
if: always()
158-
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
158+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
159159
with:
160160
name: vally-lint-results
161161
path: vally-results/

.github/workflows/validate-canvas-extensions.yml

Lines changed: 117 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -6,130 +6,133 @@ on:
66
types: [opened, synchronize, reopened]
77
paths:
88
- "extensions/**"
9+
- ".schemas/canvas.schema.json"
910

1011
permissions:
1112
contents: read
12-
pull-requests: write
1313

1414
jobs:
1515
validate:
1616
runs-on: ubuntu-latest
1717
steps:
18-
- name: Checkout code
18+
- name: Checkout
1919
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
2020
with:
2121
fetch-depth: 0
2222

23-
- name: Validate changed canvas extensions
24-
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
23+
- name: Setup Node.js
24+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
2525
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-
// Also skip external.json and other files at extensions root level
46-
if (extName !== EXTERNAL_ASSETS_DIR && !extName.includes('.')) {
47-
changedExtDirs.add(path.join(EXTENSIONS_DIR, extName));
48-
}
49-
}
50-
}
51-
52-
if (changedExtDirs.size === 0) {
53-
console.log('No canvas extension directories changed — skipping validation.');
54-
return;
55-
}
56-
57-
console.log(`Validating ${changedExtDirs.size} extension(s): ${[...changedExtDirs].join(', ')}`);
58-
59-
const errors = [];
60-
61-
for (const extDir of changedExtDirs) {
62-
if (!fs.existsSync(extDir)) {
63-
// Directory was deleted — skip
64-
console.log(`${extDir} no longer exists (deleted?), skipping.`);
65-
continue;
66-
}
67-
68-
const extName = path.basename(extDir);
69-
70-
// Rule 1: must contain extension.mjs
71-
const mainFile = path.join(extDir, 'extension.mjs');
72-
if (!fs.existsSync(mainFile)) {
73-
errors.push(
74-
`**\`${extDir}\`**: missing required \`extension.mjs\`. ` +
75-
`Canvas extensions must have their entry point named \`extension.mjs\`.`
76-
);
77-
}
78-
79-
// Rule 2: must contain assets/preview.png
80-
const previewFile = path.join(extDir, 'assets', 'preview.png');
81-
if (!fs.existsSync(previewFile)) {
82-
errors.push(
83-
`**\`${extDir}\`**: missing required \`assets/preview.png\`. ` +
84-
`Canvas extensions must include a screenshot at \`assets/preview.png\` ` +
85-
`so reviewers and users can preview the extension before installing it.`
86-
);
87-
}
88-
}
89-
90-
if (errors.length === 0) {
91-
console.log('✅ All changed canvas extensions pass validation.');
92-
return;
93-
}
94-
95-
const isFork = context.payload.pull_request.head.repo.fork;
96-
const body = [
97-
'❌ **Canvas extension validation failed**',
98-
'',
99-
'The following issue(s) were found in changed canvas extension(s):',
100-
'',
101-
...errors.map(e => `- ${e}`),
102-
'',
103-
'---',
104-
'',
105-
'### Required structure for canvas extensions',
106-
'',
107-
'Each extension folder under `extensions/` must contain:',
108-
'',
109-
'| Path | Required | Description |',
110-
'|------|----------|-------------|',
111-
'| `extension.mjs` | ✅ | Entry point for the canvas extension |',
112-
'| `assets/preview.png` | ✅ | Screenshot shown on the website and in the marketplace |',
113-
'',
114-
'Please add the missing file(s) and push an update to this PR.',
115-
].join('\n');
116-
117-
if (!isFork) {
118-
try {
119-
await github.rest.pulls.createReview({
120-
owner: context.repo.owner,
121-
repo: context.repo.repo,
122-
pull_number: context.issue.number,
123-
event: 'REQUEST_CHANGES',
124-
body
125-
});
126-
} catch (error) {
127-
core.warning(`Could not post PR review: ${error.message}`);
128-
core.warning(body);
129-
}
130-
} else {
131-
core.warning('PR is from a fork — skipping createReview to avoid permission errors.');
132-
core.warning(body);
133-
}
134-
135-
core.setFailed(`Canvas extension validation failed with ${errors.length} error(s).`);
26+
node-version: "22"
27+
cache: "npm"
28+
29+
- name: Install dependencies
30+
run: npm ci --ignore-scripts
31+
32+
- name: Validate changed canvas extensions
33+
run: |
34+
set -euo pipefail
35+
36+
# Validate schema structure once, even for schema-only PRs.
37+
if ! node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json; then
38+
echo "❌ .schemas/canvas.schema.json failed schema compilation"
39+
exit 1
40+
fi
41+
42+
# Collect changed extension directories.
43+
# Use null-terminated (-z) output from git diff so filenames containing newlines
44+
# or other special characters are read atomically (matches the pattern in skill-check.yml).
45+
# Each extracted name is then validated against a strict allowlist regex before use,
46+
# rejecting anything containing shell metacharacters ($, (, ), spaces, etc.).
47+
declare -A seen_dirs=()
48+
changed_extensions=()
49+
errors=()
50+
51+
while IFS= read -r -d '' file; do
52+
case "$file" in
53+
extensions/*)
54+
ext_name="${file#extensions/}"
55+
ext_name="${ext_name%%/*}"
56+
57+
if [ "$ext_name" = "external-assets" ]; then
58+
continue
59+
fi
60+
61+
# Allowlist: extension directory names must be lowercase alphanumeric + hyphens only.
62+
# Fail for disallowed names so a PR cannot bypass validation by using a nonconforming folder.
63+
if [[ ! "$ext_name" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then
64+
errors+=("\`extensions/$ext_name\`: invalid extension directory name (must match ^[a-z0-9][a-z0-9-]*$).")
65+
continue
66+
fi
67+
68+
if [ -z "${seen_dirs[$ext_name]+x}" ]; then
69+
seen_dirs["$ext_name"]=1
70+
changed_extensions+=("extensions/$ext_name")
71+
fi
72+
;;
73+
esac
74+
done < <(git diff --name-only -z "origin/${{ github.base_ref }}...HEAD")
75+
76+
if [ "${#changed_extensions[@]}" -eq 0 ]; then
77+
if [ "${#errors[@]}" -ne 0 ]; then
78+
echo "❌ Canvas extension validation failed:"
79+
for error in "${errors[@]}"; do
80+
echo "- $error"
81+
done
82+
exit 1
83+
fi
84+
echo "No canvas extension directories changed — skipping validation."
85+
exit 0
86+
fi
87+
88+
echo "Validating ${#changed_extensions[@]} extension(s): ${changed_extensions[*]}"
89+
90+
for ext_dir in "${changed_extensions[@]}"; do
91+
if [ ! -d "$ext_dir" ]; then
92+
echo "$ext_dir no longer exists (deleted?), skipping."
93+
continue
94+
fi
95+
96+
if [ ! -f "$ext_dir/extension.mjs" ]; then
97+
errors+=("\`$ext_dir\`: missing required \`extension.mjs\`.")
98+
fi
99+
100+
if [ ! -f "$ext_dir/canvas.json" ]; then
101+
errors+=("\`$ext_dir\`: missing required \`canvas.json\`.")
102+
continue
103+
fi
104+
105+
if [ ! -f "$ext_dir/assets/preview.png" ]; then
106+
errors+=("\`$ext_dir\`: missing required \`assets/preview.png\`.")
107+
fi
108+
109+
if ! schema_output="$(node ./eng/validate-json-schema.mjs --schema .schemas/canvas.schema.json --data "$ext_dir/canvas.json" 2>&1)"; then
110+
condensed_output="$(echo "$schema_output" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')"
111+
errors+=("\`$ext_dir/canvas.json\`: schema validation failed against \`.schemas/canvas.schema.json\` ($condensed_output).")
112+
continue
113+
fi
114+
115+
mapfile -t screenshot_paths < <(
116+
node -e 'const fs = require("fs"); const manifest = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const paths = [manifest?.screenshots?.icon?.path, manifest?.screenshots?.gallery?.path].filter((value) => typeof value === "string" && value.trim().length > 0); for (const value of [...new Set(paths)]) { console.log(value); }' "$ext_dir/canvas.json"
117+
)
118+
119+
for screenshot_path in "${screenshot_paths[@]}"; do
120+
if [[ ! "$screenshot_path" =~ ^assets/([A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*\.(png|jpe?g|gif|webp|svg)$ ]]; then
121+
errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` is not a valid assets path.")
122+
continue
123+
fi
124+
if [ ! -f "$ext_dir/$screenshot_path" ]; then
125+
errors+=("\`$ext_dir/canvas.json\`: screenshot path \`$screenshot_path\` does not exist in the extension directory.")
126+
fi
127+
done
128+
done
129+
130+
if [ "${#errors[@]}" -ne 0 ]; then
131+
echo "❌ Canvas extension validation failed:"
132+
for error in "${errors[@]}"; do
133+
echo "- $error"
134+
done
135+
exit 1
136+
fi
137+
138+
echo "✅ All changed canvas extensions passed validation."

0 commit comments

Comments
 (0)