@stacksjs/github 0.70.366 → 0.70.367

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ alerts, kanban→PR links, etc.).
11
11
 
12
12
  list of orgs, with on-disk caching and stale-while-revalidate semantics.
13
13
 
14
- - Lower-level helpers (`fetchAllRepos`, `fetchRepoStatus`, `fetchBotPRCounts`,
14
+ - Lower-level helpers (`fetchAllRepos`, `fetchRepoStatus`, `fetchPullRequestCounts`,
15
15
 
16
16
  `fetchRepoActiveRuns`) for callers that need a single dimension.
17
17
 
package/dist/bots.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  /**
2
- * Count open PRs authored by a GitHub App across every repo in an org.
2
+ * Count open PRs matching GitHub search qualifiers across every repo in an org.
3
3
  * Returns a map keyed by `owner/repo` so callers can attribute counts back
4
- * to the right repo card. Used to surface Renovate / GitHub Actions bot
5
- * traffic in the dashboard.
4
+ * to the right repo card.
5
+ */
6
+ export declare function fetchPullRequestCounts(org: string, qualifiers: string): Promise<Map<string, number>>;
7
+ /**
8
+ * Count open pull requests authored by a GitHub App.
9
+ *
10
+ * @deprecated Prefer {@link fetchPullRequestCounts} for new integrations so
11
+ * branch-based automation such as buddy-bot can be identified accurately.
6
12
  */
7
13
  export declare function fetchBotPRCounts(org: string, authorSlug: string): Promise<Map<string, number>>;
package/dist/bots.js CHANGED
@@ -1 +1 @@
1
- import{ghFetch,GITHUB_API}from"./client";export async function fetchBotPRCounts(org,authorSlug){const counts=new Map;let page=1;while(!0){const q=`is:pr is:open org:${org} author:app/${authorSlug}`,res=await ghFetch(`${GITHUB_API}/search/issues?q=${encodeURIComponent(q)}&per_page=100&page=${page}`);if(!res.ok)break;const data=await res.json();if(!data.items||data.items.length===0)break;for(const item of data.items){const fullName=item.repository_url.replace(`${GITHUB_API}/repos/`,"");counts.set(fullName,(counts.get(fullName)??0)+1)}if(data.items.length<100)break;page++}return counts}
1
+ import{ghFetch,GITHUB_API}from"./client";export async function fetchPullRequestCounts(org,qualifiers){const counts=new Map;let page=1;while(!0){const q=`is:pr is:open org:${org} ${qualifiers}`,res=await ghFetch(`${GITHUB_API}/search/issues?q=${encodeURIComponent(q)}&per_page=100&page=${page}`);if(!res.ok)break;const data=await res.json();if(!data.items||data.items.length===0)break;for(const item of data.items){const fullName=item.repository_url.replace(`${GITHUB_API}/repos/`,"");counts.set(fullName,(counts.get(fullName)??0)+1)}if(data.items.length<100)break;page++}return counts}export function fetchBotPRCounts(org,authorSlug){return fetchPullRequestCounts(org,`author:app/${authorSlug}`)}
package/dist/dashboard.js CHANGED
@@ -1 +1 @@
1
- import{fetchBotPRCounts}from"./bots";import{mapWithConcurrency}from"./client";import{fetchAllRepos}from"./repos";import{fetchRepoActiveRuns}from"./runners";import{fetchRepoStatus}from"./runs";const DEFAULT_TTL_MS=30000,DEFAULT_CACHE_PATH=".cache/dashboard.json",DEFAULT_RUNNER_CAP=20,caches=new Map;function entryFor(path){let e=caches.get(path);if(!e){e={data:null,savedAt:0,inflight:null,diskLoaded:!1};caches.set(path,e)}return e}async function loadCacheFromDisk(entry,path){if(entry.diskLoaded)return;entry.diskLoaded=!0;try{const file=Bun.file(path);if(!await file.exists())return;const stored=await file.json();entry.data=stored.data;entry.savedAt=stored.savedAt}catch{}}async function saveCacheToDisk(path,data,savedAt){try{await Bun.write(path,JSON.stringify({data,savedAt}))}catch(err){console.warn("[github/dashboard] cache write failed:",err)}}async function buildDashboardData(opts){const orgs=opts.orgs,runnerCaps=opts.runnerCaps??{},defaultRunnerCap=opts.defaultRunnerCap??DEFAULT_RUNNER_CAP,repos=await fetchAllRepos(orgs,opts.ignoreRepos),statuses=await mapWithConcurrency(repos,8,(r)=>fetchRepoStatus(r.owner,r.name,r.default_branch)),prCountMaps=await Promise.all(orgs.flatMap((org)=>[fetchBotPRCounts(org,"renovate").then((m)=>({type:"renovate",map:m})),fetchBotPRCounts(org,"github-actions").then((m)=>({type:"actions",map:m}))])),renovateCounts=new Map,actionsCounts=new Map;for(const{type,map}of prCountMaps){const target=type==="renovate"?renovateCounts:actionsCounts;for(const[k,v]of map)target.set(k,(target.get(k)??0)+v)}for(const s of statuses){const rCount=renovateCounts.get(s.fullName)??0,aCount=actionsCounts.get(s.fullName)??0;s.renovatePRs=rCount;s.actionsPRs=aCount;if(rCount>0)s.renovatePRsUrl=`https://github.com/${s.fullName}/pulls?q=${encodeURIComponent("is:pr is:open author:app/renovate")}`;if(aCount>0)s.actionsPRsUrl=`https://github.com/${s.fullName}/pulls?q=${encodeURIComponent("is:pr is:open author:app/github-actions")}`}const runnerCounts=await mapWithConcurrency(repos,8,async(r)=>({owner:r.owner,...await fetchRepoActiveRuns(r.owner,r.name)})),runners={};for(const org of orgs)runners[org]={running:0,queued:0,cap:runnerCaps[org]??defaultRunnerCap};for(const c of runnerCounts){if(!runners[c.owner])runners[c.owner]={running:0,queued:0,cap:runnerCaps[c.owner]??defaultRunnerCap};runners[c.owner].running+=c.running;runners[c.owner].queued+=c.queued}const order={failure:0,error:1,pending:2,success:3,no_runs:4};statuses.sort((a,b)=>(order[a.status]??5)-(order[b.status]??5));return{repos:statuses,fetchedAt:new Date().toISOString(),total:statuses.length,passing:statuses.filter((r)=>r.status==="success").length,failing:statuses.filter((r)=>r.status==="failure"||r.status==="error").length,pending:statuses.filter((r)=>r.status==="pending").length,noRuns:statuses.filter((r)=>r.status==="no_runs").length,runners}}export async function getDashboardData(opts){const ttl=opts.cacheTtlMs??DEFAULT_TTL_MS,path=opts.cachePath??DEFAULT_CACHE_PATH,entry=entryFor(path);await loadCacheFromDisk(entry,path);const now=Date.now();if(entry.data){if(now-entry.savedAt>=ttl&&!entry.inflight){entry.inflight=buildDashboardData(opts).then(async(data)=>{entry.data=data;entry.savedAt=Date.now();await saveCacheToDisk(path,data,entry.savedAt);return data}).finally(()=>{entry.inflight=null});entry.inflight.catch((err)=>console.warn("[github/dashboard] refresh failed:",err))}return entry.data}if(!entry.inflight)entry.inflight=buildDashboardData(opts).then(async(data)=>{entry.data=data;entry.savedAt=Date.now();await saveCacheToDisk(path,data,entry.savedAt);return data}).finally(()=>{entry.inflight=null});return entry.inflight}export function clearDashboardCache(cachePath=DEFAULT_CACHE_PATH){caches.delete(cachePath)}
1
+ import{fetchPullRequestCounts}from"./bots";import{mapWithConcurrency}from"./client";import{fetchAllRepos}from"./repos";import{fetchRepoActiveRuns}from"./runners";import{fetchRepoStatus}from"./runs";const DEFAULT_TTL_MS=30000,DEFAULT_CACHE_PATH=".cache/dashboard.json",DEFAULT_RUNNER_CAP=20,DASHBOARD_CACHE_VERSION=2,caches=new Map;function entryFor(path){let e=caches.get(path);if(!e){e={data:null,savedAt:0,inflight:null,diskLoaded:!1};caches.set(path,e)}return e}async function loadCacheFromDisk(entry,path){if(entry.diskLoaded)return;entry.diskLoaded=!0;try{const file=Bun.file(path);if(!await file.exists())return;const stored=await file.json();if(stored.version!==DASHBOARD_CACHE_VERSION)return;entry.data=stored.data;entry.savedAt=stored.savedAt}catch{}}async function saveCacheToDisk(path,data,savedAt){try{await Bun.write(path,JSON.stringify({version:DASHBOARD_CACHE_VERSION,data,savedAt}))}catch(err){console.warn("[github/dashboard] cache write failed:",err)}}async function buildDashboardData(opts){const orgs=opts.orgs,runnerCaps=opts.runnerCaps??{},defaultRunnerCap=opts.defaultRunnerCap??DEFAULT_RUNNER_CAP,repos=await fetchAllRepos(orgs,opts.ignoreRepos),statuses=await mapWithConcurrency(repos,8,(r)=>fetchRepoStatus(r.owner,r.name,r.default_branch)),prCountMaps=await Promise.all(orgs.flatMap((org)=>[fetchPullRequestCounts(org,"head:buddy-bot").then((m)=>({type:"buddy-bot",map:m})),fetchPullRequestCounts(org,"author:app/github-actions -head:buddy-bot").then((m)=>({type:"actions",map:m}))])),buddyBotCounts=new Map,actionsCounts=new Map;for(const{type,map}of prCountMaps){const target=type==="buddy-bot"?buddyBotCounts:actionsCounts;for(const[k,v]of map)target.set(k,(target.get(k)??0)+v)}for(const s of statuses){const buddyBotCount=buddyBotCounts.get(s.fullName)??0,aCount=actionsCounts.get(s.fullName)??0;s.buddyBotPRs=buddyBotCount;s.actionsPRs=aCount;if(buddyBotCount>0)s.buddyBotPRsUrl=`https://github.com/${s.fullName}/pulls?q=${encodeURIComponent("is:pr is:open head:buddy-bot")}`;if(aCount>0)s.actionsPRsUrl=`https://github.com/${s.fullName}/pulls?q=${encodeURIComponent("is:pr is:open author:app/github-actions -head:buddy-bot")}`}const runnerCounts=await mapWithConcurrency(repos,8,async(r)=>({owner:r.owner,...await fetchRepoActiveRuns(r.owner,r.name)})),runners={};for(const org of orgs)runners[org]={running:0,queued:0,cap:runnerCaps[org]??defaultRunnerCap};for(const c of runnerCounts){if(!runners[c.owner])runners[c.owner]={running:0,queued:0,cap:runnerCaps[c.owner]??defaultRunnerCap};runners[c.owner].running+=c.running;runners[c.owner].queued+=c.queued}const order={failure:0,error:1,pending:2,success:3,no_runs:4};statuses.sort((a,b)=>(order[a.status]??5)-(order[b.status]??5));return{repos:statuses,fetchedAt:new Date().toISOString(),total:statuses.length,passing:statuses.filter((r)=>r.status==="success").length,failing:statuses.filter((r)=>r.status==="failure"||r.status==="error").length,pending:statuses.filter((r)=>r.status==="pending").length,noRuns:statuses.filter((r)=>r.status==="no_runs").length,runners}}export async function getDashboardData(opts){const ttl=opts.cacheTtlMs??DEFAULT_TTL_MS,path=opts.cachePath??DEFAULT_CACHE_PATH,entry=entryFor(path);await loadCacheFromDisk(entry,path);const now=Date.now();if(entry.data){if(now-entry.savedAt>=ttl&&!entry.inflight){entry.inflight=buildDashboardData(opts).then(async(data)=>{entry.data=data;entry.savedAt=Date.now();await saveCacheToDisk(path,data,entry.savedAt);return data}).finally(()=>{entry.inflight=null});entry.inflight.catch((err)=>console.warn("[github/dashboard] refresh failed:",err))}return entry.data}if(!entry.inflight)entry.inflight=buildDashboardData(opts).then(async(data)=>{entry.data=data;entry.savedAt=Date.now();await saveCacheToDisk(path,data,entry.savedAt);return data}).finally(()=>{entry.inflight=null});return entry.inflight}export function clearDashboardCache(cachePath=DEFAULT_CACHE_PATH){caches.delete(cachePath)}
package/dist/index.d.ts CHANGED
@@ -32,7 +32,7 @@ export type {
32
32
  * `fetchRepoStatus`, …) are also exported so other surfaces can pull a
33
33
  * single dimension without pulling the whole snapshot through.
34
34
  */
35
- export { fetchBotPRCounts } from './bots';
35
+ export { fetchBotPRCounts, fetchPullRequestCounts } from './bots';
36
36
  export { ghFetch, ghHeaders, githubHeaders, githubJson, githubRequest, GITHUB_API, mapWithConcurrency } from './client';
37
37
  export { clearDashboardCache, getDashboardData } from './dashboard';
38
38
  export { detectNewlyFailedRuns } from './failure-detector';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export{fetchBotPRCounts}from"./bots";export{ghFetch,ghHeaders,githubHeaders,githubJson,githubRequest,GITHUB_API,mapWithConcurrency}from"./client";export{clearDashboardCache,getDashboardData}from"./dashboard";export{detectNewlyFailedRuns}from"./failure-detector";export{fetchAllRepos}from"./repos";export{fetchRepoActiveRuns}from"./runners";export{fetchRunJobs,fetchWorkflowRuns}from"./run-history";export{detectRunnerPressure}from"./runner-pressure-detector";export{fetchFailedJobs,fetchRepoStatus}from"./runs";export{createPullRequestWithFiles,fetchRepositoryFile,fetchRepositoryTree}from"./pull-requests";
1
+ export{fetchBotPRCounts,fetchPullRequestCounts}from"./bots";export{ghFetch,ghHeaders,githubHeaders,githubJson,githubRequest,GITHUB_API,mapWithConcurrency}from"./client";export{clearDashboardCache,getDashboardData}from"./dashboard";export{detectNewlyFailedRuns}from"./failure-detector";export{fetchAllRepos}from"./repos";export{fetchRepoActiveRuns}from"./runners";export{fetchRunJobs,fetchWorkflowRuns}from"./run-history";export{detectRunnerPressure}from"./runner-pressure-detector";export{fetchFailedJobs,fetchRepoStatus}from"./runs";export{createPullRequestWithFiles,fetchRepositoryFile,fetchRepositoryTree}from"./pull-requests";
package/dist/runs.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import{ghFetch,GITHUB_API}from"./client";async function fillLatestCommit(base,owner,name,branch){try{const res=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/commits?sha=${branch}&per_page=1`);if(!res.ok)return;const commits=await res.json(),[c]=commits;if(!c)return;base.commitSha=c.sha.slice(0,7);base.commitMessage=c.commit.message.split(`
2
- `)[0]??null;base.commitUrl=`https://github.com/${owner}/${name}/commit/${c.sha}`;base.commitAuthor=c.commit.author?.name??c.author?.login??null;base.updatedAt=c.commit.author?.date??null}catch{}}export async function fetchFailedJobs(owner,name,runId){try{const res=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest`);if(!res.ok)return[];return(await res.json()).jobs.filter((j)=>j.conclusion&&j.conclusion!=="success"&&j.conclusion!=="skipped").map((j)=>({name:j.name,conclusion:j.conclusion,url:j.html_url}))}catch{return[]}}export async function fetchRepoStatus(owner,name,defaultBranch){const base={name,owner,fullName:`${owner}/${name}`,url:`https://github.com/${owner}/${name}`,defaultBranch,status:"no_runs",conclusion:null,workflowName:null,commitSha:null,commitMessage:null,commitUrl:null,commitAuthor:null,commitCount:null,updatedAt:null,runUrl:null,failedJobs:[],renovatePRs:0,renovatePRsUrl:null,actionsPRs:0,actionsPRsUrl:null};try{const res=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs?branch=${defaultBranch}&event=push&per_page=1`);if(!res.ok){base.status="error";await fillLatestCommit(base,owner,name,defaultBranch);return base}const data=await res.json(),[run]=data.workflow_runs??[];if(!run){await fillLatestCommit(base,owner,name,defaultBranch);return base}base.workflowName=run.name;base.commitSha=run.head_sha.slice(0,7);base.commitMessage=run.head_commit?.message.split(`
2
+ `)[0]??null;base.commitUrl=`https://github.com/${owner}/${name}/commit/${c.sha}`;base.commitAuthor=c.commit.author?.name??c.author?.login??null;base.updatedAt=c.commit.author?.date??null}catch{}}export async function fetchFailedJobs(owner,name,runId){try{const res=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest`);if(!res.ok)return[];return(await res.json()).jobs.filter((j)=>j.conclusion&&j.conclusion!=="success"&&j.conclusion!=="skipped").map((j)=>({name:j.name,conclusion:j.conclusion,url:j.html_url}))}catch{return[]}}export async function fetchRepoStatus(owner,name,defaultBranch){const base={name,owner,fullName:`${owner}/${name}`,url:`https://github.com/${owner}/${name}`,defaultBranch,status:"no_runs",conclusion:null,workflowName:null,commitSha:null,commitMessage:null,commitUrl:null,commitAuthor:null,commitCount:null,updatedAt:null,runUrl:null,failedJobs:[],buddyBotPRs:0,buddyBotPRsUrl:null,actionsPRs:0,actionsPRsUrl:null};try{const res=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs?branch=${defaultBranch}&event=push&per_page=1`);if(!res.ok){base.status="error";await fillLatestCommit(base,owner,name,defaultBranch);return base}const data=await res.json(),[run]=data.workflow_runs??[];if(!run){await fillLatestCommit(base,owner,name,defaultBranch);return base}base.workflowName=run.name;base.commitSha=run.head_sha.slice(0,7);base.commitMessage=run.head_commit?.message.split(`
3
3
  `)[0]??null;base.commitUrl=`https://github.com/${owner}/${name}/commit/${run.head_sha}`;base.commitAuthor=run.head_commit?.author?.name??run.actor?.login??null;base.updatedAt=run.updated_at;base.runUrl=run.html_url;if(run.status==="completed"){base.status=run.conclusion==="success"?"success":"failure";base.conclusion=run.conclusion;if(base.status==="failure")base.failedJobs=await fetchFailedJobs(owner,name,run.id)}else{base.status="pending";base.conclusion=run.status}}catch{base.status="error"}return base}
package/dist/types.d.ts CHANGED
@@ -20,8 +20,8 @@ export declare interface RepoStatus {
20
20
  updatedAt: string | null
21
21
  runUrl: string | null
22
22
  failedJobs: FailedJob[]
23
- renovatePRs: number
24
- renovatePRsUrl: string | null
23
+ buddyBotPRs: number
24
+ buddyBotPRsUrl: string | null
25
25
  actionsPRs: number
26
26
  actionsPRsUrl: string | null
27
27
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/github",
3
3
  "type": "module",
4
- "version": "0.70.366",
4
+ "version": "0.70.367",
5
5
  "description": "GitHub API client used by Stacks framework features (dashboard CI surface, notifications, runner alerts).",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [