From 6cb90a051d06f72b6377705502ea8996538c5a8c Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Thu, 13 Aug 2026 19:20:09 +0900 Subject: [PATCH 1/2] ci: scope smoke verify to changed examples on pull requests PRs now run make verify only for example directories touched by the diff, falling back to the full suite when a shared file (Makefile, scripts/, workflow, requirements, docs) changes or no example dir is affected. Non-PR events (push to main, cron, upstream-released, workflow_dispatch) always run the full suite. --- .github/workflows/smoke-test.yml | 57 +++++++++++++++++++++++++++++++- Makefile | 10 ++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 5576313..4a778ca 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -35,6 +35,61 @@ jobs: - name: Checkout uses: actions/checkout@v4 + with: + # Full history so PR-scoped verify can diff against the base commit. + fetch-depth: 0 + + - name: Scope verify to changed examples (pull requests only) + env: + EVENT_NAME: ${{ github.event_name }} + PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + # Non-PR events (push to main, cron, upstream release, manual dispatch) + # always run the full suite: a dependency bump or scheduled run must + # exercise every example, not just a diff. + if [ "$EVENT_NAME" != "pull_request" ]; then + echo "VERIFY_PATHS=." >> "$GITHUB_ENV" + echo "Full verify (event=$EVENT_NAME)" + exit 0 + fi + + git fetch --no-tags --depth=1 origin "$PR_BASE_SHA" 2>/dev/null || true + CHANGED=$(git diff --name-only "$PR_BASE_SHA" HEAD) + echo "Changed files:" + printf '%s\n' "$CHANGED" + + # Example roots are directories that own an expected/ folder. + EXROOTS=$(find . -type d -name expected -not -path '*/node_modules/*' \ + | sed 's#/expected$##;s#^\./##' | sort -u) + + SELECTED="" + FALLBACK=0 + while IFS= read -r f; do + [ -z "$f" ] && continue + MATCH="" + for root in $EXROOTS; do + case "$f" in + "$root"/*) MATCH="$root"; break ;; + esac + done + if [ -n "$MATCH" ]; then + case " $SELECTED " in *" $MATCH "*) : ;; *) SELECTED="$SELECTED $MATCH" ;; esac + else + # A file outside every example dir (Makefile, scripts/, workflow, + # shared requirements, docs) can affect any example -> full run. + echo "Shared/global change forces full verify: $f" + FALLBACK=1 + fi + done <<< "$CHANGED" + + if [ "$FALLBACK" -eq 1 ] || [ -z "${SELECTED// /}" ]; then + echo "VERIFY_PATHS=." >> "$GITHUB_ENV" + echo "Full verify (fallback=$FALLBACK, selected='${SELECTED# }')" + else + echo "VERIFY_PATHS=${SELECTED# }" >> "$GITHUB_ENV" + echo "Scoped verify: ${SELECTED# }" + printf '### Scoped smoke: `%s`\n' "${SELECTED# }" >> "$GITHUB_STEP_SUMMARY" + fi - name: Setup Python uses: actions/setup-python@v5 @@ -129,7 +184,7 @@ jobs: PY - name: Run make verify - run: make verify + run: make verify VERIFY_PATHS="${VERIFY_PATHS:-.}" - name: Smoke test cubrid-mcp-server run: python -c "import cubrid_mcp_server; print('cubrid-mcp-server import OK')" diff --git a/Makefile b/Makefile index 9a8eb13..cb5ce79 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,10 @@ DOCKER_COMPOSE := docker compose NORMALIZE := bash scripts/normalize_output.sh PYTHON := python3 +# Search roots for `make verify`. Defaults to the whole tree; CI narrows this to +# only the changed example directories on pull requests (see smoke-test.yml). +VERIFY_PATHS ?= . + help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' @@ -27,10 +31,10 @@ clean: ## Stop and remove all data $(DOCKER_COMPOSE) down -v @echo "✓ Cleaned up all containers and volumes" -verify: ## Verify example outputs against expected results - @echo "Verifying example outputs..." +verify: ## Verify example outputs against expected results (VERIFY_PATHS scopes the search roots) + @echo "Verifying example outputs in: $(VERIFY_PATHS)" @PASS=0; FAIL=0; SKIP=0; \ - for expected in $$(find . -path '*/expected/*.expected' | sort); do \ + for expected in $$(find $(VERIFY_PATHS) -path '*/expected/*.expected' | sort); do \ dir=$$(dirname "$$(dirname "$$expected")"); \ base=$$(basename "$$expected" .expected); \ script="$$dir/$$base.py"; \ From d1318db6337cba8fc9825263ed66295f1807e285 Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Thu, 13 Aug 2026 19:22:55 +0900 Subject: [PATCH 2/2] ci: reject unsafe example paths in scoped verify Validate each selected example root against [A-Za-z0-9._/-]; any path with shell metacharacters, spaces, or newlines forces the full-suite fallback, closing the injection and word-splitting surface from PR-controlled directory names. --- .github/workflows/smoke-test.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke-test.yml b/.github/workflows/smoke-test.yml index 4a778ca..9bc6c71 100644 --- a/.github/workflows/smoke-test.yml +++ b/.github/workflows/smoke-test.yml @@ -73,7 +73,18 @@ jobs: esac done if [ -n "$MATCH" ]; then - case " $SELECTED " in *" $MATCH "*) : ;; *) SELECTED="$SELECTED $MATCH" ;; esac + # Reject any path with characters outside a safe allowlist before it + # reaches $GITHUB_ENV / make / find. This closes the injection and + # word-splitting surface from PR-controlled directory names; the + # allowlist excludes spaces and shell metacharacters, so the + # space-separated `find $(VERIFY_PATHS)` in the Makefile stays safe. + case "$MATCH" in + *[!A-Za-z0-9._/-]*) + echo "Unsafe example path forces full verify: $MATCH" + FALLBACK=1 ;; + *) + case " $SELECTED " in *" $MATCH "*) : ;; *) SELECTED="$SELECTED $MATCH" ;; esac ;; + esac else # A file outside every example dir (Makefile, scripts/, workflow, # shared requirements, docs) can affect any example -> full run.