← back to Cli Printing Press
chore(ci): Gate main CI by changed file scope and add a skill-docs check (#809)
6138e88e7f646aef936f6f1a4523fe9dc7be3c7a · 2026-05-09 10:38:30 -0700 · Trevin Chow
* Gate CI by path for docs and skill updates
* Parse skill frontmatter as YAML
Files touched
A .github/scripts/validate-skill-docs.shM .github/workflows/lint.ymlM .github/workflows/main-ci.ymlM .mergify.yml
Diff
commit 6138e88e7f646aef936f6f1a4523fe9dc7be3c7a
Author: Trevin Chow <trevin@trevinchow.com>
Date: Sat May 9 10:38:30 2026 -0700
chore(ci): Gate main CI by changed file scope and add a skill-docs check (#809)
* Gate CI by path for docs and skill updates
* Parse skill frontmatter as YAML
---
.github/scripts/validate-skill-docs.sh | 47 ++++++++++++++++
.github/workflows/lint.yml | 46 +++++++++++++++-
.github/workflows/main-ci.yml | 99 ++++++++++++++++++++++++++++++++--
.mergify.yml | 2 +
4 files changed, 188 insertions(+), 6 deletions(-)
diff --git a/.github/scripts/validate-skill-docs.sh b/.github/scripts/validate-skill-docs.sh
new file mode 100644
index 00000000..ddfc17cd
--- /dev/null
+++ b/.github/scripts/validate-skill-docs.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+shopt -s nullglob
+
+status=0
+for skill in skills/*/SKILL.md; do
+ if [[ "$(sed -n '1p' "$skill")" != "---" ]]; then
+ echo "::error file=$skill::SKILL.md must start with YAML frontmatter"
+ status=1
+ continue
+ fi
+
+ end_line="$(awk 'NR > 1 && $0 == "---" { print NR; exit }' "$skill")"
+ if [[ -z "$end_line" ]]; then
+ echo "::error file=$skill::SKILL.md frontmatter must close with ---"
+ status=1
+ continue
+ fi
+
+ if ! sed -n "2,$((end_line - 1))p" "$skill" | ruby -ryaml -e '
+ begin
+ frontmatter = YAML.safe_load($stdin.read, permitted_classes: [Symbol], aliases: false)
+ rescue Psych::Exception => e
+ warn "invalid YAML frontmatter: #{e.message}"
+ exit 2
+ end
+
+ unless frontmatter.is_a?(Hash)
+ warn "frontmatter must be a YAML mapping"
+ exit 2
+ end
+
+ %w[name description].each do |field|
+ value = frontmatter[field]
+ if value.nil? || value.to_s.strip.empty?
+ warn "frontmatter must include #{field}"
+ exit 2
+ end
+ end
+ '; then
+ echo "::error file=$skill::SKILL.md frontmatter must be valid YAML with name and description"
+ status=1
+ fi
+done
+
+exit "$status"
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d926fceb..f7fa9ec1 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -87,7 +87,7 @@ jobs:
needs_tests=false
for file in "${changed[@]}"; do
case "$file" in
- .github/workflows/lint.yml|go.mod|go.sum|cmd/*|catalog/*|internal/*|scripts/*|skills/*|docs/SKILLS.md|testdata/*)
+ .github/workflows/lint.yml|go.mod|go.sum|cmd/*|catalog/*|internal/*|scripts/*|testdata/*)
needs_tests=true
break
;;
@@ -181,7 +181,7 @@ jobs:
- name: Skip Go tests
if: needs.test-changes.outputs.needs_tests != 'true'
run: |
- echo "Skipping Go tests: no Go, module, CI workflow, catalog, script, skill, or testdata changes."
+ echo "Skipping Go tests: no Go, module, CI workflow, catalog, script, or testdata changes."
- name: Check test shards
if: needs.test-changes.outputs.needs_tests == 'true'
@@ -320,3 +320,45 @@ jobs:
fi
echo "All generated compile shards passed."
+
+ skill-docs:
+ name: skill-docs
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Check whether skill docs validation is needed
+ id: changes
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ git fetch --no-tags --prune origin "${{ github.base_ref }}"
+ mapfile -t changed < <(git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD")
+
+ echo "Changed files:"
+ printf ' %s\n' "${changed[@]}"
+
+ needs_skill_docs=false
+ for file in "${changed[@]}"; do
+ case "$file" in
+ skills/*|docs/SKILLS.md|.github/scripts/validate-skill-docs.sh|.github/workflows/lint.yml)
+ needs_skill_docs=true
+ break
+ ;;
+ esac
+ done
+
+ echo "needs_skill_docs=$needs_skill_docs" >> "$GITHUB_OUTPUT"
+
+ - name: Skip skill docs validation
+ if: steps.changes.outputs.needs_skill_docs != 'true'
+ run: |
+ echo "Skipping skill docs validation: no skill docs, skill guidance, or skill-doc workflow files changed."
+
+ - name: Validate skill docs
+ if: steps.changes.outputs.needs_skill_docs == 'true'
+ run: bash .github/scripts/validate-skill-docs.sh
diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml
index ce06b3be..44cea1cc 100644
--- a/.github/workflows/main-ci.yml
+++ b/.github/workflows/main-ci.yml
@@ -26,9 +26,88 @@ permissions:
contents: read
jobs:
+ classify:
+ name: classify full-suite scope
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ needs_build: ${{ steps.changes.outputs.needs_build }}
+ needs_tests: ${{ steps.changes.outputs.needs_tests }}
+ needs_golden: ${{ steps.changes.outputs.needs_golden }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Classify changed files
+ id: changes
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ needs_build=false
+ needs_tests=false
+ needs_golden=false
+
+ if [[ "${{ github.event_name }}" == "push" ]]; then
+ # Keep the post-merge alarm full-strength on main. PRs get the
+ # scoped path below so docs-only changes do not burn the suite.
+ needs_build=true
+ needs_tests=true
+ needs_golden=true
+ else
+ git fetch --no-tags --prune origin "${{ github.base_ref }}"
+ mapfile -t changed < <(git diff --name-only --diff-filter=ACMR "origin/${{ github.base_ref }}...HEAD")
+
+ echo "Changed files:"
+ printf ' %s\n' "${changed[@]}"
+
+ for file in "${changed[@]}"; do
+ case "$file" in
+ .github/workflows/main-ci.yml)
+ needs_build=true
+ needs_tests=true
+ needs_golden=true
+ ;;
+ *.go|go.mod|go.sum|cmd/*|catalog/*|internal/*)
+ needs_build=true
+ needs_tests=true
+ ;;
+ esac
+
+ case "$file" in
+ .github/workflows/main-ci.yml|.github/workflows/lint.yml|scripts/*|testdata/*)
+ needs_tests=true
+ ;;
+ esac
+
+ case "$file" in
+ .github/workflows/main-ci.yml|.github/workflows/golden.yml|cmd/*|internal/*|catalog/*|testdata/golden/*|scripts/golden.sh|go.mod|go.sum)
+ needs_golden=true
+ ;;
+ esac
+ done
+ fi
+
+ {
+ echo "needs_build=$needs_build"
+ echo "needs_tests=$needs_tests"
+ echo "needs_golden=$needs_golden"
+ } >> "$GITHUB_OUTPUT"
+
+ {
+ echo "### Full-suite scope"
+ echo
+ echo "- build: \`$needs_build\`"
+ echo "- tests: \`$needs_tests\`"
+ echo "- golden: \`$needs_golden\`"
+ } >> "$GITHUB_STEP_SUMMARY"
+
full-build:
name: full-build
runs-on: ubuntu-latest
+ needs: classify
+ if: needs.classify.outputs.needs_build == 'true'
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
@@ -47,6 +126,8 @@ jobs:
full-test:
name: full-test
runs-on: ubuntu-latest
+ needs: classify
+ if: needs.classify.outputs.needs_tests == 'true'
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
@@ -65,6 +146,8 @@ jobs:
full-golden:
name: full-golden
runs-on: ubuntu-latest
+ needs: classify
+ if: needs.classify.outputs.needs_golden == 'true'
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
@@ -83,6 +166,7 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
needs:
+ - classify
- full-build
- full-test
- full-golden
@@ -92,17 +176,24 @@ jobs:
- name: Check full-suite jobs
shell: bash
run: |
- if [[ "${{ needs.full-build.result }}" != "success" ]]; then
+ if [[ "${{ needs.classify.result }}" != "success" ]]; then
+ echo "change classification failed or was cancelled."
+ exit 1
+ fi
+ if [[ "${{ needs.classify.outputs.needs_build }}" == "true" && "${{ needs.full-build.result }}" != "success" ]]; then
echo "full-build failed or was cancelled."
exit 1
fi
- if [[ "${{ needs.full-test.result }}" != "success" ]]; then
+ if [[ "${{ needs.classify.outputs.needs_tests }}" == "true" && "${{ needs.full-test.result }}" != "success" ]]; then
echo "full-test failed or was cancelled."
exit 1
fi
- if [[ "${{ needs.full-golden.result }}" != "success" ]]; then
+ if [[ "${{ needs.classify.outputs.needs_golden }}" == "true" && "${{ needs.full-golden.result }}" != "success" ]]; then
echo "full-golden failed or was cancelled."
exit 1
fi
- echo "Full build, test, and golden verification passed."
+ echo "Full-suite scope:"
+ echo " build=${{ needs.classify.outputs.needs_build }} result=${{ needs.full-build.result }}"
+ echo " tests=${{ needs.classify.outputs.needs_tests }} result=${{ needs.full-test.result }}"
+ echo " golden=${{ needs.classify.outputs.needs_golden }} result=${{ needs.full-golden.result }}"
diff --git a/.mergify.yml b/.mergify.yml
index 25b4514c..878c30fb 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -8,6 +8,7 @@ queue_rules:
- check-success = go-lint
- check-success = golden
- check-success = pr-title
+ - check-success = skill-docs
- check-success = test
merge_method: squash
update_method: rebase
@@ -38,4 +39,5 @@ merge_protections:
- check-success = go-lint
- check-success = golden
- check-success = pr-title
+ - check-success = skill-docs
- check-success = test
← 296093c4 Document publish PR novel commands and body-file usage (#808
·
back to Cli Printing Press
·
fix(cli): correct HTTP Basic auth env vars (#810) 9bb46da6 →