From 65238f17f7cb7d6d79b3408636e9e174d068b2c1 Mon Sep 17 00:00:00 2001 From: tmn73 Date: Fri, 14 Aug 2026 06:58:09 -0500 Subject: [PATCH 1/3] Add a repository scope setting for organization repositories The repository list is built from ownerAffiliations:OWNER, so it only ever contains repositories you personally own. Anyone whose day-to-day work lives in an organization cannot find those repositories through the panel's search, filters, or sort, even though the sections above already surface notifications, review requests and pull requests from them. The scope is an enum rather than a boolean because it describes what the list contains rather than toggling a behaviour, and it leaves room for a narrower or wider value later. It defaults to owned, so an existing install is unchanged. The heading reads REPOSITORIES instead of OWNED REPOSITORIES when the wider scope is in effect. It follows the scope reported by the payload rather than the setting, so it cannot claim to list organization repositories before the refresh that fetches them has landed. --- Panel.qml | 5 ++++- README.md | 5 +++-- Service.qml | 11 ++++++++++- manifest.json | 2 ++ omarchy-github-fetch | 15 ++++++++++++--- tests/helper-test.sh | 14 ++++++++++++++ 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/Panel.qml b/Panel.qml index a9ab025..9e0f7c6 100644 --- a/Panel.qml +++ b/Panel.qml @@ -379,7 +379,10 @@ Panel { PanelSectionHeader { width: parent.width - text: "OWNED REPOSITORIES " + root.displayedRepositories.length + "/" + github.repositories.length + // The heading follows the scope actually fetched, not the setting, + // so it cannot claim to list organization repositories before a + // refresh has brought them in. + text: (github.repositoryScope === "owned" ? "OWNED REPOSITORIES " : "REPOSITORIES ") + root.displayedRepositories.length + "/" + github.repositories.length foreground: root.foreground fontFamily: root.fontFamily } diff --git a/README.md b/README.md index 3acadd1..e2ad7ce 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The dashboard is ordered by urgency so the most actionable work appears first: - **Assigned issues** — keep track of open issues assigned to you - **Active GitHub Actions** — monitor queued, pending, requested, waiting, and running workflows - **Recent workflow failures** — jump directly to failed, timed-out, or action-required runs -- **Owned repositories** — browse every repository you own with open issue, open PR, star, and active workflow counts +- **Repositories** — browse the repositories you own, and optionally those you reach through an organization, with open issue, open PR, star, and active workflow counts Repository search, metric filters, and sorting make even large GitHub accounts manageable. Filter to repositories with issues, PRs, stars, or active Actions, then sort by the metric that matters. @@ -135,6 +135,7 @@ Configure the widget through Omarchy's bar widget settings: - Refresh interval - Include archived repositories - Include forks +- Repository scope: owned repositories, or owned and organization repositories - Include review requests and issues from archived repositories - Include review requests on drafts - Maximum displayed repositories @@ -170,7 +171,7 @@ The shell watches local plugin files, making QML iteration fast. `Service.qml` schedules an executable helper, `omarchy-github-fetch`, which calls GitHub exclusively through `gh api` and processes responses with `jq`. -- GraphQL retrieves every owned repository and exact aggregate counts. +- GraphQL retrieves every repository in the configured scope and exact aggregate counts. - REST retrieves notifications and workflow runs. - GitHub issue search retrieves review requests and assigned issues. - GraphQL search retrieves your authored pull requests together with the head commit's `statusCheckRollup`, so check state costs no extra request. diff --git a/Service.qml b/Service.qml index 0f46ec3..ac8194c 100644 --- a/Service.qml +++ b/Service.qml @@ -13,6 +13,7 @@ Item { property string state: "loading" property string message: "Loading GitHub…" property string login: "" + property string repositoryScope: "owned" property string fetchedAt: "" property var notifications: [] property int notificationsRevision: 0 @@ -83,6 +84,13 @@ Item { return text === "true" || text === "yes" || text === "on" || text === "1"; } + // The setting drives the next fetch; `repositoryScope` reports what the + // last payload actually contained, so the panel never labels the list + // before the data behind the label has arrived. + function repositoryScopeSetting() { + return String(setting("repositoryScope", "Owned")).toLowerCase().indexOf("organization") === -1 ? "owned" : "organizations"; + } + function actionMode() { var value = String(setting("actionScanBehavior", "Recent repositories")).toLowerCase(); if (value === "off") @@ -99,7 +107,7 @@ Item { } function command() { - return [helperPath(), "--include-archived", boolSetting("includeArchived", false) ? "true" : "false", "--include-forks", boolSetting("includeForks", false) ? "true" : "false", "--include-archived-reviews", boolSetting("includeArchivedReviewRequests", false) ? "true" : "false", "--include-draft-reviews", boolSetting("includeDraftReviewRequests", false) ? "true" : "false", "--action-scan", actionMode(), "--action-repo-limit", String(intSetting("actionScanRepoLimit", 15, 5, 200)), "--concurrency", String(intSetting("actionScanConcurrency", 6, 1, 12)), "--failed-days", String(intSetting("failedActionDays", 7, 1, 30)), "--failed-limit", String(intSetting("failedActionLimit", 20, 1, 100))]; + return [helperPath(), "--include-archived", boolSetting("includeArchived", false) ? "true" : "false", "--include-forks", boolSetting("includeForks", false) ? "true" : "false", "--repository-scope", repositoryScopeSetting(), "--include-archived-reviews", boolSetting("includeArchivedReviewRequests", false) ? "true" : "false", "--include-draft-reviews", boolSetting("includeDraftReviewRequests", false) ? "true" : "false", "--action-scan", actionMode(), "--action-repo-limit", String(intSetting("actionScanRepoLimit", 15, 5, 200)), "--concurrency", String(intSetting("actionScanConcurrency", 6, 1, 12)), "--failed-days", String(intSetting("failedActionDays", 7, 1, 30)), "--failed-limit", String(intSetting("failedActionLimit", 20, 1, 100))]; } function refresh() { @@ -121,6 +129,7 @@ Item { state = String(data.state || "error"); message = String(data.message || ""); login = String(data.login || ""); + repositoryScope = String(data.repositoryScope || "owned"); fetchedAt = String(data.fetchedAt || ""); notifications = Array.isArray(data.notifications) ? data.notifications : []; notificationsRevision++; diff --git a/manifest.json b/manifest.json index b3b23ed..d620570 100644 --- a/manifest.json +++ b/manifest.json @@ -22,6 +22,7 @@ "refreshIntervalSec": 900, "includeArchived": false, "includeForks": false, + "repositoryScope": "Owned", "includeArchivedReviewRequests": false, "includeDraftReviewRequests": false, "maxDisplayedRepos": 25, @@ -35,6 +36,7 @@ { "key": "refreshIntervalSec", "type": "integer", "label": "Refresh interval (seconds)", "min": 60, "max": 3600, "step": 60, "defaultValue": 900 }, { "key": "includeArchived", "type": "boolean", "label": "Include archived repositories", "defaultValue": false }, { "key": "includeForks", "type": "boolean", "label": "Include forked repositories", "defaultValue": false }, + { "key": "repositoryScope", "type": "enum", "label": "Repository scope", "options": ["Owned", "Owned and organizations"], "defaultValue": "Owned", "description": "List only the repositories you own, or also those you reach through an organization." }, { "key": "includeArchivedReviewRequests", "type": "boolean", "label": "Include review requests and issues from archived repositories", "defaultValue": false, "description": "An archived repository is read-only, so the request cannot be withdrawn and the work cannot be done." }, { "key": "includeDraftReviewRequests", "type": "boolean", "label": "Include review requests on drafts", "defaultValue": false, "description": "Draft pull requests are not ready for review, but some teams do ask for early feedback on them." }, { "key": "maxDisplayedRepos", "type": "integer", "label": "Maximum displayed repositories", "min": 10, "max": 500, "step": 5, "defaultValue": 25, "description": "Caps rendered repository rows while search and filtering continue to use the complete fetched list." }, diff --git a/omarchy-github-fetch b/omarchy-github-fetch index 31f7ac9..8ee48d2 100755 --- a/omarchy-github-fetch +++ b/omarchy-github-fetch @@ -7,6 +7,7 @@ include_archived=false include_forks=false include_archived_reviews=false include_draft_reviews=false +repository_scope="owned" action_scan="recent" action_limit=15 concurrency=6 @@ -26,6 +27,9 @@ Usage: omarchy-github-fetch [options] archived repositories (default: false) --include-draft-reviews BOOL Include review requests on draft pull requests (default: false) + --repository-scope MODE owned, or organizations to also list the + repositories you reach through an organization + (default: owned) --action-scan MODE off, recent, or all (default: recent) --action-repo-limit N Repositories scanned in recent mode (default: 15) --concurrency N Concurrent Actions requests, 1-12 (default: 6) @@ -48,6 +52,7 @@ while (($#)); do --include-forks) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; include_forks=$2; shift 2 ;; --include-archived-reviews) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; include_archived_reviews=$2; shift 2 ;; --include-draft-reviews) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; include_draft_reviews=$2; shift 2 ;; + --repository-scope) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; repository_scope=${2,,}; shift 2 ;; --action-scan) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; action_scan=${2,,}; shift 2 ;; --action-repo-limit) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; action_limit=$2; shift 2 ;; --concurrency) [[ $# -ge 2 ]] || { echo "missing value for $1" >&2; exit 2; }; concurrency=$2; shift 2 ;; @@ -66,9 +71,13 @@ integer "$concurrency" && ((concurrency >= 1 && concurrency <= 12)) || { echo "i integer "$failed_days" && ((failed_days >= 1 && failed_days <= 30)) || { echo "invalid --failed-days" >&2; exit 2; } integer "$failed_limit" && ((failed_limit >= 1 && failed_limit <= 100)) || { echo "invalid --failed-limit" >&2; exit 2; } [[ $action_scan == off || $action_scan == recent || $action_scan == all ]] || { echo "invalid --action-scan" >&2; exit 2; } +[[ $repository_scope == owned || $repository_scope == organizations ]] || { echo "invalid --repository-scope" >&2; exit 2; } +# Interpolated into the GraphQL document, so it comes from the validated enum +# above. The argument itself never reaches the query. +if [[ $repository_scope == organizations ]]; then owner_affiliations="[OWNER,ORGANIZATION_MEMBER]"; else owner_affiliations="OWNER"; fi emit_state() { - jq -n --arg state "$1" --arg message "$2" '{schemaVersion:1,state:$state,message:$message,login:"",fetchedAt:(now|todateiso8601),notifications:[],reviewRequests:[],assignedIssues:[],myPullRequests:[],myPullRequestsTotal:0,actions:[],failedActions:[],repositories:[],warnings:[],rateLimit:null}' + jq -n --arg state "$1" --arg message "$2" --arg scope "$repository_scope" '{schemaVersion:1,state:$state,message:$message,login:"",repositoryScope:$scope,fetchedAt:(now|todateiso8601),notifications:[],reviewRequests:[],assignedIssues:[],myPullRequests:[],myPullRequestsTotal:0,actions:[],failedActions:[],repositories:[],warnings:[],rateLimit:null}' } command -v jq >/dev/null 2>&1 || { printf '%s\n' '{"schemaVersion":1,"state":"error","message":"jq is required.","notifications":[],"reviewRequests":[],"assignedIssues":[],"myPullRequests":[],"myPullRequestsTotal":0,"actions":[],"failedActions":[],"repositories":[],"warnings":[]}' ; exit 0; } @@ -232,7 +241,7 @@ cursor=null printf '[]\n' >"$tmp/repos.json" graphql_ok=true while :; do - if ! page=$(gh api graphql -f query='query($cursor:String) { viewer { login repositories(first:100,after:$cursor,ownerAffiliations:OWNER,orderBy:{field:UPDATED_AT,direction:DESC}) { nodes { name nameWithOwner url isArchived isFork stargazerCount updatedAt issues(states:OPEN){totalCount} pullRequests(states:OPEN){totalCount} } pageInfo { hasNextPage endCursor } } } rateLimit { remaining resetAt cost } }' -F cursor="$cursor" 2>"$tmp/graphql.err"); then api_error repositories "$tmp/graphql.err"; graphql_ok=false; break; fi + if ! page=$(gh api graphql -f query='query($cursor:String) { viewer { login repositories(first:100,after:$cursor,ownerAffiliations:'"$owner_affiliations"',orderBy:{field:UPDATED_AT,direction:DESC}) { nodes { name nameWithOwner url isArchived isFork stargazerCount updatedAt issues(states:OPEN){totalCount} pullRequests(states:OPEN){totalCount} } pageInfo { hasNextPage endCursor } } } rateLimit { remaining resetAt cost } }' -F cursor="$cursor" 2>"$tmp/graphql.err"); then api_error repositories "$tmp/graphql.err"; graphql_ok=false; break; fi printf '%s\n' "$page" >"$tmp/page.json" login=$(jq -r '.data.viewer.login // ""' "$tmp/page.json") jq '[.data.viewer.repositories.nodes[] | {name,nameWithOwner,url,archived:.isArchived,fork:.isFork,stars:.stargazerCount,issues:.issues.totalCount,prs:.pullRequests.totalCount,updatedAt,activeActions:0}]' "$tmp/page.json" >"$tmp/page.repos" @@ -280,4 +289,4 @@ repo_count=$(jq 'length' "$tmp/repos.json") if [[ $graphql_ok == false && $repo_count -eq 0 ]]; then state=error; message="GitHub repositories could not be loaded."; else state=ready; message=""; fi rate_remaining=$(jq -r '.remaining // 999999' "$tmp/rate.json") if { [[ $rate_remaining =~ ^[0-9]+$ ]] && ((rate_remaining<=0)); } || grep -qiE 'rate.?limit|secondary rate' "$warnings"; then state=rate-limited; message="GitHub API rate limit reached."; fi -jq -n --arg state "$state" --arg message "$message" --arg login "${login:-}" --slurpfile notifications "$tmp/notifications.json" --slurpfile reviews "$tmp/reviews.json" --slurpfile issues "$tmp/issues.json" --slurpfile mypulls "$tmp/mypulls.json" --slurpfile mypullstotal "$tmp/mypulls-total.json" --slurpfile actions "$tmp/actions.json" --slurpfile failed "$tmp/failed.json" --slurpfile repositories "$tmp/repos.json" --argjson warnings "$warnings_json" --slurpfile rate "$tmp/rate.json" '{schemaVersion:1,state:$state,message:$message,login:$login,fetchedAt:(now|todateiso8601),notifications:$notifications[0],reviewRequests:$reviews[0],assignedIssues:$issues[0],myPullRequests:$mypulls[0],myPullRequestsTotal:$mypullstotal[0],actions:$actions[0],failedActions:$failed[0],repositories:$repositories[0],warnings:$warnings,rateLimit:$rate[0]}' +jq -n --arg state "$state" --arg message "$message" --arg login "${login:-}" --arg scope "$repository_scope" --slurpfile notifications "$tmp/notifications.json" --slurpfile reviews "$tmp/reviews.json" --slurpfile issues "$tmp/issues.json" --slurpfile mypulls "$tmp/mypulls.json" --slurpfile mypullstotal "$tmp/mypulls-total.json" --slurpfile actions "$tmp/actions.json" --slurpfile failed "$tmp/failed.json" --slurpfile repositories "$tmp/repos.json" --argjson warnings "$warnings_json" --slurpfile rate "$tmp/rate.json" '{schemaVersion:1,state:$state,message:$message,login:$login,repositoryScope:$scope,fetchedAt:(now|todateiso8601),notifications:$notifications[0],reviewRequests:$reviews[0],assignedIssues:$issues[0],myPullRequests:$mypulls[0],myPullRequestsTotal:$mypullstotal[0],actions:$actions[0],failedActions:$failed[0],repositories:$repositories[0],warnings:$warnings,rateLimit:$rate[0]}' diff --git a/tests/helper-test.sh b/tests/helper-test.sh index 25182cb..dd80632 100755 --- a/tests/helper-test.sh +++ b/tests/helper-test.sh @@ -9,6 +9,7 @@ assert_jq() { jq -e "$1" <<<"$2" >/dev/null || fail "$3"; } bash -n "$HELPER" "$HELPER" --help >/dev/null if "$HELPER" --action-scan invalid >/dev/null 2>&1; then fail "invalid scan mode succeeded"; fi +if "$HELPER" --repository-scope invalid >/dev/null 2>&1; then fail "invalid repository scope succeeded"; fi if "$HELPER" --failed-days 0 >/dev/null 2>&1; then fail "invalid failed window succeeded"; fi if "$HELPER" --mark-notification-read nope >/dev/null 2>&1; then fail "invalid notification id succeeded"; fi if "$HELPER" --mark-all-read-before nope >/dev/null 2>&1; then fail "invalid last-read timestamp succeeded"; fi @@ -118,6 +119,7 @@ assert_jq '.notifications|length == 2 and .[0].url == "https://github.com/octoca assert_jq '.reviewRequests|length == 1 and .[0].repository == "octocat/hello"' "$out" "review requests" assert_jq '(.assignedIssues|length == 1) and (.assignedIssues[0].url|endswith("/issues/8"))' "$out" "assigned issues" assert_jq '(.actions|length == 1) and (.failedActions|length == 1)' "$out" "active and failed actions separated" +assert_jq '.repositoryScope == "owned"' "$out" "default repository scope reported" assert_jq '.rateLimit.remaining == 4999 and (.warnings|length) == 0' "$out" "rate limit and warnings" assert_jq '(.myPullRequests|length == 2) and (.myPullRequests[0].id == "octocat/hello#7") and (.myPullRequests[0].checks == "FAILURE")' "$out" "authored pull requests with check rollup" assert_jq '(.myPullRequests[1].checks == "NONE") and (.myPullRequests[1].draft == true)' "$out" "missing rollup falls back to NONE" @@ -192,6 +194,18 @@ assert_jq '.state == "error" and .notificationId == "123"' "$mark_setup_failed" assert_jq '.state == "error" and .lastReadAt == "2020-01-03T00:00:00Z"' "$bulk_setup_failed" "bulk mark setup failure reports an error" assert_jq '.state == "error"' "$fetch_setup_failed" "refresh setup failure reports an error" +# Each scope run truncates the log first, so the greps below read only the run +# they belong to rather than an earlier one that used the other affiliation. +: >"$GH_TEST_LOG" +out_owned=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off) +assert_jq '.repositoryScope == "owned"' "$out_owned" "default repository scope reported" +grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG" || fail "default scope did not query owned repositories" +: >"$GH_TEST_LOG" +out_scoped=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off --repository-scope organizations) +assert_jq '.repositoryScope == "organizations"' "$out_scoped" "organization scope reported" +grep -q 'ownerAffiliations:\[OWNER,ORGANIZATION_MEMBER\],' "$GH_TEST_LOG" || fail "organization scope did not reach the query" +if grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG"; then fail "owned affiliation used despite the organization scope"; fi + # The Actions scan runs in xargs subshells, which only see exported functions. # An unexported helper there degrades every warning to the generic fallback # instead of the API's own explanation. From 1ed155c1aa9f3aceb2e132b07e89008c321d2bbc Mon Sep 17 00:00:00 2001 From: tmn73 Date: Fri, 14 Aug 2026 07:23:29 -0500 Subject: [PATCH 2/3] Address self-review findings on the repository scope Rename the payload echo to fetchedRepositoryScope and the derived getter to repositoryMode, mirroring actionScanBehavior and actionMode. The property and the setting key shared a name with different vocabularies, so a reader could reasonably take the heading for setting-driven and 'simplify' it back into the bug the design avoids. Match the setting against its known options instead of testing for the substring 'organization'. A later option such as 'Organizations only' would have matched and silently widened the scope, giving back exactly the room the enum was chosen to leave. Cover the QML half in the source suites: the setting mapping, the helper argument, and the scope-aware heading. Both regressions were verified to fail their new assertion. Drop the stale 'owned' wording from the Actions scan description, the plugin description, the repository dashboard section, and the empty state, which would otherwise have contradicted the heading above it. --- Panel.qml | 9 ++++----- README.md | 2 +- Service.qml | 16 ++++++++-------- manifest.json | 4 ++-- tests/helper-test.sh | 24 ++++++++++++------------ tests/panel-source-test.sh | 4 ++++ tests/service-source-test.sh | 7 +++++++ 7 files changed, 38 insertions(+), 28 deletions(-) diff --git a/Panel.qml b/Panel.qml index 9e0f7c6..7306b77 100644 --- a/Panel.qml +++ b/Panel.qml @@ -379,10 +379,9 @@ Panel { PanelSectionHeader { width: parent.width - // The heading follows the scope actually fetched, not the setting, - // so it cannot claim to list organization repositories before a - // refresh has brought them in. - text: (github.repositoryScope === "owned" ? "OWNED REPOSITORIES " : "REPOSITORIES ") + root.displayedRepositories.length + "/" + github.repositories.length + // Driven by the fetched scope, not the setting, so it cannot claim + // to list organization repositories before a refresh brings them in. + text: (github.fetchedRepositoryScope === "owned" ? "OWNED REPOSITORIES " : "REPOSITORIES ") + root.displayedRepositories.length + "/" + github.repositories.length foreground: root.foreground fontFamily: root.fontFamily } @@ -453,7 +452,7 @@ Panel { Text { visible: root.displayedRepositories.length === 0 width: parent.width - text: github.repositories.length === 0 ? "No owned repositories loaded." : "No repositories match these filters." + text: github.repositories.length === 0 ? "No repositories loaded." : "No repositories match these filters." color: root.dim font.family: root.fontFamily font.pixelSize: Style.font.body diff --git a/README.md b/README.md index e2ad7ce..a2891c0 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ The notifications footer also carries **Mark all read**. The first click capture ## Repository dashboard -Every owned repository includes: +Every listed repository includes: - Open issue count - Open pull request count diff --git a/Service.qml b/Service.qml index ac8194c..f29e2c6 100644 --- a/Service.qml +++ b/Service.qml @@ -13,7 +13,7 @@ Item { property string state: "loading" property string message: "Loading GitHub…" property string login: "" - property string repositoryScope: "owned" + property string fetchedRepositoryScope: "owned" property string fetchedAt: "" property var notifications: [] property int notificationsRevision: 0 @@ -84,11 +84,11 @@ Item { return text === "true" || text === "yes" || text === "on" || text === "1"; } - // The setting drives the next fetch; `repositoryScope` reports what the - // last payload actually contained, so the panel never labels the list - // before the data behind the label has arrived. - function repositoryScopeSetting() { - return String(setting("repositoryScope", "Owned")).toLowerCase().indexOf("organization") === -1 ? "owned" : "organizations"; + // Matched against the known options rather than by substring, so an option + // added later falls back to the narrower scope instead of silently widening + // it. `fetchedRepositoryScope` reports what the last payload contained. + function repositoryMode() { + return String(setting("repositoryScope", "Owned")).toLowerCase() === "owned and organizations" ? "organizations" : "owned"; } function actionMode() { @@ -107,7 +107,7 @@ Item { } function command() { - return [helperPath(), "--include-archived", boolSetting("includeArchived", false) ? "true" : "false", "--include-forks", boolSetting("includeForks", false) ? "true" : "false", "--repository-scope", repositoryScopeSetting(), "--include-archived-reviews", boolSetting("includeArchivedReviewRequests", false) ? "true" : "false", "--include-draft-reviews", boolSetting("includeDraftReviewRequests", false) ? "true" : "false", "--action-scan", actionMode(), "--action-repo-limit", String(intSetting("actionScanRepoLimit", 15, 5, 200)), "--concurrency", String(intSetting("actionScanConcurrency", 6, 1, 12)), "--failed-days", String(intSetting("failedActionDays", 7, 1, 30)), "--failed-limit", String(intSetting("failedActionLimit", 20, 1, 100))]; + return [helperPath(), "--include-archived", boolSetting("includeArchived", false) ? "true" : "false", "--include-forks", boolSetting("includeForks", false) ? "true" : "false", "--repository-scope", repositoryMode(), "--include-archived-reviews", boolSetting("includeArchivedReviewRequests", false) ? "true" : "false", "--include-draft-reviews", boolSetting("includeDraftReviewRequests", false) ? "true" : "false", "--action-scan", actionMode(), "--action-repo-limit", String(intSetting("actionScanRepoLimit", 15, 5, 200)), "--concurrency", String(intSetting("actionScanConcurrency", 6, 1, 12)), "--failed-days", String(intSetting("failedActionDays", 7, 1, 30)), "--failed-limit", String(intSetting("failedActionLimit", 20, 1, 100))]; } function refresh() { @@ -129,7 +129,7 @@ Item { state = String(data.state || "error"); message = String(data.message || ""); login = String(data.login || ""); - repositoryScope = String(data.repositoryScope || "owned"); + fetchedRepositoryScope = String(data.repositoryScope || "owned"); fetchedAt = String(data.fetchedAt || ""); notifications = Array.isArray(data.notifications) ? data.notifications : []; notificationsRevision++; diff --git a/manifest.json b/manifest.json index d620570..fe5fb96 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "version": "0.2.2", "author": "Rob Zolkos", "license": "MIT", - "description": "A keyboard-friendly GitHub inbox for notifications, reviews, your own pull requests, assigned issues, Actions, and owned repositories.", + "description": "A keyboard-friendly GitHub inbox for notifications, reviews, your own pull requests, assigned issues, Actions, and repositories.", "kinds": ["bar-widget"], "activation": "on-demand", "entryPoints": { @@ -40,7 +40,7 @@ { "key": "includeArchivedReviewRequests", "type": "boolean", "label": "Include review requests and issues from archived repositories", "defaultValue": false, "description": "An archived repository is read-only, so the request cannot be withdrawn and the work cannot be done." }, { "key": "includeDraftReviewRequests", "type": "boolean", "label": "Include review requests on drafts", "defaultValue": false, "description": "Draft pull requests are not ready for review, but some teams do ask for early feedback on them." }, { "key": "maxDisplayedRepos", "type": "integer", "label": "Maximum displayed repositories", "min": 10, "max": 500, "step": 5, "defaultValue": 25, "description": "Caps rendered repository rows while search and filtering continue to use the complete fetched list." }, - { "key": "actionScanBehavior", "type": "enum", "label": "Actions scan", "options": ["Off", "Recent repositories", "All repositories"], "defaultValue": "Recent repositories", "description": "Scan no repositories, the most recently updated repositories, or every owned repository for active and recent failed workflow runs." }, + { "key": "actionScanBehavior", "type": "enum", "label": "Actions scan", "options": ["Off", "Recent repositories", "All repositories"], "defaultValue": "Recent repositories", "description": "Scan no repositories, the most recently updated repositories, or every repository in the configured scope for active and recent failed workflow runs." }, { "key": "actionScanRepoLimit", "type": "integer", "label": "Recent repository scan limit", "min": 5, "max": 200, "step": 5, "defaultValue": 15 }, { "key": "actionScanConcurrency", "type": "integer", "label": "Actions scan concurrency", "min": 1, "max": 12, "step": 1, "defaultValue": 6 }, { "key": "failedActionDays", "type": "integer", "label": "Failed Actions window (days)", "min": 1, "max": 30, "step": 1, "defaultValue": 7 }, diff --git a/tests/helper-test.sh b/tests/helper-test.sh index dd80632..66b3b1c 100755 --- a/tests/helper-test.sh +++ b/tests/helper-test.sh @@ -147,6 +147,18 @@ out_archived=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off --include-archi assert_jq '.myPullRequests|length == 2' "$out_archived" "authored pull requests survive the archived setting" grep -q 'author:@me' "$GH_TEST_LOG" || fail "authored pull request search did not run" if grep -q 'archived:false' "$GH_TEST_LOG"; then fail "archived filter applied despite --include-archived true"; fi +# Each scope run truncates the log first, so the greps below read only the run +# they belong to rather than an earlier one that used the other affiliation. +: >"$GH_TEST_LOG" +out_owned=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off) +assert_jq '.repositoryScope == "owned"' "$out_owned" "default repository scope reported with the owned affiliation" +grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG" || fail "default scope did not query owned repositories" +: >"$GH_TEST_LOG" +out_scoped=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off --repository-scope organizations) +assert_jq '.repositoryScope == "organizations"' "$out_scoped" "organization scope reported" +grep -q 'ownerAffiliations:\[OWNER,ORGANIZATION_MEMBER\],' "$GH_TEST_LOG" || fail "organization scope did not reach the query" +if grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG"; then fail "owned affiliation used despite the organization scope"; fi + : >"$GH_TEST_LOG" mark=$(PATH="$sandbox:$PATH" "$HELPER" --mark-notification-read 123) assert_jq '.state == "ready" and .notificationId == "123"' "$mark" "mark notification read" @@ -194,18 +206,6 @@ assert_jq '.state == "error" and .notificationId == "123"' "$mark_setup_failed" assert_jq '.state == "error" and .lastReadAt == "2020-01-03T00:00:00Z"' "$bulk_setup_failed" "bulk mark setup failure reports an error" assert_jq '.state == "error"' "$fetch_setup_failed" "refresh setup failure reports an error" -# Each scope run truncates the log first, so the greps below read only the run -# they belong to rather than an earlier one that used the other affiliation. -: >"$GH_TEST_LOG" -out_owned=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off) -assert_jq '.repositoryScope == "owned"' "$out_owned" "default repository scope reported" -grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG" || fail "default scope did not query owned repositories" -: >"$GH_TEST_LOG" -out_scoped=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off --repository-scope organizations) -assert_jq '.repositoryScope == "organizations"' "$out_scoped" "organization scope reported" -grep -q 'ownerAffiliations:\[OWNER,ORGANIZATION_MEMBER\],' "$GH_TEST_LOG" || fail "organization scope did not reach the query" -if grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG"; then fail "owned affiliation used despite the organization scope"; fi - # The Actions scan runs in xargs subshells, which only see exported functions. # An unexported helper there degrades every warning to the generic fallback # instead of the API's own explanation. diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index ea4dac7..45a78e8 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -31,3 +31,7 @@ assert_contains $'PanelActionButton {\n visible: linkRow.showReadAction\n "notification row marking is enabled during refresh" echo "panel source tests passed" +assert_contains 'github.fetchedRepositoryScope === "owned" ? "OWNED REPOSITORIES " : "REPOSITORIES "' \ + "the repository heading does not follow the fetched scope" +assert_contains '"No repositories loaded."' \ + "the repository empty state still claims a scope" diff --git a/tests/service-source-test.sh b/tests/service-source-test.sh index f111f7e..75c9b6f 100755 --- a/tests/service-source-test.sh +++ b/tests/service-source-test.sh @@ -8,6 +8,13 @@ fail() { echo "FAIL: $*" >&2; exit 1; } assert_contains() { [[ $SERVICE_SOURCE == *"$1"* ]] || fail "$2" } +assert_contains 'String(setting("repositoryScope", "Owned")).toLowerCase() === "owned and organizations" ? "organizations" : "owned"' \ + "an unrecognised repository scope no longer falls back to the narrower one" +assert_contains '"--repository-scope", repositoryMode()' \ + "the repository scope setting is not passed to the helper" +assert_contains 'fetchedRepositoryScope = String(data.repositoryScope || "owned");' \ + "the panel cannot tell which scope the payload was fetched with" + assert_not_contains() { [[ $SERVICE_SOURCE != *"$1"* ]] || fail "$2" } From d90c37f9f586a82a9a4554493bdd39f25f5d1602 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Fri, 14 Aug 2026 12:40:08 -0400 Subject: [PATCH 3/3] Clarify repository scope and API cost --- README.md | 49 +++++++++++++++++++++++++----------- Service.qml | 2 +- manifest.json | 4 +-- tests/helper-test.sh | 10 ++++++-- tests/panel-source-test.sh | 3 ++- tests/service-source-test.sh | 2 ++ 6 files changed, 49 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index a2891c0..0123fdb 100644 --- a/README.md +++ b/README.md @@ -130,21 +130,40 @@ Use the filter chips to show all repositories or only repositories with a non-ze ## Settings -Configure the widget through Omarchy's bar widget settings: - -- Refresh interval -- Include archived repositories -- Include forks -- Repository scope: owned repositories, or owned and organization repositories -- Include review requests and issues from archived repositories -- Include review requests on drafts -- Maximum displayed repositories -- Actions scan mode: off, recent repositories, or all repositories -- Number of recent repositories to scan -- Actions request concurrency -- Failed Actions time window and maximum result count - -The defaults deliberately balance freshness and GitHub API usage. Accounts that need exhaustive workflow monitoring can select **All repositories**. +Configure the widget through Omarchy's bar widget settings. Existing installations retain the narrower repository scope and bounded Actions scan: + +| Setting | Default | +| --- | --- | +| Refresh interval | 900 seconds (15 minutes) | +| Include archived repositories | Off | +| Include forks | Off | +| Repository scope | **Owned** | +| Include review requests and issues from archived repositories | Off | +| Include review requests on drafts | Off | +| Maximum displayed repositories | 25 | +| Actions scan | **Recent repositories** | +| Recent repository scan limit | 15 | +| Actions request concurrency | 6 | +| Failed Actions window | 7 days | +| Maximum failed Actions | 20 | + +**Repository scope** controls both the repository dashboard and the candidate repositories for Actions scanning. **Owned and organizations** is opt-in. With the default **Recent repositories** scan, Actions requests remain capped to the 15 most recently updated repositories in that wider scope. + +**All repositories** is also opt-in and starts six paginated Actions request streams per repository on every refresh. Combining it with **Owned and organizations** can consume substantial GitHub API capacity in large organizations. Use **Recent repositories** or **Off** for a bounded scan, and increase the refresh interval when broader monitoring is required. + +Set these options from the command line after installing the plugin: + +```bash +omarchy bar set robzolkos.github repositoryScope "Owned and organizations" +omarchy bar set robzolkos.github actionScanBehavior "Recent repositories" +``` + +Restore the narrowest behavior with: + +```bash +omarchy bar set robzolkos.github repositoryScope "Owned" +omarchy bar set robzolkos.github actionScanBehavior "Off" +``` Review requests and assigned issues from archived repositories are hidden by default because archived repositories are read-only. Review requests on draft pull requests are also hidden by default, while teams that use drafts for early feedback can include them. Each behavior has its own setting. diff --git a/Service.qml b/Service.qml index f29e2c6..35d945d 100644 --- a/Service.qml +++ b/Service.qml @@ -96,7 +96,7 @@ Item { if (value === "off") return "off"; - if (value.indexOf("all") === 0) + if (value === "all repositories") return "all"; return "recent"; diff --git a/manifest.json b/manifest.json index fe5fb96..d5a45ef 100644 --- a/manifest.json +++ b/manifest.json @@ -36,11 +36,11 @@ { "key": "refreshIntervalSec", "type": "integer", "label": "Refresh interval (seconds)", "min": 60, "max": 3600, "step": 60, "defaultValue": 900 }, { "key": "includeArchived", "type": "boolean", "label": "Include archived repositories", "defaultValue": false }, { "key": "includeForks", "type": "boolean", "label": "Include forked repositories", "defaultValue": false }, - { "key": "repositoryScope", "type": "enum", "label": "Repository scope", "options": ["Owned", "Owned and organizations"], "defaultValue": "Owned", "description": "List only the repositories you own, or also those you reach through an organization." }, + { "key": "repositoryScope", "type": "enum", "label": "Repository scope", "options": ["Owned", "Owned and organizations"], "defaultValue": "Owned", "description": "List only repositories you own, or include organization repositories. This scope also supplies candidates for Actions scanning." }, { "key": "includeArchivedReviewRequests", "type": "boolean", "label": "Include review requests and issues from archived repositories", "defaultValue": false, "description": "An archived repository is read-only, so the request cannot be withdrawn and the work cannot be done." }, { "key": "includeDraftReviewRequests", "type": "boolean", "label": "Include review requests on drafts", "defaultValue": false, "description": "Draft pull requests are not ready for review, but some teams do ask for early feedback on them." }, { "key": "maxDisplayedRepos", "type": "integer", "label": "Maximum displayed repositories", "min": 10, "max": 500, "step": 5, "defaultValue": 25, "description": "Caps rendered repository rows while search and filtering continue to use the complete fetched list." }, - { "key": "actionScanBehavior", "type": "enum", "label": "Actions scan", "options": ["Off", "Recent repositories", "All repositories"], "defaultValue": "Recent repositories", "description": "Scan no repositories, the most recently updated repositories, or every repository in the configured scope for active and recent failed workflow runs." }, + { "key": "actionScanBehavior", "type": "enum", "label": "Actions scan", "options": ["Off", "Recent repositories", "All repositories"], "defaultValue": "Recent repositories", "description": "Scan no repositories, a capped set of recent repositories, or every repository in the configured scope. All repositories can use substantial API capacity with organization scope." }, { "key": "actionScanRepoLimit", "type": "integer", "label": "Recent repository scan limit", "min": 5, "max": 200, "step": 5, "defaultValue": 15 }, { "key": "actionScanConcurrency", "type": "integer", "label": "Actions scan concurrency", "min": 1, "max": 12, "step": 1, "defaultValue": 6 }, { "key": "failedActionDays", "type": "integer", "label": "Failed Actions window (days)", "min": 1, "max": 30, "step": 1, "defaultValue": 7 }, diff --git a/tests/helper-test.sh b/tests/helper-test.sh index 66b3b1c..66fe804 100755 --- a/tests/helper-test.sh +++ b/tests/helper-test.sh @@ -62,6 +62,12 @@ if [[ $1 == api && $2 == graphql ]]; then # being conflated with a pending run. cat <<'JSON' {"data":{"search":{"issueCount":2,"nodes":[{"number":7,"title":"Ship it","url":"https://github.com/octocat/hello/pull/7","updatedAt":"2026-01-05T00:00:00Z","isDraft":false,"repository":{"nameWithOwner":"octocat/hello"},"commits":{"nodes":[{"commit":{"statusCheckRollup":{"state":"FAILURE"}}}]}},{"number":9,"title":"No CI here","url":"https://github.com/octocat/quiet/pull/9","updatedAt":"2026-01-04T00:00:00Z","isDraft":true,"repository":{"nameWithOwner":"octocat/quiet"},"commits":{"nodes":[{"commit":{"statusCheckRollup":null}}]}}]}}} +JSON + exit 0 + fi + if [[ $* == *'ownerAffiliations:[OWNER,ORGANIZATION_MEMBER]'* ]]; then + cat <<'JSON' +{"data":{"viewer":{"login":"octocat","repositories":{"nodes":[{"name":"hello","nameWithOwner":"octocat/hello","url":"https://github.com/octocat/hello","isArchived":false,"isFork":false,"stargazerCount":42,"updatedAt":"2026-01-01T00:00:00Z","issues":{"totalCount":3},"pullRequests":{"totalCount":2}},{"name":"work","nameWithOwner":"acme/work","url":"https://github.com/acme/work","isArchived":false,"isFork":false,"stargazerCount":7,"updatedAt":"2026-01-06T00:00:00Z","issues":{"totalCount":4},"pullRequests":{"totalCount":5}},{"name":"old","nameWithOwner":"octocat/old","url":"https://github.com/octocat/old","isArchived":true,"isFork":false,"stargazerCount":1,"updatedAt":"2020-01-01T00:00:00Z","issues":{"totalCount":0},"pullRequests":{"totalCount":0}}],"pageInfo":{"hasNextPage":false,"endCursor":null}}},"rateLimit":{"remaining":4998,"resetAt":"2026-01-01T01:00:00Z","cost":2}}} JSON exit 0 fi @@ -151,11 +157,11 @@ if grep -q 'archived:false' "$GH_TEST_LOG"; then fail "archived filter applied d # they belong to rather than an earlier one that used the other affiliation. : >"$GH_TEST_LOG" out_owned=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off) -assert_jq '.repositoryScope == "owned"' "$out_owned" "default repository scope reported with the owned affiliation" +assert_jq '.repositoryScope == "owned" and (.repositories|length) == 1 and ([.repositories[].nameWithOwner]|index("acme/work")|not)' "$out_owned" "owned scope excludes organization repositories" grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG" || fail "default scope did not query owned repositories" : >"$GH_TEST_LOG" out_scoped=$(PATH="$sandbox:$PATH" "$HELPER" --action-scan off --repository-scope organizations) -assert_jq '.repositoryScope == "organizations"' "$out_scoped" "organization scope reported" +assert_jq '.repositoryScope == "organizations" and (.repositories|length) == 2 and ([.repositories[].nameWithOwner]|index("acme/work") != null)' "$out_scoped" "organization scope includes organization repositories" grep -q 'ownerAffiliations:\[OWNER,ORGANIZATION_MEMBER\],' "$GH_TEST_LOG" || fail "organization scope did not reach the query" if grep -q 'ownerAffiliations:OWNER,' "$GH_TEST_LOG"; then fail "owned affiliation used despite the organization scope"; fi diff --git a/tests/panel-source-test.sh b/tests/panel-source-test.sh index 45a78e8..6ea765f 100755 --- a/tests/panel-source-test.sh +++ b/tests/panel-source-test.sh @@ -30,8 +30,9 @@ assert_contains $'function markSelectedRead() {\n if (github.loading || githu assert_contains $'PanelActionButton {\n visible: linkRow.showReadAction\n enabled: !github.loading && !github.marking' \ "notification row marking is enabled during refresh" -echo "panel source tests passed" assert_contains 'github.fetchedRepositoryScope === "owned" ? "OWNED REPOSITORIES " : "REPOSITORIES "' \ "the repository heading does not follow the fetched scope" assert_contains '"No repositories loaded."' \ "the repository empty state still claims a scope" + +echo "panel source tests passed" diff --git a/tests/service-source-test.sh b/tests/service-source-test.sh index 75c9b6f..b76d14f 100755 --- a/tests/service-source-test.sh +++ b/tests/service-source-test.sh @@ -14,6 +14,8 @@ assert_contains '"--repository-scope", repositoryMode()' \ "the repository scope setting is not passed to the helper" assert_contains 'fetchedRepositoryScope = String(data.repositoryScope || "owned");' \ "the panel cannot tell which scope the payload was fetched with" +assert_contains $'if (value === "all repositories")\n return "all";' \ + "the full Actions scan does not require an exact setting match" assert_not_contains() { [[ $SERVICE_SOURCE != *"$1"* ]] || fail "$2"