@stacksjs/github 0.70.258 → 0.70.260
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/dist/bots.js +1 -21
- package/dist/client.js +1 -92
- package/dist/dashboard.js +1 -110
- package/dist/failure-detector.js +1 -67
- package/dist/index.js +1 -10
- package/dist/pull-requests.js +1 -83
- package/dist/repos.js +1 -26
- package/dist/run-history.js +2 -63
- package/dist/runner-pressure-detector.js +1 -27
- package/dist/runners.js +1 -26
- package/dist/runs.js +3 -84
- package/package.json +1 -1
package/dist/bots.js
CHANGED
|
@@ -1,21 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export async function fetchBotPRCounts(org, authorSlug) {
|
|
3
|
-
const counts = new Map;
|
|
4
|
-
let page = 1;
|
|
5
|
-
while (!0) {
|
|
6
|
-
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}`);
|
|
7
|
-
if (!res.ok)
|
|
8
|
-
break;
|
|
9
|
-
const data = await res.json();
|
|
10
|
-
if (!data.items || data.items.length === 0)
|
|
11
|
-
break;
|
|
12
|
-
for (const item of data.items) {
|
|
13
|
-
const fullName = item.repository_url.replace(`${GITHUB_API}/repos/`, "");
|
|
14
|
-
counts.set(fullName, (counts.get(fullName) ?? 0) + 1);
|
|
15
|
-
}
|
|
16
|
-
if (data.items.length < 100)
|
|
17
|
-
break;
|
|
18
|
-
page++;
|
|
19
|
-
}
|
|
20
|
-
return counts;
|
|
21
|
-
}
|
|
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}
|
package/dist/client.js
CHANGED
|
@@ -1,92 +1 @@
|
|
|
1
|
-
export const GITHUB_API
|
|
2
|
-
function getToken() {
|
|
3
|
-
const token = process.env.GITHUB_TOKEN;
|
|
4
|
-
if (!token)
|
|
5
|
-
throw Error("GITHUB_TOKEN environment variable is required");
|
|
6
|
-
return token;
|
|
7
|
-
}
|
|
8
|
-
function resolveToken(token) {
|
|
9
|
-
if (token)
|
|
10
|
-
return token;
|
|
11
|
-
return getToken();
|
|
12
|
-
}
|
|
13
|
-
export function ghHeaders() {
|
|
14
|
-
return {
|
|
15
|
-
Authorization: `Bearer ${getToken()}`,
|
|
16
|
-
Accept: "application/vnd.github+json",
|
|
17
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
export function githubHeaders(token) {
|
|
21
|
-
return {
|
|
22
|
-
Authorization: `Bearer ${resolveToken(token)}`,
|
|
23
|
-
Accept: "application/vnd.github+json",
|
|
24
|
-
"X-GitHub-Api-Version": "2022-11-28"
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
function retryDelay(res, attempt) {
|
|
28
|
-
if (!(res.status === 429 || res.status === 403 && (res.headers.get("x-ratelimit-remaining") === "0" || !!res.headers.get("retry-after"))))
|
|
29
|
-
return null;
|
|
30
|
-
const retryAfterHeader = res.headers.get("retry-after"), resetHeader = res.headers.get("x-ratelimit-reset");
|
|
31
|
-
if (retryAfterHeader)
|
|
32
|
-
return Number(retryAfterHeader) * 1000;
|
|
33
|
-
if (resetHeader)
|
|
34
|
-
return Math.max(0, Number(resetHeader) * 1000 - Date.now()) + 500;
|
|
35
|
-
return 1000 * 2 ** attempt;
|
|
36
|
-
}
|
|
37
|
-
export async function githubRequest(path, init = {}, options = {}, attempt = 0) {
|
|
38
|
-
const fetcher = options.fetch ?? globalThis.fetch, url = path.startsWith("http") ? path : `${options.apiUrl ?? GITHUB_API}${path.startsWith("/") ? path : `/${path}`}`, response = await fetcher(url, {
|
|
39
|
-
...init,
|
|
40
|
-
headers: { ...githubHeaders(options.token), ...init.headers }
|
|
41
|
-
});
|
|
42
|
-
if (response.ok || attempt >= 3)
|
|
43
|
-
return response;
|
|
44
|
-
const waitMs = retryDelay(response, attempt);
|
|
45
|
-
if (waitMs === null)
|
|
46
|
-
return response;
|
|
47
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(waitMs, 30000)));
|
|
48
|
-
return githubRequest(path, init, options, attempt + 1);
|
|
49
|
-
}
|
|
50
|
-
export async function githubJson(path, init = {}, options = {}) {
|
|
51
|
-
const response = await githubRequest(path, init, options);
|
|
52
|
-
if (!response.ok) {
|
|
53
|
-
const payload = await response.text();
|
|
54
|
-
let message = payload;
|
|
55
|
-
try {
|
|
56
|
-
message = JSON.parse(payload).message || payload;
|
|
57
|
-
} catch {}
|
|
58
|
-
throw Error(`GitHub API ${response.status}: ${message || response.statusText}`);
|
|
59
|
-
}
|
|
60
|
-
if (response.status === 204)
|
|
61
|
-
return;
|
|
62
|
-
return await response.json();
|
|
63
|
-
}
|
|
64
|
-
export async function ghFetch(url, attempt = 0) {
|
|
65
|
-
const res = await fetch(url, { headers: ghHeaders() });
|
|
66
|
-
if (res.ok || attempt >= 3)
|
|
67
|
-
return res;
|
|
68
|
-
if (!(res.status === 429 || res.status === 403 && (res.headers.get("x-ratelimit-remaining") === "0" || res.headers.get("retry-after"))))
|
|
69
|
-
return res;
|
|
70
|
-
const retryAfterHeader = res.headers.get("retry-after"), resetHeader = res.headers.get("x-ratelimit-reset");
|
|
71
|
-
let waitMs = 1000 * 2 ** attempt;
|
|
72
|
-
if (retryAfterHeader)
|
|
73
|
-
waitMs = Number(retryAfterHeader) * 1000;
|
|
74
|
-
else if (resetHeader)
|
|
75
|
-
waitMs = Math.max(0, Number(resetHeader) * 1000 - Date.now()) + 500;
|
|
76
|
-
await new Promise((r) => setTimeout(r, Math.min(waitMs, 30000)));
|
|
77
|
-
return ghFetch(url, attempt + 1);
|
|
78
|
-
}
|
|
79
|
-
export async function mapWithConcurrency(items, limit, fn) {
|
|
80
|
-
const results = Array.from({ length: items.length });
|
|
81
|
-
let next = 0;
|
|
82
|
-
async function worker() {
|
|
83
|
-
while (!0) {
|
|
84
|
-
const i = next++;
|
|
85
|
-
if (i >= items.length)
|
|
86
|
-
return;
|
|
87
|
-
results[i] = await fn(items[i]);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
91
|
-
return results;
|
|
92
|
-
}
|
|
1
|
+
export const GITHUB_API="https://api.github.com";function getToken(){const token=process.env.GITHUB_TOKEN;if(!token)throw Error("GITHUB_TOKEN environment variable is required");return token}function resolveToken(token){if(token)return token;return getToken()}export function ghHeaders(){return{Authorization:`Bearer ${getToken()}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"}}export function githubHeaders(token){return{Authorization:`Bearer ${resolveToken(token)}`,Accept:"application/vnd.github+json","X-GitHub-Api-Version":"2022-11-28"}}function retryDelay(res,attempt){if(!(res.status===429||res.status===403&&(res.headers.get("x-ratelimit-remaining")==="0"||!!res.headers.get("retry-after"))))return null;const retryAfterHeader=res.headers.get("retry-after"),resetHeader=res.headers.get("x-ratelimit-reset");if(retryAfterHeader)return Number(retryAfterHeader)*1000;if(resetHeader)return Math.max(0,Number(resetHeader)*1000-Date.now())+500;return 1000*2**attempt}export async function githubRequest(path,init={},options={},attempt=0){const fetcher=options.fetch??globalThis.fetch,url=path.startsWith("http")?path:`${options.apiUrl??GITHUB_API}${path.startsWith("/")?path:`/${path}`}`,response=await fetcher(url,{...init,headers:{...githubHeaders(options.token),...init.headers}});if(response.ok||attempt>=3)return response;const waitMs=retryDelay(response,attempt);if(waitMs===null)return response;await new Promise((resolve)=>setTimeout(resolve,Math.min(waitMs,30000)));return githubRequest(path,init,options,attempt+1)}export async function githubJson(path,init={},options={}){const response=await githubRequest(path,init,options);if(!response.ok){const payload=await response.text();let message=payload;try{message=JSON.parse(payload).message||payload}catch{}throw Error(`GitHub API ${response.status}: ${message||response.statusText}`)}if(response.status===204)return;return await response.json()}export async function ghFetch(url,attempt=0){const res=await fetch(url,{headers:ghHeaders()});if(res.ok||attempt>=3)return res;if(!(res.status===429||res.status===403&&(res.headers.get("x-ratelimit-remaining")==="0"||res.headers.get("retry-after"))))return res;const retryAfterHeader=res.headers.get("retry-after"),resetHeader=res.headers.get("x-ratelimit-reset");let waitMs=1000*2**attempt;if(retryAfterHeader)waitMs=Number(retryAfterHeader)*1000;else if(resetHeader)waitMs=Math.max(0,Number(resetHeader)*1000-Date.now())+500;await new Promise((r)=>setTimeout(r,Math.min(waitMs,30000)));return ghFetch(url,attempt+1)}export async function mapWithConcurrency(items,limit,fn){const results=Array.from({length:items.length});let next=0;async function worker(){while(!0){const i=next++;if(i>=items.length)return;results[i]=await fn(items[i])}}await Promise.all(Array.from({length:Math.min(limit,items.length)},()=>worker()));return results}
|
package/dist/dashboard.js
CHANGED
|
@@ -1,110 +1 @@
|
|
|
1
|
-
import { fetchBotPRCounts }
|
|
2
|
-
import { mapWithConcurrency } from "./client";
|
|
3
|
-
import { fetchAllRepos } from "./repos";
|
|
4
|
-
import { fetchRepoActiveRuns } from "./runners";
|
|
5
|
-
import { fetchRepoStatus } from "./runs";
|
|
6
|
-
const DEFAULT_TTL_MS = 30000, DEFAULT_CACHE_PATH = ".cache/dashboard.json", DEFAULT_RUNNER_CAP = 20, caches = new Map;
|
|
7
|
-
function entryFor(path) {
|
|
8
|
-
let e = caches.get(path);
|
|
9
|
-
if (!e) {
|
|
10
|
-
e = { data: null, savedAt: 0, inflight: null, diskLoaded: !1 };
|
|
11
|
-
caches.set(path, e);
|
|
12
|
-
}
|
|
13
|
-
return e;
|
|
14
|
-
}
|
|
15
|
-
async function loadCacheFromDisk(entry, path) {
|
|
16
|
-
if (entry.diskLoaded)
|
|
17
|
-
return;
|
|
18
|
-
entry.diskLoaded = !0;
|
|
19
|
-
try {
|
|
20
|
-
const file = Bun.file(path);
|
|
21
|
-
if (!await file.exists())
|
|
22
|
-
return;
|
|
23
|
-
const stored = await file.json();
|
|
24
|
-
entry.data = stored.data;
|
|
25
|
-
entry.savedAt = stored.savedAt;
|
|
26
|
-
} catch {}
|
|
27
|
-
}
|
|
28
|
-
async function saveCacheToDisk(path, data, savedAt) {
|
|
29
|
-
try {
|
|
30
|
-
await Bun.write(path, JSON.stringify({ data, savedAt }));
|
|
31
|
-
} catch (err) {
|
|
32
|
-
console.warn("[github/dashboard] cache write failed:", err);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
async function buildDashboardData(opts) {
|
|
36
|
-
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) => [
|
|
37
|
-
fetchBotPRCounts(org, "renovate").then((m) => ({ type: "renovate", map: m })),
|
|
38
|
-
fetchBotPRCounts(org, "github-actions").then((m) => ({ type: "actions", map: m }))
|
|
39
|
-
])), renovateCounts = new Map, actionsCounts = new Map;
|
|
40
|
-
for (const { type, map } of prCountMaps) {
|
|
41
|
-
const target = type === "renovate" ? renovateCounts : actionsCounts;
|
|
42
|
-
for (const [k, v] of map)
|
|
43
|
-
target.set(k, (target.get(k) ?? 0) + v);
|
|
44
|
-
}
|
|
45
|
-
for (const s of statuses) {
|
|
46
|
-
const rCount = renovateCounts.get(s.fullName) ?? 0, aCount = actionsCounts.get(s.fullName) ?? 0;
|
|
47
|
-
s.renovatePRs = rCount;
|
|
48
|
-
s.actionsPRs = aCount;
|
|
49
|
-
if (rCount > 0)
|
|
50
|
-
s.renovatePRsUrl = `https://github.com/${s.fullName}/pulls?q=${encodeURIComponent("is:pr is:open author:app/renovate")}`;
|
|
51
|
-
if (aCount > 0)
|
|
52
|
-
s.actionsPRsUrl = `https://github.com/${s.fullName}/pulls?q=${encodeURIComponent("is:pr is:open author:app/github-actions")}`;
|
|
53
|
-
}
|
|
54
|
-
const runnerCounts = await mapWithConcurrency(repos, 8, async (r) => ({
|
|
55
|
-
owner: r.owner,
|
|
56
|
-
...await fetchRepoActiveRuns(r.owner, r.name)
|
|
57
|
-
})), runners = {};
|
|
58
|
-
for (const org of orgs)
|
|
59
|
-
runners[org] = { running: 0, queued: 0, cap: runnerCaps[org] ?? defaultRunnerCap };
|
|
60
|
-
for (const c of runnerCounts) {
|
|
61
|
-
if (!runners[c.owner])
|
|
62
|
-
runners[c.owner] = { running: 0, queued: 0, cap: runnerCaps[c.owner] ?? defaultRunnerCap };
|
|
63
|
-
runners[c.owner].running += c.running;
|
|
64
|
-
runners[c.owner].queued += c.queued;
|
|
65
|
-
}
|
|
66
|
-
const order = { failure: 0, error: 1, pending: 2, success: 3, no_runs: 4 };
|
|
67
|
-
statuses.sort((a, b) => (order[a.status] ?? 5) - (order[b.status] ?? 5));
|
|
68
|
-
return {
|
|
69
|
-
repos: statuses,
|
|
70
|
-
fetchedAt: new Date().toISOString(),
|
|
71
|
-
total: statuses.length,
|
|
72
|
-
passing: statuses.filter((r) => r.status === "success").length,
|
|
73
|
-
failing: statuses.filter((r) => r.status === "failure" || r.status === "error").length,
|
|
74
|
-
pending: statuses.filter((r) => r.status === "pending").length,
|
|
75
|
-
noRuns: statuses.filter((r) => r.status === "no_runs").length,
|
|
76
|
-
runners
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
export async function getDashboardData(opts) {
|
|
80
|
-
const ttl = opts.cacheTtlMs ?? DEFAULT_TTL_MS, path = opts.cachePath ?? DEFAULT_CACHE_PATH, entry = entryFor(path);
|
|
81
|
-
await loadCacheFromDisk(entry, path);
|
|
82
|
-
const now = Date.now();
|
|
83
|
-
if (entry.data) {
|
|
84
|
-
if (now - entry.savedAt >= ttl && !entry.inflight) {
|
|
85
|
-
entry.inflight = buildDashboardData(opts).then(async (data) => {
|
|
86
|
-
entry.data = data;
|
|
87
|
-
entry.savedAt = Date.now();
|
|
88
|
-
await saveCacheToDisk(path, data, entry.savedAt);
|
|
89
|
-
return data;
|
|
90
|
-
}).finally(() => {
|
|
91
|
-
entry.inflight = null;
|
|
92
|
-
});
|
|
93
|
-
entry.inflight.catch((err) => console.warn("[github/dashboard] refresh failed:", err));
|
|
94
|
-
}
|
|
95
|
-
return entry.data;
|
|
96
|
-
}
|
|
97
|
-
if (!entry.inflight)
|
|
98
|
-
entry.inflight = buildDashboardData(opts).then(async (data) => {
|
|
99
|
-
entry.data = data;
|
|
100
|
-
entry.savedAt = Date.now();
|
|
101
|
-
await saveCacheToDisk(path, data, entry.savedAt);
|
|
102
|
-
return data;
|
|
103
|
-
}).finally(() => {
|
|
104
|
-
entry.inflight = null;
|
|
105
|
-
});
|
|
106
|
-
return entry.inflight;
|
|
107
|
-
}
|
|
108
|
-
export function clearDashboardCache(cachePath = DEFAULT_CACHE_PATH) {
|
|
109
|
-
caches.delete(cachePath);
|
|
110
|
-
}
|
|
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)}
|
package/dist/failure-detector.js
CHANGED
|
@@ -1,67 +1 @@
|
|
|
1
|
-
const DEFAULT_COOLDOWN_MS =
|
|
2
|
-
"failure",
|
|
3
|
-
"error",
|
|
4
|
-
"timed_out",
|
|
5
|
-
"startup_failure"
|
|
6
|
-
]);
|
|
7
|
-
function isFailed(repo) {
|
|
8
|
-
if (repo.status === "failure" || repo.status === "error")
|
|
9
|
-
return !0;
|
|
10
|
-
if (repo.conclusion && FAILED_CONCLUSIONS.has(repo.conclusion))
|
|
11
|
-
return !0;
|
|
12
|
-
return !1;
|
|
13
|
-
}
|
|
14
|
-
function isInFlight(repo) {
|
|
15
|
-
return repo.status === "pending";
|
|
16
|
-
}
|
|
17
|
-
export function detectNewlyFailedRuns(snapshot, previousStates, options = {}) {
|
|
18
|
-
const cooldownMs = options.cooldownMs ?? DEFAULT_COOLDOWN_MS, now = options.now ?? Date.now(), transitions = [];
|
|
19
|
-
for (const repo of snapshot.repos) {
|
|
20
|
-
if (!isFailed(repo) || isInFlight(repo))
|
|
21
|
-
continue;
|
|
22
|
-
const prev = previousStates.get(repo.fullName);
|
|
23
|
-
if (prev?.lastConclusion ? FAILED_CONCLUSIONS.has(prev.lastConclusion) || prev.lastConclusion === "failure" || prev.lastConclusion === "error" : !1) {
|
|
24
|
-
const currentRunId = parseRunIdFromUrl(repo.runUrl);
|
|
25
|
-
if (currentRunId !== null && prev?.lastRunId !== null && currentRunId === prev?.lastRunId)
|
|
26
|
-
continue;
|
|
27
|
-
if (prev?.lastNotifiedAt && isWithinCooldown(prev.lastNotifiedAt, cooldownMs, now))
|
|
28
|
-
continue;
|
|
29
|
-
transitions.push(toTransition(repo, prev?.lastConclusion ?? null));
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (prev?.lastNotifiedAt && isWithinCooldown(prev.lastNotifiedAt, cooldownMs, now))
|
|
33
|
-
continue;
|
|
34
|
-
transitions.push(toTransition(repo, prev?.lastConclusion ?? null));
|
|
35
|
-
}
|
|
36
|
-
return transitions;
|
|
37
|
-
}
|
|
38
|
-
function isWithinCooldown(lastNotifiedAt, cooldownMs, now) {
|
|
39
|
-
if (cooldownMs === 0)
|
|
40
|
-
return !1;
|
|
41
|
-
const ts = Date.parse(lastNotifiedAt);
|
|
42
|
-
if (Number.isNaN(ts))
|
|
43
|
-
return !1;
|
|
44
|
-
return now - ts < cooldownMs;
|
|
45
|
-
}
|
|
46
|
-
function parseRunIdFromUrl(runUrl) {
|
|
47
|
-
if (!runUrl)
|
|
48
|
-
return null;
|
|
49
|
-
const match = runUrl.match(/\/actions\/runs\/(\d+)/);
|
|
50
|
-
if (!match)
|
|
51
|
-
return null;
|
|
52
|
-
const id = Number(match[1]);
|
|
53
|
-
return Number.isFinite(id) ? id : null;
|
|
54
|
-
}
|
|
55
|
-
function toTransition(repo, previousConclusion) {
|
|
56
|
-
return {
|
|
57
|
-
repoFullName: repo.fullName,
|
|
58
|
-
conclusion: repo.conclusion ?? repo.status,
|
|
59
|
-
runId: parseRunIdFromUrl(repo.runUrl),
|
|
60
|
-
workflowName: repo.workflowName,
|
|
61
|
-
commitSha: repo.commitSha,
|
|
62
|
-
commitMessage: repo.commitMessage,
|
|
63
|
-
commitAuthor: repo.commitAuthor,
|
|
64
|
-
runUrl: repo.runUrl,
|
|
65
|
-
previousConclusion
|
|
66
|
-
};
|
|
67
|
-
}
|
|
1
|
+
const DEFAULT_COOLDOWN_MS=300000,FAILED_CONCLUSIONS=new Set(["failure","error","timed_out","startup_failure"]);function isFailed(repo){if(repo.status==="failure"||repo.status==="error")return!0;if(repo.conclusion&&FAILED_CONCLUSIONS.has(repo.conclusion))return!0;return!1}function isInFlight(repo){return repo.status==="pending"}export function detectNewlyFailedRuns(snapshot,previousStates,options={}){const cooldownMs=options.cooldownMs??DEFAULT_COOLDOWN_MS,now=options.now??Date.now(),transitions=[];for(const repo of snapshot.repos){if(!isFailed(repo)||isInFlight(repo))continue;const prev=previousStates.get(repo.fullName);if(prev?.lastConclusion?FAILED_CONCLUSIONS.has(prev.lastConclusion)||prev.lastConclusion==="failure"||prev.lastConclusion==="error":!1){const currentRunId=parseRunIdFromUrl(repo.runUrl);if(currentRunId!==null&&prev?.lastRunId!==null&¤tRunId===prev?.lastRunId)continue;if(prev?.lastNotifiedAt&&isWithinCooldown(prev.lastNotifiedAt,cooldownMs,now))continue;transitions.push(toTransition(repo,prev?.lastConclusion??null));continue}if(prev?.lastNotifiedAt&&isWithinCooldown(prev.lastNotifiedAt,cooldownMs,now))continue;transitions.push(toTransition(repo,prev?.lastConclusion??null))}return transitions}function isWithinCooldown(lastNotifiedAt,cooldownMs,now){if(cooldownMs===0)return!1;const ts=Date.parse(lastNotifiedAt);if(Number.isNaN(ts))return!1;return now-ts<cooldownMs}function parseRunIdFromUrl(runUrl){if(!runUrl)return null;const match=runUrl.match(/\/actions\/runs\/(\d+)/);if(!match)return null;const id=Number(match[1]);return Number.isFinite(id)?id:null}function toTransition(repo,previousConclusion){return{repoFullName:repo.fullName,conclusion:repo.conclusion??repo.status,runId:parseRunIdFromUrl(repo.runUrl),workflowName:repo.workflowName,commitSha:repo.commitSha,commitMessage:repo.commitMessage,commitAuthor:repo.commitAuthor,runUrl:repo.runUrl,previousConclusion}}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
export { ghFetch, ghHeaders, githubHeaders, githubJson, githubRequest, GITHUB_API, mapWithConcurrency } from "./client";
|
|
3
|
-
export { clearDashboardCache, getDashboardData } from "./dashboard";
|
|
4
|
-
export { detectNewlyFailedRuns } from "./failure-detector";
|
|
5
|
-
export { fetchAllRepos } from "./repos";
|
|
6
|
-
export { fetchRepoActiveRuns } from "./runners";
|
|
7
|
-
export { fetchRunJobs, fetchWorkflowRuns } from "./run-history";
|
|
8
|
-
export { detectRunnerPressure } from "./runner-pressure-detector";
|
|
9
|
-
export { fetchFailedJobs, fetchRepoStatus } from "./runs";
|
|
10
|
-
export { createPullRequestWithFiles, fetchRepositoryFile, fetchRepositoryTree } from "./pull-requests";
|
|
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";
|
package/dist/pull-requests.js
CHANGED
|
@@ -1,83 +1 @@
|
|
|
1
|
-
import { githubJson } from "
|
|
2
|
-
function repoPath(owner, repo) {
|
|
3
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(owner) || !/^[A-Za-z0-9_.-]+$/.test(repo))
|
|
4
|
-
throw Error("GitHub owner and repository names contain invalid characters");
|
|
5
|
-
return `/repos/${owner}/${repo}`;
|
|
6
|
-
}
|
|
7
|
-
function validateBranch(branch) {
|
|
8
|
-
if (!branch || branch.length > 240 || branch.startsWith("/") || branch.endsWith("/") || branch.includes("..") || /[~^:?*[\\\s]/.test(branch))
|
|
9
|
-
throw Error(`Invalid Git branch name: ${branch}`);
|
|
10
|
-
}
|
|
11
|
-
function validateFiles(files) {
|
|
12
|
-
if (!files.length)
|
|
13
|
-
throw Error("At least one file change is required");
|
|
14
|
-
if (files.length > 100)
|
|
15
|
-
throw Error("A pull request may change at most 100 files");
|
|
16
|
-
const seen = new Set;
|
|
17
|
-
let bytes = 0;
|
|
18
|
-
for (const file of files) {
|
|
19
|
-
if (!file.path || file.path.startsWith("/") || file.path.includes("\\") || file.path.split("/").includes(".."))
|
|
20
|
-
throw Error(`Unsafe repository path: ${file.path}`);
|
|
21
|
-
if (seen.has(file.path))
|
|
22
|
-
throw Error(`Duplicate repository path: ${file.path}`);
|
|
23
|
-
seen.add(file.path);
|
|
24
|
-
bytes += Buffer.byteLength(file.content);
|
|
25
|
-
}
|
|
26
|
-
if (bytes > 5242880)
|
|
27
|
-
throw Error("Combined file content exceeds the 5 MiB safety limit");
|
|
28
|
-
}
|
|
29
|
-
export async function fetchRepositoryTree(owner, repo, ref, options = {}) {
|
|
30
|
-
const payload = await githubJson(`${repoPath(owner, repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`, {}, options);
|
|
31
|
-
return { sha: payload.sha, truncated: !!payload.truncated, entries: payload.tree ?? [] };
|
|
32
|
-
}
|
|
33
|
-
export async function fetchRepositoryFile(owner, repo, path, ref, options = {}) {
|
|
34
|
-
validateFiles([{ path, content: "" }]);
|
|
35
|
-
const payload = await githubJson(`${repoPath(owner, repo)}/contents/${path.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`, {}, options);
|
|
36
|
-
if (payload.type !== "file" || payload.encoding !== "base64" || typeof payload.content !== "string")
|
|
37
|
-
throw Error(`GitHub path is not a base64 encoded file: ${path}`);
|
|
38
|
-
const maxBytes = options.maxBytes ?? 262144;
|
|
39
|
-
if ((payload.size ?? 0) > maxBytes)
|
|
40
|
-
throw Error(`GitHub file exceeds the ${maxBytes} byte safety limit: ${path}`);
|
|
41
|
-
const content = Buffer.from(payload.content.replace(/\n/g, ""), "base64");
|
|
42
|
-
if (content.byteLength > maxBytes)
|
|
43
|
-
throw Error(`Decoded GitHub file exceeds the ${maxBytes} byte safety limit: ${path}`);
|
|
44
|
-
return content.toString("utf8");
|
|
45
|
-
}
|
|
46
|
-
export async function createPullRequestWithFiles(options) {
|
|
47
|
-
validateBranch(options.branch);
|
|
48
|
-
validateFiles(options.files);
|
|
49
|
-
const basePath = repoPath(options.owner, options.repo), client = { token: options.token, apiUrl: options.apiUrl, fetch: options.fetch };
|
|
50
|
-
let base = options.base;
|
|
51
|
-
if (!base)
|
|
52
|
-
base = (await githubJson(basePath, {}, client)).default_branch;
|
|
53
|
-
if (!base)
|
|
54
|
-
throw Error("GitHub repository has no default branch");
|
|
55
|
-
const reference = await githubJson(`${basePath}/git/ref/heads/${encodeURIComponent(base)}`, {}, client), baseCommit = await githubJson(`${basePath}/git/commits/${reference.object.sha}`, {}, client), blobs = await Promise.all(options.files.map(async (file) => {
|
|
56
|
-
const blob = await githubJson(`${basePath}/git/blobs`, {
|
|
57
|
-
method: "POST",
|
|
58
|
-
body: JSON.stringify({ content: file.content, encoding: "utf-8" })
|
|
59
|
-
}, client);
|
|
60
|
-
return { path: file.path, mode: "100644", type: "blob", sha: blob.sha };
|
|
61
|
-
})), tree = await githubJson(`${basePath}/git/trees`, {
|
|
62
|
-
method: "POST",
|
|
63
|
-
body: JSON.stringify({ base_tree: baseCommit.tree.sha, tree: blobs })
|
|
64
|
-
}, client), commit = await githubJson(`${basePath}/git/commits`, {
|
|
65
|
-
method: "POST",
|
|
66
|
-
body: JSON.stringify({ message: options.commitMessage, tree: tree.sha, parents: [reference.object.sha] })
|
|
67
|
-
}, client);
|
|
68
|
-
await githubJson(`${basePath}/git/refs`, {
|
|
69
|
-
method: "POST",
|
|
70
|
-
body: JSON.stringify({ ref: `refs/heads/${options.branch}`, sha: commit.sha })
|
|
71
|
-
}, client);
|
|
72
|
-
const pull = await githubJson(`${basePath}/pulls`, {
|
|
73
|
-
method: "POST",
|
|
74
|
-
body: JSON.stringify({
|
|
75
|
-
title: options.title,
|
|
76
|
-
body: options.body,
|
|
77
|
-
head: options.branch,
|
|
78
|
-
base,
|
|
79
|
-
draft: options.draft ?? !0
|
|
80
|
-
})
|
|
81
|
-
}, client);
|
|
82
|
-
return { number: pull.number, url: pull.html_url, branch: options.branch, base, commitSha: commit.sha };
|
|
83
|
-
}
|
|
1
|
+
import{githubJson}from"./client";function repoPath(owner,repo){if(!/^[A-Za-z0-9_.-]+$/.test(owner)||!/^[A-Za-z0-9_.-]+$/.test(repo))throw Error("GitHub owner and repository names contain invalid characters");return`/repos/${owner}/${repo}`}function validateBranch(branch){if(!branch||branch.length>240||branch.startsWith("/")||branch.endsWith("/")||branch.includes("..")||/[~^:?*[\\\s]/.test(branch))throw Error(`Invalid Git branch name: ${branch}`)}function validateFiles(files){if(!files.length)throw Error("At least one file change is required");if(files.length>100)throw Error("A pull request may change at most 100 files");const seen=new Set;let bytes=0;for(const file of files){if(!file.path||file.path.startsWith("/")||file.path.includes("\\")||file.path.split("/").includes(".."))throw Error(`Unsafe repository path: ${file.path}`);if(seen.has(file.path))throw Error(`Duplicate repository path: ${file.path}`);seen.add(file.path);bytes+=Buffer.byteLength(file.content)}if(bytes>5242880)throw Error("Combined file content exceeds the 5 MiB safety limit")}export async function fetchRepositoryTree(owner,repo,ref,options={}){const payload=await githubJson(`${repoPath(owner,repo)}/git/trees/${encodeURIComponent(ref)}?recursive=1`,{},options);return{sha:payload.sha,truncated:!!payload.truncated,entries:payload.tree??[]}}export async function fetchRepositoryFile(owner,repo,path,ref,options={}){validateFiles([{path,content:""}]);const payload=await githubJson(`${repoPath(owner,repo)}/contents/${path.split("/").map(encodeURIComponent).join("/")}?ref=${encodeURIComponent(ref)}`,{},options);if(payload.type!=="file"||payload.encoding!=="base64"||typeof payload.content!=="string")throw Error(`GitHub path is not a base64 encoded file: ${path}`);const maxBytes=options.maxBytes??262144;if((payload.size??0)>maxBytes)throw Error(`GitHub file exceeds the ${maxBytes} byte safety limit: ${path}`);const content=Buffer.from(payload.content.replace(/\n/g,""),"base64");if(content.byteLength>maxBytes)throw Error(`Decoded GitHub file exceeds the ${maxBytes} byte safety limit: ${path}`);return content.toString("utf8")}export async function createPullRequestWithFiles(options){validateBranch(options.branch);validateFiles(options.files);const basePath=repoPath(options.owner,options.repo),client={token:options.token,apiUrl:options.apiUrl,fetch:options.fetch};let base=options.base;if(!base)base=(await githubJson(basePath,{},client)).default_branch;if(!base)throw Error("GitHub repository has no default branch");const reference=await githubJson(`${basePath}/git/ref/heads/${encodeURIComponent(base)}`,{},client),baseCommit=await githubJson(`${basePath}/git/commits/${reference.object.sha}`,{},client),blobs=await Promise.all(options.files.map(async(file)=>{const blob=await githubJson(`${basePath}/git/blobs`,{method:"POST",body:JSON.stringify({content:file.content,encoding:"utf-8"})},client);return{path:file.path,mode:"100644",type:"blob",sha:blob.sha}})),tree=await githubJson(`${basePath}/git/trees`,{method:"POST",body:JSON.stringify({base_tree:baseCommit.tree.sha,tree:blobs})},client),commit=await githubJson(`${basePath}/git/commits`,{method:"POST",body:JSON.stringify({message:options.commitMessage,tree:tree.sha,parents:[reference.object.sha]})},client);await githubJson(`${basePath}/git/refs`,{method:"POST",body:JSON.stringify({ref:`refs/heads/${options.branch}`,sha:commit.sha})},client);const pull=await githubJson(`${basePath}/pulls`,{method:"POST",body:JSON.stringify({title:options.title,body:options.body,head:options.branch,base,draft:options.draft??!0})},client);return{number:pull.number,url:pull.html_url,branch:options.branch,base,commitSha:commit.sha}}
|
package/dist/repos.js
CHANGED
|
@@ -1,26 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
export async function fetchAllRepos(orgs, ignore = [".github"]) {
|
|
3
|
-
const ignored = new Set(ignore), all = [];
|
|
4
|
-
for (const org of orgs) {
|
|
5
|
-
let page = 1;
|
|
6
|
-
while (!0) {
|
|
7
|
-
const res = await ghFetch(`${GITHUB_API}/orgs/${org}/repos?per_page=100&page=${page}&type=public`);
|
|
8
|
-
if (!res.ok)
|
|
9
|
-
break;
|
|
10
|
-
const repos = await res.json();
|
|
11
|
-
if (repos.length === 0)
|
|
12
|
-
break;
|
|
13
|
-
for (const repo of repos)
|
|
14
|
-
all.push({
|
|
15
|
-
name: repo.name,
|
|
16
|
-
owner: repo.owner.login,
|
|
17
|
-
full_name: repo.full_name,
|
|
18
|
-
html_url: repo.html_url,
|
|
19
|
-
default_branch: repo.default_branch,
|
|
20
|
-
archived: repo.archived
|
|
21
|
-
});
|
|
22
|
-
page++;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return all.filter((r) => !r.archived && !ignored.has(r.name));
|
|
26
|
-
}
|
|
1
|
+
import{ghFetch,GITHUB_API}from"./client";export async function fetchAllRepos(orgs,ignore=[".github"]){const ignored=new Set(ignore),all=[];for(const org of orgs){let page=1;while(!0){const res=await ghFetch(`${GITHUB_API}/orgs/${org}/repos?per_page=100&page=${page}&type=public`);if(!res.ok)break;const repos=await res.json();if(repos.length===0)break;for(const repo of repos)all.push({name:repo.name,owner:repo.owner.login,full_name:repo.full_name,html_url:repo.html_url,default_branch:repo.default_branch,archived:repo.archived});page++}}return all.filter((r)=>!r.archived&&!ignored.has(r.name))}
|
package/dist/run-history.js
CHANGED
|
@@ -1,63 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
function
|
|
3
|
-
return sha.length > 7 ? sha.slice(0, 7) : sha;
|
|
4
|
-
}
|
|
5
|
-
function diffMs(start, end) {
|
|
6
|
-
if (!start || !end)
|
|
7
|
-
return null;
|
|
8
|
-
const s = Date.parse(start), e = Date.parse(end);
|
|
9
|
-
if (Number.isNaN(s) || Number.isNaN(e))
|
|
10
|
-
return null;
|
|
11
|
-
if (e < s)
|
|
12
|
-
return null;
|
|
13
|
-
return e - s;
|
|
14
|
-
}
|
|
15
|
-
export async function fetchWorkflowRuns(owner, name, options = {}) {
|
|
16
|
-
const limit = Math.max(1, Math.min(options.limit ?? 20, 100)), params = new URLSearchParams;
|
|
17
|
-
params.set("per_page", String(limit));
|
|
18
|
-
if (options.branch)
|
|
19
|
-
params.set("branch", options.branch);
|
|
20
|
-
if (options.event)
|
|
21
|
-
params.set("event", options.event);
|
|
22
|
-
const res = await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs?${params.toString()}`);
|
|
23
|
-
if (!res.ok)
|
|
24
|
-
return [];
|
|
25
|
-
return ((await res.json()).workflow_runs ?? []).map((r) => ({
|
|
26
|
-
id: r.id,
|
|
27
|
-
status: r.status,
|
|
28
|
-
conclusion: r.conclusion,
|
|
29
|
-
name: r.name,
|
|
30
|
-
headBranch: r.head_branch,
|
|
31
|
-
headSha: r.head_sha,
|
|
32
|
-
headShaShort: shortSha(r.head_sha),
|
|
33
|
-
commitMessage: r.head_commit?.message?.split(`
|
|
34
|
-
`)[0] ?? null,
|
|
35
|
-
commitAuthor: r.head_commit?.author?.name ?? r.actor?.login ?? null,
|
|
36
|
-
event: r.event,
|
|
37
|
-
url: r.html_url,
|
|
38
|
-
startedAt: r.run_started_at ?? r.created_at ?? null,
|
|
39
|
-
updatedAt: r.updated_at,
|
|
40
|
-
durationMs: diffMs(r.run_started_at ?? r.created_at, r.updated_at)
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
export async function fetchRunJobs(owner, name, runId) {
|
|
44
|
-
const res = await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${runId}/jobs?per_page=100`);
|
|
45
|
-
if (!res.ok)
|
|
46
|
-
return [];
|
|
47
|
-
return ((await res.json()).jobs ?? []).map((j) => ({
|
|
48
|
-
id: j.id,
|
|
49
|
-
name: j.name,
|
|
50
|
-
status: j.status,
|
|
51
|
-
conclusion: j.conclusion,
|
|
52
|
-
startedAt: j.started_at,
|
|
53
|
-
completedAt: j.completed_at,
|
|
54
|
-
durationMs: diffMs(j.started_at, j.completed_at),
|
|
55
|
-
url: j.html_url,
|
|
56
|
-
steps: (j.steps ?? []).map((s) => ({
|
|
57
|
-
name: s.name,
|
|
58
|
-
status: s.status,
|
|
59
|
-
conclusion: s.conclusion,
|
|
60
|
-
number: s.number
|
|
61
|
-
}))
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
1
|
+
import{ghFetch,GITHUB_API}from"./client";function shortSha(sha){return sha.length>7?sha.slice(0,7):sha}function diffMs(start,end){if(!start||!end)return null;const s=Date.parse(start),e=Date.parse(end);if(Number.isNaN(s)||Number.isNaN(e))return null;if(e<s)return null;return e-s}export async function fetchWorkflowRuns(owner,name,options={}){const limit=Math.max(1,Math.min(options.limit??20,100)),params=new URLSearchParams;params.set("per_page",String(limit));if(options.branch)params.set("branch",options.branch);if(options.event)params.set("event",options.event);const res=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs?${params.toString()}`);if(!res.ok)return[];return((await res.json()).workflow_runs??[]).map((r)=>({id:r.id,status:r.status,conclusion:r.conclusion,name:r.name,headBranch:r.head_branch,headSha:r.head_sha,headShaShort:shortSha(r.head_sha),commitMessage:r.head_commit?.message?.split(`
|
|
2
|
+
`)[0]??null,commitAuthor:r.head_commit?.author?.name??r.actor?.login??null,event:r.event,url:r.html_url,startedAt:r.run_started_at??r.created_at??null,updatedAt:r.updated_at,durationMs:diffMs(r.run_started_at??r.created_at,r.updated_at)}))}export async function fetchRunJobs(owner,name,runId){const res=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${runId}/jobs?per_page=100`);if(!res.ok)return[];return((await res.json()).jobs??[]).map((j)=>({id:j.id,name:j.name,status:j.status,conclusion:j.conclusion,startedAt:j.started_at,completedAt:j.completed_at,durationMs:diffMs(j.started_at,j.completed_at),url:j.html_url,steps:(j.steps??[]).map((s)=>({name:s.name,status:s.status,conclusion:s.conclusion,number:s.number}))}))}
|
|
@@ -1,27 +1 @@
|
|
|
1
|
-
export function detectRunnerPressure(samples,
|
|
2
|
-
const now = options.now ?? Date.now(), windowMs = options.windowMinutes * 60000, cutoffMs = now - windowMs, byOrg = new Map;
|
|
3
|
-
for (const s of samples) {
|
|
4
|
-
const t = Date.parse(s.sampledAt);
|
|
5
|
-
if (Number.isNaN(t) || t < cutoffMs)
|
|
6
|
-
continue;
|
|
7
|
-
const list = byOrg.get(s.org) ?? [];
|
|
8
|
-
list.push(s);
|
|
9
|
-
byOrg.set(s.org, list);
|
|
10
|
-
}
|
|
11
|
-
const actions = [];
|
|
12
|
-
for (const [org, orgSamples] of byOrg.entries()) {
|
|
13
|
-
if (orgSamples.length === 0)
|
|
14
|
-
continue;
|
|
15
|
-
orgSamples.sort((a, b) => Date.parse(a.sampledAt) - Date.parse(b.sampledAt));
|
|
16
|
-
const oldestMs = Date.parse(orgSamples[0].sampledAt), sustainedMs = Date.parse(orgSamples[orgSamples.length - 1].sampledAt) - oldestMs;
|
|
17
|
-
if (sustainedMs < windowMs - 1000)
|
|
18
|
-
continue;
|
|
19
|
-
const allAboveOrEqual = orgSamples.every((s) => s.queued >= options.queuedThreshold), allBelow = orgSamples.every((s) => s.queued < options.queuedThreshold), isAlerting = alertStates.get(org)?.alerting ?? !1, current = orgSamples[orgSamples.length - 1];
|
|
20
|
-
if (isAlerting) {
|
|
21
|
-
if (allBelow)
|
|
22
|
-
actions.push({ org, action: "clear", current, sustainedMs });
|
|
23
|
-
} else if (allAboveOrEqual)
|
|
24
|
-
actions.push({ org, action: "fire", current, sustainedMs });
|
|
25
|
-
}
|
|
26
|
-
return actions;
|
|
27
|
-
}
|
|
1
|
+
export function detectRunnerPressure(samples,alertStates,options){const now=options.now??Date.now(),windowMs=options.windowMinutes*60000,cutoffMs=now-windowMs,byOrg=new Map;for(const s of samples){const t=Date.parse(s.sampledAt);if(Number.isNaN(t)||t<cutoffMs)continue;const list=byOrg.get(s.org)??[];list.push(s);byOrg.set(s.org,list)}const actions=[];for(const[org,orgSamples]of byOrg.entries()){if(orgSamples.length===0)continue;orgSamples.sort((a,b)=>Date.parse(a.sampledAt)-Date.parse(b.sampledAt));const oldestMs=Date.parse(orgSamples[0].sampledAt),sustainedMs=Date.parse(orgSamples[orgSamples.length-1].sampledAt)-oldestMs;if(sustainedMs<windowMs-1000)continue;const allAboveOrEqual=orgSamples.every((s)=>s.queued>=options.queuedThreshold),allBelow=orgSamples.every((s)=>s.queued<options.queuedThreshold),isAlerting=alertStates.get(org)?.alerting??!1,current=orgSamples[orgSamples.length-1];if(isAlerting){if(allBelow)actions.push({org,action:"clear",current,sustainedMs})}else if(allAboveOrEqual)actions.push({org,action:"fire",current,sustainedMs})}return actions}
|
package/dist/runners.js
CHANGED
|
@@ -1,26 +1 @@
|
|
|
1
|
-
import { ghFetch,
|
|
2
|
-
export async function fetchRepoActiveRuns(owner, name) {
|
|
3
|
-
try {
|
|
4
|
-
const runRes = await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs?status=in_progress&per_page=100`);
|
|
5
|
-
if (!runRes.ok)
|
|
6
|
-
return { running: 0, queued: 0 };
|
|
7
|
-
const runs = (await runRes.json()).workflow_runs ?? [];
|
|
8
|
-
if (runs.length === 0)
|
|
9
|
-
return { running: 0, queued: 0 };
|
|
10
|
-
return (await Promise.all(runs.map(async (r) => {
|
|
11
|
-
const jobsRes = await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${r.id}/jobs`);
|
|
12
|
-
if (!jobsRes.ok)
|
|
13
|
-
return { running: 0, queued: 0 };
|
|
14
|
-
const jobsData = await jobsRes.json();
|
|
15
|
-
let running = 0, queued = 0;
|
|
16
|
-
for (const j of jobsData.jobs ?? [])
|
|
17
|
-
if (j.status === "in_progress")
|
|
18
|
-
running++;
|
|
19
|
-
else if (j.status === "queued")
|
|
20
|
-
queued++;
|
|
21
|
-
return { running, queued };
|
|
22
|
-
}))).reduce((a, b) => ({ running: a.running + b.running, queued: a.queued + b.queued }), { running: 0, queued: 0 });
|
|
23
|
-
} catch {
|
|
24
|
-
return { running: 0, queued: 0 };
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
import{ghFetch,GITHUB_API}from"./client";export async function fetchRepoActiveRuns(owner,name){try{const runRes=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs?status=in_progress&per_page=100`);if(!runRes.ok)return{running:0,queued:0};const runs=(await runRes.json()).workflow_runs??[];if(runs.length===0)return{running:0,queued:0};return(await Promise.all(runs.map(async(r)=>{const jobsRes=await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${r.id}/jobs`);if(!jobsRes.ok)return{running:0,queued:0};const jobsData=await jobsRes.json();let running=0,queued=0;for(const j of jobsData.jobs??[])if(j.status==="in_progress")running++;else if(j.status==="queued")queued++;return{running,queued}}))).reduce((a,b)=>({running:a.running+b.running,queued:a.queued+b.queued}),{running:0,queued:0})}catch{return{running:0,queued:0}}}
|
package/dist/runs.js
CHANGED
|
@@ -1,84 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
async function
|
|
3
|
-
|
|
4
|
-
const res = await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/commits?sha=${branch}&per_page=1`);
|
|
5
|
-
if (!res.ok)
|
|
6
|
-
return;
|
|
7
|
-
const commits = await res.json(), [c] = commits;
|
|
8
|
-
if (!c)
|
|
9
|
-
return;
|
|
10
|
-
base.commitSha = c.sha.slice(0, 7);
|
|
11
|
-
base.commitMessage = c.commit.message.split(`
|
|
12
|
-
`)[0] ?? null;
|
|
13
|
-
base.commitUrl = `https://github.com/${owner}/${name}/commit/${c.sha}`;
|
|
14
|
-
base.commitAuthor = c.commit.author?.name ?? c.author?.login ?? null;
|
|
15
|
-
base.updatedAt = c.commit.author?.date ?? null;
|
|
16
|
-
} catch {}
|
|
17
|
-
}
|
|
18
|
-
export async function fetchFailedJobs(owner, name, runId) {
|
|
19
|
-
try {
|
|
20
|
-
const res = await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs/${runId}/jobs?filter=latest`);
|
|
21
|
-
if (!res.ok)
|
|
22
|
-
return [];
|
|
23
|
-
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 }));
|
|
24
|
-
} catch {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
export async function fetchRepoStatus(owner, name, defaultBranch) {
|
|
29
|
-
const base = {
|
|
30
|
-
name,
|
|
31
|
-
owner,
|
|
32
|
-
fullName: `${owner}/${name}`,
|
|
33
|
-
url: `https://github.com/${owner}/${name}`,
|
|
34
|
-
defaultBranch,
|
|
35
|
-
status: "no_runs",
|
|
36
|
-
conclusion: null,
|
|
37
|
-
workflowName: null,
|
|
38
|
-
commitSha: null,
|
|
39
|
-
commitMessage: null,
|
|
40
|
-
commitUrl: null,
|
|
41
|
-
commitAuthor: null,
|
|
42
|
-
commitCount: null,
|
|
43
|
-
updatedAt: null,
|
|
44
|
-
runUrl: null,
|
|
45
|
-
failedJobs: [],
|
|
46
|
-
renovatePRs: 0,
|
|
47
|
-
renovatePRsUrl: null,
|
|
48
|
-
actionsPRs: 0,
|
|
49
|
-
actionsPRsUrl: null
|
|
50
|
-
};
|
|
51
|
-
try {
|
|
52
|
-
const res = await ghFetch(`${GITHUB_API}/repos/${owner}/${name}/actions/runs?branch=${defaultBranch}&event=push&per_page=1`);
|
|
53
|
-
if (!res.ok) {
|
|
54
|
-
base.status = "error";
|
|
55
|
-
await fillLatestCommit(base, owner, name, defaultBranch);
|
|
56
|
-
return base;
|
|
57
|
-
}
|
|
58
|
-
const data = await res.json(), [run] = data.workflow_runs ?? [];
|
|
59
|
-
if (!run) {
|
|
60
|
-
await fillLatestCommit(base, owner, name, defaultBranch);
|
|
61
|
-
return base;
|
|
62
|
-
}
|
|
63
|
-
base.workflowName = run.name;
|
|
64
|
-
base.commitSha = run.head_sha.slice(0, 7);
|
|
65
|
-
base.commitMessage = run.head_commit?.message.split(`
|
|
66
|
-
`)[0] ?? null;
|
|
67
|
-
base.commitUrl = `https://github.com/${owner}/${name}/commit/${run.head_sha}`;
|
|
68
|
-
base.commitAuthor = run.head_commit?.author?.name ?? run.actor?.login ?? null;
|
|
69
|
-
base.updatedAt = run.updated_at;
|
|
70
|
-
base.runUrl = run.html_url;
|
|
71
|
-
if (run.status === "completed") {
|
|
72
|
-
base.status = run.conclusion === "success" ? "success" : "failure";
|
|
73
|
-
base.conclusion = run.conclusion;
|
|
74
|
-
if (base.status === "failure")
|
|
75
|
-
base.failedJobs = await fetchFailedJobs(owner, name, run.id);
|
|
76
|
-
} else {
|
|
77
|
-
base.status = "pending";
|
|
78
|
-
base.conclusion = run.status;
|
|
79
|
-
}
|
|
80
|
-
} catch {
|
|
81
|
-
base.status = "error";
|
|
82
|
-
}
|
|
83
|
-
return base;
|
|
84
|
-
}
|
|
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(`
|
|
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/github",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.70.
|
|
4
|
+
"version": "0.70.260",
|
|
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": [
|