Repository navigation
374 lines (316 loc) · 15.8 KB
/
Copy pathvalidate.yml
File metadata and controls
374 lines (316 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
name: Validate
on:
pull_request:
branches: [main]
# edited: re-check when the PR title changes (version-bump-check).
types: [opened, synchronize, reopened, edited]
push:
branches: [main]
permissions: {}
jobs:
validate-registry:
permissions:
contents: read
name: Validate registry.json
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Validate JSON syntax
run: python3 -c "import json; json.load(open('registry.json'))"
- name: Validate registry schema
run: python3 scripts/validate_registry.py
validate-docs:
permissions:
contents: read
name: Validate docs site
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check docs/index.html exists
run: test -f docs/index.html
validate-scaffold:
permissions:
contents: read
name: Validate scaffold
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt -r requirements-test.txt
- name: Check scaffold syntax
run: python3 -m py_compile scaffold/create-tool.py scaffold/generator.py
- name: Test scaffold dry run
# --no-register: this is a throwaway probe; it must not mutate the
# checkout's registry.json (registration is covered hermetically by
# tests/test_scaffold_born_green.py against a temp registry root).
run: |
python3 scaffold/create-tool.py \
--name "CI Test Plugin" \
--description "Automated test" \
--mcp-server \
--skills 2 \
--rules 1 \
--no-register \
--output /tmp/scaffold-test
test -f /tmp/scaffold-test/ci-test-plugin/.cursor-plugin/plugin.json
test -f /tmp/scaffold-test/ci-test-plugin/.github/workflows/validate.yml
test -f /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml
test -f /tmp/scaffold-test/ci-test-plugin/.github/workflows/pages.yml
test -f /tmp/scaffold-test/ci-test-plugin/.github/workflows/stale.yml
test -f /tmp/scaffold-test/ci-test-plugin/.github/workflows/drift-check.yml
test -f /tmp/scaffold-test/ci-test-plugin/.github/workflows/label-sync.yml
test -f /tmp/scaffold-test/ci-test-plugin/.github/dependabot.yml
test -f /tmp/scaffold-test/ci-test-plugin/site.json
test -f /tmp/scaffold-test/ci-test-plugin/mcp-tools.json
test -f /tmp/scaffold-test/ci-test-plugin/mcp-server/server.py
echo "Scaffold test passed"
python3 scaffold/create-tool.py \
--name "CI Deployed Service" \
--description "Automated deployed-service test" \
--type mcp-server \
--deployed-service \
--no-register \
--output /tmp/scaffold-service
test -f /tmp/scaffold-service/ci-deployed-service/.drift-check.json
test ! -f /tmp/scaffold-service/ci-deployed-service/.github/workflows/publish.yml
test -f /tmp/scaffold-service/ci-deployed-service/.github/workflows/release.yml
grep -q '"private": true' /tmp/scaffold-service/ci-deployed-service/package.json
grep -qv 'gh workflow run publish.yml' /tmp/scaffold-service/ci-deployed-service/.github/workflows/release.yml
echo "Deployed-service scaffold test passed"
python3 scaffold/create-tool.py \
--name "CI Npm Server" \
--description "Automated npm mcp-server test" \
--type mcp-server \
--no-register \
--output /tmp/scaffold-npm
- uses: actions/setup-node@v6
with:
node-version: 22
# Born-green clause C7: the generated repo's own CI commands pass on
# the untouched generated tree.
- name: Generated mcp-server builds and tests green
working-directory: /tmp/scaffold-npm/ci-npm-server
run: |
npm install --no-audit --no-fund
npm run build
npm test
test -f dist/index.js
- name: Scaffold YAML well-formed
run: |
# Catches Jinja2 trim_blocks bugs that collapse adjacent expression lines.
python3 -c "
import yaml, sys
paths = [
'/tmp/scaffold-test/ci-test-plugin/.github/workflows/validate.yml',
'/tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml',
'/tmp/scaffold-test/ci-test-plugin/.github/workflows/pages.yml',
'/tmp/scaffold-test/ci-test-plugin/.github/workflows/stale.yml',
'/tmp/scaffold-test/ci-test-plugin/.github/workflows/drift-check.yml',
'/tmp/scaffold-test/ci-test-plugin/.github/workflows/label-sync.yml',
'/tmp/scaffold-test/ci-test-plugin/.github/dependabot.yml',
]
fail = False
for p in paths:
try:
with open(p) as f:
yaml.safe_load(f)
print(f'OK {p}')
except yaml.YAMLError as e:
print(f'FAIL {p}: {e}', file=sys.stderr)
fail = True
sys.exit(1 if fail else 0)
"
- name: Scaffold regression checks for DTD#41 patterns
run: |
# Meta action pins are DERIVED from the meta VERSION at generation
# time, so assert against the derived train rather than a hardcoded
# literal (which would itself go stale on the next meta release).
VERSION=$(cat VERSION)
IFS='.' read -r META_MAJOR META_MINOR META_PATCH <<< "$VERSION"
# validate-counts job present
grep -q 'validate-counts' /tmp/scaffold-test/ci-test-plugin/.github/workflows/validate.yml \
|| { echo "::error::validate.yml missing validate-counts job"; exit 1; }
# drift-check pinned to @vMAJOR.MINOR (derived from meta VERSION)
grep -q "drift-check@v${META_MAJOR}.${META_MINOR}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/drift-check.yml \
|| { echo "::error::drift-check.yml not pinned to derived @v${META_MAJOR}.${META_MINOR}"; exit 1; }
# release.yml consumes release-doc-sync@vMAJOR (derived from meta VERSION)
grep -q "release-doc-sync@v${META_MAJOR}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \
|| { echo "::error::release.yml does not consume derived release-doc-sync@v${META_MAJOR}"; exit 1; }
# release.yml pins release-doc-sync meta-repo-ref to the full meta release tag
grep -q "meta-repo-ref: v${META_MAJOR}.${META_MINOR}.${META_PATCH}" /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \
|| { echo "::error::release.yml meta-repo-ref not pinned to derived v${META_MAJOR}.${META_MINOR}.${META_PATCH}"; exit 1; }
# release.yml has the initial-release version-handling branch
grep -q 'Initial release' /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \
|| { echo "::error::release.yml missing initial-release branch"; exit 1; }
# release.yml does floating tag automation
grep -q 'tag -f "v\$major"' /tmp/scaffold-test/ci-test-plugin/.github/workflows/release.yml \
|| { echo "::error::release.yml missing floating major tag automation"; exit 1; }
# label-sync uses self-healing per-label gh label create --force
grep -q 'gh label create.*--force' /tmp/scaffold-test/ci-test-plugin/.github/workflows/label-sync.yml \
|| { echo "::error::label-sync.yml missing self-healing label create"; exit 1; }
# dependabot has github-actions ecosystem
grep -q 'package-ecosystem: "github-actions"' /tmp/scaffold-test/ci-test-plugin/.github/dependabot.yml \
|| { echo "::error::dependabot.yml missing github-actions ecosystem"; exit 1; }
# CLAUDE.md has **Version:** line so release-doc-sync can update it
grep -q '^\*\*Version:\*\* 0\.1\.0' /tmp/scaffold-test/ci-test-plugin/CLAUDE.md \
|| { echo "::error::CLAUDE.md missing **Version:** 0.1.0 line"; exit 1; }
# ROADMAP.md has standards-version marker and **Current:** line
grep -q '<!-- standards-version:' /tmp/scaffold-test/ci-test-plugin/ROADMAP.md \
|| { echo "::error::ROADMAP.md missing standards-version marker"; exit 1; }
grep -q '^\*\*Current:\*\* v0\.1\.0' /tmp/scaffold-test/ci-test-plugin/ROADMAP.md \
|| { echo "::error::ROADMAP.md missing **Current:** v0.1.0 line"; exit 1; }
# AGENTS.md keeps standards-version marker
grep -q '<!-- standards-version:' /tmp/scaffold-test/ci-test-plugin/AGENTS.md \
|| { echo "::error::AGENTS.md missing standards-version marker"; exit 1; }
# LICENSE has correct CC-BY-NC-ND-4.0 copyright (TM Hospitality Strategies, not TMHSDigital).
# The year is DERIVED from the current UTC year at generation time, so assert against
# that rather than a hardcoded literal (which would itself go stale every January 1).
YEAR=$(date -u +%Y)
grep -q "Copyright (c) ${YEAR} TM Hospitality Strategies" /tmp/scaffold-test/ci-test-plugin/LICENSE \
|| { echo "::error::LICENSE missing canonical 'Copyright (c) ${YEAR} TM Hospitality Strategies' line"; exit 1; }
grep -q 'SPDX-License-Identifier: CC-BY-NC-ND-4.0' /tmp/scaffold-test/ci-test-plugin/LICENSE \
|| { echo "::error::LICENSE missing SPDX-License-Identifier"; exit 1; }
# CONTRIBUTING.md has DCO inbound grant section
grep -q 'Developer Certificate of Origin' /tmp/scaffold-test/ci-test-plugin/CONTRIBUTING.md \
|| { echo "::error::CONTRIBUTING.md missing DCO section"; exit 1; }
grep -q 'Signed-off-by' /tmp/scaffold-test/ci-test-plugin/CONTRIBUTING.md \
|| { echo "::error::CONTRIBUTING.md missing Signed-off-by guidance"; exit 1; }
echo "All DTD#41 scaffold regression checks passed"
- name: Scaffold leak scan
run: |
if grep -rnE "tmhospitalitystrateg[i]es" /tmp/scaffold-test; then
echo "::error::Scaffold output contains the old business email. Update scaffold/create-tool.py default."
exit 1
fi
sync-check:
permissions:
contents: read
name: Registry sync check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Check derived artifacts match registry
run: python3 scripts/sync_from_registry.py --check
safety-scan:
permissions:
contents: read
name: Public-repo safety scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: No business email leak
run: |
# Pattern uses a character class so this workflow file does not match itself.
if grep -rnE --include='*.py' --include='*.md' --include='*.j2' --include='*.json' --include='*.html' "tmhospitalitystrateg[i]es" . ; then
echo "::error::Remove the real business email. Use contact@users.noreply.github.com or a placeholder."
exit 1
fi
- name: No local filesystem paths in docs
run: |
if grep -rnE --include='*.md' '(^|[^A-Za-z])[A-Z]:\\' . ; then
echo "::error::Windows drive-letter paths leaked in documentation. Replace with generic /path/to/... or \$VAR."
exit 1
fi
if grep -rnE --include='*.md' '/Users/[A-Za-z0-9_-]+/|/home/[A-Za-z0-9_-]+/' . ; then
echo "::error::Home directory paths leaked in documentation. Replace with generic paths."
exit 1
fi
- name: No unsafe DOM sinks in catalog site
run: |
if grep -rnE --include='*.js' --include='*.html' 'innerHTML|outerHTML|\beval\(|new Function\(' docs/ ; then
echo "::error::Unsafe DOM sink reintroduced in docs/. Use textContent; never eval registry data."
exit 1
fi
- name: No committed secrets
run: |
if git ls-files | grep -E '(^|/)\.env$|secrets\.json$|\.pem$|\.key$|\.p12$' ; then
echo "::error::Credential file committed. Remove and rotate any exposed secret."
exit 1
fi
version-bump-check:
permissions:
contents: read
name: feat/fix commits require VERSION bump
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Verify VERSION bump accompanies feat/fix commits
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# Squash merges use the PR title as the commit subject on main,
# so the title is checked like a commit (DTD#119).
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
# DTD#24: field separator must NOT be NUL (\x00). Bash command
# substitution silently strips NUL bytes (with a warning to
# stderr that nothing inspects), gluing SHA+SUBJECT+BODY into
# one field and breaking the awk parse downstream. Use \x1f
# (Unit Separator) which preserves cleanly through bash. The
# record separator (\x1e Record Separator) was already safe.
COMMITS="$(git log "${BASE_SHA}..${HEAD_SHA}" --pretty=format:'%H%x1f%s%x1f%b%x1e')"
NEEDS_BUMP="false"
if echo "$PR_TITLE" | grep -qF '[skip version]'; then
echo "skip | PR title | $PR_TITLE (opted out with [skip version])"
elif echo "$PR_TITLE" | grep -qiE '^(feat|fix)(\([^)]*\))?!?:'; then
echo "need | PR title | $PR_TITLE"
NEEDS_BUMP="true"
else
echo "ok | PR title | $PR_TITLE"
fi
if [ -z "$COMMITS" ] && [ "$NEEDS_BUMP" = "false" ]; then
echo "No new commits to check."
exit 0
fi
while IFS= read -r -d $'\x1e' record; do
[ -z "$record" ] && continue
SHA="$(echo "$record" | awk -F $'\x1f' '{print $1}')"
SUBJECT="$(echo "$record" | awk -F $'\x1f' '{print $2}')"
BODY="$(echo "$record" | awk -F $'\x1f' '{print $3}')"
# Escape hatch: commit opts out.
if echo "$SUBJECT $BODY" | grep -qF '[skip version]'; then
echo "skip | $SHA | $SUBJECT (opted out with [skip version])"
continue
fi
if echo "$SUBJECT" | grep -qiE '^(feat|fix)(\([^)]*\))?!?:'; then
echo "need | $SHA | $SUBJECT"
NEEDS_BUMP="true"
else
echo "ok | $SHA | $SUBJECT"
fi
done <<< "$COMMITS"
if [ "$NEEDS_BUMP" = "false" ]; then
echo "No feat: or fix: commits in this PR. VERSION bump not required."
exit 0
fi
if git diff --name-only "${BASE_SHA}..${HEAD_SHA}" | grep -qxF 'VERSION'; then
echo "VERSION was modified in this PR. Check passed."
exit 0
fi
echo "::error::feat: or fix: commits require a VERSION bump. Update VERSION and commit before merging. Use '[skip version]' in the commit subject or body to opt out for workflow-only changes."
exit 1
test-drift-check:
permissions:
contents: read
name: Run drift_check tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Install test dependencies
run: pip install -r requirements-test.txt
- name: Run drift_check tests
run: pytest tests/ -v