mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
* ci: add automated PR labeling system Add two independent workflows for PR auto-labeling: - Scope labels via actions/labeler (path glob matching) - Size, risk, and contributor tier via custom shell script Includes idempotent label bootstrap script (create-labels.sh). Co-Authored-By: Claude Opus 4.6 <[email protected]> * ci: temporarily use pull_request trigger for testing Switch to pull_request so workflows run from the PR branch. Will revert to pull_request_target before merge. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(ci): use absolute path for search/issues API call gh api requires a leading slash for REST endpoints. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(ci): use gh pr list instead of search API for contributor count The search/issues API returns 404 with the default GITHUB_TOKEN. gh pr list --state merged works with standard permissions. Co-Authored-By: Claude Opus 4.6 <[email protected]> * ci: revert to pull_request_target for fork PR support Restore pull_request_target trigger and base branch checkout now that testing is complete. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
140 lines
4.8 KiB
Bash
Executable File
140 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Classify a PR by size, risk, and contributor tier.
|
|
# Called by the pr-label-classify workflow.
|
|
#
|
|
# Inputs (env vars):
|
|
# PR_NUMBER — pull request number
|
|
# REPO — owner/repo (e.g. "user/ironclaw")
|
|
#
|
|
# Requires: gh CLI, jq
|
|
|
|
set -euo pipefail
|
|
|
|
PR_NUMBER="${PR_NUMBER:?PR_NUMBER is required}"
|
|
REPO="${REPO:?REPO is required}"
|
|
|
|
# ─── helpers ────────────────────────────────────────────────────────────────
|
|
|
|
# Remove all labels in a dimension except the desired one.
|
|
# Usage: set_exclusive_label "size" "size: M"
|
|
set_exclusive_label() {
|
|
local prefix="$1" desired="$2"
|
|
|
|
# Fetch current labels on the PR
|
|
local current
|
|
current=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')
|
|
|
|
# Remove any existing label with the same prefix
|
|
while IFS= read -r label; do
|
|
[[ -z "$label" ]] && continue
|
|
if [[ "$label" == "${prefix}:"* && "$label" != "$desired" ]]; then
|
|
gh pr edit "$PR_NUMBER" --repo "$REPO" --remove-label "$label" 2>/dev/null || true
|
|
fi
|
|
done <<< "$current"
|
|
|
|
# Add the desired label
|
|
gh pr edit "$PR_NUMBER" --repo "$REPO" --add-label "$desired"
|
|
}
|
|
|
|
# ─── size ───────────────────────────────────────────────────────────────────
|
|
|
|
classify_size() {
|
|
# Sum changed lines across non-doc files
|
|
local total
|
|
total=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" \
|
|
--paginate --jq '
|
|
[.[] | select(.filename | test("\\.(md|txt|rst|adoc)$") | not) | .changes]
|
|
| add // 0
|
|
')
|
|
|
|
local label
|
|
if (( total < 10 )); then label="size: XS"
|
|
elif (( total < 50 )); then label="size: S"
|
|
elif (( total < 200 )); then label="size: M"
|
|
elif (( total < 500 )); then label="size: L"
|
|
else label="size: XL"
|
|
fi
|
|
|
|
echo "Size: ${total} changed lines -> ${label}"
|
|
set_exclusive_label "size" "$label"
|
|
}
|
|
|
|
# ─── risk ───────────────────────────────────────────────────────────────────
|
|
|
|
classify_risk() {
|
|
# If "risk: manual" is present, skip — it's a sticky override
|
|
local current
|
|
current=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json labels --jq '.labels[].name')
|
|
if echo "$current" | grep -qx "risk: manual"; then
|
|
echo "Risk: skipped (manual override)"
|
|
return
|
|
fi
|
|
|
|
# Fetch changed file paths
|
|
local files
|
|
files=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" \
|
|
--paginate --jq '.[].filename')
|
|
|
|
local risk="low"
|
|
|
|
while IFS= read -r file; do
|
|
[[ -z "$file" ]] && continue
|
|
|
|
case "$file" in
|
|
# High risk: safety, secrets, auth, crypto, setup, orchestrator auth
|
|
src/safety/*|src/secrets/*|src/llm/session.rs|src/orchestrator/auth.rs|\
|
|
src/channels/web/auth.rs|src/setup/*)
|
|
risk="high"
|
|
break # can't go higher
|
|
;;
|
|
|
|
# Medium risk: agent core, config, database, worker, tools, channels
|
|
src/agent/*|src/config.rs|src/settings.rs|src/db/*|src/worker/*|\
|
|
src/tools/*|src/channels/*|src/orchestrator/*|src/context/*|\
|
|
src/hooks/*|src/sandbox/*|src/extensions/*|Cargo.toml|\
|
|
.github/workflows/*)
|
|
# Only upgrade, never downgrade
|
|
[[ "$risk" != "high" ]] && risk="medium"
|
|
;;
|
|
|
|
# Low risk: docs, tests, estimation, evaluation, history, etc.
|
|
*)
|
|
;;
|
|
esac
|
|
done <<< "$files"
|
|
|
|
echo "Risk: ${risk}"
|
|
set_exclusive_label "risk" "risk: ${risk}"
|
|
}
|
|
|
|
# ─── contributor tier ───────────────────────────────────────────────────────
|
|
|
|
classify_contributor() {
|
|
# Get PR author
|
|
local author
|
|
author=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json author --jq '.author.login')
|
|
|
|
# Count merged PRs by this author in this repo
|
|
local count
|
|
count=$(gh pr list --repo "$REPO" --state merged --author "$author" \
|
|
--limit 100 --json number --jq 'length')
|
|
|
|
local label
|
|
if (( count == 0 )); then label="contributor: new"
|
|
elif (( count < 6 )); then label="contributor: regular"
|
|
elif (( count < 20 )); then label="contributor: experienced"
|
|
else label="contributor: core"
|
|
fi
|
|
|
|
echo "Contributor: ${author} has ${count} merged PRs -> ${label}"
|
|
set_exclusive_label "contributor" "$label"
|
|
}
|
|
|
|
# ─── main ───────────────────────────────────────────────────────────────────
|
|
|
|
echo "Classifying PR #${PR_NUMBER} in ${REPO}..."
|
|
classify_size
|
|
classify_risk
|
|
classify_contributor
|
|
echo "Done."
|