diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/SKILL.md b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/SKILL.md new file mode 100644 index 0000000..303716c --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/SKILL.md @@ -0,0 +1,14 @@ +--- +name: dependabot-cleanup +description: Validate, merge, and clean up open Dependabot PRs on Argos (Angular upgrades excluded). Use when asked to go through, merge, or clean up Dependabot PRs. +--- + +Run the scripts from the workspace root. Don't write gh, git, npm or cargo commands yourself, and read a log only when a step fails. + +1. `bash scripts/list.sh` lists the PRs. +2. Run `bash scripts/validate.sh ` for each PR, in the background. Show the user a table of the results. +3. After the user approves, run `bash scripts/merge.sh ...`. +4. If a PR is blocked by CI, run `bash scripts/ci.sh `. + - If develop has the same failure, draft a ticket (`create-ticket`) and fix it first. + - Once develop has the fix, run `bash scripts/rebase.sh `, then `merge.sh` again. +5. Ask the user which PRs to clean up, then run `bash scripts/cleanup.sh ...`. diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/ci.sh b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/ci.sh new file mode 100755 index 0000000..311c7c7 --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/ci.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# ci.sh : why a PR's CI is red. Prints each failing check, the step and first error lines of each failing job +# (gates skipped), how far develop has moved since the PR's base, and develop's latest result per +# workflow. A failure develop shares, or one develop has fixed since, is not the PR's. +set -euo pipefail +# shellcheck source=lib.sh +. "$(dirname "$0")/lib.sh" +pr=${1:?usage: ci.sh } +branch=$(pr_branch "$pr") +checks=$(gh pr checks "$pr" -R "$R" --json name,bucket,link -q '.[] | select(.bucket == "fail") | "\(.name)\t\(.link)"' || true) +[ -n "$checks" ] || { echo "#$pr no failing checks" && exit 0; } +while IFS=$'\t' read -r name link; do echo "FAIL $name $link"; done <<<"$checks" +# gate jobs only repeat another job's result, so show the jobs they gate (the rest if all are gates) +jobs=$(grep -v -- $'-gate\t' <<<"$checks" || true) && jobs=${jobs:-$checks} +while IFS=$'\t' read -r name link; do + echo "--- $name" + job=${link##*/} + gh run view -R "$R" --job "$job" --log-failed 2>/dev/null | cut -f3- | sed -E 's/^[0-9T:.-]+Z //' | + awk '/##\[group\]Run / { step = $0; n = 0; next } /##\[error\]/ { print step; for (i = 0; i < n; i++) print m[i]; print; exit } + tolower($0) ~ /error|fail|panick/ && $0 !~ /^npm error *$/ && n < 10 { m[n++] = $0 }' | cut -c1-200 +done <<<"$jobs" +g() { git -C "$DELPHI_REPOS/argos" "$@"; } +g fetch -q origin develop "$branch" +echo "--- develop is $(g rev-list --count "$(g merge-base origin/develop "origin/$branch")..origin/develop") commits past this PR's base" +gh run list -R "$R" -b develop -L 20 --json workflowName,conclusion \ + -q 'unique_by(.workflowName)[] | "develop \(.workflowName): \(.conclusion)"' diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/cleanup.sh b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/cleanup.sh new file mode 100755 index 0000000..b3f2af2 --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/cleanup.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# cleanup.sh ...: for each merged or closed Dependabot PR, remove worktrees/argos/, +# the local branch and the branch on GitHub, then empty folders left under worktrees/argos/. +# Refuses open PRs. Prints one line per PR. +set -euo pipefail +# shellcheck source=lib.sh +. "$(dirname "$0")/lib.sh" +[ $# -gt 0 ] || { echo "usage: cleanup.sh ..." >&2 && exit 2; } +g() { git -C "$DELPHI_REPOS/argos" "$@"; } +for pr in "$@"; do + read -r state branch author < <(gh pr view "$pr" -R "$R" --json state,headRefName,author -q '"\(.state) \(.headRefName) \(.author.login)"') + case $author in *dependabot*) ;; *) echo "#$pr skipped: author is $author" && continue ;; esac + [ "$state" != OPEN ] || { echo "#$pr skipped: still open" && continue; } + wt=$DELPHI_WORKTREES/argos/$branch + [ ! -d "$wt" ] || g worktree remove --force "$wt" + g branch -q -D "$branch" 2>/dev/null || true + gh api -X DELETE "repos/$R/git/refs/heads/$branch" >/dev/null 2>&1 && remote="remote deleted" || remote="no remote branch" + echo "#$pr cleaned $branch ($remote)" +done +find "$DELPHI_WORKTREES/argos/dependabot" -name .DS_Store -delete 2>/dev/null || true +find "$DELPHI_WORKTREES/argos/dependabot" -depth -type d -empty -delete 2>/dev/null || true +g worktree prune && g fetch -q --prune origin diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/lib.sh b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/lib.sh new file mode 100644 index 0000000..df87ac4 --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/lib.sh @@ -0,0 +1,8 @@ +# lib.sh: sourced by dependabot-cleanup's scripts. Sets R (the Argos repo on GitHub), loads the +# workspace layout from $DELPHI_ROOT, and puts each PR's logs in $LOGS//. +# shellcheck shell=bash source=/dev/null +. "${DELPHI_ROOT:?unset: start the session at the workspace root, on Delphi 0.2.0+ (pull its ws/ branch), or source /.delphi/paths.sh}/.delphi/paths.sh" +R=Northeastern-Electric-Racing/Argos +LOGS=${TMPDIR:-/tmp} && export LOGS=${LOGS%/}/argos-dependabot +# pr_branch : the PR's head branch +pr_branch() { gh pr view "$1" -R "$R" --json headRefName -q .headRefName; } diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/list.sh b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/list.sh new file mode 100755 index 0000000..129d9d1 --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/list.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# list.sh: open Dependabot PRs on Argos, Angular upgrades left out (they need their own migration +# work). Prints one line per PR: # . +set -euo pipefail +# shellcheck source=lib.sh +. "$(dirname "$0")/lib.sh" +gh pr list -R "$R" --author app/dependabot --state open --limit 100 --json number,headRefName,title \ + -q '.[] | select((.title + .headRefName) | test("angular"; "i") | not) + | "#\(.number) \(.headRefName | split("/")[1]) \(.headRefName) \(.title)"' diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/merge.sh b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/merge.sh new file mode 100755 index 0000000..573dfc1 --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/merge.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# merge.sh <pr>...: for each Dependabot PR in turn, approve it as you, wait for CI, and merge it +# (merge commit). Waits out GitHub's UNKNOWN mergeability after the previous merge. Prints one line +# per PR: merged, or blocked <why>; exits 1 if any is blocked. +set -euo pipefail +# shellcheck source=lib.sh +. "$(dirname "$0")/lib.sh" +[ $# -gt 0 ] || { echo "usage: merge.sh <pr>..." >&2 && exit 2; } +rc=0 +for pr in "$@"; do + blocked() { echo "#$pr blocked: $1" && rc=1; } + author=$(gh pr view "$pr" -R "$R" --json author -q .author.login) + case $author in *dependabot*) ;; *) blocked "author is $author, not Dependabot" && continue ;; esac + gh pr review "$pr" -R "$R" --approve -b "Validated locally (dependabot-cleanup)." >/dev/null 2>&1 || true + if ! gh pr checks "$pr" -R "$R" --watch --fail-fast >/dev/null 2>&1; then blocked "CI failing (ci.sh $pr)" && continue; fi + state=UNKNOWN + for _ in $(seq 20); do + state=$(gh pr view "$pr" -R "$R" --json mergeable -q .mergeable) + [ "$state" = UNKNOWN ] || break + sleep 5 + done + case $state in + MERGEABLE) if out=$(gh pr merge "$pr" -R "$R" --merge 2>&1); then echo "#$pr merged"; else blocked "${out##*$'\n'}"; fi ;; + CONFLICTING) blocked "conflicts with develop (rebase.sh $pr)" ;; + *) blocked "mergeable still $state" ;; + esac +done +exit $rc diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/rebase.sh b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/rebase.sh new file mode 100755 index 0000000..a83e1a7 --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/rebase.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# rebase.sh <pr>...: ask Dependabot to rebase each PR onto develop, so CI reruns with develop's fixes +# (rerunning a check reuses the old merge commit). +set -euo pipefail +# shellcheck source=lib.sh +. "$(dirname "$0")/lib.sh" +[ $# -gt 0 ] || { echo "usage: rebase.sh <pr>..." >&2 && exit 2; } +for pr in "$@"; do gh pr comment "$pr" -R "$R" -b "@dependabot rebase" >/dev/null && echo "#$pr rebase requested"; done diff --git a/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/validate.sh b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/validate.sh new file mode 100755 index 0000000..0203042 --- /dev/null +++ b/software/application-software/argos/workspaces/argos-dev/.claude/skills/dependabot-cleanup/scripts/validate.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# validate.sh <pr>: check a Dependabot PR locally in worktrees/argos/<its branch> (made or reset to +# the PR's head). The diff may only touch manifests, lockfiles and workflows; then it runs CI's +# checks for each folder whose manifest changed: angular-client (npm ci, build, tests, lint, +# prettier) or scylla-server (build, clippy, unit tests; integration tests need Postgres, so CI runs +# them). Prints one line, PASS or FAIL <step>, with its log; exits 1 on FAIL. +set -euo pipefail +# shellcheck source=lib.sh +. "$(dirname "$0")/lib.sh" +pr=${1:?usage: validate.sh <pr>} +branch=$(pr_branch "$pr") +log=$LOGS/$pr && rm -rf "$log" && mkdir -p "$log" +wt=$(bash "$DELPHI_ROOT/.delphi/new-worktree.sh" argos "$branch") +fail() { echo "#$pr FAIL $1 log=$log${2:+/$2.log}" && exit 1; } +[ -z "$(git -C "$wt" status --porcelain)" ] || fail "worktree has local changes ($wt)" +git -C "$wt" fetch -q origin develop "$branch" && git -C "$wt" reset -q --hard "origin/$branch" # Dependabot force-pushes + +files=$(git -C "$wt" diff --name-only origin/develop...HEAD) +other=$(echo "$files" | grep -Ev '(^|/)(package(-lock)?\.json|Cargo\.(toml|lock))$|^\.github/workflows/' || true) +[ -z "$other" ] || fail "scope: touches $(echo "$other" | paste -sd, -)" + +run() { # run <step> <dir> <cmd...>: log to $log/<step>.log, fail on error + local step=$1 dir=$2 && shift 2 + (cd "$wt/$dir" && "$@") >"$log/$step.log" 2>&1 || fail "$step" "$step" +} +ran="" +for dir in $(echo "$files" | grep -E '(package(-lock)?\.json|Cargo\.(toml|lock))$' | xargs -n1 dirname | sort -u); do + case $dir in + angular-client) + run npm-ci "$dir" npm ci + run ng-build "$dir" npm run build + run ng-test "$dir" npm run test:ci + run ng-lint "$dir" npm run lint -- --max-warnings=0 + run prettier "$dir" npm run prettier-check ;; + scylla-server) + run cargo-build "$dir" cargo build --locked + run clippy "$dir" cargo clippy --all-targets --locked + run cargo-test "$dir" cargo test --locked ;; + *) run "npm-ci-${dir//\//-}" "$dir" npm ci ;; + esac + ran="$ran $dir" +done +echo "#$pr PASS${ran:- scope only (CI checks workflows)} log=$log"