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
80 changes: 80 additions & 0 deletions .github/scorecard/SCORECARD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Coder Registry Module Scorecard

**100 pts** = **Universal criteria (75)** + **one track (25)**

Score each criterion as **0 / half / full**.

## Scoring rubric

### Universal criteria — 75 pts

| Criterion | Pts | Pass = |
| -------------------------------------------------- | -----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Presentation & Onboarding** | **25** | |
| Configuration-mode examples | 12 | If the module has many options, each major mode has a documented example with sensible defaults, for example provider choice, app vs headless |
| Coder-context framing | 8 | Explains what the module adds on top of Coder, names both Coder and the target tool, and shows where Coder fits in the flow |
| Visual preview | 5 | README includes an image, GIF, or video of the module in action |
| **Credential Hygiene** | **20** | |
| Secrets marked sensitive | 16 | Sensitive inputs are marked `sensitive = true`, and README examples avoid inline secrets |
| Non-hardcoded auth path | 4 | If possible, README shows a path that avoids pasting raw keys into templates, for example ServiceAccount, IAM/OAuth/external auth, API key helper, or AI Gateway |
| **Restricted-Network Readiness** _(if applicable)_ | **20** | |
| Mirrorable artifact source | 10 | Download or install URL is configurable so it can point to an internal mirror or artifact store instead of the public internet |
| Bring-your-own binary | 6 | Download or install can be disabled entirely when the tool is already baked into the image |
| Egress transparency | 4 | External endpoints are documented, plus notes for restricted or air-gapped environments |
| **Engineering Quality** | **10** | |
| Input quality | 6 | Inputs have clear descriptions, sensible defaults, and `validation` where appropriate |
| Test coverage | 4 | Clear testing story, `.tftest.hcl` primarily covers business logic, TypeScript tests cover end-to-end behavior |

### Agent track — 25 pts

| Criterion | Pts | Pass = |
| --------------------- | --: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| AI governance | 10 | Documented support for Coder AI Gateway and/or Agent Firewall, including how Coder governs auth, routing, or policy enforcement for the agent |
| Dashboard entry point | 5 | Documented `coder_app` support, built-in or example |
| Session continuity | 5 | Documented support for continuing an existing agent session across reconnects or relaunches, either through native resume/session ID support or a persistent session manager such as boo, tmux, or screen |
| Managed configuration | 5 | Documented support for managed MCP, settings, policies, or workdir config |

### IDE track — 25 pts

| Criterion | Pts | Pass = |
| ------------------------------------------ | --: | ---------------------------------------------------------------------------- |
| Dashboard entry point | 7 | `coder_app` support with proper launch behavior |
| Managed configuration | 6 | Documented support for managed IDE settings or config |
| Configurable folder or workdir | 6 | Documented support for opening or starting in a configured folder or workdir |
| Pre-installed extensions _(web IDEs only)_ | 6 | For web IDEs, documented support for pre-installing extensions |

### Utility track

Utility modules are scored on Universal criteria only, then normalized:

`round(raw / 75 * 100)`

## Notes

| Topic | Rule |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Track assignment | Every score should record which track was used |
| Internal building blocks | Modules whose README cautions against direct use (for example "we do not recommend using this module directly") are excluded from scoring entirely |
| If applicable | Criteria or themes marked _if applicable_ are excluded when the concern does not exist by construction, for example a module that downloads nothing. Excluded points are removed from the denominator and the final score is normalized to 100 |
| N/A handling | Outside _if applicable_ criteria, missing support scores zero. Only Utility modules skip the track section |
| Scoring | Full = implemented and documented; half = partial, awkward, or under-documented; zero = absent |

## Grading

| Score | Badge |
| -----: | ---------- |
| 90-100 | Exemplary |
| 75-89 | Strong |
| 50-74 | Adequate |
| <50 | Needs work |

## Output format

Every scorecard leads with a theme-level summary table, then a collapsed
drilldown with per-criterion tables grouped by theme.

## Live scorecards

Every scored module is listed in the pinned
[📊 Module Scorecards](https://github.com/coder/registry/discussions/1011)
discussion, which links each module's dedicated scorecard discussion.
159 changes: 159 additions & 0 deletions .github/scorecard/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/**
* Shared constants, types, and helpers for the module scorecard scripts.
*/

export const REPO_OWNER = "coder";
export const REPO_NAME = "registry";
export const REPO_ID = "R_kgDOOVbRAA"; // coder/registry
export const CATEGORY_ID = "DIC_kwDOOVbRAM4DBMLT"; // "Modules" category

export const MARKER = "<!-- module-scorecard -->";
export const INDEX_MARKER = "<!-- module-scorecard-index -->";
export const INDEX_TITLE = "📊 Module Scorecards";

export interface SummaryScores {
track: "Agent" | "IDE" | "Utility";
presentation: string;
integration: string;
credential: string;
network: string;
engineering: string;
overall: string;
overallNum: number;
}

export interface Result extends SummaryScores {
module: string;
name: string;
url: string;
scoredAt: string;
}

export async function graphql<T>(
query: string,
variables: Record<string, unknown> = {},
): Promise<T> {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GITHUB_DISCUSSIONS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
const body = (await res.json()) as { data?: T; errors?: unknown[] };
if (!res.ok || body.errors?.length) {
throw new Error(
`GitHub GraphQL error: ${JSON.stringify(body.errors ?? body)}`,
);
}
return body.data!;
}

// Parses the theme-level summary table out of scorecard markdown (either a
// raw scorecard or a discussion body containing one).
export function parseSummary(markdown: string): SummaryScores | null {
const lines = markdown.split("\n");
const headerIdx = lines.findIndex(
(l) =>
l.startsWith("|") && l.includes("Overall") && l.includes("Presentation"),
);
if (headerIdx === -1) return null;
const headers = lines[headerIdx]
.split("|")
.map((c) => c.trim())
.filter(Boolean);
const cells = lines[headerIdx + 2]
?.split("|")
.map((c) => c.trim().replace(/\*\*/g, ""))
.filter(Boolean);
if (!cells || cells.length !== headers.length) return null;
const get = (name: string) => {
const i = headers.findIndex((h) => h.toLowerCase().includes(name));
return i === -1 ? "—" : cells[i];
};
const integrationIdx = headers.findIndex((h) => /integration/i.test(h));
const track: SummaryScores["track"] =
integrationIdx === -1
? "Utility"
: /agent/i.test(headers[integrationIdx])
? "Agent"
: "IDE";
const overall = get("overall");
return {
track,
presentation: get("presentation"),
integration: integrationIdx === -1 ? "—" : cells[integrationIdx],
credential: get("credential"),
network: get("restricted"),
engineering: get("engineering"),
overall,
overallNum: Number(overall.match(/(\d+)\s*\/\s*100/)?.[1] ?? 0),
};
}

export interface DiscussionRef {
id: string;
title: string;
url: string;
body: string;
}

export async function findDiscussionByTitle(
title: string,
): Promise<DiscussionRef | null> {
const q = `"${title}" in:title repo:${REPO_OWNER}/${REPO_NAME}`;
const data = await graphql<{ search: { nodes: DiscussionRef[] } }>(
`
query ($q: String!) {
search(query: $q, type: DISCUSSION, first: 10) {
nodes {
... on Discussion {
id
title
url
body
}
}
}
}
`,
{ q },
);
return data.search.nodes.find((n) => n.title === title) ?? null;
}

// Finds a module's scorecard discussion by the registry URL in its body,
// regardless of title. Used to detect display_name renames, where the
// title lookup misses but the discussion still exists. GitHub search
// tokenizes URLs, so search the module slug and filter by the exact URL.
export async function findDiscussionByModuleUrl(
module: string,
): Promise<DiscussionRef | null> {
const q = `${module} in:body repo:${REPO_OWNER}/${REPO_NAME}`;
const data = await graphql<{ search: { nodes: DiscussionRef[] } }>(
`
query ($q: String!) {
search(query: $q, type: DISCUSSION, first: 20) {
nodes {
... on Discussion {
id
title
url
body
}
}
}
}
`,
{ q },
);
return (
data.search.nodes.find(
(n) =>
n.body.includes(MARKER) &&
!n.body.includes(INDEX_MARKER) &&
n.body.includes(`registry.coder.com/modules/coder/${module})`),
) ?? null
);
}
Loading
Loading