Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions migrations/0037_badge_enabled.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- #541: opt-in flag for the public README status badge. Default 0 (off) — the unauthenticated badge

Check warning on line 1 in migrations/0037_badge_enabled.sql

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in migrations/0037_badge_enabled.sql

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in migrations/0037_badge_enabled.sql

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in migrations/0037_badge_enabled.sql

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in migrations/0037_badge_enabled.sql

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in migrations/0037_badge_enabled.sql

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
-- endpoint only serves whitelisted metrics for installed repos that have explicitly opted in.
ALTER TABLE repository_settings ADD COLUMN badge_enabled INTEGER NOT NULL DEFAULT 0;
110 changes: 110 additions & 0 deletions src/api/badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { PublicRepoQuality, QueueHealthLevel } from "../services/public-repo-quality";

Check warning on line 1 in src/api/badge.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/api/badge.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/api/badge.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in src/api/badge.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/api/badge.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/api/badge.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

// Self-rendered README status badge (#541). Renders ONLY the public-safe whitelisted metrics from
// `PublicRepoQuality` — no external badge service, no contributor/reward/trust data. All text is XML-escaped
// before it reaches the SVG so the unauthenticated, embeddable surface cannot be turned into an injection
// vector even if upstream values ever change shape.

const LABEL = "gittensory";

const QUEUE_COLORS: Record<QueueHealthLevel, string> = {
low: "#3fb950",
medium: "#d29922",
high: "#db6d28",
critical: "#f85149",
};

const LOW_REAL_CONTRIBUTION_PCT = 50;
const UNAVAILABLE_COLOR = "#9e9e9e";

export type ShieldsBadge = {
schemaVersion: 1;
label: string;
message: string;
color: string;
cacheSeconds: number;
};

export function buildBadgeMessage(quality: PublicRepoQuality): string {
const real = quality.realContributionPct === null ? "real n/a" : `${quality.realContributionPct}% real`;
const merge =
quality.medianTimeToMergeHours === null ? "merge n/a" : `merge ${formatDuration(quality.medianTimeToMergeHours)}`;
return `${real} · ${merge} · queue ${quality.queueHealthLevel}`;
}

export function buildBadgeColor(quality: PublicRepoQuality): string {
// Color tracks queue health, but a low real-contribution share dominates the signal.
if (quality.realContributionPct !== null && quality.realContributionPct < LOW_REAL_CONTRIBUTION_PCT) {
return QUEUE_COLORS.high;
}
return QUEUE_COLORS[quality.queueHealthLevel];
}

export function buildShieldsBadge(quality: PublicRepoQuality, cacheSeconds: number): ShieldsBadge {
return {
schemaVersion: 1,
label: LABEL,
message: buildBadgeMessage(quality),
color: buildBadgeColor(quality),
cacheSeconds,
};
}

export function renderBadgeSvg(quality: PublicRepoQuality): string {
return renderFlatBadge(LABEL, buildBadgeMessage(quality), buildBadgeColor(quality));
}

export function renderUnavailableBadgeSvg(): string {
return renderFlatBadge(LABEL, "unavailable", UNAVAILABLE_COLOR);
}

function formatDuration(hours: number): string {
if (hours < 1) return "<1h";
if (hours < 48) return `${Math.round(hours)}h`;
return `${Math.round(hours / 24)}d`;
}

// Minimal flat ("shields"-style) badge. Widths are approximated from character count; exactness is not
// required for a README badge and keeps the renderer dependency-free.
function renderFlatBadge(label: string, message: string, color: string): string {
const labelText = escapeXml(label);
const messageText = escapeXml(message);
const labelWidth = textWidth(label);
const messageWidth = textWidth(message);
const totalWidth = labelWidth + messageWidth;
const labelMid = labelWidth / 2;
const messageMid = labelWidth + messageWidth / 2;
return [
`<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${labelText}: ${messageText}">`,
`<title>${labelText}: ${messageText}</title>`,
`<rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>`,
`<rect width="${labelWidth}" height="20" rx="3" fill="#24292f"/>`,
`<rect x="${labelWidth}" width="${messageWidth}" height="20" rx="3" fill="${escapeXml(color)}"/>`,
`<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">`,
`<text x="${labelMid}" y="14">${labelText}</text>`,
`<text x="${messageMid}" y="14">${messageText}</text>`,
`</g></svg>`,
].join("");
}

function textWidth(text: string): number {
// ~6.5px per character + 10px horizontal padding, clamped to a sane minimum.
return Math.max(40, Math.round(text.length * 6.5) + 10);
}

export function escapeXml(value: string): string {
return value.replace(/[&<>"']/g, (char) => {
switch (char) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
case '"':
return "&quot;";
default:
return "&#39;";
}
});
}
41 changes: 41 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Hono, type Context } from "hono";

Check warning on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/api/routes.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
import { z } from "zod";
import { analyzePRQueue, type AuthorRole, type ChecksStatus } from "../queue-intelligence";
import { completeGitHubWebOAuth, createSessionFromGitHubToken, pollGitHubDeviceFlow, startGitHubDeviceFlow, startGitHubWebOAuth } from "../auth/github-oauth";
Expand Down Expand Up @@ -158,6 +158,8 @@
import { buildOperatorDashboardPayload } from "../services/operator-dashboard";
import { buildSelfDogfoodRegistrationPack, resolveSelfDogfoodRepoFullName } from "../services/self-dogfood-registration-pack";
import { buildSubnetInterfaceDescriptor } from "../services/subnet-interface";
import { buildPublicRepoQuality, type PublicRepoQuality } from "../services/public-repo-quality";
import { buildShieldsBadge, renderBadgeSvg, renderUnavailableBadgeSvg } from "./badge";
import {
buildWeeklyValueReport,
formatWeeklyValueReportMarkdown,
Expand Down Expand Up @@ -246,6 +248,18 @@
type AppBindings = { Bindings: Env };
type AppContext = Context<AppBindings>;

// Resolves the public README badge metrics for a repo, enforcing the two gates in one place: the repo must
// be installed AND have opted in via `badgeEnabled`. Returns null (→ a benign "unavailable" badge) for any
// repo that is unknown, uninstalled, or has not opted in — so no metrics are ever served otherwise.
async function loadPublicRepoBadge(env: Env, owner: string, repo: string): Promise<PublicRepoQuality | null> {
const repository = await getRepository(env, `${owner}/${repo}`);
if (!repository || !repository.isInstalled) return null;
const settings = await getRepositorySettings(env, repository.fullName);
if (!settings.badgeEnabled) return null;
const pullRequests = await listPullRequests(env, repository.fullName);
return buildPublicRepoQuality(pullRequests);
}

async function recordRouteProductUsage(
c: AppContext,
event: {
Expand Down Expand Up @@ -567,6 +581,7 @@
requireLinkedIssue: z.boolean().default(false),
backfillEnabled: z.boolean().default(true),
privateTrustEnabled: z.boolean().default(true),
badgeEnabled: z.boolean().default(false),
commandAuthorization: z
.object({
default: z.array(z.enum(["maintainer", "collaborator", "pr_author", "confirmed_miner"])).max(4).optional(),
Expand Down Expand Up @@ -736,6 +751,30 @@
}
});

// Public-safe README status badge (#541). Unauthenticated and embeddable: it serves ONLY whitelisted,
// repo-level metrics, and ONLY for installed repos that opted in via the `badgeEnabled` setting. Excluded
// from requiresApiToken above; aggressively cached + stale-while-revalidate like the public stats route.
app.get("/v1/public/repos/:owner/:repo/badge.svg", async (c) => {
const quality = await loadPublicRepoBadge(c.env, c.req.param("owner"), c.req.param("repo"));
c.header("Content-Type", "image/svg+xml; charset=utf-8");
if (!quality) {
c.header("Cache-Control", "public, max-age=300");
return c.body(renderUnavailableBadgeSvg(), 404);
}
c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400");
return c.body(renderBadgeSvg(quality));
});

app.get("/v1/public/repos/:owner/:repo/badge.json", async (c) => {
const quality = await loadPublicRepoBadge(c.env, c.req.param("owner"), c.req.param("repo"));
if (!quality) {
c.header("Cache-Control", "public, max-age=300");
return c.json({ schemaVersion: 1, label: "gittensory", message: "unavailable", color: "#9e9e9e", cacheSeconds: 300 }, 404);
}
c.header("Cache-Control", "public, max-age=600, stale-while-revalidate=86400");
return c.json(buildShieldsBadge(quality, 600));
});

app.get("/v1/auth/github/start", async (c) => {
try {
const start = await startGitHubWebOAuth(c.env, c.req.url, c.req.query("returnTo"));
Expand Down Expand Up @@ -2882,6 +2921,7 @@
requireLinkedIssue: parsed.data.requireLinkedIssue,
backfillEnabled: parsed.data.backfillEnabled,
privateTrustEnabled: parsed.data.privateTrustEnabled,
badgeEnabled: parsed.data.badgeEnabled,
commandAuthorization: normalizeCommandAuthorizationPolicy(parsed.data.commandAuthorization).policy,
}),
);
Expand Down Expand Up @@ -4453,6 +4493,7 @@
if (path === "/health") return false;
if (path === "/v1/mcp/compatibility") return false;
if (/^\/v1\/public\/github\/repos\/[^/]+\/[^/]+\/stats$/.test(path)) return false;
if (/^\/v1\/public\/repos\/[^/]+\/[^/]+\/badge\.(svg|json)$/.test(path)) return false;
if (path === "/v1/public/subnet-interface") return false;
if (path === "/openapi.json") return false;
if (path === "/mcp") return false;
Expand Down
5 changes: 5 additions & 0 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, desc, eq, gte, inArray, not, or, sql, type SQL } from "drizzle-orm";

Check warning on line 1 in src/db/repositories.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/db/repositories.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/db/repositories.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in src/db/repositories.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/db/repositories.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/db/repositories.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
import { getDb } from "./client";
import {
advisories,
Expand Down Expand Up @@ -415,6 +415,7 @@
requireLinkedIssue: false,
backfillEnabled: true,
privateTrustEnabled: true,
badgeEnabled: false,
commandAuthorization: normalizeCommandAuthorizationPolicy(DEFAULT_COMMAND_AUTHORIZATION_POLICY).policy,
};
}
Expand Down Expand Up @@ -446,6 +447,7 @@
requireLinkedIssue: row.requireLinkedIssue,
backfillEnabled: row.backfillEnabled,
privateTrustEnabled: row.privateTrustEnabled,
badgeEnabled: row.badgeEnabled,
commandAuthorization: parseCommandAuthorizationPolicy(row.commandAuthorizationJson),
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -481,6 +483,7 @@
requireLinkedIssue: settings.requireLinkedIssue ?? false,
backfillEnabled: settings.backfillEnabled ?? true,
privateTrustEnabled: settings.privateTrustEnabled ?? true,
badgeEnabled: settings.badgeEnabled ?? false,
commandAuthorization: normalizeCommandAuthorizationPolicy(settings.commandAuthorization).policy,
};
const db = getDb(env.DB);
Expand Down Expand Up @@ -514,6 +517,7 @@
requireLinkedIssue: resolved.requireLinkedIssue,
backfillEnabled: resolved.backfillEnabled,
privateTrustEnabled: resolved.privateTrustEnabled,
badgeEnabled: resolved.badgeEnabled,
commandAuthorizationJson: jsonString(resolved.commandAuthorization),
updatedAt: nowIso(),
})
Expand Down Expand Up @@ -548,6 +552,7 @@
requireLinkedIssue: resolved.requireLinkedIssue,
backfillEnabled: resolved.backfillEnabled,
privateTrustEnabled: resolved.privateTrustEnabled,
badgeEnabled: resolved.badgeEnabled,
commandAuthorizationJson: jsonString(resolved.commandAuthorization),
updatedAt: nowIso(),
},
Expand Down
1 change: 1 addition & 0 deletions src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { index, integer, real, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";

Check warning on line 1 in src/db/schema.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/db/schema.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/db/schema.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in src/db/schema.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/db/schema.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/db/schema.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
// Timestamp columns use a drizzle $defaultFn so an insert that omits the column gets a real ISO-8601
// timestamp. A static `.default("CURRENT_TIMESTAMP")` would make drizzle inject the literal STRING
// "CURRENT_TIMESTAMP" (it applies static defaults client-side, never reaching SQLite's CURRENT_TIMESTAMP),
Expand Down Expand Up @@ -66,6 +66,7 @@
requireLinkedIssue: integer("require_linked_issue", { mode: "boolean" }).notNull().default(false),
backfillEnabled: integer("backfill_enabled", { mode: "boolean" }).notNull().default(true),
privateTrustEnabled: integer("private_trust_enabled", { mode: "boolean" }).notNull().default(true),
badgeEnabled: integer("badge_enabled", { mode: "boolean" }).notNull().default(false),
commandAuthorizationJson: text("command_authorization_json").notNull().default("{}"),
createdAt: text("created_at").notNull().$defaultFn(() => nowIso()),
updatedAt: text("updated_at").notNull().$defaultFn(() => nowIso()),
Expand Down
78 changes: 78 additions & 0 deletions src/services/public-repo-quality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { PullRequestRecord } from "../types";

Check warning on line 1 in src/services/public-repo-quality.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/services/public-repo-quality.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/services/public-repo-quality.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in src/services/public-repo-quality.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/services/public-repo-quality.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/services/public-repo-quality.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

// Public-safe repository quality metrics for the unauthenticated README badge (#541).
//
// HARD whitelist: this module derives ONLY three coarse, repo-level, public-safe metrics from cached
// pull-request records — median time-to-merge, the share of non-slop merged contributions, and a coarse
// queue-health level. It never reads or exposes contributor-level data, reward/trust values, or private
// scoreability context. Pure and deterministic (clock injected) so the badge surface stays auditable.

export type QueueHealthLevel = "low" | "medium" | "high" | "critical";

export type PublicRepoQuality = {
/** Median hours from PR open to merge across known merged PRs. `null` when none are known. */
medianTimeToMergeHours: number | null;
/** Share (0-100) of *assessed* merged PRs whose slop band is clean/low. `null` when none assessed. */
realContributionPct: number | null;
queueHealthLevel: QueueHealthLevel;
/** Counts only — included for transparency, never contributor-level detail. */
mergedSampleSize: number;
assessedSampleSize: number;
};

const STALE_OPEN_PR_DAYS = 14;
const MS_PER_HOUR = 3_600_000;
const MS_PER_DAY = 86_400_000;
const NON_SLOP_BANDS: ReadonlySet<string> = new Set(["clean", "low"]);

export function buildPublicRepoQuality(pullRequests: PullRequestRecord[], now: number = Date.now()): PublicRepoQuality {
const merged = pullRequests.filter(isMergedPullRequest);
const mergeDurations = merged
.map(mergeDurationHours)
.filter((hours): hours is number => hours !== null);
const assessed = merged.filter((pr) => typeof pr.slopBand === "string" && pr.slopBand.trim().length > 0);
const nonSlop = assessed.filter((pr) => NON_SLOP_BANDS.has((pr.slopBand as string).toLowerCase()));

return {
medianTimeToMergeHours: mergeDurations.length > 0 ? Math.round(median(mergeDurations)) : null,
realContributionPct: assessed.length > 0 ? Math.round((nonSlop.length / assessed.length) * 100) : null,
queueHealthLevel: resolveQueueHealthLevel(pullRequests, now),
mergedSampleSize: merged.length,
assessedSampleSize: assessed.length,
};
}

function isMergedPullRequest(pr: PullRequestRecord): boolean {
return Boolean(pr.mergedAt) || pr.state.toLowerCase() === "merged";
}

function mergeDurationHours(pr: PullRequestRecord): number | null {
if (!pr.mergedAt || !pr.createdAt) return null;
const merged = Date.parse(pr.mergedAt);
const created = Date.parse(pr.createdAt);
if (!Number.isFinite(merged) || !Number.isFinite(created) || merged < created) return null;
return (merged - created) / MS_PER_HOUR;
}

function median(values: number[]): number {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 0) return ((sorted[mid - 1] as number) + (sorted[mid] as number)) / 2;
return sorted[mid] as number;
}

// Coarse, public-safe queue level derived only from open-PR volume and staleness — deliberately simpler
// than the internal QueueHealth signal so no private-derived value reaches this unauthenticated surface.
function resolveQueueHealthLevel(pullRequests: PullRequestRecord[], now: number): QueueHealthLevel {
const open = pullRequests.filter((pr) => pr.state.toLowerCase() === "open");
const openCount = open.length;
const staleCount = open.filter((pr) => {
const stamp = Date.parse(pr.updatedAt ?? pr.createdAt ?? "");
return Number.isFinite(stamp) && (now - stamp) / MS_PER_DAY >= STALE_OPEN_PR_DAYS;
}).length;

if (openCount >= 50 || staleCount >= 20) return "critical";
if (openCount >= 20 || staleCount >= 8) return "high";
if (openCount >= 5 || staleCount >= 2) return "medium";
return "low";
}
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type JsonPrimitive = string | number | boolean | null;

Check warning on line 1 in src/types.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/types.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/types.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in src/types.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in src/types.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in src/types.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };

export type JobMessage =
Expand Down Expand Up @@ -444,6 +444,9 @@
requireLinkedIssue: boolean;
backfillEnabled: boolean;
privateTrustEnabled: boolean;
/** Opt-in for the public, unauthenticated README status badge (#541). Always populated by the DB layer
* (default false); optional so existing settings fixtures/callers need not be touched. */
badgeEnabled?: boolean | undefined;
commandAuthorization?: RepositoryCommandAuthorizationPolicy | undefined;
createdAt?: string | null | undefined;
updatedAt?: string | null | undefined;
Expand Down
56 changes: 56 additions & 0 deletions test/integration/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

Check warning on line 1 in test/integration/api.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in test/integration/api.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in test/integration/api.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.

Check warning on line 1 in test/integration/api.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #541.

Check notice on line 1 in test/integration/api.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #541.

Check notice on line 1 in test/integration/api.test.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
import { createSessionForGitHubUser, hashToken } from "../../src/auth/security";
import {
upsertBounty,
Expand Down Expand Up @@ -193,6 +193,62 @@
await expect(unavailable.json()).resolves.toMatchObject({ error: "github_repo_stats_unavailable" });
});

it("serves the public README badge only for installed, opted-in repos (#541)", async () => {
const app = createApp();
const env = createTestEnv();

// Installed + opted in, with assessed merged PRs.
await upsertRepositoryFromGitHub(env, { name: "badged", full_name: "acme/badged", private: false, owner: { login: "acme" }, default_branch: "main" }, 555);
await upsertRepositorySettings(env, { repoFullName: "acme/badged", badgeEnabled: true });
await upsertPullRequestFromGitHub(env, "acme/badged", { number: 1, title: "Feature", state: "merged", created_at: "2026-06-01T00:00:00Z", merged_at: "2026-06-01T04:00:00Z", labels: [] });
await upsertPullRequestFromGitHub(env, "acme/badged", { number: 2, title: "Slop", state: "merged", created_at: "2026-06-02T00:00:00Z", merged_at: "2026-06-02T06:00:00Z", labels: [] });
await updatePullRequestSlopAssessment(env, "acme/badged", 1, { slopRisk: 0, slopBand: "clean" });
await updatePullRequestSlopAssessment(env, "acme/badged", 2, { slopRisk: 80, slopBand: "high" });

const svg = await app.request("/v1/public/repos/acme/badged/badge.svg", {}, env);
expect(svg.status).toBe(200);
expect(svg.headers.get("content-type")).toContain("image/svg+xml");
expect(svg.headers.get("cache-control")).toContain("stale-while-revalidate");
const svgBody = await svg.text();
expect(svgBody.startsWith("<svg")).toBe(true);
expect(svgBody).toContain("gittensory");
expect(svgBody).toContain("% real");
expect(svgBody).not.toMatch(FORBIDDEN_PUBLIC_REPORT_TERMS);

const json = await app.request("/v1/public/repos/acme/badged/badge.json", {}, env);
expect(json.status).toBe(200);
await expect(json.json()).resolves.toMatchObject({ schemaVersion: 1, label: "gittensory", message: expect.stringContaining("real") });

// Installed but NOT opted in → unavailable, no metrics.
await upsertRepositoryFromGitHub(env, { name: "private", full_name: "acme/private", private: false, owner: { login: "acme" }, default_branch: "main" }, 556);
const notOptedIn = await app.request("/v1/public/repos/acme/private/badge.svg", {}, env);
expect(notOptedIn.status).toBe(404);
expect(await notOptedIn.text()).toContain("unavailable");

// Opted in but NOT installed → unavailable.
await upsertRepositoryFromGitHub(env, { name: "uninstalled", full_name: "acme/uninstalled", private: false, owner: { login: "acme" }, default_branch: "main" });
await upsertRepositorySettings(env, { repoFullName: "acme/uninstalled", badgeEnabled: true });
const notInstalled = await app.request("/v1/public/repos/acme/uninstalled/badge.svg", {}, env);
expect(notInstalled.status).toBe(404);

// Unknown repo → unavailable shields payload.
const unknown = await app.request("/v1/public/repos/acme/missing/badge.json", {}, env);
expect(unknown.status).toBe(404);
await expect(unknown.json()).resolves.toMatchObject({ message: "unavailable" });
});

it("persists the badgeEnabled opt-in through the settings write endpoint (#541)", async () => {
const app = createApp();
const env = createTestEnv();
const response = await app.request(
"/v1/internal/repos/acme/badged/settings",
{ method: "POST", headers: { authorization: `Bearer ${env.INTERNAL_JOB_TOKEN}`, "content-type": "application/json" }, body: JSON.stringify({ badgeEnabled: true }) },
env,
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ repoFullName: "acme/badged", badgeEnabled: true });
});

it("rejects invalid public GitHub repo stats paths before calling GitHub", async () => {
const app = createApp();
const env = createTestEnv();
Expand Down
Loading
Loading