Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
7 changes: 6 additions & 1 deletion .github/workflows/compliance-audit-and-improvement.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ jobs:
repos_with_findings: ${{ steps.audit.outputs.repos_with_findings }}
issues_added: ${{ steps.audit.outputs.issues_added }}
issues_existing: ${{ steps.audit.outputs.issues_existing }}
issues_retriggered: ${{ steps.audit.outputs.issues_retriggered }}
issues_removed: ${{ steps.audit.outputs.issues_removed }}
steps:
- name: Checkout .github repo
Expand Down Expand Up @@ -83,12 +84,14 @@ jobs:
if [ -f "$COUNTS_FILE" ]; then
ISSUES_ADDED=$(jq '.added' "$COUNTS_FILE")
ISSUES_EXISTING=$(jq '.existing' "$COUNTS_FILE")
ISSUES_RETRIGGERED=$(jq '.retriggered // 0' "$COUNTS_FILE")
ISSUES_REMOVED=$(jq '.removed' "$COUNTS_FILE")
else
ISSUES_ADDED=0; ISSUES_EXISTING=0; ISSUES_REMOVED=0
ISSUES_ADDED=0; ISSUES_EXISTING=0; ISSUES_RETRIGGERED=0; ISSUES_REMOVED=0
fi
echo "issues_added=$ISSUES_ADDED" >> "$GITHUB_OUTPUT"
echo "issues_existing=$ISSUES_EXISTING" >> "$GITHUB_OUTPUT"
echo "issues_retriggered=$ISSUES_RETRIGGERED" >> "$GITHUB_OUTPUT"
echo "issues_removed=$ISSUES_REMOVED" >> "$GITHUB_OUTPUT"

- name: Write step summary
Expand Down Expand Up @@ -332,6 +335,7 @@ jobs:
- Repos with findings: ${{ needs.audit.outputs.repos_with_findings }}
- Issues added (new): ${{ needs.audit.outputs.issues_added }}
- Issues existing (updated): ${{ needs.audit.outputs.issues_existing }}
- Issues re-triggered (dev-lead re-engaged on persistent findings): ${{ needs.audit.outputs.issues_retriggered }}
- Issues removed (resolved): ${{ needs.audit.outputs.issues_removed }}

### Health Survey (runtime telemetry)
Expand Down Expand Up @@ -556,6 +560,7 @@ jobs:
|--------|-------|
| Added (new) | ${{ needs.audit.outputs.issues_added }} |
| Existing (updated) | ${{ needs.audit.outputs.issues_existing }} |
| Re-triggered (dev-lead re-engaged) | ${{ needs.audit.outputs.issues_retriggered }} |
| Removed (resolved) | ${{ needs.audit.outputs.issues_removed }} |

Group by compliance issue type — one subsection per distinct check/finding type,
Expand Down
36 changes: 31 additions & 5 deletions scripts/compliance-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ ISSUE_COUNTS_FILE="$REPORT_DIR/issue-counts.json"
ISSUES_ADDED=0
ISSUES_EXISTING=0
ISSUES_REMOVED=0
ISSUES_RETRIGGERED=0

REQUIRED_WORKFLOWS=(ci.yml sonarcloud.yml dev-lead.yml dependabot-automerge.yml dependency-audit.yml agent-shield.yml pr-review-mention.yml)
# Note: codeql.yml is intentionally NOT in REQUIRED_WORKFLOWS. CodeQL is now
Expand Down Expand Up @@ -130,6 +131,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/push-protection.sh
. "$SCRIPT_DIR/lib/push-protection.sh"

# Shared dev-lead retrigger helpers — dl_dev_lead_active() and
# dl_cycle_trigger_label(). Used to re-engage dev-lead on persistent findings.
# shellcheck source=lib/dev-lead-retrigger.sh
. "$SCRIPT_DIR/lib/dev-lead-retrigger.sh"

# ---------------------------------------------------------------------------
# Ecosystem detection
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1214,14 +1220,33 @@ This finding is still open.
**Detail:** $detail

**Standard:** [$standard_ref](https://github.com/$ORG/.github/blob/main/$standard_ref)" 2>/dev/null || update_ok=false
# Ensure claude label is present on pre-existing issues regardless
gh issue edit "$existing" --repo "$ORG/$repo" --add-label "claude" 2>/dev/null || true
if [ "$update_ok" = "true" ]; then
info "Updated existing issue #$existing in $repo for: $check"
ISSUES_EXISTING=$((ISSUES_EXISTING + 1))
else
warn "Failed to update existing issue #$existing in $repo for: $check"
fi

# Re-engage dev-lead on findings that PERSIST across audits.
#
# dev-lead listens on issues:labeled and fires only once per label
# application. The `claude` label is already present on a pre-existing
# issue, so a plain --add-label is a no-op that emits no event and dev-lead
# is never re-triggered. To give it a fresh chance to produce a fix PR we
# cycle the label (remove + re-add) — UNLESS dev-lead is already working
# this issue (open dev-lead PR or `in-progress` label), in which case we
# must not interrupt it and simply ensure the label is present.
if dl_dev_lead_active "$ORG" "$repo" "$existing"; then
info "Existing issue #$existing in $repo — dev-lead already active, not re-triggering"
Comment thread
don-petry marked this conversation as resolved.
elif dl_cycle_trigger_label "$ORG" "$repo" "$existing" "claude" "$DRY_RUN"; then
info "Re-triggered dev-lead on persistent issue #$existing in $repo for: $check"
ISSUES_RETRIGGERED=$((ISSUES_RETRIGGERED + 1))
else
# Cycle failed mid-way — ensure the label is at least present so the issue
# is not left without its trigger label.
gh issue edit "$existing" --repo "$ORG/$repo" --add-label "claude" 2>/dev/null || true
warn "Failed to re-trigger dev-lead on issue #$existing in $repo for: $check"
fi
# Record existing issue for umbrella
jq --null-input \
--arg repo "$repo" \
Expand Down Expand Up @@ -1808,8 +1833,8 @@ main() {

# Write issue-management counts and append to summary (conditional on issue management running)
if [ "$CREATE_ISSUES" = "true" ] && [ "$DRY_RUN" != "true" ]; then
printf '{"added":%d,"existing":%d,"removed":%d}\n' \
"$ISSUES_ADDED" "$ISSUES_EXISTING" "$ISSUES_REMOVED" > "$ISSUE_COUNTS_FILE"
printf '{"added":%d,"existing":%d,"removed":%d,"retriggered":%d}\n' \
"$ISSUES_ADDED" "$ISSUES_EXISTING" "$ISSUES_REMOVED" "$ISSUES_RETRIGGERED" > "$ISSUE_COUNTS_FILE"
cat >> "$SUMMARY_FILE" <<HEREDOC

## Issue Management
Expand All @@ -1818,10 +1843,11 @@ main() {
|--------|-------|
| Added (new) | $ISSUES_ADDED |
| Existing (updated) | $ISSUES_EXISTING |
| Re-triggered (dev-lead re-engaged) | $ISSUES_RETRIGGERED |
| Removed (resolved) | $ISSUES_REMOVED |
HEREDOC
else
printf '{"added":0,"existing":0,"removed":0}\n' > "$ISSUE_COUNTS_FILE"
printf '{"added":0,"existing":0,"removed":0,"retriggered":0}\n' > "$ISSUE_COUNTS_FILE"
cat >> "$SUMMARY_FILE" <<HEREDOC

## Issue Management
Expand Down
59 changes: 27 additions & 32 deletions scripts/compliance-retrigger.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ DRY_RUN="${DRY_RUN:-false}"
AUDIT_LABEL="${AUDIT_LABEL:-compliance-audit}"
TRIGGER_LABEL="${TRIGGER_LABEL:-claude}"

# Shared dev-lead retrigger helpers (dl_dev_lead_active, dl_cycle_trigger_label).
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/dev-lead-retrigger.sh
. "$SCRIPT_DIR/lib/dev-lead-retrigger.sh"

ISSUES_RETRIGGERED=0
ISSUES_SKIPPED=0
WORKFLOWS_DISABLED=0
Expand All @@ -47,7 +52,10 @@ info() { echo "[info] $*"; }
warn() { echo "[warn] $*" >&2; }
error() { echo "[error] $*" >&2; }

gh_api() { gh api "$@"; }
# has_open_pr / cycle_label live in lib/dev-lead-retrigger.sh as
# dl_dev_lead_active() and dl_cycle_trigger_label(), shared with the weekly
# compliance audit so both stay in sync with dev-lead's branch-naming
# convention. Sourced via SCRIPT_DIR resolved at the top of the file.

# stale_cutoff — ISO timestamp N days ago
stale_cutoff() {
Expand All @@ -56,30 +64,6 @@ stale_cutoff() {
print((datetime.now(timezone.utc)-timedelta(days=${STALE_DAYS})).strftime('%Y-%m-%dT%H:%M:%SZ'))"
}

# has_open_pr <repo> <issue_number>
# Returns 0 (true) if there is an open PR with head ref dev-lead/issue-<number>*
has_open_pr() {
local repo="$1" issue="$2"
local count
count=$(gh_api "repos/$ORG/$repo/pulls?state=open" \
--jq "[.[] | select(.head.ref | startswith(\"dev-lead/issue-${issue}\"))] | length" \
2>/dev/null || echo "0")
[ "${count:-0}" -gt 0 ]
}

# cycle_label <repo> <issue>
# Removes and re-adds TRIGGER_LABEL so issues:labeled fires again.
cycle_label() {
local repo="$1" issue="$2"
if [ "$DRY_RUN" = "true" ]; then
info "[dry-run] would cycle '$TRIGGER_LABEL' on $repo#$issue"
return 0
fi
gh api -X DELETE "repos/$ORG/$repo/issues/$issue/labels/$TRIGGER_LABEL" 2>/dev/null || true
gh api -X POST "repos/$ORG/$repo/issues/$issue/labels" \
--field "labels[]=$TRIGGER_LABEL" >/dev/null
}

# ---------------------------------------------------------------------------
# Step 1: Check dev-lead workflow health in fleet repos
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -189,18 +173,29 @@ retrigger_stale_issues() {
continue
fi

# Check if a dev-lead PR already exists for this issue
if has_open_pr "$repo" "$number"; then
info "Skipping $repo#$number — open dev-lead PR already exists"
# Skip if dev-lead is already working this issue (open PR or in-progress).
if dl_dev_lead_active "$ORG" "$repo" "$number"; then
info "Skipping $repo#$number — dev-lead already active (open PR or in-progress)"
ISSUES_SKIPPED=$((ISSUES_SKIPPED + 1))
continue
fi

info "Re-triggering $repo#$number: $title (created $created_at)"
cycle_label "$repo" "$number"
ISSUES_RETRIGGERED=$((ISSUES_RETRIGGERED + 1))
# Brief pause to avoid flooding the API
sleep 1
if dl_cycle_trigger_label "$ORG" "$repo" "$number" "$TRIGGER_LABEL" "$DRY_RUN"; then
ISSUES_RETRIGGERED=$((ISSUES_RETRIGGERED + 1))
else
warn "Failed to re-trigger dev-lead on issue #$number in $repo — attempting to restore label"
# The label may have been deleted but the re-add failed. Restore it so the
# issue remains visible to the next sweep's search query.
if [ "$DRY_RUN" != "true" ]; then
gh api -X POST "repos/$ORG/$repo/issues/$number/labels" \
--field "labels[]=$TRIGGER_LABEL" >/dev/null 2>&1 \
&& info "Restored $TRIGGER_LABEL on $repo#$number" \
|| warn "Could not restore $TRIGGER_LABEL on $repo#$number — issue may drop out of next sweep"
fi
ISSUES_SKIPPED=$((ISSUES_SKIPPED + 1))
fi
# dl_cycle_trigger_label already sleeps 1s internally; no additional pause needed.
done <<< "$issues"

info "Re-trigger complete: ${ISSUES_RETRIGGERED} retriggered, ${ISSUES_SKIPPED} skipped"
Expand Down
101 changes: 101 additions & 0 deletions scripts/lib/dev-lead-retrigger.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# dev-lead-retrigger.sh — shared helpers for re-engaging the dev-lead agent on a
# compliance issue by cycling its trigger label.
#
# Sourced by:
# scripts/compliance-audit.sh — weekly audit; re-triggers findings that
# persist across runs (issue still open).
# scripts/compliance-retrigger.sh — daily sweep; re-triggers stale issues.
#
# Why a shared lib?
# Both entry points need the same two primitives, and both depend on how
# dev-lead names its branches (dev-lead/issue-<number>-<timestamp>, see
# scripts/dev-lead-fix-issue.sh in the agent repo). Keeping that convention in
# one place stops the two callers from drifting apart.
#
# These functions are pure helpers: they take all inputs as arguments and call
# `gh api` directly, so they do not depend on the sourcing script's own wrappers
# or globals.

# dl_dev_lead_active <org> <repo> <issue>
# Returns 0 (true) if dev-lead is ALREADY working this issue — either an open PR
# exists on a dev-lead/issue-<number>-* branch, or the issue carries the
# `in-progress` label. In that case the caller must NOT cycle the trigger label,
# to avoid interrupting or duplicating active work.
dl_dev_lead_active() {
local org="$1" repo="$2" issue="$3"

# Match the dev-lead branch exactly: "dev-lead/issue-<n>-<timestamp>".
# Require the trailing "-" so issue 1 does not match branch "issue-12-...".
# Use --paginate so repos with >100 open PRs are fully scanned.
local pr_count
pr_count=$(gh api "repos/$org/$repo/pulls?state=open&per_page=100" \
--paginate \
--jq "[.[] | select(.head.ref | type == \"string\" and startswith(\"dev-lead/issue-${issue}-\"))] | length" \
2>/dev/null | awk '{s+=$1} END {print s+0}')
Comment thread
don-petry marked this conversation as resolved.
Outdated
[ "${pr_count:-0}" -gt 0 ] && return 0

# An agent currently mid-run marks the issue `in-progress` before it pushes a
# PR — respect that window, but cap it at IN_PROGRESS_MAX_HOURS so a crashed or
# cancelled run cannot block retrigger indefinitely.
local in_progress
in_progress=$(gh api "repos/$org/$repo/issues/$issue" \
--jq '.labels[].name | select(. == "in-progress")' \
2>/dev/null || echo "")
Comment thread
don-petry marked this conversation as resolved.
Outdated

if [ -n "$in_progress" ]; then
local in_progress_max_hours="${IN_PROGRESS_MAX_HOURS:-4}"
local labeled_at
labeled_at=$(gh api "repos/$org/$repo/issues/$issue/events?per_page=100" \
--paginate \
--jq '.[] | select(.event == "labeled" and .label.name == "in-progress") | .created_at' \
2>/dev/null | tail -1 || echo "")

if [ -z "$labeled_at" ] || [ "$labeled_at" = "null" ]; then
# Cannot determine when the label was applied; trust it conservatively.
return 0
fi

local now_epoch labeled_epoch elapsed_hours
now_epoch=$(date -u +%s)
labeled_epoch=$(date -u -d "$labeled_at" +%s 2>/dev/null \
|| python3 -c "import sys, calendar, datetime; \
ts = sys.stdin.read().strip(); \
print(calendar.timegm(datetime.datetime.strptime(ts, '%Y-%m-%dT%H:%M:%SZ').timetuple()))" \
<<< "$labeled_at")
elapsed_hours=$(( (now_epoch - labeled_epoch) / 3600 ))

[ "$elapsed_hours" -le "$in_progress_max_hours" ] && return 0
# in-progress label is stale (agent likely crashed); fall through to return 1.
Comment thread
don-petry marked this conversation as resolved.
Outdated
fi

return 1
}

# dl_cycle_trigger_label <org> <repo> <issue> <label> [dry_run]
# Remove then re-add <label> so GitHub emits a fresh issues:labeled event,
# re-engaging dev-lead. dev-lead fires once per label application, so a
# persistent finding whose label is already present needs the label cycled to be
# picked up again. Returns non-zero if the re-add fails (the caller can fall
# back to ensuring the label is at least present).
dl_cycle_trigger_label() {
local org="$1" repo="$2" issue="$3" label="$4" dry_run="${5:-false}"

if [ "$dry_run" = "true" ]; then
echo "[dry-run] would cycle '$label' on $org/$repo#$issue" >&2
return 0
fi

# URL-encode the label for the REST path segment to handle names that contain
# spaces or other reserved characters.
local encoded_label
encoded_label=$(printf '%s' "$label" \
| python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=''))" 2>/dev/null \
|| printf '%s' "$label" | sed 's/ /%20/g')
gh api -X DELETE "repos/$org/$repo/issues/$issue/labels/$encoded_label" >/dev/null 2>&1 || true
Comment thread
don-petry marked this conversation as resolved.
Outdated
# Let GitHub register the removal before re-adding so the add is not coalesced
# with the delete (which would emit no labeled event).
sleep 1
gh api -X POST "repos/$org/$repo/issues/$issue/labels" \
--field "labels[]=$label" >/dev/null 2>&1 || return 1
}
Loading