- Add scripts/lib/common.sh (ORG/BOT/FORGE_BACKEND, logging, process_running) - discover.sh: dedupe review-requested+assignee PRs; JSON+summary formats - discover.sh: FORGE_BACKEND=forgejo via Forgejo API + stoke token - Fix poll-loop process detection (basename, not install path) - health-check: jq-safe JSON; backend-aware auth (gh or stoke) - install-live: require rsync/jq/tmux; soft-warn missing gh/stoke - self-test.sh offline smoke; restore/poll-once use shared config - Docs: Forgejo table, watcher.env, OPERATIONS troubleshooting
255 lines
7.3 KiB
Bash
Executable file
255 lines
7.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Org-wide discovery of actionable PRs/issues for the bot.
|
|
# Supports FORGE_BACKEND=github (default) or forgejo.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
# shellcheck source=lib/common.sh
|
|
source "${SCRIPT_DIR}/lib/common.sh"
|
|
|
|
FORMAT="${FORMAT:-json}" # json | summary
|
|
LIMIT="${LIMIT:-50}"
|
|
|
|
discover_github() {
|
|
require_cmd gh jq
|
|
|
|
local tmp
|
|
tmp="$(mktemp -d)"
|
|
# shellcheck disable=SC2064
|
|
trap "rm -rf '${tmp}'" EXIT
|
|
|
|
gh search prs --owner "${ORG}" --review-requested=@me --state open \
|
|
--json number,title,url,updatedAt,author,assignees,repository --limit "${LIMIT}" \
|
|
> "${tmp}/pr_rr.json" 2>/dev/null || echo '[]' > "${tmp}/pr_rr.json"
|
|
|
|
gh search prs --owner "${ORG}" --assignee=@me --state open \
|
|
--json number,title,url,updatedAt,author,assignees,repository --limit "${LIMIT}" \
|
|
> "${tmp}/pr_as.json" 2>/dev/null || echo '[]' > "${tmp}/pr_as.json"
|
|
|
|
gh search issues --owner "${ORG}" --assignee=@me --state open \
|
|
--json number,title,url,updatedAt,author,assignees,repository --limit "${LIMIT}" \
|
|
> "${tmp}/issues.json" 2>/dev/null || echo '[]' > "${tmp}/issues.json"
|
|
|
|
# Normalize empty/null files
|
|
for f in pr_rr pr_as issues; do
|
|
if [[ ! -s "${tmp}/${f}.json" ]] || ! jq empty "${tmp}/${f}.json" 2>/dev/null; then
|
|
echo '[]' > "${tmp}/${f}.json"
|
|
fi
|
|
done
|
|
|
|
jq -n \
|
|
--arg org "${ORG}" \
|
|
--arg bot "${BOT_LOGIN}" \
|
|
--slurpfile rr "${tmp}/pr_rr.json" \
|
|
--slurpfile as "${tmp}/pr_as.json" \
|
|
--slurpfile iss "${tmp}/issues.json" \
|
|
'
|
|
def repo_name:
|
|
.repository.nameWithOwner // .repository.full_name // "unknown/unknown";
|
|
def pr_key:
|
|
(repo_name) + "#pr:" + (.number|tostring);
|
|
|
|
($rr[0] // []) as $rr |
|
|
($as[0] // []) as $as |
|
|
($iss[0] // []) as $iss |
|
|
|
|
# set of assignee PR keys
|
|
([$as[] | pr_key] | unique) as $as_keys |
|
|
([$rr[] | pr_key] | unique) as $rr_keys |
|
|
|
|
(
|
|
[
|
|
$rr[] |
|
|
. + {
|
|
kind: "pr",
|
|
role: (if ([pr_key] | inside($as_keys)) then "reviewer+assignee" else "reviewer" end)
|
|
}
|
|
]
|
|
+ [
|
|
$as[] |
|
|
select(([pr_key] | inside($rr_keys)) | not) |
|
|
. + { kind: "pr", role: "assignee" }
|
|
]
|
|
) as $prs |
|
|
|
|
(
|
|
[ $iss[] | . + { kind: "issue", role: "assignee" } ]
|
|
) as $issues |
|
|
|
|
{
|
|
backend: "github",
|
|
org: $org,
|
|
bot: $bot,
|
|
generated_at: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
|
|
counts: {
|
|
prs: ($prs | length),
|
|
issues: ($issues | length),
|
|
total: (($prs | length) + ($issues | length))
|
|
},
|
|
prs: $prs,
|
|
issues: $issues
|
|
}
|
|
'
|
|
}
|
|
|
|
discover_forgejo() {
|
|
require_cmd jq curl
|
|
|
|
local cfg token url tmp page batch n owner repo full
|
|
cfg="${STOKE_CONFIG_FILE:-${XDG_CONFIG_HOME:-$HOME/.config}/stoke/config.json}"
|
|
if [[ ! -f "${cfg}" ]]; then
|
|
echo "error: stoke config not found at ${cfg} (run: stoke auth login)" >&2
|
|
return 1
|
|
fi
|
|
token="$(jq -r '.token // empty' "${cfg}")"
|
|
url="$(jq -r '.url // empty' "${cfg}")"
|
|
url="${url:-$FORGEJO_URL}"
|
|
url="${url%/}"
|
|
if [[ -z "${token}" ]]; then
|
|
echo "error: no token in ${cfg}" >&2
|
|
return 1
|
|
fi
|
|
|
|
tmp="$(mktemp -d)"
|
|
# shellcheck disable=SC2064
|
|
trap "rm -rf '${tmp}'" EXIT
|
|
|
|
api() {
|
|
curl -fsS -H "Authorization: token ${token}" -H "Accept: application/json" "$@"
|
|
}
|
|
|
|
: > "${tmp}/repos.txt"
|
|
page=1
|
|
while (( page <= 40 )); do
|
|
batch="$(api "${url}/api/v1/orgs/${ORG}/repos?limit=50&page=${page}" 2>/dev/null || echo '[]')"
|
|
n="$(echo "${batch}" | jq 'length')"
|
|
(( n == 0 )) && break
|
|
echo "${batch}" | jq -r '.[].full_name // empty' >> "${tmp}/repos.txt"
|
|
(( n < 50 )) && break
|
|
page=$((page + 1))
|
|
done
|
|
|
|
: > "${tmp}/prs.ndjson"
|
|
: > "${tmp}/issues.ndjson"
|
|
|
|
while read -r full; do
|
|
[[ -z "${full}" ]] && continue
|
|
owner="${full%%/*}"
|
|
repo="${full#*/}"
|
|
|
|
api "${url}/api/v1/repos/${owner}/${repo}/pulls?state=open&limit=50" 2>/dev/null \
|
|
| jq -c --arg full "${full}" --arg bot "${BOT_LOGIN}" '
|
|
.[]? |
|
|
(
|
|
((.requested_reviewers // []) | map(.login) | index($bot) != null)
|
|
) as $is_rev |
|
|
(
|
|
(.assignee.login == $bot)
|
|
or (((.assignees // []) | map(.login) | index($bot)) != null)
|
|
) as $is_as |
|
|
select($is_rev or $is_as) |
|
|
{
|
|
kind: "pr",
|
|
number,
|
|
title,
|
|
url: .html_url,
|
|
updatedAt: .updated_at,
|
|
author: (.user.login // null),
|
|
head_sha: (.head.sha // null),
|
|
repository: { nameWithOwner: $full },
|
|
role: (
|
|
if $is_rev and $is_as then "reviewer+assignee"
|
|
elif $is_rev then "reviewer"
|
|
else "assignee"
|
|
end
|
|
)
|
|
}
|
|
' >> "${tmp}/prs.ndjson" 2>/dev/null || true
|
|
|
|
api "${url}/api/v1/repos/${owner}/${repo}/issues?state=open&type=issues&limit=50" 2>/dev/null \
|
|
| jq -c --arg full "${full}" --arg bot "${BOT_LOGIN}" '
|
|
.[]? |
|
|
select(.pull_request == null) |
|
|
select(
|
|
(.assignee.login == $bot)
|
|
or (((.assignees // []) | map(.login) | index($bot)) != null)
|
|
) |
|
|
{
|
|
kind: "issue",
|
|
number,
|
|
title,
|
|
url: .html_url,
|
|
updatedAt: .updated_at,
|
|
author: (.user.login // null),
|
|
repository: { nameWithOwner: $full },
|
|
role: "assignee"
|
|
}
|
|
' >> "${tmp}/issues.ndjson" 2>/dev/null || true
|
|
done < "${tmp}/repos.txt"
|
|
|
|
local prs_json issues_json
|
|
if [[ -s "${tmp}/prs.ndjson" ]]; then
|
|
prs_json="$(jq -s '.' "${tmp}/prs.ndjson")"
|
|
else
|
|
prs_json='[]'
|
|
fi
|
|
if [[ -s "${tmp}/issues.ndjson" ]]; then
|
|
issues_json="$(jq -s '.' "${tmp}/issues.ndjson")"
|
|
else
|
|
issues_json='[]'
|
|
fi
|
|
|
|
jq -n \
|
|
--arg org "${ORG}" \
|
|
--arg bot "${BOT_LOGIN}" \
|
|
--arg url "${url}" \
|
|
--argjson prs "${prs_json}" \
|
|
--argjson issues "${issues_json}" \
|
|
'{
|
|
backend: "forgejo",
|
|
forge_url: $url,
|
|
org: $org,
|
|
bot: $bot,
|
|
generated_at: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
|
|
counts: {
|
|
prs: ($prs | length),
|
|
issues: ($issues | length),
|
|
total: (($prs | length) + ($issues | length))
|
|
},
|
|
prs: $prs,
|
|
issues: $issues
|
|
}'
|
|
}
|
|
|
|
print_summary() {
|
|
jq -r '
|
|
"backend=\(.backend) org=\(.org) bot=\(.bot)",
|
|
"counts: prs=\(.counts.prs) issues=\(.counts.issues) total=\(.counts.total)",
|
|
"",
|
|
(if (.prs|length)>0 then "PRs:" else empty end),
|
|
(.prs[]? | " [\(.role)] \(.repository.nameWithOwner // "unknown")!\(.number) — \(.title)"),
|
|
(if (.issues|length)>0 then "Issues:" else empty end),
|
|
(.issues[]? | " [\(.role)] \(.repository.nameWithOwner // "unknown")#\(.number) — \(.title)"),
|
|
(if .counts.total == 0 then "(no actionable items)" else empty end)
|
|
'
|
|
}
|
|
|
|
main() {
|
|
local payload
|
|
case "${FORGE_BACKEND}" in
|
|
github) payload="$(discover_github)" ;;
|
|
forgejo) payload="$(discover_forgejo)" ;;
|
|
*)
|
|
echo "error: unknown FORGE_BACKEND=${FORGE_BACKEND} (use github or forgejo)" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
case "${FORMAT}" in
|
|
json) echo "${payload}" | jq . ;;
|
|
summary) echo "${payload}" | print_summary ;;
|
|
*) echo "error: FORMAT must be json or summary" >&2; exit 2 ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|