feat(gcp-to-aws): ground the derived GCP baseline in a source-side rate card (stacked on #149) - #156
Merged
Conversation
…Discovery previously required Terraform with heroku_* resources, which most startups on Heroku don't have. Adds a consent-gated, read-only live discovery path: an interactive capture step (exact-command whitelist, config-var key names only, auth:token banned) writes raw CLI output to live-capture/, and a new 'live' fragment parses it into the inventory. When Terraform is also present, live wins for current state and disagreements surface as drift (unmanaged resources, plan changes, config conflicts) — never silently resolved. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs of the Discover phase.
…covery previously required Terraform, app code, or billing exports. Adds a consent-gated, read-only live discovery sub-file: gcloud preflight, Cloud Asset Inventory fast path with a 16-row per-service fallback, --format=json projections that capture env var NAMES only, Terraform-style identity synthesis so downstream design-refs work unchanged, deterministic edge inference from resolved config, and Step 3S simplified clustering. When Terraform is also present, the IaC inventory is the base and disagreements surface as drift (unmanaged resources, console resizes, config conflicts) — with the rule that absence of evidence from a failed capture is not drift. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs. Stacked on the Heroku live-discovery branch (awslabs#148).
…a dumps and relationship queries Two design decisions in discover-live.md were implicit and invite well-meaning 'optimization': (1) gcloud asset list --content-type=resource returns full resource.data in one call but includes env var values, breaking the values-never-captured contract; (2) CAI relationships.* queries parse on the standard search endpoint but the data requires SCC Premium/Enterprise or Gemini Cloud Assist — without the entitlement they return nothing, and most relationship types aren't search-available anyway. Both now stated inline with do-not-change guidance.
… close capture gaps - Restore graviton content in SKILL.md that a stale whole-file copy dropped (rebuilt from main + feature edits; conditional-load row, file tree, Defaults) - Offer live discovery whenever no Terraform is found, not only when NO files exist — billing-only and app-code-only workspaces now get the live path - Add bq ls fallback row so BigQuery (and its specialist gate) survives the common CAI-disabled case; mapping table references it - Drop undocumented 'gcloud run services list --region=-' (managed Cloud Run lists all regions by default); fix redis region-source wording for CAI path - Use --scope=projects/... for asset search (documented scoping flag); document why --asset-types stays unfiltered (unmapped_asset_types signal) - Mark networks/subnets/secrets/SA rows as enrichment so the CAI path keeps edge and name-inventory fidelity - Commit check_expected_drift.py asserter (stdlib-only) + README usage; validated PASS against a fresh fresh-agent scenario-B replay of this revision - Clarify merge rule 1 (overwrite any disagreeing config value, enrichment vs conflict distinction); note provisioned-vs-actual disk caveat and live-compute graviton gap as follow-ups
…pping, SA naming Validated live discovery against a real GCP project (270 CAI assets, fast path; all six applicable enrichment rows including bq ls; env-name-only projections held with real secret-adjacent env names like DB_PASSWORD present as names only). Comparison against a prior IaC discovery run surfaced three spec bugs: - google_cloud_run_service (v1) vs google_cloud_run_v2_service caused FALSE drift on the same service — Step 6 now aliases v1/v2 Run and Functions type pairs when the GCP name matches - compute.googleapis.com/Firewall was unmapped, dropping what the IaC path classifies as network_path secondaries — mapping row added - Service account 'name' now mandated as the email local-part, never the display name, or merge matching breaks
…SKILL.md live-offer blurb - Reorder capture table so vertex (16) precedes bq (17); mapping-table row references unchanged - Mark bq ls as an E row so a CAI-success run still verifies BigQuery presence (specialist gate) even if thin search missed it - SKILL.md prerequisites blurb now matches discover.md Step 1d semantics: offer live whenever no Terraform is found, not only when no files exist - Deferred (follow-up): wiring check_expected_drift.py into the mise/CI test suite
…ge fix, v1/v2 alias lock - Row 18: firewall-rules list (minimal projection) so firewalls survive the per-service path, not CAI-only (same class as the earlier bq gap) - Row 19 + edge-rule rewrite: the vpc-access-connector annotation is a CONNECTOR id, not a network — resolve connector.network via a read-only describe before emitting a network_membership edge; never fabricate one - Fixture now declares orders-api as LEGACY google_cloud_run_service (v1) while live maps to v2, locking the Step 6 type-alias rule; expected-drift.json asserts one merged resource (IaC address, live_type, no false drift pair) - Asserter: recursive env-value payload detection (name+value/valueFrom objects, raw env keys, env-name-as-key patterns) - Discover summary surfaces unmapped_asset_types count + top types - Preflight ADC fallback: probe projects describe before demanding gcloud auth login (GOOGLE_APPLICATION_CREDENTIALS / impersonation setups) - SA merge matching clarified to config.account_id (fresh-agent replay judgment call folded back in) Validated: fresh-agent replay of updated scenario B — alias rule executed as designed (one merged resource, live_type recorded, image conflict), asserter PASS, full build green.
…be uses it The ADC fallback probe referenced $GCP_PROJECT one step before the project was set. Project resolution is a local config read plus user confirmation (no credentials needed), so it now runs as step 2, with the auth check + probe as step 3.
When asset search fails because Cloud Asset API is off (default for most startup projects), offer a user-driven enable with CLI + console how-to, retry once on yes, then fall back to per-service. Agent never mutates; permission-denied skips the ask. Record cai_enable_* in the manifest and live_metadata. Co-authored-by: Cursor <cursoragent@cursor.com>
Enable alone is not enough — Cloud Asset Viewer (roles/cloudasset.viewer) is also required. Soft-ask now includes API enable, IAM guidance, and the view-assets docs link; permission-denied gets its own remediation offer. Co-authored-by: Cursor <cursoragent@cursor.com>
…rvice mode Review follow-up: rows 10/16 walked 'each region seen in the asset-search results or rows 1-9'. Fine in asset-search mode (project-wide coverage), but in per-service fallback mode the seen-region heuristic fails both ways: a Memorystore/Vertex instance in a region with no other footprint is silently missed on custom-mode VPCs, while auto-mode VPC subnets inflate the seen set to every region anyway. New whitelisted row 20 (gcloud compute regions list, names only) enumerates the walk in per-service mode; asset-search mode walks matching asset locations. If the regions list itself fails, fall back to the old heuristic WITH a capture warning naming the coverage limit. Fixtures model the walk (regions.json + empty-region redis captures) and drop the undocumented --region=- from the canned run.json command per the row-1 note.
…te card Estimate's Part 1 rung 2 ('estimate costs from discovered resource configurations') ran on remembered GCP prices -- ungrounded and unreproducible. With live discovery (awslabs#149) making TF-less runs the common case, that rung becomes the primary migrate-or-stay baseline for users without a billing export. - New references/shared/gcp-infra-pricing-cache.md: source-side GCP list rates (Cloud SQL Enterprise vCPU/RAM/storage/backup + tier decoding + HA multiplier, Memorystore capacity bands, GCE machine types, GKE cluster fee) with per-table sources, us-central1 basis, a NOT-derivable list (Cloud Run/Functions/PubSub/buckets/Autopilot/egress -> excluded with warnings, never silently), and the standard staleness contract -- the filename matches pricing-staleness.ts's pattern so the new card is freshness-gated automatically. - Part 1 rung 2 rewritten as a procedure: rate-card-only math (never remembered prices), capture-or-terraform-shaped config resolution, PD_SSD default, backup upper bound, exclusion reason enum (usage_based / no_standing_charge / unpriced_gcp), source 'inventory_estimate' (the schema's existing value), schema-shaped category breakdown + additive derivation[]/excluded_resources[]/ warnings[], mandatory not-a-bill caveat. - Part 4 egress: provisioned sizes now ground an explicit UPPER BOUND when billing is absent (previously omitted entirely); three-way gating recorded via baseline_available. - schema-estimate-infra.md: current_costs gains accuracy/derivation/ excluded_resources/warnings. - Fixtures: seed-baseline/ + expected-baseline.json (exact math: $149.45 = SQL $113.68 + Redis $35.77) + check_expected_baseline.py. Validated by two fresh-agent Part 1 replays (second run under the corrected spec: asserter PASS; first run folded back 7 findings incl. the source-enum conflict and the silent backup omission).
This was referenced Jul 20, 2026
Resolves conflicts after the workshop stack + awslabs#149/awslabs#151/awslabs#155/awslabs#162 landed: - heroku-to-aws SKILL.md + heroku-live-capture/README.md: took main (stale awslabs#148/awslabs#150 lineage; main has the merged workshop/what-if + Scenario C content). - gcp-live-capture/README.md: kept THIS branch's added 'Scenario C — derived GCP baseline' block (its genuine new content; main's side empty there). - migrate/README.md: took main (adds vercel + agent-advisor reqs; awslabs#149's gcloud GCP requirement lines already present above the conflict). New gcp-infra-pricing-cache.md auto-enrolls in pricing:staleness (6/6 fresh) and the seed-baseline fixtures pass fixtures:check (141 json / 10 asserters). Full build green.
icarthick
added a commit
to herosjourney/startups
that referenced
this pull request
Jul 22, 2026
Union-resolved mise.toml lint group: fixtures:assert (this PR) + fixtures:check + pricing:staleness (from awslabs#151/awslabs#155, now on main) all run in build. Registered the two gcp-live-capture asserters that landed via awslabs#149/awslabs#156 after this branch was cut (the PR's own registry-completeness guard correctly flagged them): - gcp-live-capture/check_expected_baseline.py (smoke — already exits 1 cleanly) - gcp-live-capture/check_expected_drift.py (smoke — added the same missing-artifact guard this PR applies to the other 4, replacing a FileNotFoundError traceback with a clean one-line FAIL + exit 1) fixtures:assert now 10/10 (3 golden, 7 smoke). Full build green.
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Changes
New
references/shared/gcp-infra-pricing-cache.mdpricing-staleness.tspattern, so the card is freshness-gated automatically from day one.Estimate spec
usage_based/no_standing_charge/unpriced_gcp),source: "inventory_estimate"(the schema's existing enum value), schema-shaped category breakdown plus additivederivation[]/excluded_resources[]/warnings[], mandatory not-a-bill caveat.baseline_available.schema-estimate-infra.md:current_costsgainsaccuracy/derivation/excluded_resources/warnings.Relationship to other PRs
feat/gcp-cli-live-discovery) — only the last commit (18b0892) is new here.Test plan
fixtures/gcp-live-capture/seed-baseline/(scenario-B resources in schema shape, no billing profile) +expected-baseline.jsonpinning the exact math ($149.45/mo = Cloud SQL $113.68 + Memorystore $35.77) +check_expected_baseline.pymise run buildgreen;fixtures:checkgreen; the new card reports fresh inpricing:staleness