Skip to content

feat(profile): migration summary, duration estimate, and external-tool signals - #25

Merged
cvega merged 6 commits into
mainfrom
feat/profile-summary
Jun 15, 2026
Merged

cvega merged 6 commits into
mainfrom
feat/profile-summary

Conversation

@cvega

@cvega cvega commented Jun 15, 2026

Copy link
Copy Markdown
Owner

What

Adds an org-level Migration summary to the profile detail page — a preparation checklist (what external tooling/reconfiguration the migration needs, and how many repos each touches) plus a coarse duration estimate with adjustable parallelism — and gathers the remaining signals needed to surface the named considerations.

Three commits, each green:

1. Migration summary + duration estimate (f0a58af)

  • Summary (summary.ts): rolls per-repo findings up by consideration, joins each one's external-tool remediation from the registry, counts affected repos, and honestly lists what's not yet crawled. Plus repo-level blocker/warn/clean tallies.
  • Estimate (estimate.ts): buckets repos into S/M/L/XL size bands (tunable hour ranges) and sums sequential work. Wall-clock = total ÷ parallelism, divided client-side so the parallelism knob (default 10, the migrate queue cap) stays live.
  • New signals folded into the batched augment query: GitHub Packages count and Git LFS usage (root .gitattributes filter=lfs), lighting up two previously-indeterminate considerations.
  • format.ts: formatHours for coarse duration display.

2. Oversized release assets (2810c1e)

  • The augment query now scans release assets (first 100 releases × first 50 assets) and sums their bytes into releaseAssetBytes. Two blockers light up: release-size-limit (≥ 10 GiB, GHEC's per-repo release cap) and metadata-archive-limit (≥ 40 GiB). Bounded → an estimate; summing is monotonic so truncation only risks a false negative just under a threshold.

3. Org rulesets (512d873)

  • GraphQL doesn't expose rulesets, so the runner makes one best-effort REST call per run (GET /orgs/{org}/rulesets) and persists the count (profile_runs.org_ruleset_count, additive column). The page shows a warning callout when any exist — an org ruleset can fail the migration outright.

Named items, now covered

Item Status
Releases > 10 GiB ✅ detected (release-size + metadata blockers)
Packages ✅ detected
Git LFS ✅ detected (root .gitattributes)
Branch protections ✅ already detected
Rulesets ✅ org-level (repo-level left as a future deeper pass)

Estimate model

Deliberately coarse per the chosen approach: size bands × parallelism, with centralized, tunable constants clearly labeled "rough estimate — calibrate as real timings accumulate." There's no published Importer throughput figure, so this is a transparent heuristic, not a guarantee.

Verification

  • bun run verify:json — all 11 gates green; coverage 87.34 / 85.74
  • New modules ~100% covered (summary, estimate, rulesets, augment, analyze)
  • Runtime smoke: seeded a run + booted the build — the API returns correct band counts, summed hours, severity-sorted prep items, blocker/warn/clean tallies, and org ruleset count; the page SSRs the full Migration summary section.

Needs live validation

The new GraphQL fields (packages, gitattributes blob, release assets) and the rulesets REST endpoint can't be exercised against a live org from here. All are defensive (partial-data recovery; rulesets degrades to 0 on any error), but a first crawl against a real org should confirm the query shapes and token scopes.

cvega added 6 commits June 14, 2026 18:17
Adds an org-level "Migration summary" to the profile detail page: a
preparation checklist (which external tooling/reconfiguration the
migration needs, and how many repos each touches) plus a coarse,
size-band duration estimate with adjustable parallelism.

The batched augment pass now also gathers each repo's GitHub Packages
count and Git LFS usage (root .gitattributes filter=lfs), lighting up
the packages and git-lfs considerations that were previously
indeterminate.

- augment: + packages{totalCount} and a .gitattributes blob lookup
- analyze: + packages and git-lfs detectors
- summary.ts: buildPreparationSummary rolls per-repo findings up by
  consideration, joins registry remediation metadata, counts affected
  repos, and honestly lists not-yet-crawled considerations
- estimate.ts: estimateDuration buckets repos into S/M/L/XL size bands
  (tunable hour ranges) and sums sequential work; wall-clock is
  total / parallelism, divided client-side so the knob stays live
- format: + formatHours for coarse duration display
- service: ProfileDetail gains { summary, estimate }
- UI: Migration summary section (readiness rollup, estimate + parallel
  input, prep checklist, coverage note)

Estimate constants are rough and centralized for calibration. The new
GraphQL fields need validation against a live org.

11 gates green; coverage 87.23/85.42.
The batched augment query now also scans release assets (first 100
releases x first 50 assets each) and sums their bytes into
RepoSignals.releaseAssetBytes. Two blockers light up:

- release-size-limit: >= 10 GiB of assets (GHEC's per-repo release cap)
- metadata-archive-limit: >= 40 GiB (the Importer's metadata limit,
  usually driven by binary release assets)

Bounded scan, so it's an estimate (the registry already marks these
estimated); summing is monotonic, so truncation only risks a false
negative just under a threshold, never a false positive.
GraphQL doesn't expose rulesets, so the runner makes one best-effort
REST call per run (GET /orgs/{org}/rulesets) and persists the count on
the run (profile_runs.org_ruleset_count, additive column). Org rulesets
aren't migrated and certain ones can fail the migration outright, so the
profile detail page surfaces a callout when any exist.

- core/auth: getSourceClients() exposes a source REST client (replaces
  getSourceGraphql, which only the profiler used)
- profile/rulesets: getOrgRulesetCount(rest, org), 0 on any error
- runner: injectable getOrgRulesetCount dep (default 0; service binds
  the REST-backed impl), runProfile now merges Partial deps
- store/schema: profile_runs.org_ruleset_count + setProfileRunRulesets
- ProfileRun gains orgRulesetCount; UI shows an org-ruleset warning

The rulesets endpoint needs validation against a live org.

11 gates green; coverage 87.34/85.74.
Adds one best-effort REST pass per run for the org-scoped resources that
aren't migrated and must be recreated on the target:

- Actions secrets & variables  (/orgs/{org}/actions/secrets|variables)
- Dependabot secrets           (/orgs/{org}/dependabot/secrets)
- Codespaces secrets           (/orgs/{org}/codespaces/secrets)
- Self-hosted runners          (/orgs/{org}/actions/runners)
- Custom property definitions  (/orgs/{org}/properties/schema)

Counts only — secret values are never exposed by these endpoints. The
six calls run concurrently and each degrades to 0 on a missing scope,
unavailable endpoint, or network error, so the crawl never fails on them.

Persisted as a JSON column profile_runs.org_resources (additive) and
surfaced in the Migration summary as an 'Organization-level resources to
recreate' block (non-zero items only). Gathered concurrently with the
org rulesets pass in the runner.

These are org-scoped, so repo-scoped copies of the same resources are
not covered — that needs the per-repo REST pass (deferred).

11 gates green; coverage 87.76/86.40.
Folds a .github/workflows tree lookup into the existing batched augment
query (object(expression: "HEAD:.github/workflows")), counting .yml/.yaml
definition files into RepoSignals.workflowFileCount. Lights up the
actions-run-history consideration per repo: workflows migrate, but their
run history and artifacts do not, so any workflow file means there's
history that will be lost.

Near-zero cost — it rides the same per-repo GraphQL alias as the other
signals, no extra requests. DETECTED considerations now 13.

11 gates green; coverage 87.76/86.40.
Two batching optimizations to get more signal from fewer, faster calls.

1. Release-partition. The release-asset scan (up to 100 releases x 50
   assets per repo, ~5k nodes) was the single field forcing the augment
   batch down to 25 repos/request. Discovery now also pulls the free
   releases.totalCount on its 100-wide page, so the runner splits repos:
   those with zero releases skip the asset scan entirely and batch at 90
   per request; only repos that actually have releases stay at 25. For a
   typical org (most repos have no releases) this cuts the augment leg of
   the crawl ~3x.

   - DiscoveredRepo gains releasesCount (was a RepoSignals/augment field)
   - augmentRepoSignals takes { scanReleases }; the query omits the
     release-asset selection when false

2. Concurrent augment chunks. The chunk loop was sequential. A bounded
   worker pool (3 in flight) now processes chunks concurrently, cutting
   wall-clock ~3x without stressing rate limits (the throttling plugin
   still guards secondary limits). Completed chunks persist regardless;
   the first chunk error fails the run after in-flight chunks settle
   (same stop-on-first-failure semantics as before).

Request budget for N repos is now ceil(N/100) discover + the partitioned
augment + 7 org-level, run with concurrency. Primary rate cost is ~1
point/request against 5k/hr; the real constraint is the per-query node
budget + 10s timeout, which the partition respects.

11 gates green; coverage 87.76/86.40.
@cvega
cvega merged commit 78eb846 into main Jun 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant