[object Object]

← 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

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 →