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
15 changes: 15 additions & 0 deletions src/lib/profile/consideration-registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ describe("consideration registry integrity", () => {
}
});

test("scope, when set, is one of the allowed values", () => {
const scopes = ["repo", "org", "migration"];
for (const g of MIGRATION_CONSIDERATIONS) {
if (g.scope !== undefined) expect(scopes, g.id).toContain(g.scope);
}
});

test("migration-scope considerations are accepted losses that route nowhere", () => {
for (const g of MIGRATION_CONSIDERATIONS.filter((x) => x.scope === "migration")) {
expect(g.kind, g.id).toBe("accepted-loss");
expect(g.routesTo, g.id).toBeNull();
expect(g.detector, g.id).toBe("always");
}
});

test("accepted-loss considerations are informational and route nowhere", () => {
for (const g of MIGRATION_CONSIDERATIONS.filter((x) => x.kind === "accepted-loss")) {
expect(g.severity, g.id).toBe("info");
Expand Down
190 changes: 189 additions & 1 deletion src/lib/profile/consideration-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,17 @@ export const MIGRATION_DOC_URL =
"https://docs.github.com/en/migrations/using-github-enterprise-importer/migrating-between-github-products/about-migrations-between-github-products";

/** Date the registry was last reconciled with the migration docs (YYYY-MM-DD). */
export const MIGRATION_DOCS_VERIFIED = "2026-06-13";
export const MIGRATION_DOCS_VERIFIED = "2026-06-15";

/**
* Where a consideration's data lives — which determines how the Profiler
* evaluates and surfaces it:
* - `repo` — per repository; detected in the per-repo crawl and shown in the matrix.
* - `org` — once per organization (secrets, runners, teams…); shown in the org panel.
* - `migration` — a whole-migration fact that always applies (mannequins, audit logs…);
* there's nothing to count, so it's listed as an always-present loss.
*/
type ConsiderationScope = "repo" | "org" | "migration";

/**
* What kind of remediation a consideration needs:
Expand Down Expand Up @@ -50,6 +60,11 @@ export interface Consideration {
label: string;
kind: ConsiderationKind;
severity: ConsiderationSeverity;
/**
* Where the data lives. Defaults to `repo` (the common case) when omitted, so
* only org- and migration-scoped entries need to state it.
*/
scope?: ConsiderationScope;
/**
* Identifier of the profiler check that detects this consideration. The
* detection function itself lives server-side and keys off this string.
Expand Down Expand Up @@ -130,6 +145,7 @@ export const MIGRATION_CONSIDERATIONS: readonly Consideration[] = [
label: "Actions secrets & variables",
kind: "recreate",
severity: "warn",
scope: "org",
detector: "actions-secrets-count",
confidence: "exact",
routesTo: "Re-create secrets/variables on the target",
Expand All @@ -153,6 +169,7 @@ export const MIGRATION_CONSIDERATIONS: readonly Consideration[] = [
label: "Self-hosted runners",
kind: "recreate",
severity: "warn",
scope: "org",
detector: "actions-runners-count",
confidence: "exact",
routesTo: "Re-register runners on the target",
Expand All @@ -164,6 +181,7 @@ export const MIGRATION_CONSIDERATIONS: readonly Consideration[] = [
label: "Dependabot secrets",
kind: "recreate",
severity: "warn",
scope: "org",
detector: "dependabot-secrets-count",
confidence: "exact",
routesTo: "Re-create Dependabot secrets",
Expand All @@ -175,12 +193,51 @@ export const MIGRATION_CONSIDERATIONS: readonly Consideration[] = [
label: "Codespaces secrets",
kind: "recreate",
severity: "warn",
scope: "org",
detector: "codespaces-secrets-count",
confidence: "exact",
routesTo: "Re-create Codespaces secrets",
summary: "Codespaces secrets are not migrated.",
docAnchor: NOT_MIGRATED,
},
{
id: "teams",
label: "Teams & team membership",
kind: "recreate",
severity: "warn",
scope: "org",
detector: "org-teams-count",
confidence: "exact",
routesTo: "Recreate teams and re-add members on the target",
summary:
"On a direct repository migration teams are not migrated; even on an organization migration, team membership is not carried over and members must be re-added.",
docAnchor: NOT_MIGRATED,
},
{
id: "github-apps",
label: "GitHub Apps & installations",
kind: "recreate",
severity: "warn",
scope: "org",
detector: "org-app-installations",
confidence: "exact",
routesTo: "Reinstall and reconfigure GitHub Apps on the target",
summary:
"GitHub Apps and their installations are not migrated; each must be reinstalled and reconfigured on the destination.",
docAnchor: NOT_MIGRATED,
},
{
id: "repo-collaborators",
label: "Repository access (collaborators & teams)",
kind: "recreate",
severity: "warn",
detector: "repo-collaborators-count",
confidence: "exact",
routesTo: "Re-grant collaborator and team access on the target",
summary:
"Direct user access and team access to a repository are not migrated — collaborators and their permissions must be re-granted after migrating.",
docAnchor: NOT_MIGRATED,
},

// ── Reconfigure: migrates but needs a target-side pass ──────────────────────
{
Expand Down Expand Up @@ -246,12 +303,25 @@ export const MIGRATION_CONSIDERATIONS: readonly Consideration[] = [
label: "Custom properties",
kind: "reconfigure",
severity: "info",
scope: "org",
detector: "custom-properties-api",
confidence: "exact",
routesTo: "Re-apply custom properties",
summary: "Repository custom properties are not migrated.",
docAnchor: NOT_MIGRATED,
},
{
id: "tag-protection",
label: "Tag protection rules",
kind: "reconfigure",
severity: "info",
detector: "tag-protection-rules",
confidence: "exact",
routesTo: "Recreate tag protection (or move it to rulesets)",
summary:
"Tag protection rules are not migrated; recreate them on the target — GitHub now recommends expressing them as rulesets.",
docAnchor: NOT_MIGRATED,
},

// ── Blockers: size/policy limits that can fail the migration ────────────────
{
Expand Down Expand Up @@ -370,4 +440,122 @@ export const MIGRATION_CONSIDERATIONS: readonly Consideration[] = [
summary: "Fork relationships between repositories are not preserved.",
docAnchor: NOT_MIGRATED,
},

// ── Always lost: whole-migration facts that apply to every run ──────────────
// Nothing to count per repo — these are intrinsic to how the Importer works,
// so they're surfaced as always-present losses rather than detected signals.
{
id: "mannequins",
label: "User attribution (mannequins)",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary:
"Imported issues, PRs, and comments are attributed to placeholder “mannequin” users until an admin reclaims each one to a real account on the target.",
docAnchor: LIMITS,
},
{
id: "audit-log",
label: "Audit logs",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary: "Audit log history is not migrated.",
docAnchor: NOT_MIGRATED,
},
{
id: "commit-status-checks",
label: "Commit status checks",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary:
"Historical commit status checks — the pass/fail marks CI posts on commits — are not migrated.",
docAnchor: NOT_MIGRATED,
},
{
id: "comment-edit-history",
label: "Comment edit history",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary:
"Only the latest version of each issue and pull request comment migrates — prior edit history is not carried over.",
docAnchor: NOT_MIGRATED,
},
{
id: "sub-issues",
label: "Sub-issue relationships",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary: "Parent/child sub-issue relationships are not preserved by the migration.",
docAnchor: NOT_MIGRATED,
},
{
id: "cross-repo-references",
label: "Cross-repository references",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary:
"Autolinked references between pull requests and issues in different repositories are not migrated and will not resolve.",
docAnchor: NOT_MIGRATED,
},
{
id: "mention-relinking",
label: "@mention re-linking",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary:
"Mentions of users, teams, and organizations keep their original text but are not re-linked to accounts on the target.",
docAnchor: NOT_MIGRATED,
},
{
id: "secret-scanning-states",
label: "Secret-scanning remediation states",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary:
"Remediation states of secret-scanning results (resolved, false-positive, …) are not migrated.",
docAnchor: NOT_MIGRATED,
},
{
id: "activity-feed",
label: "Repository activity feed",
kind: "accepted-loss",
severity: "info",
scope: "migration",
detector: "always",
confidence: "exact",
routesTo: null,
summary: "The repository activity feed is not migrated.",
docAnchor: NOT_MIGRATED,
},
];
10 changes: 10 additions & 0 deletions src/lib/server/profile/analyze.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ function cleanSignals(over: Partial<RepoSignals> = {}): RepoSignals {
workflowFileCount: 0,
webhooksCount: 0,
hasCodeScanningAlerts: false,
collaboratorsCount: 0,
tagProtectionCount: 0,
...over,
};
}
Expand Down Expand Up @@ -101,6 +103,8 @@ describe("analyzeRepo", () => {
webhooksCount: 2,
hasPages: true,
hasCodeScanningAlerts: true,
collaboratorsCount: 5,
tagProtectionCount: 2,
}),
);

Expand All @@ -127,6 +131,12 @@ describe("analyzeRepo", () => {
expect(finding(profile, "webhooks")?.evidence).toBe("2 webhooks");
expect(finding(profile, "pages")?.status).toBe("applies");
expect(finding(profile, "code-scanning-history")?.status).toBe("applies");
expect(finding(profile, "repo-collaborators")?.evidence).toBe(
"5 direct collaborators (access not migrated)",
);
expect(finding(profile, "tag-protection")?.evidence).toBe(
"2 tag protection rules (recreate as rulesets)",
);
});

test("rolls up applying considerations by severity", () => {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/server/profile/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,15 @@ const DETECTORS: Record<string, Detector> = {
? `${count(s.branchProtectionRulesUsingUnmigratedFeatures, "rule")} using unmigrated features`
: null,
rulesets: (s) => (s.rulesetCount > 0 ? count(s.rulesetCount, "ruleset") : null),
"tag-protection": (s) =>
s.tagProtectionCount > 0
? `${count(s.tagProtectionCount, "tag protection rule")} (recreate as rulesets)`
: null,
webhooks: (s) => (s.webhooksCount > 0 ? count(s.webhooksCount, "webhook") : null),
"repo-collaborators": (s) =>
s.collaboratorsCount > 0
? `${count(s.collaboratorsCount, "direct collaborator")} (access not migrated)`
: null,
pages: (s) => (s.hasPages ? "GitHub Pages enabled (reconfigure on the target)" : null),
"code-scanning-history": (s) =>
s.hasCodeScanningAlerts ? "code scanning alerts present (history & states not migrated)" : null,
Expand Down Expand Up @@ -144,6 +152,8 @@ export const ORG_EVALUATED_CONSIDERATION_IDS: readonly string[] = [
"dependabot-secrets",
"codespaces-secrets",
"custom-properties",
"teams",
"github-apps",
];

function summarize(findings: ConsiderationFinding[]): ProfileSummary {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/server/profile/augment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,8 @@ function countsToSignals(repo: DiscoveredRepo, node: RepoCountsNode | null): Rep
releaseAssetBytes: 0,
webhooksCount: 0,
hasCodeScanningAlerts: false,
collaboratorsCount: 0,
tagProtectionCount: 0,
// Cheap counts.
issuesCount: node?.issues.totalCount ?? 0,
pullRequestsCount: node?.pullRequests.totalCount ?? 0,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/server/profile/estimate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ function signals(diskUsageKb: number | null): RepoSignals {
workflowFileCount: 0,
webhooksCount: 0,
hasCodeScanningAlerts: false,
collaboratorsCount: 0,
tagProtectionCount: 0,
};
}

Expand Down
11 changes: 9 additions & 2 deletions src/lib/server/profile/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ function parse(frame: string): ProfileSseEvent {
}

const progress = (over: Partial<{ profiled: number; total: number; repo: string }> = {}) =>
({ type: "progress", profiled: 1, total: 3, repo: "acme/widget", ...over }) as const;
({
type: "progress",
profiled: 1,
total: 3,
repo: "acme/widget",
phase: "counting",
...over,
}) as const;

describe("subscribeProfile / publishProfileEvent", () => {
test("delivers published events to a subscriber as SSE frames", () => {
Expand All @@ -37,7 +44,7 @@ describe("subscribeProfile / publishProfileEvent", () => {

expect(sink).toHaveLength(1);
expect(sink[0]).toBe(
'data: {"type":"progress","profiled":1,"total":3,"repo":"acme/widget"}\n\n',
'data: {"type":"progress","profiled":1,"total":3,"repo":"acme/widget","phase":"counting"}\n\n',
);
expect(parse(sink[0] as string)).toEqual(progress());
});
Expand Down
4 changes: 2 additions & 2 deletions src/lib/server/profile/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
* replay here. The bus just nudges connected clients (keyed by run id) to
* refetch, and signals a terminal `done` so they can stop streaming.
*/
import type { ProfileRunState } from "./types";
import type { ProfilePhase, ProfileRunState } from "./types";

/** An event pushed to a run's subscribers. */
export type ProfileSseEvent =
| { type: "progress"; profiled: number; total: number; repo: string }
| { type: "progress"; profiled: number; total: number; repo: string; phase: ProfilePhase }
| { type: "done"; state: ProfileRunState };

/** Live stream controllers, keyed by run id. */
Expand Down
2 changes: 2 additions & 0 deletions src/lib/server/profile/insights.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ function signals(over: Partial<RepoSignals> = {}): RepoSignals {
workflowFileCount: 0,
webhooksCount: 0,
hasCodeScanningAlerts: false,
collaboratorsCount: 0,
tagProtectionCount: 0,
...over,
};
}
Expand Down
Loading