Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 56 additions & 11 deletions .github/workflows/cli-skill-review-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ on:
required: false
type: boolean
default: true
cli_paths_include:
description: "Optional include path filters (comma or newline separated globs)"
required: false
type: string
default: ""
cli_paths_exclude:
description: "Optional exclude path filters (comma or newline separated globs)"
required: false
type: string
default: ""
enforce_cli_metadata:
description: "Require knowledge_base/CLI.md metadata when no include filters are provided"
required: false
type: boolean
default: false
secrets:
llm_token:
description: "LLM provider token used by Pi"
Expand All @@ -61,25 +76,55 @@ jobs:
cancel-in-progress: true

steps:
- name: Checkout
- name: Checkout repository under review
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Resolve workflow source repository and ref
id: workflow_source
env:
WORKFLOW_REF: ${{ github.workflow_ref }}
run: |
set -euo pipefail
repo_with_path="${WORKFLOW_REF%@*}"
source_ref="${WORKFLOW_REF##*@}"
source_repo="${repo_with_path%/.github/workflows/*}"

if [ -z "${source_repo}" ] || [ -z "${source_ref}" ]; then
echo "Unable to parse workflow source from WORKFLOW_REF=${WORKFLOW_REF}"
exit 1
fi

echo "repo=${source_repo}" >> "$GITHUB_OUTPUT"
echo "ref=${source_ref}" >> "$GITHUB_OUTPUT"

- name: Checkout workflow source repository
uses: actions/checkout@v4
with:
repository: ${{ steps.workflow_source.outputs.repo }}
ref: ${{ steps.workflow_source.outputs.ref }}
path: .cli-skill-infra
token: ${{ secrets.gh_token }}

- name: Verify internal cli-skill assets
run: |
set -euo pipefail
test -f .cli-skill-infra/scripts/check-cli-scope.py
test -f .cli-skill-infra/scripts/build-pr-report.py
test -d .cli-skill-infra/cli-skill

- name: Check CLI metadata and changed paths
id: cli_scope
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CLI_PATHS_INCLUDE: ${{ inputs.cli_paths_include }}
CLI_PATHS_EXCLUDE: ${{ inputs.cli_paths_exclude }}
ENFORCE_CLI_METADATA: ${{ inputs.enforce_cli_metadata }}
run: |
set -euo pipefail
python3 scripts/check-cli-scope.py

- name: Setup Node
if: ${{ steps.cli_scope.outputs.proceed == 'true' }}
uses: actions/setup-node@v6
with:
node-version: 24
python3 .cli-skill-infra/scripts/check-cli-scope.py

- name: Configure custom LLM provider
if: ${{ steps.cli_scope.outputs.proceed == 'true' }}
Expand Down Expand Up @@ -111,7 +156,7 @@ jobs:
if: ${{ steps.cli_scope.outputs.proceed == 'true' }}
run: |
mkdir -p ~/.pi/skills
cp -R cli-skill ~/.pi/skills/
cp -R .cli-skill-infra/cli-skill ~/.pi/skills/

- name: Run Pi agent
if: ${{ steps.cli_scope.outputs.proceed == 'true' }}
Expand All @@ -130,7 +175,7 @@ jobs:

Scope rules:
- If command is /cli-review, perform only CLI standard compliance checks.
- Use cli-skill/references/cli-standard.md as the sole review standard.
- Use .cli-skill-infra/cli-skill/references/cli-standard.md as the sole review standard.

Repository target path: ${{ inputs.target_path }}

Expand Down Expand Up @@ -163,7 +208,7 @@ jobs:
COMMAND: ${{ inputs.command }}
run: |
set -euo pipefail
python3 scripts/build-pr-report.py
python3 .cli-skill-infra/scripts/build-pr-report.py

- name: Fail when agent run is unsuccessful
if: ${{ steps.cli_scope.outputs.proceed == 'true' && inputs.fail_on_agent_error && steps.pi.outputs.success != 'true' }}
Expand Down
82 changes: 82 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,85 @@ Optional inputs:

- /cli-review is standards compliance only and evaluates against [cli-skill/references/cli-standard.md](cli-skill/references/cli-standard.md).
- Session exports are uploaded as workflow artifacts.

## Use from Another Repository (One-File Setup)

Another repository can consume the reusable workflow by adding a single workflow file. No helper scripts, skill files, or metadata files are required in the consumer repository.

### 1. Add a workflow in the consumer repository

Create a workflow file in the consumer repository (for example, .github/workflows/cli-review.yml):

```yaml
name: CLI Skill Review

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: write

jobs:
cli-review:
uses: shaftoe/cli-skill/.github/workflows/cli-skill-review-reusable.yml@v1
with:
command: /cli-review
provider: openrouter
model: moonshotai/kimi-k2.6
thinking_level: medium
pr_number: ${{ github.event.pull_request.number }}
post_pr_comment: true
fail_on_agent_error: true

# Optional input-based path filters (comma or newline separated globs)
cli_paths_include: |
cmd/**
internal/cli/**
scripts/demo_cli.py
cli_paths_exclude: |
docs/archive/**
**/*.md

# Optional strict mode. When true, knowledge_base/CLI.md is required
# if cli_paths_include is not provided.
enforce_cli_metadata: false
secrets:
llm_token: ${{ secrets.OPENROUTER_API_KEY }}
gh_token: ${{ secrets.GITHUB_TOKEN }}
```

### 2. Configure secrets in the consumer repository

- OPENROUTER_API_KEY

The workflow uses the repository-provided GITHUB_TOKEN for PR APIs.

### 3. Inputs contract

Required inputs:

- provider

Optional inputs:

- command (default: /cli-review)
- target_path (default: .)
- model (default: moonshotai/kimi-k2.6)
- thinking_level (default: medium)
- pr_number
- post_pr_comment (default: true)
- fail_on_agent_error (default: true)
- cli_paths_include (default: empty)
- cli_paths_exclude (default: empty)
- enforce_cli_metadata (default: false)

Required secrets:

- llm_token
- gh_token

### 4. Version pinning

Use a tagged major version (for example, @v1) for a stable interface. Use a commit SHA pin for maximum reproducibility.
8 changes: 4 additions & 4 deletions cli-skill/commands/cli-check-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Analyze the CLI help system only, using canonical help rules.

## Execution Order

1. Run `shared/cli-discovery-preflight.md`
2. Read `cli-skill/references/cli-help.md` in full
3. Read `cli-skill/references/cli-help-examples.md` in full
4. Use parse targets relevant to help generation and command registration
1. Check for the existence of `cli-review/0-cli-discovery-preflight/`. If it does not exist, run `shared/cli-discovery-preflight.md`
2. Use preflight outputs from `cli-review/0-cli-discovery-preflight/`
3. Read `cli-skill/references/cli-help.md` in full
4. Read `cli-skill/references/cli-help-examples.md` in full
5. Evaluate help against `cli-help.md` only

## Scope
Expand Down
103 changes: 82 additions & 21 deletions scripts/check-cli-scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,45 @@ def fail(message: str) -> None:
sys.exit(1)


def find_cli_doc(root: pathlib.Path) -> pathlib.Path:
def write_proceed(output_path: pathlib.Path, proceed: bool) -> None:
with output_path.open("a", encoding="utf-8") as file_handle:
file_handle.write(f"proceed={'true' if proceed else 'false'}\n")


def parse_pattern_list(value: str) -> list[str]:
if not value:
return []

parts = []
for line in value.splitlines():
for item in line.split(","):
stripped = item.strip()
if stripped:
parts.append(stripped)
return parts


def find_cli_doc(root: pathlib.Path, enforce_metadata: bool) -> pathlib.Path | None:
kb_dir = root / "knowledge_base"
if not kb_dir.exists():
if not enforce_metadata:
return None
fail(
"Missing knowledge_base/CLI.md. This workflow only runs for repositories that declare CLI scope metadata. "
"Add knowledge_base/CLI.md. For repositories with a CLI, include a YAML entry like "
"'paths: [cmd/, internal/cli/, docs/cli/]' listing all repo paths that affect CLI behavior, help, or command UX. "
"If the repository has no CLI, declare 'has_cli: false' or include a clear sentence such as 'This repository has no CLI.'"
"Either add knowledge_base/CLI.md (with 'paths: [...]' for CLI repositories or 'has_cli: false' for non-CLI repositories) "
"or provide path filters via CLI_PATHS_INCLUDE."
)

cli_candidates = sorted(
path for path in kb_dir.iterdir() if path.is_file() and path.name.lower() == "cli.md"
)
if not cli_candidates:
if not enforce_metadata:
return None
fail(
"Missing knowledge_base/CLI.md. This workflow only runs for repositories that declare CLI scope metadata. "
"Add knowledge_base/CLI.md. For repositories with a CLI, include a YAML entry like "
"'paths: [cmd/, internal/cli/, docs/cli/]' listing all repo paths that affect CLI behavior, help, or command UX. "
"If the repository has no CLI, declare 'has_cli: false' or include a clear sentence such as 'This repository has no CLI.'"
"Either add knowledge_base/CLI.md (with 'paths: [...]' for CLI repositories or 'has_cli: false' for non-CLI repositories) "
"or provide path filters via CLI_PATHS_INCLUDE."
)
return cli_candidates[0]

Expand Down Expand Up @@ -87,6 +107,15 @@ def matches(spec: str, changed_file: str) -> bool:
return changed_file == spec or changed_file.startswith(spec + "/")


def select_matching(
changed: list[str], include_specs: list[str], exclude_specs: list[str]
) -> list[str]:
selected = [item for item in changed if any(matches(spec, item) for spec in include_specs)]
if not exclude_specs:
return selected
return [item for item in selected if not any(matches(spec, item) for spec in exclude_specs)]


def changed_files() -> list[str]:
base_sha = os.environ.get("BASE_SHA", "").strip()
head_sha = os.environ.get("HEAD_SHA", "").strip()
Expand Down Expand Up @@ -118,39 +147,71 @@ def main() -> None:
fail("GITHUB_OUTPUT is not set.")
output_path = pathlib.Path(output_env)

cli_doc = find_cli_doc(root)
base_sha = os.environ.get("BASE_SHA", "").strip()
head_sha = os.environ.get("HEAD_SHA", "").strip()
if not base_sha or not head_sha:
write_proceed(output_path, True)
print("Success: no pull request diff context; proceeding by default")
return

include_specs = parse_pattern_list(os.environ.get("CLI_PATHS_INCLUDE", ""))
exclude_specs = parse_pattern_list(os.environ.get("CLI_PATHS_EXCLUDE", ""))
enforce_metadata = os.environ.get("ENFORCE_CLI_METADATA", "false").lower() == "true"
changed = changed_files()

if include_specs:
matched = select_matching(changed, include_specs, exclude_specs)
write_proceed(output_path, bool(matched))
if matched:
print("CLI-relevant changes detected via input path filters:")
for item in matched:
print(f"- {item}")
else:
print("Success: no CLI changes matched input path filters")
return

cli_doc = find_cli_doc(root, enforce_metadata=enforce_metadata)
if cli_doc is None:
write_proceed(output_path, True)
print(
"Success: no CLI metadata found; proceeding by default. "
"Set ENFORCE_CLI_METADATA=true to require knowledge_base/CLI.md."
)
return

text = cli_doc.read_text(encoding="utf-8")
cli_present = has_cli(text)

with output_path.open("a", encoding="utf-8") as file_handle:
if cli_present is False:
file_handle.write("proceed=false\n")
print("Success: no CLI found")
return
if cli_present is False:
write_proceed(output_path, False)
print("Success: no CLI found")
return

paths = parse_paths(text)
if not paths:
if not enforce_metadata:
write_proceed(output_path, True)
print(
"Success: CLI metadata does not declare paths; proceeding by default. "
"Set ENFORCE_CLI_METADATA=true to require a YAML 'paths' entry."
)
return
fail(
f"{cli_doc} does not declare a YAML 'paths' entry. For CLI repositories, add a line like "
"'paths: [cmd/, internal/cli/, docs/cli/]' so the workflow can determine whether a pull request changes CLI-relevant files. "
"If the repository has no CLI, declare 'has_cli: false' or include a clear sentence such as 'This repository has no CLI.'"
)

changed = changed_files()
matched = [item for item in changed if any(matches(spec, item) for spec in paths)]
matched = select_matching(changed, paths, exclude_specs)

with output_path.open("a", encoding="utf-8") as file_handle:
if matched:
file_handle.write("proceed=true\n")
else:
file_handle.write("proceed=false\n")
write_proceed(output_path, bool(matched))

if matched:
print("CLI-relevant changes detected:")
for item in matched:
print(f"- {item}")
else:
print("success: no CLI changes")
print("Success: no CLI changes")


if __name__ == "__main__":
Expand Down
Loading