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
4 changes: 2 additions & 2 deletions .github/workflows/reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ name: reusable
#
# jobs:
# evals:
# uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.1
# uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.2
# secrets:
# api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# with:
Expand Down Expand Up @@ -63,7 +63,7 @@ name: reusable
# let a reusable workflow interpolate that pin into `uses: amd/skillscope@...`,
# so each job checks out this repository at `job.workflow_sha` (the commit the
# caller referenced) and runs the composite action from that tree. Pinning
# `@v0.1.1` grades with v0.1.1, `@main` grades with main, and a branch grades
# `@v0.1.2` grades with v0.1.2, `@main` grades with main, and a branch grades
# with that branch -- so there is no release ritual and nothing to keep in step.

on:
Expand Down
30 changes: 13 additions & 17 deletions .github/workflows/skill-evals.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ name: skill-evals
#
# jobs:
# skill-evals:
# uses: amd/skillscope/.github/workflows/skill-evals.yml@v0.1.1
# uses: amd/skillscope/.github/workflows/skill-evals.yml@v0.1.2
# secrets: inherit
# with:
# skill_globs: skills/*
Expand Down Expand Up @@ -62,7 +62,7 @@ name: skill-evals
# let a reusable workflow interpolate that pin into `uses: amd/skillscope@...`,
# so each job checks out this repository at `job.workflow_sha` (the commit the
# caller referenced) and runs the composite action from that tree. Pinning
# `@v0.1.1` grades with v0.1.1, `@main` grades with main, and a branch grades
# `@v0.1.2` grades with v0.1.2, `@main` grades with main, and a branch grades
# with that branch -- so there is no release ritual and nothing to keep in step.

on:
Expand Down Expand Up @@ -273,7 +273,8 @@ jobs:
- name: Check out repository
uses: actions/checkout@v4
with:
# Need the merge base so `git diff` can see what the pull request changed.
# Both commits' full history, so selection can find their merge base
# and diff what the pull request changed from there.
fetch-depth: 0

- name: Check out skillscope
Expand Down Expand Up @@ -302,6 +303,10 @@ jobs:
# it was asked for. Written in Python so the arguments are quoted rather
# than pasted -- a label like `don't merge` would otherwise arrive at the
# CLI in pieces.
#
# Which commits changed what is left to `select --since`, along with
# every other decision: it diffs from the merge base, so a branch whose
# base has moved on is still planned for what the branch did.
- name: Work out how to select
id: how
shell: python
Expand All @@ -323,22 +328,18 @@ jobs:
run: |
import os
import shlex
import subprocess

event = os.environ["EVENT"]
skills = os.environ.get("SKILLS", "").strip()
base, head = os.environ.get("BASE", ""), os.environ.get("HEAD", "")
extended = os.environ["EXTENDED"]

changed = ""
if event == "pull_request" and base:
changed = subprocess.run(
["git", "diff", "--name-only", base, head],
check=True,
capture_output=True,
text=True,
).stdout
args = ["--changed", "--labels", os.environ.get("LABELS", ""), extended]
args = [
"--since", base, head,
"--labels", os.environ.get("LABELS", ""),
extended,
]
if os.environ.get("IGNORE_GATES"):
args.append("--ignore-gates")
else:
Expand All @@ -363,10 +364,6 @@ jobs:
if value:
args += [flag, value]

with open("changed-files.txt", "w", encoding="utf-8") as handle:
handle.write(changed)
print("Changed files:\n" + (changed or "(none)"))

# Quoted rather than pasted: a JSON label array and a pull-request
# label like `don't merge` both have to survive the trip intact.
line = "args=" + " ".join(shlex.quote(a) for a in args)
Expand All @@ -381,7 +378,6 @@ jobs:
command: select
args: ${{ steps.how.outputs.args }}
skills: ${{ inputs.skill_globs }}
stdin: changed-files.txt
python-version: ${{ inputs.python_version }}

- name: Emit the plan
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Simply add an `evals.json` file to your skill and a workflow that points to our
```yaml
jobs:
evals:
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.1
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.2
secrets:
api_key: ${{ secrets.ANTHROPIC_API_KEY }}
with:
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ description: >-

# Pin the tag you want to run, the same way you pin the reusable workflow:
#
# - uses: amd/skillscope@v0.1.1
# - uses: amd/skillscope@v0.1.2
#
# The Python in that checkout is the harness. There is no second version to
# resolve, and nothing here fetches a different ref.
Expand Down
4 changes: 2 additions & 2 deletions bootstrap/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""Run one skillscope command from the composite action's own checkout.

Callers pin a tag on the action or on a reusable workflow in this repo
(``amd/skillscope@v0.1.1``, ``.../reusable.yml@v0.1.1``). This script installs
(``amd/skillscope@v0.1.2``, ``.../reusable.yml@v0.1.2``). This script installs
*that* checkout with ``uvx`` and execs the command. It does not fetch some
other ref: the ``uses:`` pin is the harness.

Expand Down Expand Up @@ -91,7 +91,7 @@ def main() -> int:
if not (source / "pyproject.toml").is_file():
raise SystemExit(
f"error: {source} has no pyproject.toml. The action must run from "
"a skillscope checkout (for example amd/skillscope@v0.1.1)."
"a skillscope checkout (for example amd/skillscope@v0.1.2)."
)
version = packaged_version(source) or "unknown"

Expand Down
23 changes: 18 additions & 5 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ one runner per skill:
```yaml
jobs:
evals:
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.1
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.2
secrets:
api_key: ${{ secrets.ANTHROPIC_API_KEY }}
with:
Expand Down Expand Up @@ -280,7 +280,7 @@ pays for.
```yaml
jobs:
skill-evals:
uses: amd/skillscope/.github/workflows/skill-evals.yml@v0.1.1
uses: amd/skillscope/.github/workflows/skill-evals.yml@v0.1.2
secrets: inherit
with:
routing_room: my-skill,its-neighbour
Expand All @@ -295,7 +295,7 @@ workflow file documents every one.
To run a single command instead of a pipeline, use the action directly:

```yaml
- uses: amd/skillscope@v0.1.1
- uses: amd/skillscope@v0.1.2
with:
command: structural
```
Expand All @@ -304,7 +304,20 @@ Deciding what to run by hand is also possible: `select` emits the plan for a
change as JSON.

```bash
git diff --name-only main HEAD | skillscope select --changed
skillscope select --since main HEAD
```

`--since` takes the two commits a pull request names and works the changed
paths out from their merge base, so a branch is planned for what it changed
rather than for how it differs from a base that has moved on without it.
Without that, everything merged into the base since the branch left it comes
back in the diff, and a base commit touching an [infra
path](#configuring-the-repo-under-test) re-runs the whole catalog.

Pass a list of paths instead when it was worked out some other way:

```bash
git diff --name-only main...HEAD | skillscope select --changed
```

## Versions
Expand All @@ -315,7 +328,7 @@ the tag you want to run:
```yaml
jobs:
evals:
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.1
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.2
```

That tag's checkout is what grades your skills. Bump the ref in that one line
Expand Down
2 changes: 1 addition & 1 deletion examples/amd-skills-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ permissions:
jobs:
evals:
name: AMD Skills Checks
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.1
uses: amd/skillscope/.github/workflows/reusable.yml@v0.1.2
secrets:
api_key: ${{ secrets.ANTHROPIC_API_KEY }}
with:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "skillscope"
version = "0.1.1"
version = "0.1.2"
description = "Routing and behavioral test harness for agent skills."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion skillscope/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@

__all__ = ["__version__"]

__version__ = "0.1.1"
__version__ = "0.1.2"
36 changes: 30 additions & 6 deletions skillscope/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@
skillscope routing --only qwen-on-mi300x --keep-logs eval-logs

# what CI should run for a change
git diff --name-only BASE HEAD | skillscope select --changed
skillscope select --since BASE HEAD

# the same, from a list of paths worked out some other way
git diff --name-only BASE...HEAD | skillscope select --changed

Reports go to stdout as markdown, to ``$GITHUB_STEP_SUMMARY`` under Actions,
and to a JSON artifact under ``.skillscope/runs/`` in the repo under test.
Expand Down Expand Up @@ -228,6 +231,20 @@ def cmd_template(args: argparse.Namespace) -> int:
return 0


def _changed_for_select(args: argparse.Namespace) -> set[str]:
"""The changed paths to plan from, however the caller chose to say them."""
if args.since:
lines = select_module.changed_paths(*args.since)
# The plan is the only thing on stdout, so what it was decided from
# goes to stderr, where a CI log still shows it.
print("Changed files:", file=sys.stderr)
for path in lines or ["(none)"]:
print(f" {path}", file=sys.stderr)
else:
lines = sys.stdin.read().splitlines()
return {line.strip().replace("\\", "/") for line in lines if line.strip()}


def cmd_select(args: argparse.Namespace) -> int:
available = datasets.skills_with_datasets()
if args.all:
Expand All @@ -240,11 +257,7 @@ def cmd_select(args: argparse.Namespace) -> int:
return 1
skills, needs_routing = requested, True
else:
changed = {
line.strip().replace("\\", "/")
for line in sys.stdin.read().splitlines()
if line.strip()
}
changed = _changed_for_select(args)
skills = select_module.select_from_changes(changed)
needs_routing = select_module.routing_needed(changed, args.extended)

Expand Down Expand Up @@ -747,6 +760,17 @@ def build_parser() -> argparse.ArgumentParser:
mode = select_parser.add_mutually_exclusive_group(required=True)
mode.add_argument("--all", action="store_true", help="Every skill with a dataset.")
mode.add_argument("--changed", action="store_true", help="Read changed paths from stdin.")
mode.add_argument(
"--since",
nargs=2,
metavar=("BASE", "HEAD"),
help=(
"The two commits a pull request names. Changed paths are worked "
"out from their merge base, so a branch is planned for what it "
"changed rather than for how it differs from a base that has moved "
"on. Both commits' history has to be in the checkout."
),
)
mode.add_argument(
"--names", metavar="A,B,C", help="An explicit comma-separated skill list."
)
Expand Down
53 changes: 53 additions & 0 deletions skillscope/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

from . import config, datasets
Expand All @@ -65,6 +67,57 @@
EXTENDED_SUFFIX = "/" + datasets.EXTENDED_DATASET_RELPATH.as_posix()


def changed_paths(base: str, head: str) -> list[str]:
"""What a branch changed, given the two commits a pull request names.

``git diff base head`` answers a different question -- how those two trees
differ -- and the difference matters as soon as the base branch moves on
without the branch. Everything merged into the base since the branch left
it comes back in that diff, in reverse, as though this branch had touched
it. One skill's pull request then re-runs its neighbours, and a base commit
that happened to touch an infra path re-runs the entire catalog.

So diff from the merge base, which is the only commit both sides agree on
and so the only one that makes the answer "what did this branch do".

Falls back to the plain diff when there is no common ancestor to be found
-- a clone shallow enough not to contain one, or histories that really are
unrelated -- because selecting too much costs a slow run, and selecting too
little ships an untested change.
"""
root = config.active().root

def git(*args: str) -> subprocess.CompletedProcess:
return subprocess.run(
["git", "-C", str(root), *args],
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)

fork_point = git("merge-base", base, head)
if fork_point.returncode == 0 and fork_point.stdout.strip():
base = fork_point.stdout.strip()
else:
print(
f"warning: no merge base for {base} and {head}, so selection is "
"falling back to the plain diff between them. It may name files "
"this branch never touched.\n"
f"{fork_point.stderr.strip()}",
file=sys.stderr,
)

diff = git("diff", "--name-only", base, head)
if diff.returncode != 0:
raise SystemExit(
f"error: could not diff {base}..{head} in {root}. CI needs the "
"history of both commits to work out what changed, so check out "
"with fetch-depth: 0.\n" + diff.stderr.strip()
)
return [line.strip() for line in diff.stdout.splitlines() if line.strip()]


def infra_paths() -> set[str]:
"""Paths that change the shared engine rather than one skill.

Expand Down
Loading
Loading