@stacksjs/github 0.70.53

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 ADDED
@@ -0,0 +1,26 @@
1
+ # @stacksjs/github
2
+
3
+ GitHub API client used by Stacks framework features. Currently powers the
4
+ dashboard CI surface ([#1844](https://github.com/stacksjs/stacks/issues/1844));
5
+ designed to be reused by future surfaces (failing-CI notifications, runner
6
+ alerts, kanban→PR links, etc.).
7
+
8
+ ## What's in here
9
+
10
+ - `getDashboardData()` — aggregated CI/runner snapshot across a configured
11
+
12
+ list of orgs, with on-disk caching and stale-while-revalidate semantics.
13
+
14
+ - Lower-level helpers (`fetchAllRepos`, `fetchRepoStatus`, `fetchBotPRCounts`,
15
+
16
+ `fetchRepoActiveRuns`) for callers that need a single dimension.
17
+
18
+ - A retrying `ghFetch` that respects GitHub's `Retry-After` /
19
+
20
+ `x-ratelimit-reset` headers so callers don't have to.
21
+
22
+ ## Auth
23
+
24
+ A `GITHUB_TOKEN` environment variable is required. The token only needs
25
+ `public_repo` scope for the default reads — bump permissions only if you
26
+ add endpoints that need them.
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ // @bun
2
+ export {
3
+ mapWithConcurrency,
4
+ ghHeaders,
5
+ ghFetch,
6
+ getDashboardData,
7
+ fetchWorkflowRuns,
8
+ fetchRunJobs,
9
+ fetchRepoStatus,
10
+ fetchRepoActiveRuns,
11
+ fetchFailedJobs,
12
+ fetchBotPRCounts,
13
+ fetchAllRepos,
14
+ detectRunnerPressure,
15
+ detectNewlyFailedRuns,
16
+ clearDashboardCache,
17
+ GITHUB_API
18
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Count open PRs authored by a GitHub App across every repo in an org.
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.
6
+ */
7
+ export declare function fetchBotPRCounts(org: string, authorSlug: string): Promise<Map<string, number>>;
@@ -0,0 +1,15 @@
1
+ export declare function ghHeaders(): Record<string, string>;
2
+ /**
3
+ * `fetch` against the GitHub API that retries on secondary rate limits.
4
+ *
5
+ * GitHub signals back-off via either a `Retry-After` header (preferred) or
6
+ * an `x-ratelimit-reset` epoch second. The retry budget is bounded so a
7
+ * permanently rate-limited token doesn't hang callers indefinitely.
8
+ */
9
+ export declare function ghFetch(url: string, attempt?: number): Promise<Response>;
10
+ /**
11
+ * Run `fn` over `items` with at most `limit` concurrent invocations.
12
+ * Result indices match input indices.
13
+ */
14
+ export declare function mapWithConcurrency<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]>;
15
+ export declare const GITHUB_API: 'https://api.github.com';
@@ -0,0 +1,17 @@
1
+ import type { DashboardData, DashboardOptions } from './types';
2
+ /**
3
+ * Aggregated CI/runner snapshot across the configured orgs.
4
+ *
5
+ * Stale-while-revalidate: if a cached snapshot exists, it is returned
6
+ * immediately and a background refresh kicks off once the TTL has elapsed.
7
+ * First-ever call (no in-memory and no on-disk cache) waits for the build
8
+ * so the dashboard renders against real data instead of `null`.
9
+ */
10
+ export declare function getDashboardData(opts: DashboardOptions): Promise<DashboardData>;
11
+ /**
12
+ * Drop the in-memory cache for a given cache path. Disk cache is left
13
+ * alone — callers that want a true reset should `Bun.write` an empty file
14
+ * or delete it before re-fetching. Used by tests and by an eventual
15
+ * "Refresh now" UI action.
16
+ */
17
+ export declare function clearDashboardCache(cachePath?: unknown): void;
@@ -0,0 +1,59 @@
1
+ import type { RepoStatus } from './types';
2
+ /**
3
+ * Compute the failed-transition list.
4
+ *
5
+ * The "transition" is gated on:
6
+ *
7
+ * 1. The repo is currently in a failed state.
8
+ * 2. The repo is NOT in flight (pending). Pending → failure happens
9
+ * mid-run; firing on it would just produce noise once the run
10
+ * finishes and the same failure resurfaces with a final
11
+ * conclusion.
12
+ * 3. The previous state was *not* failed. (Sticky-red repos don't
13
+ * keep firing.)
14
+ * 4. The run id changed — if the same failed run id is still
15
+ * surfacing, this is a duplicate poll, not a new failure.
16
+ * 5. Cooldown: `lastNotifiedAt` is either null or older than
17
+ * `cooldownMs` ago.
18
+ */
19
+ export declare function detectNewlyFailedRuns(snapshot: { repos: RepoStatus[] }, previousStates: Map<string, PreviousRunState>, options?: DetectOptions): FailedTransition[];
20
+ /**
21
+ * Pure failure-transition detector (stacksjs/stacks#1849).
22
+ *
23
+ * Given a fresh CI snapshot and the previous per-repo states the
24
+ * dashboard knows about, return the list of repos that *just*
25
+ * transitioned to a failed conclusion — the moment worth firing a
26
+ * notification on. Anything still failing, or newly passing, or
27
+ * still in flight, is silenced.
28
+ *
29
+ * Pure for a reason: easy to unit-test the transition matrix without
30
+ * a DB, a notify implementation, or the network. The persistence
31
+ * layer + notification fan-out live in defaults so framework
32
+ * packages stay infrastructure-free.
33
+ *
34
+ * @see PreviousRunState what the persistence layer hands in
35
+ * @see FailedTransition what we return — caller decides how to notify
36
+ */
37
+ /** What a caller stored about a repo on its last snapshot pass. */
38
+ export declare interface PreviousRunState {
39
+ repoFullName: string
40
+ lastConclusion: string | null
41
+ lastRunId: number | null
42
+ lastNotifiedAt: string | null
43
+ }
44
+ /** Per-transition payload the detector hands back to the caller. */
45
+ export declare interface FailedTransition {
46
+ repoFullName: string
47
+ conclusion: string
48
+ runId: number | null
49
+ workflowName: string | null
50
+ commitSha: string | null
51
+ commitMessage: string | null
52
+ commitAuthor: string | null
53
+ runUrl: string | null
54
+ previousConclusion: string | null
55
+ }
56
+ export declare interface DetectOptions {
57
+ cooldownMs?: number
58
+ now?: number
59
+ }
@@ -0,0 +1,35 @@
1
+ export type { DetectOptions, FailedTransition, PreviousRunState } from './failure-detector';
2
+ export type { WorkflowJob, WorkflowRun } from './run-history';
3
+ export type {
4
+ DetectOptions as PressureDetectOptions,
5
+ PressureAction,
6
+ RunnerAlertState,
7
+ RunnerSample,
8
+ } from './runner-pressure-detector';
9
+ export type {
10
+ DashboardData,
11
+ DashboardOptions,
12
+ FailedJob,
13
+ OrgRunnerUsage,
14
+ Repo,
15
+ RepoStatus,
16
+ RepoStatusKind,
17
+ } from './types';
18
+ /**
19
+ * @stacksjs/github — thin GitHub API client used by Stacks framework
20
+ * features (dashboard CI surface, future runner alerts, kanban→PR links).
21
+ *
22
+ * Top-level entry: {@link getDashboardData} returns the aggregated snapshot
23
+ * the dashboard renders. Lower-level helpers (`fetchAllRepos`,
24
+ * `fetchRepoStatus`, …) are also exported so other surfaces can pull a
25
+ * single dimension without pulling the whole snapshot through.
26
+ */
27
+ export { fetchBotPRCounts } from './bots';
28
+ export { ghFetch, ghHeaders, GITHUB_API, mapWithConcurrency } from './client';
29
+ export { clearDashboardCache, getDashboardData } from './dashboard';
30
+ export { detectNewlyFailedRuns } from './failure-detector';
31
+ export { fetchAllRepos } from './repos';
32
+ export { fetchRepoActiveRuns } from './runners';
33
+ export { fetchRunJobs, fetchWorkflowRuns } from './run-history';
34
+ export { detectRunnerPressure } from './runner-pressure-detector';
35
+ export { fetchFailedJobs, fetchRepoStatus } from './runs';
@@ -0,0 +1,7 @@
1
+ import type { Repo } from './types';
2
+ /**
3
+ * Fetch all non-archived public repos across `orgs`, filtering out any names
4
+ * in `ignore` (defaults to `.github` which exists in every org and never has
5
+ * project CI on it).
6
+ */
7
+ export declare function fetchAllRepos(orgs: string[], ignore?: string[]): Promise<Repo[]>;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The N most recent workflow runs for a repo. Each entry is a
3
+ * lightweight summary — failed-job detail is fetched on-demand via
4
+ * {@link fetchRunJobs} so the drawer only pays the round-trip cost
5
+ * when the user actually expands a row.
6
+ */
7
+ export declare function fetchWorkflowRuns(owner: string, name: string, options?: FetchRunsOptions): Promise<WorkflowRun[]>;
8
+ /**
9
+ * Per-job detail for a single run. Used by the drilldown drawer when
10
+ * the user expands a failing run to see *which* step broke. Different
11
+ * shape from `fetchFailedJobs` in `runs.ts` (which is at-a-glance
12
+ * "failed job names" for the card) — here we want everything,
13
+ * including step-level breakdown + timing.
14
+ *
15
+ * Returns an empty array on error so the drawer can render an empty
16
+ * state rather than a 500.
17
+ */
18
+ export declare function fetchRunJobs(owner: string, name: string, runId: number): Promise<WorkflowJob[]>;
19
+ /**
20
+ * Recent-workflow-runs + per-run job detail for the dashboard CI
21
+ * drilldown (stacksjs/stacks#1848).
22
+ *
23
+ * Distinct from `runs.ts` which is the *aggregator* — it folds the
24
+ * latest run per repo into a `RepoStatus` for the at-a-glance card.
25
+ * Here we want history: the last N runs for a single repo, plus the
26
+ * per-job detail for a single run. Different shapes, different
27
+ * callers, easier to keep them in separate files than to overload
28
+ * one module.
29
+ */
30
+ export declare interface WorkflowRun {
31
+ id: number
32
+ status: 'queued' | 'in_progress' | 'completed' | string
33
+ conclusion: 'success' | 'failure' | 'cancelled' | 'skipped' | 'timed_out' | 'action_required' | 'neutral' | 'startup_failure' | null
34
+ name: string
35
+ headBranch: string | null
36
+ headSha: string
37
+ headShaShort: string
38
+ commitMessage: string | null
39
+ commitAuthor: string | null
40
+ event: string
41
+ url: string
42
+ startedAt: string | null
43
+ updatedAt: string
44
+ durationMs: number | null
45
+ }
46
+ export declare interface WorkflowJob {
47
+ id: number
48
+ name: string
49
+ status: 'queued' | 'in_progress' | 'completed' | string
50
+ conclusion: WorkflowRun['conclusion']
51
+ startedAt: string | null
52
+ completedAt: string | null
53
+ durationMs: number | null
54
+ url: string
55
+ steps: Array<{
56
+ name: string
57
+ status: WorkflowJob['status']
58
+ conclusion: WorkflowRun['conclusion']
59
+ number: number
60
+ }>
61
+ }
62
+ declare interface FetchRunsOptions {
63
+ limit?: number
64
+ branch?: string
65
+ event?: string
66
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Compute the per-org actions.
3
+ *
4
+ * Algorithm:
5
+ *
6
+ * 1. Bucket samples by org.
7
+ * 2. For each org, slice the samples within the last
8
+ * `windowMinutes` of `now` (called `window`).
9
+ * 3. Skip orgs with `window.length === 0` (no samples ever) or
10
+ * where the window is shorter than `windowMinutes`
11
+ * (insufficient data — sustained-pressure can't be proven yet).
12
+ * 4. If the org is currently `alerting`:
13
+ * - If every sample in the window is < threshold → emit
14
+ * `clear` action.
15
+ * 5. If the org is NOT alerting:
16
+ * - If every sample in the window is >= threshold → emit
17
+ * `fire` action.
18
+ *
19
+ * Conditions that DON'T trigger anything:
20
+ *
21
+ * - Mixed window (some above, some below threshold) → still
22
+ * converging; wait for the next refresh.
23
+ * - Sticky alerting with sustained pressure → already alerting,
24
+ * no new action needed.
25
+ * - Sticky clear with sustained calm → no action needed.
26
+ */
27
+ export declare function detectRunnerPressure(samples: RunnerSample[], alertStates: Map<string, RunnerAlertState>, options: DetectOptions): PressureAction[];
28
+ /**
29
+ * Pure runner-pressure detector (stacksjs/stacks#1850).
30
+ *
31
+ * Given a time-series of per-org runner samples + the dashboard's
32
+ * memory of which orgs are currently "alerting", return the
33
+ * actions to take: fire a fresh alert, clear an existing one, or do
34
+ * nothing.
35
+ *
36
+ * **Hysteresis** matters here. Once an org has been alerted on
37
+ * (queue > threshold sustained for one window), we DON'T re-fire
38
+ * just because the queue spikes a second time within the same hour.
39
+ * The org has to first *clear* — drop below threshold for a full
40
+ * window — before another fire becomes possible. This is the
41
+ * difference between "useful CI pressure paging" and "every dashboard
42
+ * load spams Slack".
43
+ *
44
+ * Pure for the same reason as `failure-detector.ts`: easy to unit-
45
+ * test the hysteresis transitions, the persistence + fan-out live
46
+ * in defaults.
47
+ */
48
+ /** One time-series row per org per snapshot refresh. */
49
+ export declare interface RunnerSample {
50
+ org: string
51
+ running: number
52
+ queued: number
53
+ cap: number
54
+ sampledAt: string
55
+ }
56
+ /** Dashboard's last-known per-org alert state. */
57
+ export declare interface RunnerAlertState {
58
+ org: string
59
+ alerting: boolean
60
+ lastAlertedAt: string | null
61
+ lastClearedAt: string | null
62
+ }
63
+ /** Action the caller should take for one org. */
64
+ export declare interface PressureAction {
65
+ org: string
66
+ action: 'fire' | 'clear'
67
+ current: RunnerSample
68
+ sustainedMs: number
69
+ }
70
+ export declare interface DetectOptions {
71
+ queuedThreshold: number
72
+ windowMinutes: number
73
+ now?: number
74
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Count jobs (not runs) currently using runners. A workflow run can be
3
+ * "in_progress" while some of its matrix jobs are still queued — only the
4
+ * actually-running jobs occupy runners, so we sum at the job level.
5
+ */
6
+ export declare function fetchRepoActiveRuns(owner: string, name: string): Promise<{ running: number, queued: number }>;
@@ -0,0 +1,3 @@
1
+ import type { FailedJob, RepoStatus } from './types';
2
+ export declare function fetchFailedJobs(owner: string, name: string, runId: number): Promise<FailedJob[]>;
3
+ export declare function fetchRepoStatus(owner: string, name: string, defaultBranch: string): Promise<RepoStatus>;
@@ -0,0 +1,59 @@
1
+ export declare interface FailedJob {
2
+ name: string
3
+ conclusion: string
4
+ url: string
5
+ }
6
+ export declare interface RepoStatus {
7
+ name: string
8
+ owner: string
9
+ fullName: string
10
+ url: string
11
+ defaultBranch: string
12
+ status: RepoStatusKind
13
+ conclusion: string | null
14
+ workflowName: string | null
15
+ commitSha: string | null
16
+ commitMessage: string | null
17
+ commitUrl: string | null
18
+ commitAuthor: string | null
19
+ commitCount: number | null
20
+ updatedAt: string | null
21
+ runUrl: string | null
22
+ failedJobs: FailedJob[]
23
+ renovatePRs: number
24
+ renovatePRsUrl: string | null
25
+ actionsPRs: number
26
+ actionsPRsUrl: string | null
27
+ }
28
+ export declare interface OrgRunnerUsage {
29
+ running: number
30
+ queued: number
31
+ cap: number
32
+ }
33
+ export declare interface DashboardData {
34
+ repos: RepoStatus[]
35
+ fetchedAt: string
36
+ total: number
37
+ passing: number
38
+ failing: number
39
+ pending: number
40
+ noRuns: number
41
+ runners: Record<string, OrgRunnerUsage>
42
+ }
43
+ export declare interface DashboardOptions {
44
+ orgs: string[]
45
+ runnerCaps?: Record<string, number>
46
+ defaultRunnerCap?: number
47
+ ignoreRepos?: string[]
48
+ cacheTtlMs?: number
49
+ cachePath?: string
50
+ }
51
+ export declare interface Repo {
52
+ name: string
53
+ owner: string
54
+ full_name: string
55
+ html_url: string
56
+ default_branch: string
57
+ archived: boolean
58
+ }
59
+ export type RepoStatusKind = 'success' | 'failure' | 'pending' | 'no_runs' | 'error';
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@stacksjs/github",
3
+ "type": "module",
4
+ "version": "0.70.53",
5
+ "description": "GitHub API client used by Stacks framework features (dashboard CI surface, notifications, runner alerts).",
6
+ "author": "Chris Breuer",
7
+ "contributors": [
8
+ "Chris Breuer <chris@stacksjs.com>"
9
+ ],
10
+ "license": "MIT",
11
+ "funding": "https://github.com/sponsors/chrisbbreuer",
12
+ "homepage": "https://github.com/stacksjs/stacks/tree/main/storage/framework/core/github#readme",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/stacksjs/stacks.git",
16
+ "directory": "./storage/framework/core/github"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/stacksjs/stacks/issues"
20
+ },
21
+ "keywords": [
22
+ "github",
23
+ "ci",
24
+ "actions",
25
+ "workflows",
26
+ "dashboard",
27
+ "stacks"
28
+ ],
29
+ "exports": {
30
+ ".": {
31
+ "bun": "./src/index.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "module": "dist/index.js",
36
+ "types": "dist/index.d.ts",
37
+ "files": [
38
+ "README.md",
39
+ "dist"
40
+ ],
41
+ "scripts": {
42
+ "build": "bun build.ts",
43
+ "typecheck": "bun tsc --noEmit",
44
+ "prepublishOnly": "bun run build"
45
+ },
46
+ "devDependencies": {
47
+ "better-dx": "^0.2.12"
48
+ },
49
+ "sideEffects": false
50
+ }