Skip to content

feat: add reusable PR risk classifier#4

Merged
topcoder1 merged 1 commit into
mainfrom
feat/pr-classify
Apr 30, 2026
Merged

feat: add reusable PR risk classifier#4
topcoder1 merged 1 commit into
mainfrom
feat/pr-classify

Conversation

@topcoder1

Copy link
Copy Markdown
Owner

Summary

New reusable workflow `pr-classify.yml` + classifier script `classify.mjs`. Caller repos point at `topcoder1/ci-workflows/.github/workflows/pr-classify.yml@main`, ship a `.github/risk-paths.yml` with their own path rules, and consume the resulting `outputs.risk_class`.

Architecture

  • Central engine (this PR): the workflow + classify.mjs. Stable contract.
  • Per-repo rules: each consumer's `.github/risk-paths.yml`. Project-specific because risk paths don't generalize across repos.
  • The caller checks out their own repo; the workflow fetches `classify.mjs` from `topcoder1/ci-workflows` at runtime via `gh api` so callers don't vendor the script.

Smoke test (local)

15/15 tests pass:

```
Single-file: trivial, standard, sensitive, blocked, safe_test, safe_deps,
multi-class sensitive, multi-class blocked, etc.
Multi-file: highest-priority resolution across {trivial+blocked},
{trivial+safe_test}, {safe_test+sensitive}, {standard+trivial}
```

Caller usage

```yaml
jobs:
classify:
uses: topcoder1/ci-workflows/.github/workflows/pr-classify.yml@main
```

After this lands, downstream consumers reference the output:

```yaml
some-gated-job:
needs: classify
if: needs.classify.outputs.risk_class == 'sensitive'
```

Pairs with

  • Implementation plan: `~/.claude/plans/wxa-jake-ai-2026-04-30-auto-merge-impl.md` §4
  • First consumer: `whois-api-llc/wxa-jake-ai` (separate PR adds the caller workflow + risk-paths.yml)

🤖 Generated with Claude Code

Workflow that reads .github/risk-paths.yml from the caller repo, classifies
the PR's changed files, and exposes the highest-priority risk class as
both a PR label and a job output for downstream auto-merge gating.

Priority (high → low):
  blocked > sensitive > standard > safe_test > safe_deps > safe_config > trivial

`standard` is the implicit fallback for unmatched files — fail-safe.

Includes a Node script (classify.mjs) using yaml@2 + minimatch@10 for
deterministic glob matching. Smoke-tested locally with 15 cases covering
single-file, multi-file, and priority-resolution behavior.

Workflow fetches its own classify.mjs via the GitHub API at runtime so
caller repos don't need to vendor the script. Caller only owns
.github/risk-paths.yml (per-repo rules) plus the caller workflow.

Pairs with the auto-merge implementation plan §4 in:
  ~/.claude/plans/wxa-jake-ai-2026-04-30-auto-merge-impl.md

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
rules = parse(readFileSync(RULES_PATH, 'utf8'));
} catch (e) {
fail(`failed to read ${RULES_PATH}: ${e.message}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: rules is not guarded against null

yaml.parse() returns null for an empty file or one that contains only comments. The try/catch above catches I/O and parse errors, but a valid-but-empty YAML file silently produces null. The first call to rules[cls] in classify() (line 66) then throws an uncaught TypeError: Cannot read properties of null (reading 'blocked'), which exits with code 1 and a stack trace instead of the clean fail() message.

The "Verify risk-paths.yml exists" workflow step only checks for file presence, so this path is reachable.

Suggested change
}
}
if (!rules || typeof rules !== 'object' || Array.isArray(rules)) {
fail(`${RULES_PATH} must be a non-empty YAML mapping (got ${rules === null ? 'null' : typeof rules})`);
}

@claude

claude Bot commented Apr 30, 2026

Copy link
Copy Markdown

Flagged 1 issue inline — null-rules dereference in classify.mjs when risk-paths.yml is empty or comment-only.

@topcoder1
topcoder1 merged commit 52b2690 into main Apr 30, 2026
1 check passed
@topcoder1
topcoder1 deleted the feat/pr-classify branch April 30, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant